feat(init): support teamai init . and auto-bootstrap from project.yaml - #277
feat(init): support teamai init . and auto-bootstrap from project.yaml#277jeff-r2026 wants to merge 3 commits into
teamai init . and auto-bootstrap from project.yaml#277Conversation
…aml (#198) - Add `teamai init .` shorthand: auto-detects cwd's git remote URL and implies `--scope project`, so users don't need to type the full repo URL - Write `.teamai/project.yaml` (portable declaration file) during project-scope init — contains only safe-to-commit fields (repo URL, defaultRole, scope) - Auto-bootstrap in `pull()`: when project.yaml exists but config.yaml is missing (clone-after state), automatically clone team-repo, write local config, and inject hooks — no manual `teamai init` needed for teammates - Add `normalizeRemoteUrl()` and `getCwdGitRemoteUrl()` git utilities - Add `ProjectDeclarationSchema` Zod type and I/O helpers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jeff-r2026
left a comment
There was a problem hiding this comment.
Code Review
Overall a well-structured feature with solid edge case handling and good test coverage (33 new tests). A few items to address before merge:
🟡 Medium: Missing doc updates
Per project conventions (CLAUDE.md):
Large changes must update all affected docs... A behavior change whose docs still describe the old behavior is an incomplete PR.
teamai init . is a new user-visible command and project.yaml auto-bootstrap is a new concept. Please update README.md, README.zh-CN.md, and docs/usage-guide.* (both languages) to document:
- The
init .shorthand and what it does - The
project.yamlbootstrap flow for teammates
🟢 Low: .gitignore content duplicated between init.ts and pull.ts
The .gitignore entries list is copy-pasted verbatim in both src/init.ts (~L697) and src/pull.ts (~L1150). If a future entry is added to one, the other will drift silently. Consider extracting a shared constant (e.g. TEAMAI_GITIGNORE_ENTRIES in types.ts or a helper function).
🟢 Low: scope field in ProjectDeclarationSchema is too permissive
scope: ScopeEnum.default('project'),project.yaml only ever means project scope, but ScopeEnum also accepts 'user'. A teammate could hand-edit it to 'user', producing confusing bootstrap behavior. Consider z.literal('project').default('project') to reject invalid values, or remove the field entirely since it's always 'project'.
🟢 Low: saveProjectDeclaration doesn't ensure parent directory
export async function saveProjectDeclaration(decl, projectRoot) {
const declPath = getProjectDeclarationPath(projectRoot);
await writeFile(declPath, YAML.stringify(decl));
}This will fail if .teamai/ doesn't exist. In practice the caller has already created it, but as a standalone API it's fragile. An await ensureDir(path.dirname(declPath)) would make it self-contained.
Positives
- Clean non-blocking error handling (auth failure, clone failure,
$HOMEedge case) - Idempotent bootstrap — skips when
config.yamlalready exists - SSH→HTTPS normalization is well-tested with good edge cases
resolveInitRepoconflict detection prevents confusing--repo+.combinations
- Extract PROJECT_GITIGNORE_ENTRIES shared constant (types.ts) to
eliminate duplicated .gitignore lists between init.ts and pull.ts
- Tighten ProjectDeclaration scope to z.literal('project') — rejects
hand-edited 'user' values that would cause confusing behavior
- Add ensureDir to saveProjectDeclaration for standalone safety
- Update README (both languages) and usage-guide (both languages):
document `init .` shorthand and project.yaml auto-bootstrap flow
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jeff-r2026
left a comment
There was a problem hiding this comment.
Re-review (after commit b2c84fb)
上一轮的四个反馈(gitignore 去重、scope 收紧、ensureDir、文档更新)已全部修复。代码整体质量很好。
还有两个小点供参考:
🟢 Low: normalizeRemoteUrl 对 SSH 和 HTTPS 的 .git 后缀处理不一致
SSH 输入总是输出带 .git 的 URL:
git@github.com:org/repo → https://github.com/org/repo.git ✓
git@github.com:org/repo.git → https://github.com/org/repo.git ✓
但 HTTPS 输入保持原样:
https://github.com/org/repo → https://github.com/org/repo (no .git)
https://github.com/org/repo.git → https://github.com/org/repo.git (has .git)
这意味着同一个 repo,如果队友 A 用 SSH remote 跑 init .,队友 B 用 HTTPS remote 跑 init .,project.yaml 中的 repo 值可能不同(repo.git vs repo)。不影响功能(parseRepoInput 两种都能解析),但会造成不必要的 diff。
建议统一:HTTPS 路径也 ensure .git 后缀,或者两者都 strip .git。
🟢 Low: team-repo/ 不在 PROJECT_GITIGNORE_ENTRIES 中
team-repo/ 目录(克隆下来的整个 team repo)不在 .gitignore 列表中。这是 pre-existing 的问题(main 分支也是如此),但既然这个 PR 引入了 PROJECT_GITIGNORE_ENTRIES 共享常量,是个好时机补上 team-repo/ 以防意外提交。
除此之外 LGTM。逻辑、测试、文档都到位了。
- normalizeRemoteUrl now appends .git to HTTPS URLs without it, ensuring consistent project.yaml content regardless of SSH vs HTTPS remotes - Add team-repo/ to PROJECT_GITIGNORE_ENTRIES to prevent accidental commits Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Closes #198
teamai init .: new shorthand that auto-detects the cwd's git remote URL and implies--scope project. No more typing the full repo URL when initializing from inside a project.teamai/project.yaml: a portable declaration file (repo URL + optional defaultRole) written during project-scope init. This file is NOT gitignored and should be committed — it's the bootstrap signal for teammatespull()detectsproject.yamlbut noconfig.yaml(clone-after state), it automatically clones the team-repo, writes local config, and injects hooks. Teammates just need teamai installed — no manualteamai initneededNew utilities
normalizeRemoteUrl()/getCwdGitRemoteUrl()— git remote detection with SSH→HTTPS normalizationProjectDeclarationSchema— Zod schema for the portable declarationloadProjectDeclaration()/saveProjectDeclaration()— I/O helpersEdge case handling
origin, falls back to first$HOMEas cwd → skips bootstrap to avoid path collisionTest plan
npx tsc --noEmitpassesnpm run buildsucceedsteamai init .in a git repo correctly detects remote, sets project scope, writes project.yaml🤖 Generated with Claude Code