Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ All notable changes to this project will be documented in this file. See [standa

### ✨ Features

- `teamai members --invite` generates an AI-ready onboarding invitation that keeps Git commands and secrets away from non-technical members, then verifies a team skill in a fresh session ([#517](https://github.com/Tencent/teamai-cli/issues/517)).
- Multi-project management: `role` and `project` together resolve resource namespaces, and project-private learnings are isolated ([#426](https://github.com/Tencent/teamai-cli/pull/426), for [#375](https://github.com/Tencent/teamai-cli/issues/375)).
- Data partitions auto-migrate a legacy `.teamai`, resume interrupted migrations, smoke-check the clone, and keep a git-ignored backup ([#439](https://github.com/Tencent/teamai-cli/pull/439), for [#374](https://github.com/Tencent/teamai-cli/issues/374)).

### 🐛 Bug Fixes

- `teamai doctor` no longer assumes TGit before initialization and now exits with code 1 when any diagnostic check fails ([#517](https://github.com/Tencent/teamai-cli/issues/517)).
- MCP `requires` is resolved from `PATH` (including Windows `PATHEXT`), so `teamai mcp inject` no longer skips servers such as `uvx` on Windows ([#540](https://github.com/Tencent/teamai-cli/pull/540), for [#539](https://github.com/Tencent/teamai-cli/issues/539)).
- The GitHub and CNB providers resolve their CLI to a launchable absolute path and start it through cross-spawn, so on Windows they no longer answer "installed" while every call fails silently ([#520](https://github.com/Tencent/teamai-cli/pull/520)).
- `enabledAgents` now also gates CLI builtin deploy, CLAUDE.md-class injects, and last-pull skip-sync targets, so an already-installed tool outside the whitelist is not written to ([#510](https://github.com/Tencent/teamai-cli/issues/510)).
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ npm install -g teamai-cli

Create a shared-experience repo on your git host (GitHub, GitLab, GitCode, CNB, TGit, or a private Git service), **grant write access to team members**, then run `teamai init https://github.com/yourorg/yourrepo`.

After initialization, run `teamai members --invite` and send the generated invitation to each member. It guides their AI tool through a no-Git-command, no-secret-in-chat setup and verifies the first skill in a fresh session.

> **No team repo yet?** Start from a template pre-loaded with production-ready skills, rules, and review agents. Browse the [teamai-hub](https://github.com/teamai-hub) org, click **Use this template**, then `teamai init` against your new repo.

### Team members

Paste the invitation from your admin into the AI tool you want to equip. Let it run the setup; onboarding is complete only after `teamai doctor` passes, a local team skill is listed, and that skill responds in a fresh session.

If your admin sent only the repo URL, the manual fallback is:

```bash
# Choose one, depending on where you want resources installed

Expand Down Expand Up @@ -254,7 +260,7 @@ Insight into how the team actually uses its AI tools, and a starting point for t
| `teamai codebase --reconcile` | Reconcile product documentation with extracted code knowledge |
| `teamai codebase --lint` | Knowledge graph health check |
| `teamai ci extract-mr --url <url>` | CI: extract knowledge from MR, post comments, write after merge |
| `teamai members` | List team members |
| `teamai members [--invite]` | List team members or print an AI-ready onboarding invitation |
| `teamai projects` | Bind a working directory to one or more logical projects |
| `teamai roles` | Manage team roles and namespaces |
| `teamai tags` | Manage tag-based skill/rule filtering |
Expand All @@ -263,7 +269,7 @@ Insight into how the team actually uses its AI tools, and a starting point for t
| `teamai remove <type> <name>` | Remove a resource and open MR |
| `teamai session save` | Record a privacy-scrubbed session summary to a monthly log (`--push` feeds `digest`) |
| `teamai digest` | Generate weekly team usage digest |
| `teamai doctor` | Diagnose configuration issues |
| `teamai doctor` | Diagnose configuration issues; exits with code 1 when any check fails |
| `teamai uninstall` | Remove all teamai resources and hooks |

## License
Expand Down
10 changes: 8 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ npm install -g teamai-cli

在 Git 托管平台(GitHub、GitLab、GitCode、CNB、TGit,或私有 Git 服务)创建共享经验仓库,**授予团队成员写权限**,然后运行 `teamai init https://github.com/yourorg/yourrepo`。

初始化后运行 `teamai members --invite`,把生成的邀请信息发给成员。成员无需输入 Git 命令或在对话中粘贴密钥,AI 工具会完成接入,并在新会话中验证首个 Skill。

> **还没有团队仓库?** 可以从内置了成套 skills、rules、review agents 的模板起步。浏览 [teamai-hub](https://github.com/teamai-hub) org,点 **Use this template** 生成自己的仓库,再对它执行 `teamai init`。

### 团队成员

把管理员发来的邀请信息粘贴到你想使用的 AI 工具中,让它代为完成接入。只有 `teamai doctor` 通过、本地能列出团队 Skill,且该 Skill 在新会话中实际响应,才算接入完成。

如果管理员只发了仓库地址,可按以下方式手动接入:

```bash
# 二选一:按你想要的安装范围选择其中一条

Expand Down Expand Up @@ -254,7 +260,7 @@ teamai recall maintenance --update-quality # 为过时 skills / docs 生
| `teamai codebase --reconcile` | 将产品文档与提取的代码知识进行对账 |
| `teamai codebase --lint` | 知识图谱健康检查 |
| `teamai ci extract-mr --url <url>` | CI:从 MR 提取知识、发评论、合并后写入 |
| `teamai members` | 查看团队成员 |
| `teamai members [--invite]` | 查看团队成员或生成可交给 AI 执行的接入邀请 |
| `teamai projects` | 将工作目录绑定到一个或多个逻辑项目 |
| `teamai roles` | 管理团队角色和命名空间 |
| `teamai tags` | 管理基于标签的 skill/rule 过滤 |
Expand All @@ -263,7 +269,7 @@ teamai recall maintenance --update-quality # 为过时 skills / docs 生
| `teamai remove <type> <name>` | 删除资源并创建 MR |
| `teamai session save` | 将脱敏后的 session 摘要记录到月度日志(`--push` 可喂给 `digest`) |
| `teamai digest` | 生成团队周报 |
| `teamai doctor` | 诊断配置问题 |
| `teamai doctor` | 诊断配置问题;任一检查失败时以状态码 1 退出 |
| `teamai uninstall` | 移除所有 teamai 资源和 hooks |

## 许可证
Expand Down
18 changes: 16 additions & 2 deletions docs/usage-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,17 @@ With inheritance enabled, `teamai pull` refreshes user `skills`, `rules`, `docs`

## Member Onboarding

Once the admin shares the team repo URL with members:
The admin generates a self-contained invitation after initialization:

```bash
teamai members --invite
```

The member pastes the invitation into the AI tool they want to equip. The AI performs setup without asking the member to type Git commands or paste credentials into chat. It uses the provider's official sign-in flow and asks only for decisions it cannot infer.

Onboarding has one user-centered success contract: within eight minutes, `teamai doctor` exits with code 0, `teamai list skills --source local --agent <agent-id>` lists at least one team skill, and the member confirms that skill responds in a fresh AI session.

If the admin shares only the team repo URL, use this manual fallback:

**Project-scoped teams (default):**

Expand Down Expand Up @@ -394,6 +404,8 @@ teamai init --http https://your-team-host/api --token <api-key>
**Verify:**

```bash
teamai doctor # All checks pass; exit code 0
teamai list skills --source local --agent claude
teamai status # View status
teamai members # View team members
teamai list # All resource types (skills|rules|docs|env|agents|hooks|mcp) + local skills
Expand Down Expand Up @@ -465,7 +477,7 @@ The existing SessionStart hook runs `teamai pull`. When the `packages` declarati
```bash
teamai packages # Install every team declaration
teamai packages --dry-run # Preview native commands without installing or writing files
teamai doctor # Check runtimes and declared package/marketplace/plugin status
teamai doctor # Check setup; exits 1 when any check fails
```

After a successful install, TeamAI writes a local snapshot to `teamai.lock` under the active scope's `.teamai` directory. The lock records installed versions and the declaration hash used by the SessionStart hint; it is not stored in the team repository. In user scope, machine-wide npm tools and Claude plugins are acknowledged once, while project npm dependencies are acknowledged separately for each working directory so installing in one repository cannot silence another repository's hint.
Expand Down Expand Up @@ -1412,6 +1424,8 @@ teamai remove agents <name>
teamai remove mcp <name>
```

`teamai doctor` exits with code 0 only when every check passes, and code 1 when any check fails. Before initialization, it reports the missing configuration without assuming a Git provider.

Auto-update runs in the Stop hook and is controlled by two tiers:

| Tier | File | Field | Value |
Expand Down
18 changes: 16 additions & 2 deletions docs/usage-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,17 @@ teamai init https://github.com/yourorg/java-service-teamai --inherit-user-scope

## 成员接入

管理员将团队仓库地址分享给成员后:
管理员完成初始化后,生成一段自包含的邀请信息:

```bash
teamai members --invite
```

成员把邀请信息粘贴到想使用的 AI 工具中。AI 会代为完成接入,不要求成员输入 Git 命令,也不要求把凭据粘贴进对话;认证使用托管平台的官方登录流程,只询问无法自行判断的选择。

接入只有一个以用户结果为中心的验收标准:8 分钟内,`teamai doctor` 以状态码 0 退出,`teamai list skills --source local --agent <agent-id>` 至少列出一个团队 Skill,并且成员确认该 Skill 在新的 AI 会话中实际响应。

如果管理员只分享了团队仓库地址,可按以下方式手动接入:

**项目级团队(默认):**

Expand Down Expand Up @@ -379,6 +389,8 @@ teamai init --http https://your-team-host/api --token <api-key>
**验证:**

```bash
teamai doctor # 所有检查通过;退出码为 0
teamai list skills --source local --agent claude
teamai status # 查看状态
teamai members # 查看团队成员
teamai list # 全部资源类型(skills|rules|docs|env|agents|hooks|mcp)+ 本地 skills
Expand Down Expand Up @@ -450,7 +462,7 @@ Claude 插件 target 使用 `plugin@marketplace` 格式。`claude-plugins-offici
```bash
teamai packages # 安装团队声明的全部包和插件
teamai packages --dry-run # 预览底层命令,不安装也不写文件
teamai doctor # 检查运行环境及声明的包、marketplace、插件状态
teamai doctor # 检查接入状态;任一检查失败时退出码为 1
```

安装成功后,TeamAI 会在当前 scope 的 `.teamai` 目录下写入本地快照 `teamai.lock`。该文件记录已安装版本,以及供 SessionStart 提示比对的声明哈希,不会写入团队仓库。在 user scope 下,全局 npm 工具和 Claude 插件只需确认一次;项目 npm 依赖会按工作目录分别确认,避免在一个仓库安装后错误关闭另一个仓库的提示。
Expand Down Expand Up @@ -1379,6 +1391,8 @@ teamai remove agents <name>
teamai remove mcp <name>
```

仅当所有检查通过时,`teamai doctor` 才以状态码 0 退出;任一检查失败时以状态码 1 退出。尚未初始化时,它只报告缺少配置,不会臆测 Git 托管平台。

自动更新在 Stop hook 中执行,可通过两层控制:

| 层级 | 文件 | 字段 | 值 |
Expand Down
56 changes: 52 additions & 4 deletions src/__tests__/doctor.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import path from 'node:path';

// ── Mocks ────────────────────────────────────────────────

Expand Down Expand Up @@ -35,13 +36,16 @@ import { loadLocalConfig, loadTeamConfig } from '../config.js';
import { pathExists, readFileSafe } from '../utils/fs.js';
import { TEAMAI_HOOK_SUBCOMMANDS } from '../hooks.js';
import { log } from '../utils/logger.js';
import { isGfInstalled, gfIsAuthenticated } from '../providers/tgit/index.js';
import { doctor } from '../doctor.js';

const mockedLoadLocalConfig = loadLocalConfig as Mock;
const mockedLoadTeamConfig = loadTeamConfig as Mock;
const mockedPathExists = pathExists as Mock;
const mockedReadFileSafe = readFileSafe as Mock;
const mockedLog = log as unknown as { info: Mock; success: Mock; warn: Mock; error: Mock; debug: Mock };
const mockedIsGfInstalled = isGfInstalled as Mock;
const mockedGfIsAuthenticated = gfIsAuthenticated as Mock;

const mockLocalConfig = {
repo: { localPath: '/tmp/repo', remote: 'https://git.woa.com/team/repo.git' },
Expand Down Expand Up @@ -92,12 +96,17 @@ beforeEach(() => {

describe('doctor — hook checks', () => {
it('should pass when all subcommands are present in settings', async () => {
await doctor({});
mockedLoadTeamConfig.mockResolvedValue({
...mockTeamConfig,
sharing: { env: { injectShellProfile: false } },
});
const allPassed = await doctor({});

// Should show the hooks check passing (✔)
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('✔'),
);
expect(allPassed).toBe(true);
});

it('should fail when a subcommand is missing from settings', async () => {
Expand All @@ -113,7 +122,7 @@ describe('doctor — hook checks', () => {
return null;
});

await doctor({});
const allPassed = await doctor({});

// Should show the hooks check failing (✖) with fix suggestion
expect(consoleSpy).toHaveBeenCalledWith(
Expand All @@ -122,6 +131,7 @@ describe('doctor — hook checks', () => {
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('teamai hooks inject'),
);
expect(allPassed).toBe(false);
});

it('should fail when settings file does not exist', async () => {
Expand Down Expand Up @@ -180,7 +190,7 @@ describe('doctor — hook checks', () => {
// Only <projectRoot> carries the hooks (committed to the business repo).
mockedReadFileSafe.mockImplementation(async (filePath: string) => {
if (filePath.includes('settings.json')) {
return filePath.includes(projectRoot) ? buildFullHooksContent() : '{ "hooks": {} }';
return filePath.includes(path.normalize(projectRoot)) ? buildFullHooksContent() : '{ "hooks": {} }';
}
return null;
});
Expand All @@ -192,7 +202,7 @@ describe('doctor — hook checks', () => {

it('should pass env check when env/env.yaml does not exist in team repo', async () => {
mockedPathExists.mockImplementation(async (filePath: string) => {
if (filePath.includes('env/env.yaml')) return false;
if (filePath.endsWith(path.join('env', 'env.yaml'))) return false;
return true;
});
mockedReadFileSafe.mockImplementation(async (filePath: string) => {
Expand Down Expand Up @@ -282,4 +292,42 @@ describe('doctor — hook checks', () => {
// Should still show claude check
expect(allCalls.some((msg: string) => msg.includes('claude'))).toBe(true);
});

it('does not assume a provider before initialization', async () => {
mockedLoadLocalConfig.mockResolvedValue(null);
mockedLoadTeamConfig.mockResolvedValue(null);

const allPassed = await doctor({});

const allLines = consoleSpy.mock.calls.map((c) => String(c[0]));
expect(allLines).toContain(' Scope: not initialized\n');
expect(allLines).toContain(' ✖ TeamAI is not initialized');
expect(allLines.some((line) => line.includes('gf CLI'))).toBe(false);
expect(allLines.some((line) => line.includes('hooks in'))).toBe(false);
expect(mockedIsGfInstalled).not.toHaveBeenCalled();
expect(mockedGfIsAuthenticated).not.toHaveBeenCalled();
expect(allPassed).toBe(false);
});

it('checks hooks only for agents selected during onboarding', async () => {
mockedLoadLocalConfig.mockResolvedValue({
...mockLocalConfig,
enabledAgents: ['claude'],
});
mockedLoadTeamConfig.mockResolvedValue({
...mockTeamConfig,
sharing: { env: { injectShellProfile: false } },
toolPaths: {
claude: { settings: '.claude/settings.json', skills: '.claude/skills' },
codex: { settings: '.codex/hooks.json', skills: '.codex/skills' },
},
});

const allPassed = await doctor({});

const allLines = consoleSpy.mock.calls.map((c) => String(c[0]));
expect(allLines.some((line) => line.includes('hooks in claude settings'))).toBe(true);
expect(allLines.some((line) => line.includes('hooks in codex settings'))).toBe(false);
expect(allPassed).toBe(true);
});
});
Loading
Loading