Skip to content

Commit 9a2c908

Browse files
author
SqlRush
committed
Scope plugin install updates in CLI
1 parent ab80226 commit 9a2c908

11 files changed

Lines changed: 191 additions & 40 deletions

File tree

cmd/claude/main.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,8 @@ func runPluginListCLI(state *bootstrap.State, args []string, stdout io.Writer, s
376376
return 1
377377
}
378378
settings := runnerMergedSettings(runner)
379-
installedPlugins := pluginpkg.LoadPluginDirs(pluginpkg.ProjectPluginDirs(runner.WorkingDirectory))
380-
installed := pluginCLIInstalledEntries(installedPlugins, settings)
379+
installedPlugins := pluginpkg.LoadPluginDirs(pluginpkg.InstalledPluginDirs(runner.WorkingDirectory))
380+
installed := pluginCLIInstalledEntries(installedPlugins, settings, runner.WorkingDirectory)
381381
if *jsonOutput {
382382
encoder := json.NewEncoder(stdout)
383383
encoder.SetIndent("", " ")
@@ -426,23 +426,23 @@ func runPluginInstallCLI(state *bootstrap.State, args []string, stdout io.Writer
426426
return 2
427427
}
428428
if flags.NArg() != 1 {
429-
fmt.Fprintln(stderr, "ccgo plugin install: usage: claude plugin install [--scope project] <plugin>")
429+
fmt.Fprintln(stderr, "ccgo plugin install: usage: claude plugin install [--scope project|user] <plugin>")
430430
return 2
431431
}
432432
scope = strings.ToLower(strings.TrimSpace(scope))
433433
if scope == "" {
434434
scope = "project"
435435
}
436-
if scope != "project" {
437-
fmt.Fprintf(stderr, "ccgo plugin install: scope %q is not supported yet; use project\n", scope)
436+
if scope != "project" && scope != "user" {
437+
fmt.Fprintf(stderr, "ccgo plugin install: scope %q is not supported yet; use project or user\n", scope)
438438
return 2
439439
}
440440
settings, err := pluginCLISettingsFromFiles(state.CWD())
441441
if err != nil {
442442
fmt.Fprintf(stderr, "ccgo plugin install: %v\n", err)
443443
return 1
444444
}
445-
result, err := pluginpkg.InstallMarketplacePlugin(strings.TrimSpace(flags.Arg(0)), state.CWD(), settings)
445+
result, err := pluginpkg.InstallMarketplacePluginInScope(strings.TrimSpace(flags.Arg(0)), state.CWD(), scope, settings)
446446
if err != nil {
447447
fmt.Fprintf(stderr, "ccgo plugin install: %v\n", err)
448448
return 1
@@ -464,23 +464,23 @@ func runPluginUpdateCLI(state *bootstrap.State, args []string, stdout io.Writer,
464464
return 2
465465
}
466466
if flags.NArg() != 1 {
467-
fmt.Fprintln(stderr, "ccgo plugin update: usage: claude plugin update [--scope project] <plugin>")
467+
fmt.Fprintln(stderr, "ccgo plugin update: usage: claude plugin update [--scope project|user|all] <plugin>")
468468
return 2
469469
}
470470
scope = strings.ToLower(strings.TrimSpace(scope))
471471
if scope == "" {
472472
scope = "project"
473473
}
474-
if scope != "project" {
475-
fmt.Fprintf(stderr, "ccgo plugin update: scope %q is not supported yet; use project\n", scope)
474+
if scope != "project" && scope != "user" && scope != "all" {
475+
fmt.Fprintf(stderr, "ccgo plugin update: scope %q is not supported yet; use project, user, or all\n", scope)
476476
return 2
477477
}
478478
settings, err := pluginCLISettingsFromFiles(state.CWD())
479479
if err != nil {
480480
fmt.Fprintf(stderr, "ccgo plugin update: %v\n", err)
481481
return 1
482482
}
483-
result, err := pluginpkg.UpdateInstalledMarketplacePlugins(strings.TrimSpace(flags.Arg(0)), state.CWD(), settings)
483+
result, err := pluginpkg.UpdateInstalledMarketplacePluginsInScope(strings.TrimSpace(flags.Arg(0)), state.CWD(), scope, settings)
484484
if err != nil {
485485
fmt.Fprintf(stderr, "ccgo plugin update: %v\n", err)
486486
return 1
@@ -546,7 +546,7 @@ func runPluginDisableAllCLI(state *bootstrap.State, settingsPath string, stdout
546546
fmt.Fprintf(stderr, "ccgo plugin disable: %v\n", err)
547547
return 1
548548
}
549-
plugins := pluginpkg.LoadPluginDirs(pluginpkg.ProjectPluginDirs(state.CWD()))
549+
plugins := pluginpkg.LoadPluginDirs(pluginpkg.InstalledPluginDirs(state.CWD()))
550550
states := map[string]bool{}
551551
for _, plugin := range plugins {
552552
if pluginpkg.PluginEnabled(plugin, settings.EnabledPlugins) && strings.TrimSpace(plugin.Name) != "" {
@@ -844,13 +844,13 @@ func pluginCLILoadOptionalSettings(path string) (contracts.Settings, error) {
844844
return contracts.Settings{}, fmt.Errorf("load settings %s: %w", path, err)
845845
}
846846

847-
func pluginCLIInstalledEntries(plugins []pluginpkg.LoadedPlugin, settings contracts.Settings) []pluginCLIListEntry {
847+
func pluginCLIInstalledEntries(plugins []pluginpkg.LoadedPlugin, settings contracts.Settings, cwd string) []pluginCLIListEntry {
848848
out := make([]pluginCLIListEntry, 0, len(plugins))
849849
for _, plugin := range plugins {
850850
entry := pluginCLIListEntry{
851851
ID: pluginCLIID(plugin),
852852
Version: pluginCLIVersion(plugin.Version),
853-
Scope: "project",
853+
Scope: pluginpkg.InstalledPluginScope(cwd, plugin.Root),
854854
Enabled: pluginpkg.PluginEnabled(plugin, settings.EnabledPlugins),
855855
InstallPath: plugin.Root,
856856
}
@@ -3397,7 +3397,7 @@ func runnerPluginSummaries(runner conversation.Runner) []printStreamPlugin {
33973397
}
33983398

33993399
func runnerLocalPlugins(runner conversation.Runner) []pluginpkg.LoadedPlugin {
3400-
return pluginpkg.LoadPluginDirsWithSettings(pluginpkg.ProjectPluginDirs(runner.WorkingDirectory), runnerMergedSettings(runner))
3400+
return pluginpkg.LoadPluginDirsWithSettings(pluginpkg.InstalledPluginDirs(runner.WorkingDirectory), runnerMergedSettings(runner))
34013401
}
34023402

34033403
func runnerMergedSettings(runner conversation.Runner) contracts.Settings {

cmd/claude/main_test.go

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,9 +2214,22 @@ func TestRunPluginInstallCLI(t *testing.T) {
22142214
stdout.Reset()
22152215
stderr.Reset()
22162216
code = run([]string{"--cwd", project, "plugin", "install", "--scope", "user", "market demo"}, strings.NewReader(""), &stdout, &stderr)
2217-
if code != 2 || !strings.Contains(stderr.String(), `scope "user" is not supported yet`) {
2217+
if code != 0 {
22182218
t.Fatalf("user scope exit=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
22192219
}
2220+
userInstalledDir := filepath.Join(configHome, "plugins", "market-demo")
2221+
for _, want := range []string{
2222+
"Plugin installed",
2223+
"Installed path: " + userInstalledDir,
2224+
"Status: installed",
2225+
} {
2226+
if !strings.Contains(stdout.String(), want) {
2227+
t.Fatalf("user install stdout missing %q: %q", want, stdout.String())
2228+
}
2229+
}
2230+
if data, err := os.ReadFile(filepath.Join(userInstalledDir, "assets", "README.md")); err != nil || string(data) != "asset" {
2231+
t.Fatalf("user installed asset data=%q err=%v", data, err)
2232+
}
22202233
}
22212234

22222235
func TestRunPluginUpdateCLI(t *testing.T) {
@@ -2286,11 +2299,40 @@ func TestRunPluginUpdateCLI(t *testing.T) {
22862299
t.Fatalf("updated asset data=%q err=%v", data, err)
22872300
}
22882301

2302+
stdout.Reset()
2303+
stderr.Reset()
2304+
code = run([]string{"--cwd", project, "plugin", "install", "--scope", "user", "market demo"}, strings.NewReader(""), &stdout, &stderr)
2305+
if code != 0 {
2306+
t.Fatalf("user install exit = %d stderr=%s", code, stderr.String())
2307+
}
2308+
if err := os.WriteFile(filepath.Join(marketDir, "plugin.json"), []byte(`{"name":"market demo","version":"3.0.0","description":"Deploy marketplace plugin"}`), 0o644); err != nil {
2309+
t.Fatal(err)
2310+
}
2311+
if err := os.WriteFile(filepath.Join(marketDir, "assets", "README.md"), []byte("v3"), 0o644); err != nil {
2312+
t.Fatal(err)
2313+
}
2314+
userInstalledDir := filepath.Join(configHome, "plugins", "market-demo")
2315+
22892316
stdout.Reset()
22902317
stderr.Reset()
22912318
code = run([]string{"--cwd", project, "plugin", "update", "--scope", "user", "market demo"}, strings.NewReader(""), &stdout, &stderr)
2292-
if code != 2 || !strings.Contains(stderr.String(), `scope "user" is not supported yet`) {
2293-
t.Fatalf("user scope exit=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
2319+
if code != 0 {
2320+
t.Fatalf("user update exit=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
2321+
}
2322+
for _, want := range []string{
2323+
"Plugin update",
2324+
"Updated plugins: 1",
2325+
"- market demo -> " + userInstalledDir,
2326+
} {
2327+
if !strings.Contains(stdout.String(), want) {
2328+
t.Fatalf("user update stdout missing %q: %q", want, stdout.String())
2329+
}
2330+
}
2331+
if data, err := os.ReadFile(filepath.Join(userInstalledDir, "plugin.json")); err != nil || !strings.Contains(string(data), `"version":"3.0.0"`) {
2332+
t.Fatalf("user updated plugin json=%q err=%v", data, err)
2333+
}
2334+
if data, err := os.ReadFile(filepath.Join(userInstalledDir, "assets", "README.md")); err != nil || string(data) != "v3" {
2335+
t.Fatalf("user updated asset data=%q err=%v", data, err)
22942336
}
22952337

22962338
stdout.Reset()

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ M4 补充:tool executor 会围绕 `PreToolUse`、`PostToolUse`、`PermissionDe
3737

3838
M10 补充:plugin command/agent 的 allowed tool frontmatter 解析现在只在顶层逗号或空白处分隔,保留括号、方括号和引号内的逗号/空白,避免 `Bash(git commit -m "x,y")` 这类 tool pattern 被误拆。
3939

40-
M8 补充:CLI `plugin marketplace list` 现在可列出 settings 中已配置的 marketplace;`--json` 输出按名称排序的 source/repo/url/path/package/installLocation 结构,普通文本输出与无配置提示也已覆盖。CLI `plugin marketplace add [--scope user|project|local] [--type ...] <name> <source>` 和 `plugin marketplace remove [--scope user|project|local] <name>` 现在复用 `internal/config` settings 文件写入 helper,可按目标 scope 写入/删除 `extraKnownMarketplaces`,并在写入前复用 marketplace source validation。CLI `plugin marketplace update [name]` 现在可按全部或指定 marketplace 触发现有 URL/git/github/npm/settings cache 加载刷新路径,命名 update 会用轻量 settings 读取避免启动时提前刷新全部 marketplace。CLI `plugin install --scope project <plugin>` 现在复用 `internal/plugins` 共享安装 API,把 marketplace 插件复制到项目 `.claude/plugins/<safe-name>`,并保留冲突检测、重复安装识别和 symlink/non-regular file 拒绝。CLI `plugin update --scope project <plugin>` 现在复用 `internal/plugins` 共享更新 API,把已安装同名项目插件替换为最新 marketplace 副本。CLI `plugin enable|disable [--scope user|project|local] <plugin>` 与 `plugin disable --all --scope ...` 现在复用 `internal/config` settings 文件写入 helper,按目标 scope 更新 `enabledPlugins` 并覆盖基本参数冲突。
40+
M8 补充:CLI `plugin marketplace list` 现在可列出 settings 中已配置的 marketplace;`--json` 输出按名称排序的 source/repo/url/path/package/installLocation 结构,普通文本输出与无配置提示也已覆盖。CLI `plugin marketplace add [--scope user|project|local] [--type ...] <name> <source>` 和 `plugin marketplace remove [--scope user|project|local] <name>` 现在复用 `internal/config` settings 文件写入 helper,可按目标 scope 写入/删除 `extraKnownMarketplaces`,并在写入前复用 marketplace source validation。CLI `plugin marketplace update [name]` 现在可按全部或指定 marketplace 触发现有 URL/git/github/npm/settings cache 加载刷新路径,命名 update 会用轻量 settings 读取避免启动时提前刷新全部 marketplace。运行时插件发现现在会合并项目链 `.claude/plugins` 和用户级 `${CLAUDE_CONFIG_DIR}/plugins`,项目同名插件优先。CLI `plugin install --scope project|user <plugin>` 现在复用 `internal/plugins` 共享安装 API,把 marketplace 插件复制到目标 scope 的 `plugins/<safe-name>`,并保留冲突检测、重复安装识别和 symlink/non-regular file 拒绝。CLI `plugin update --scope project|user|all <plugin>` 现在复用 `internal/plugins` 共享更新 API,把目标 scope 已安装同名插件替换为最新 marketplace 副本。CLI `plugin enable|disable [--scope user|project|local] <plugin>` 与 `plugin disable --all --scope ...` 现在复用 `internal/config` settings 文件写入 helper,按目标 scope 更新 `enabledPlugins` 并覆盖基本参数冲突。
4141

4242
M10 补充:新增 `TaskOutput`/`AgentOutputTool``KillTask`/`TaskStop` 内置工具;`TaskOutput` 可列出当前 session 的 sidechain task,或按 task/sidechain ID 读取状态、summary、tail 输出和 agent metadata,`KillTask` 会通过 sidechain manager 写入 cancelled lifecycle summary。完整 AgentTool 执行循环、progress event streaming、resume command UI 和 worktree isolation 仍未完成。
4343

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ test/parity/ # golden tests against TS/official behavior
143143
- 本轮补充:`strictPluginOnlyCustomization` 已开始按 policy settings 生效,当前覆盖已实现的 `skills``mcp``hooks` 和 agent-metadata surfaces:锁定后 user/project/local skills 与 legacy commands 不加载,manual MCP server sources 不加载但 plugin/admin policy 仍保留,settings hooks 只保留 policy hooks 且 plugin hooks 继续运行;`agents` 锁定时非 plugin/bundled/admin prompt command 的 `agent``context: fork``effort` frontmatter metadata 会被清除。完整 forked agent runtime/UI edge cases 仍按缺口追踪。
144144
- 本轮补充:settings 文件读取现在有 path-keyed cache,按 size/mode/mtime 指纹复用内容并提供 `ResetSettingsCache`;新增 settings change detector,可对 settings 文件快照区分 created/modified/deleted,并在检测到变化时清空 settings 文件缓存;Runner turn-start 会在检测到 user/project/local settings 文件变化时重载 merged settings 并同步刷新 plugin MCP server app-state。完整后台 watcher 和 remote managed 非 daemon 后台 refresh 生命周期仍按缺口追踪。
145145
- 本轮补充:新增 settings JSON Schema generation,`SettingsJSONSchema()` 会从 `contracts.Settings` 反射生成 draft 2020-12 schema,补强 `$schema` const、permission mode enum、`strictPluginOnlyCustomization` union 和 login method enum;`/config show schema` 现在可展示 schema ID、draft、字段数和生成大小,完整 Zod message parity 仍按缺口追踪。
146-
- 本轮补充:settings validation 现在覆盖官方 marketplace source union 的基础语义,适用于 `extraKnownMarketplaces[].source`、`strictKnownMarketplaces[]` 和 `blockedMarketplaces[]`:校验 source discriminator、各 source 类型必填字段、URL source 绝对 URL、headers string record、github/git sparsePaths string array、settings-source plugins array/item path shape、settings-source name 安全/保留名,并继续覆盖 inline marketplace key/name 一致性;`internal/plugins` 新增 marketplace policy resolver,blocked settings 优先、strict allowlist 非空时拒绝未列入来源,`/plugin marketplaces` 会展示 allow/block decision;本地 plugin manifest 可声明 `marketplace`/`marketplaceName`/`marketplace_name`/`source.name`,带 settings 的 plugin loader 会对这些来源执行 marketplace policy,`extraKnownMarketplaces.<name>.source.source=settings` 的 `plugins` 本地 root、`source=directory` 的本地 marketplace 目录、`source=file` catalog、`source=url` 远端 JSON catalog、`source=git` clone cache、`source=github` repo clone cache 以及 `source=npm` package cache 的 plugin roots 都会进入 plugin load path 并继承 marketplace 名称;URL catalog 支持配置 headers、大小限制、JSON 校验、写入 `marketplace-cache/catalogs` 并在后续请求失败时缓存回退;git/github source 会按 URL clone 到 `marketplace-cache/git`,后续加载 fetch/pull,支持 `ref` checkout 和 repo 内 `path` 指向单插件、插件目录或 catalog JSON,github `repo: owner/name` 会规范化为 GitHub clone URL;npm source 会用 `npm pack` 解包到 `marketplace-cache/npm`,失败时可从已有缓存回退,且支持 package 内 `path` 指向单插件、插件目录或 catalog JSON;`/plugin show/search` 会标注 blocked reason;headless `/plugin available [query]`、`/plugin marketplace plugins|search|show` 会浏览 configured marketplace 插件并标注 available/installed/update 状态;CLI `plugin list --json --available` 会输出 installed/available marketplace JSON,CLI `plugin marketplace list --json` 会列出已配置 marketplace 的 source/repo/url/path/package/installLocation 投影,CLI `plugin marketplace add/remove --scope user|project|local` 会按目标 scope 写入或删除 `extraKnownMarketplaces` 并复用 source validation,CLI `plugin marketplace update [name]` 会按全部或指定 marketplace 触发现有 cache 刷新路径,CLI `plugin install --scope project <plugin>` 与 headless `/plugin install <name>` 共享安装 API 并复制插件到项目 `.claude/plugins/<safe-name>`,不覆盖冲突路径并拒绝 symlink/non-regular 文件;CLI `plugin update --scope project <plugin>` 与 headless `/plugin update [name]` 共享更新/目录替换 API,把已安装同名项目插件替换为最新 marketplace 副本;CLI `plugin enable|disable [--scope user|project|local] <plugin>` 和 `plugin disable --all --scope ...` 共享 settings 文件写入 helper,按目标 scope 更新 `enabledPlugins`;headless install/update 后刷新 plugin MCP server app-state。完整 marketplace TUI/UI、后台自动更新和策略 lifecycle 仍按缺口追踪。
146+
- 本轮补充:settings validation 现在覆盖官方 marketplace source union 的基础语义,适用于 `extraKnownMarketplaces[].source`、`strictKnownMarketplaces[]` 和 `blockedMarketplaces[]`:校验 source discriminator、各 source 类型必填字段、URL source 绝对 URL、headers string record、github/git sparsePaths string array、settings-source plugins array/item path shape、settings-source name 安全/保留名,并继续覆盖 inline marketplace key/name 一致性;`internal/plugins` 新增 marketplace policy resolver,blocked settings 优先、strict allowlist 非空时拒绝未列入来源,`/plugin marketplaces` 会展示 allow/block decision;本地 plugin manifest 可声明 `marketplace`/`marketplaceName`/`marketplace_name`/`source.name`,带 settings 的 plugin loader 会对这些来源执行 marketplace policy,`extraKnownMarketplaces.<name>.source.source=settings` 的 `plugins` 本地 root、`source=directory` 的本地 marketplace 目录、`source=file` catalog、`source=url` 远端 JSON catalog、`source=git` clone cache、`source=github` repo clone cache 以及 `source=npm` package cache 的 plugin roots 都会进入 plugin load path 并继承 marketplace 名称;URL catalog 支持配置 headers、大小限制、JSON 校验、写入 `marketplace-cache/catalogs` 并在后续请求失败时缓存回退;git/github source 会按 URL clone 到 `marketplace-cache/git`,后续加载 fetch/pull,支持 `ref` checkout 和 repo 内 `path` 指向单插件、插件目录或 catalog JSON,github `repo: owner/name` 会规范化为 GitHub clone URL;npm source 会用 `npm pack` 解包到 `marketplace-cache/npm`,失败时可从已有缓存回退,且支持 package 内 `path` 指向单插件、插件目录或 catalog JSON;`/plugin show/search` 会标注 blocked reason;运行时已安装插件发现会合并项目链 `.claude/plugins` 和用户级 `${CLAUDE_CONFIG_DIR}/plugins`,项目同名插件优先;headless `/plugin available [query]`、`/plugin marketplace plugins|search|show` 会浏览 configured marketplace 插件并标注 available/installed/update 状态;CLI `plugin list --json --available` 会输出 installed/available marketplace JSON,CLI `plugin marketplace list --json` 会列出已配置 marketplace 的 source/repo/url/path/package/installLocation 投影,CLI `plugin marketplace add/remove --scope user|project|local` 会按目标 scope 写入或删除 `extraKnownMarketplaces` 并复用 source validation,CLI `plugin marketplace update [name]` 会按全部或指定 marketplace 触发现有 cache 刷新路径,CLI `plugin install --scope project|user <plugin>` 与 headless `/plugin install <name>` 共享安装 API 并复制插件到目标 scope 的插件目录,不覆盖冲突路径并拒绝 symlink/non-regular 文件;CLI `plugin update --scope project|user|all <plugin>` 与 headless `/plugin update [name]` 共享更新/目录替换 API,把目标 scope 已安装同名插件替换为最新 marketplace 副本;CLI `plugin enable|disable [--scope user|project|local] <plugin>` 和 `plugin disable --all --scope ...` 共享 settings 文件写入 helper,按目标 scope 更新 `enabledPlugins`;headless install/update 后刷新 plugin MCP server app-state。完整 marketplace TUI/UI、后台自动更新和策略 lifecycle 仍按缺口追踪。
147147

148148
### M4: Tool framework、permissions、sandbox
149149

0 commit comments

Comments
 (0)