feat(cli): add --scope option to install command#440
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a --scope flag (supporting user or project) to the install command, enabling explicit control over the installation target. It adds interactive scope selection for TTY environments, updates agent-specific directory paths, and refactors the installation logic for better testability. Feedback focuses on two issues in resolver.ts: ensuring homedir() is used as a fallback for user directory detection and improving the reliability of scope determination when the current working directory matches the home directory.
| const resolved = await resolveExplicitAgents(agentList, options.cwd, options.home ?? homedir()) | ||
| return dedupeByRoot(resolved) | ||
| } | ||
| const detected = options.detected ?? await detectAll(options.cwd, options.home ?? '') |
There was a problem hiding this comment.
在 resolveInstallTargets 中,当 options.home 未定义时,detectAll 被传入了空字符串 ''。这会导致 detectAll 无法正确探测到用户目录(home directory)下的 Agent 技能目录(例如 ~/.claude/skills),因为 userRoots('') 会生成错误的路径(如 /.claude/skills)。建议统一使用 homedir() 作为默认值,这与同一文件中的其他逻辑(如第 37 行和第 59 行)保持一致。
| const detected = options.detected ?? await detectAll(options.cwd, options.home ?? '') | |
| const detected = options.detected ?? await detectAll(options.cwd, options.home ?? homedir()) |
There was a problem hiding this comment.
不采纳此建议。这里 detectAll 使用 options.home ?? '' 是有意保留的兼容性行为,不是 bug:
- 当前
installCommand调用 resolver 时不传home,沿用options.home ?? '',意味着裸skillhub install foo(无任何参数)的非交互模式只探测 cwd 下的 agent 目录,无候选时 fallback 到<cwd>/.agents/skills。这是 PR 之前的现有行为。 - 如果按建议改为
?? homedir(),会让裸install同时探测用户级目录(如~/.codex/skills),可能从"无候选 fallback"变成"探测到 user 候选直接安装"或"多候选报错",破坏现有非交互用户脚本。 - spec 中明确将这一点列为兼容性原则:仅在显式
--scope路径才使用真实homedir()(见 resolver.ts:62scopedHome = options.home ?? homedir()),而scope === undefined路径完全保持现状。 - 注:第 37 行的
resolveExplicitAgents沿用options.home ?? homedir()也是 PR 之前的现有行为,不是本 PR 引入。
建议保留当前实现。
| const candidateScope: AgentCandidate['scope'] = scope !== undefined | ||
| ? scope | ||
| : (root.startsWith(cwd) ? 'project' : 'user') |
There was a problem hiding this comment.
这里的逻辑仍然依赖 root.startsWith(cwd) 来判定 scope,这在 cwd 等于 home 时会产生误判(将用户级目录误标为 project)。虽然 PR 在 generateScopedCandidates 中通过显式传入 scope 避免了此问题,但在 legacy 路径(scope 为 undefined 时)依然存在此缺陷。既然已经重构了 resolveExplicitAgents,建议使用更可靠的判定方式:检查 root 是否属于 profile.userRoots(home)。
| const candidateScope: AgentCandidate['scope'] = scope !== undefined | |
| ? scope | |
| : (root.startsWith(cwd) ? 'project' : 'user') | |
| const candidateScope: AgentCandidate['scope'] = scope !== undefined | |
| ? scope | |
| : (profile.userRoots(home).includes(root) ? 'user' : 'project') |
There was a problem hiding this comment.
已采纳并修复(commit 9de30ae)。改用 profile.userRoots(home) 的 Set 成员判断来标记 candidate.scope,避免 cwd === home 时 root.startsWith(cwd) 把 user roots 误标为 project。同时新增了一条 cwd === home + --agent codex 的回归测试。
- Distinguish user vs project install scope via explicit --scope flag - Interactive mode prompts for scope when --scope/--agent/--dir not provided - Non-interactive bare install preserves existing behavior (backward compatible) - Mutual exclusion: --dir cannot be combined with --scope or --agent - Symmetric fallback: --scope user falls back to ~/.agents/skills, --scope project falls back to <cwd>/.agents/skills - Strict TTY check requires both stdin and stdout TTY plus no --json - Scope-aware candidate generation avoids root.startsWith(cwd) misjudgement when cwd === home or paths overlap - Correct gemini-cli (.gemini/skills) and kiro-cli (.kiro/skills) paths in install path tables across README and guide docs - Note CLI fallback uses .agents/skills (with s) in skill protocol doc
When --agent is provided without --scope, scope was inferred via root.startsWith(cwd), which mislabels user roots as project when cwd === home. Use profile.userRoots(home) membership instead, so the candidate scope reflects the profile's intent rather than path prefix overlap. The chosen root path itself is unchanged.
9de30ae to
96681b0
Compare
…fallback Resolve historical naming drift between protocol spec and CLI by adopting the plural form across both docs: - docs/07-skill-protocol.md: drop the drift caveat; the four-tier priority is now stated as .agents/skills / ~/.agents/skills / .claude/skills / ~/.claude/skills directly. - docs/00-product-direction.md: align with the same plural form. The CLI already uses .agents/skills (cli/src/agents/profiles/generic-fallback.ts and cli/src/agents/resolver.ts). No code change required.
概述
为
skillhub install增加--scope user|project参数,让用户在安装前明确选择全局(user)还是项目级(project)安装范围。交互模式下未指定参数时会先询问 scope;非交互模式完全保持现有行为。变更内容
CLI 实现
--scope user|project选项注册在install命令上--json)下,未指定--scope/--agent/--dir时先交互询问 scope--dir不能与--scope或--agent同时使用,违反时报 usage error--scope user无候选回退到~/.agents/skills/;--scope project无候选回退到<cwd>/.agents/skills/--scope时按 profile 的userRoots/projectRoots直接生成候选,并通过pathExists过滤实际存在的目录,避免root.startsWith(cwd)在cwd === home等边界场景下误判computeStrictIsTTY同时检查stdin.isTTY、stdout.isTTY和!json,统一传给 prompt 和 resolver 的interactive参数InstallCommandDeps依赖注入接口,便于单测覆盖调用链文档更新
cli/README.md、docs/skillhub/guide/cli.md、docs/skillhub/en/guide/cli.md:新增--scope用法说明、决策表、fallback 行为;安装路径表新增_fallback_行gemini-cli实际路径为.gemini/skills、kiro-cli为.kiro/skills(与源码 profile 对齐)docs/07-skill-protocol.md:在顶部摘要和 §8.4 增加 CLI 当前 fallback 使用.agents/skills(带 s)的说明,避免协议文档与实现自相矛盾测试覆盖
cli/test/unit/agents/resolver.test.ts:新增 9 个 scope 相关用例(互斥、scope 过滤、cwd === home边界、双方向 fallback、detected 过滤)cli/test/unit/commands/install-command.test.ts(新建):18 个用例覆盖computeStrictIsTTY4 种 TTY/json 组合、resolveEffectiveScope校验逻辑、installCommand依赖注入调用链(promptScope → resolveInstallTargets 的 scope/interactive 传递)cli/test/integration/install-command.test.ts:新增 9 个黑盒用例(user/project agent 实际路径、user clean env fallback、--json成功输出不含 scope 字段、--scope invalid/--dir + --scope的 stderr JSON shape、help install输出含--scope)质量门禁
bunx tsc --noEmit通过bun test217 pass / 0 fail(31 个文件)bun run lint通过docs/skillhubVitePressnpm run build通过兼容性
完全向后兼容。
scope === undefined(未传--scope)时所有现有行为保持不变:skillhub install <slug>非交互模式继续走原detectAll路径(home ?? '')skillhub install <slug> --agent codex继续按userRoots数组非空选择 user rootskillhub install <slug> --dir <path>行为不变唯一的 UX 变更:交互模式下裸
install会从"探测到单个候选直接安装"改为"先询问 scope,再探测"。测试说明
本地验证
手动验证范围(TTY prompt 路径,自动化未覆盖)
skillhub install pdf-parser(在真实终端)→ 应弹出 user/project 选择installation cancelled