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
概述
为
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