Skip to content

feat(init): support teamai init . and auto-bootstrap from project.yaml - #277

Open
jeff-r2026 wants to merge 3 commits into
mainfrom
feat/init-dot
Open

feat(init): support teamai init . and auto-bootstrap from project.yaml#277
jeff-r2026 wants to merge 3 commits into
mainfrom
feat/init-dot

Conversation

@jeff-r2026

Copy link
Copy Markdown
Collaborator

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 teammates
  • Auto-bootstrap on session start: when pull() detects project.yaml but no config.yaml (clone-after state), it automatically clones the team-repo, writes local config, and injects hooks. Teammates just need teamai installed — no manual teamai init needed

New utilities

  • normalizeRemoteUrl() / getCwdGitRemoteUrl() — git remote detection with SSH→HTTPS normalization
  • ProjectDeclarationSchema — Zod schema for the portable declaration
  • loadProjectDeclaration() / saveProjectDeclaration() — I/O helpers

Edge case handling

  • No git remote → clear error with fallback instructions
  • Multiple remotes → prefers origin, falls back to first
  • SSH remote URLs → normalized to HTTPS in project.yaml
  • Auth/clone failure during bootstrap → warns and continues (non-blocking)
  • $HOME as cwd → skips bootstrap to avoid path collision
  • Idempotent: bootstrap skips when config.yaml already exists

Test plan

  • npx tsc --noEmit passes
  • 1924 tests pass (33 new: init-dot + auto-bootstrap)
  • npm run build succeeds
  • E2E: teamai init . in a git repo correctly detects remote, sets project scope, writes project.yaml

🤖 Generated with Claude Code

…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 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yaml bootstrap 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, $HOME edge case)
  • Idempotent bootstrap — skips when config.yaml already exists
  • SSH→HTTPS normalization is well-tested with good edge cases
  • resolveInitRepo conflict 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 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@jeff-r2026 jeff-r2026 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review #2 (after commit 2407064)

All feedback addressed. LGTM ✅

  • normalizeRemoteUrl now consistently appends .git for both SSH and HTTPS
  • team-repo/ added to PROJECT_GITIGNORE_ENTRIES
  • ✅ Tests updated to match new normalization behavior

No further issues found. Ready to merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 让项目级 .teamai/ 跟着 git 仓库走(clone 即完成项目初始化)

1 participant