Skip to content

feat(cli): add --scope option to install command#440

Open
dongmucat wants to merge 2 commits into
mainfrom
feat/cli-install-scope
Open

feat(cli): add --scope option to install command#440
dongmucat wants to merge 2 commits into
mainfrom
feat/cli-install-scope

Conversation

@dongmucat
Copy link
Copy Markdown
Collaborator

概述

skillhub install 增加 --scope user|project 参数,让用户在安装前明确选择全局(user)还是项目级(project)安装范围。交互模式下未指定参数时会先询问 scope;非交互模式完全保持现有行为。

变更内容

CLI 实现

  • --scope user|project 选项注册在 install 命令上
  • 交互模式(stdin/stdout 同时为 TTY 且未传 --json)下,未指定 --scope/--agent/--dir 时先交互询问 scope
  • 互斥规则:--dir 不能与 --scope--agent 同时使用,违反时报 usage error
  • 对称 fallback:--scope user 无候选回退到 ~/.agents/skills/--scope project 无候选回退到 <cwd>/.agents/skills/
  • Scope-aware candidate generation:显式 --scope 时按 profile 的 userRoots/projectRoots 直接生成候选,并通过 pathExists 过滤实际存在的目录,避免 root.startsWith(cwd)cwd === home 等边界场景下误判
  • 严格 TTY 判定:computeStrictIsTTY 同时检查 stdin.isTTYstdout.isTTY!json,统一传给 prompt 和 resolver 的 interactive 参数
  • InstallCommandDeps 依赖注入接口,便于单测覆盖调用链
  • scope 校验前移到 ConfigStore/CredentialsStore 之前,确保 usage error 无需 fake registry 也能触发

文档更新

  • cli/README.mddocs/skillhub/guide/cli.mddocs/skillhub/en/guide/cli.md:新增 --scope 用法说明、决策表、fallback 行为;安装路径表新增 _fallback_
  • 修正安装路径表中 gemini-cli 实际路径为 .gemini/skillskiro-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 个用例覆盖 computeStrictIsTTY 4 种 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 test 217 pass / 0 fail(31 个文件)
  • bun run lint 通过
  • docs/skillhub VitePress npm run build 通过

兼容性

完全向后兼容。scope === undefined(未传 --scope)时所有现有行为保持不变:

  • skillhub install <slug> 非交互模式继续走原 detectAll 路径(home ?? ''
  • skillhub install <slug> --agent codex 继续按 userRoots 数组非空选择 user root
  • skillhub install <slug> --dir <path> 行为不变

唯一的 UX 变更:交互模式下裸 install 会从"探测到单个候选直接安装"改为"先询问 scope,再探测"。

测试说明

本地验证

cd cli
bun install
bun test

手动验证范围(TTY prompt 路径,自动化未覆盖)

  • skillhub install pdf-parser(在真实终端)→ 应弹出 user/project 选择
  • 选择后按现有交互流程进行 agent 选择或直接安装
  • Ctrl+C 取消应正常退出并提示 installation cancelled

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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 ?? '')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

resolveInstallTargets 中,当 options.home 未定义时,detectAll 被传入了空字符串 ''。这会导致 detectAll 无法正确探测到用户目录(home directory)下的 Agent 技能目录(例如 ~/.claude/skills),因为 userRoots('') 会生成错误的路径(如 /.claude/skills)。建议统一使用 homedir() 作为默认值,这与同一文件中的其他逻辑(如第 37 行和第 59 行)保持一致。

Suggested change
const detected = options.detected ?? await detectAll(options.cwd, options.home ?? '')
const detected = options.detected ?? await detectAll(options.cwd, options.home ?? homedir())

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.

不采纳此建议。这里 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:62 scopedHome = options.home ?? homedir()),而 scope === undefined 路径完全保持现状。
  • 注:第 37 行的 resolveExplicitAgents 沿用 options.home ?? homedir() 也是 PR 之前的现有行为,不是本 PR 引入。

建议保留当前实现。

Comment thread cli/src/agents/resolver.ts Outdated
Comment on lines +137 to +139
const candidateScope: AgentCandidate['scope'] = scope !== undefined
? scope
: (root.startsWith(cwd) ? 'project' : 'user')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

这里的逻辑仍然依赖 root.startsWith(cwd) 来判定 scope,这在 cwd 等于 home 时会产生误判(将用户级目录误标为 project)。虽然 PR 在 generateScopedCandidates 中通过显式传入 scope 避免了此问题,但在 legacy 路径(scopeundefined 时)依然存在此缺陷。既然已经重构了 resolveExplicitAgents,建议使用更可靠的判定方式:检查 root 是否属于 profile.userRoots(home)

Suggested change
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')

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.

已采纳并修复(commit 9de30ae)。改用 profile.userRoots(home) 的 Set 成员判断来标记 candidate.scope,避免 cwd === homeroot.startsWith(cwd) 把 user roots 误标为 project。同时新增了一条 cwd === home + --agent codex 的回归测试。

dongmucat added 2 commits May 15, 2026 14:43
- 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.
@dongmucat dongmucat force-pushed the feat/cli-install-scope branch from 9de30ae to 96681b0 Compare May 15, 2026 06:45
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.

1 participant