Skip to content

Commit edecdac

Browse files
author
SqlRush
committed
Update marketplace plugins from CLI
1 parent 1b0c168 commit edecdac

7 files changed

Lines changed: 255 additions & 255 deletions

File tree

cmd/claude/main.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ func runPluginCLI(ctx context.Context, state *bootstrap.State, args []string, st
337337
return runPluginListCLI(state, args[1:], stdout, stderr)
338338
case "install", "i":
339339
return runPluginInstallCLI(state, args[1:], stdout, stderr)
340+
case "update":
341+
return runPluginUpdateCLI(state, args[1:], stdout, stderr)
340342
case "enable":
341343
return runPluginSetEnabledCLI(state, "enable", args[1:], stdout, stderr)
342344
case "disable":
@@ -449,6 +451,44 @@ func runPluginInstallCLI(state *bootstrap.State, args []string, stdout io.Writer
449451
return 0
450452
}
451453

454+
func runPluginUpdateCLI(state *bootstrap.State, args []string, stdout io.Writer, stderr io.Writer) int {
455+
flags := flag.NewFlagSet("claude plugin update", flag.ContinueOnError)
456+
flags.SetOutput(stderr)
457+
scope := "project"
458+
flags.StringVar(&scope, "scope", scope, "installation scope")
459+
flags.StringVar(&scope, "s", scope, "installation scope")
460+
if err := flags.Parse(args); err != nil {
461+
if err == flag.ErrHelp {
462+
return 0
463+
}
464+
return 2
465+
}
466+
if flags.NArg() != 1 {
467+
fmt.Fprintln(stderr, "ccgo plugin update: usage: claude plugin update [--scope project] <plugin>")
468+
return 2
469+
}
470+
scope = strings.ToLower(strings.TrimSpace(scope))
471+
if scope == "" {
472+
scope = "project"
473+
}
474+
if scope != "project" {
475+
fmt.Fprintf(stderr, "ccgo plugin update: scope %q is not supported yet; use project\n", scope)
476+
return 2
477+
}
478+
settings, err := pluginCLISettingsFromFiles(state.CWD())
479+
if err != nil {
480+
fmt.Fprintf(stderr, "ccgo plugin update: %v\n", err)
481+
return 1
482+
}
483+
result, err := pluginpkg.UpdateInstalledMarketplacePlugins(strings.TrimSpace(flags.Arg(0)), state.CWD(), settings)
484+
if err != nil {
485+
fmt.Fprintf(stderr, "ccgo plugin update: %v\n", err)
486+
return 1
487+
}
488+
writePluginUpdateResult(stdout, result)
489+
return 0
490+
}
491+
452492
func runPluginSetEnabledCLI(state *bootstrap.State, action string, args []string, stdout io.Writer, stderr io.Writer) int {
453493
flags := flag.NewFlagSet("claude plugin "+action, flag.ContinueOnError)
454494
flags.SetOutput(stderr)
@@ -652,6 +692,21 @@ func writePluginInstallResult(stdout io.Writer, result pluginpkg.PluginInstallRe
652692
fmt.Fprintln(stdout, strings.Join(lines, "\n"))
653693
}
654694

695+
func writePluginUpdateResult(stdout io.Writer, result pluginpkg.PluginUpdateResult) {
696+
lines := []string{
697+
"Plugin update",
698+
fmt.Sprintf("Marketplace plugins: %d", result.MarketplacePluginCount),
699+
fmt.Sprintf("Updated plugins: %d", len(result.Updated)),
700+
}
701+
if len(result.Updated) > 0 {
702+
lines = append(lines, "Updated:")
703+
for _, item := range result.Updated {
704+
lines = append(lines, "- "+item.Plugin.Name+" -> "+item.TargetPath)
705+
}
706+
}
707+
fmt.Fprintln(stdout, strings.Join(lines, "\n"))
708+
}
709+
655710
func pluginCLISettingsFromFiles(cwd string) (contracts.Settings, error) {
656711
userSettings, err := pluginCLILoadOptionalSettings(config.UserSettingsPath())
657712
if err != nil {

cmd/claude/main_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,6 +2219,88 @@ func TestRunPluginInstallCLI(t *testing.T) {
22192219
}
22202220
}
22212221

2222+
func TestRunPluginUpdateCLI(t *testing.T) {
2223+
configHome := t.TempDir()
2224+
t.Setenv("CLAUDE_CONFIG_DIR", configHome)
2225+
project := t.TempDir()
2226+
if err := os.MkdirAll(filepath.Join(project, ".git"), 0o755); err != nil {
2227+
t.Fatal(err)
2228+
}
2229+
marketDir := filepath.Join(t.TempDir(), "market-demo")
2230+
if err := os.MkdirAll(filepath.Join(marketDir, "assets"), 0o755); err != nil {
2231+
t.Fatal(err)
2232+
}
2233+
if err := os.WriteFile(filepath.Join(marketDir, "plugin.json"), []byte(`{"name":"market demo","version":"1.0.0","description":"Deploy marketplace plugin"}`), 0o644); err != nil {
2234+
t.Fatal(err)
2235+
}
2236+
if err := os.WriteFile(filepath.Join(marketDir, "assets", "README.md"), []byte("v1"), 0o644); err != nil {
2237+
t.Fatal(err)
2238+
}
2239+
settings := fmt.Sprintf(`{
2240+
"extraKnownMarketplaces": {
2241+
"team": {"source": {"source": "settings", "name": "team", "plugins": [%q]}}
2242+
},
2243+
"strictKnownMarketplaces": ["team"]
2244+
}`, marketDir)
2245+
if err := os.WriteFile(filepath.Join(configHome, "settings.json"), []byte(settings), 0o644); err != nil {
2246+
t.Fatal(err)
2247+
}
2248+
2249+
var stdout, stderr bytes.Buffer
2250+
code := run([]string{"--cwd", project, "plugin", "install", "--scope", "project", "market demo"}, strings.NewReader(""), &stdout, &stderr)
2251+
if code != 0 {
2252+
t.Fatalf("install exit = %d stderr=%s", code, stderr.String())
2253+
}
2254+
if err := os.WriteFile(filepath.Join(marketDir, "plugin.json"), []byte(`{"name":"market demo","version":"2.0.0","description":"Deploy marketplace plugin"}`), 0o644); err != nil {
2255+
t.Fatal(err)
2256+
}
2257+
if err := os.WriteFile(filepath.Join(marketDir, "assets", "README.md"), []byte("v2"), 0o644); err != nil {
2258+
t.Fatal(err)
2259+
}
2260+
resolvedProject, err := filepath.EvalSymlinks(project)
2261+
if err != nil {
2262+
t.Fatal(err)
2263+
}
2264+
installedDir := filepath.Join(resolvedProject, ".claude", "plugins", "market-demo")
2265+
2266+
stdout.Reset()
2267+
stderr.Reset()
2268+
code = run([]string{"--cwd", project, "plugin", "update", "--scope", "project", "market demo"}, strings.NewReader(""), &stdout, &stderr)
2269+
if code != 0 {
2270+
t.Fatalf("update exit = %d stderr=%s", code, stderr.String())
2271+
}
2272+
for _, want := range []string{
2273+
"Plugin update",
2274+
"Marketplace plugins: 1",
2275+
"Updated plugins: 1",
2276+
"- market demo -> " + installedDir,
2277+
} {
2278+
if !strings.Contains(stdout.String(), want) {
2279+
t.Fatalf("stdout missing %q: %q", want, stdout.String())
2280+
}
2281+
}
2282+
if data, err := os.ReadFile(filepath.Join(installedDir, "plugin.json")); err != nil || !strings.Contains(string(data), `"version":"2.0.0"`) {
2283+
t.Fatalf("updated plugin json=%q err=%v", data, err)
2284+
}
2285+
if data, err := os.ReadFile(filepath.Join(installedDir, "assets", "README.md")); err != nil || string(data) != "v2" {
2286+
t.Fatalf("updated asset data=%q err=%v", data, err)
2287+
}
2288+
2289+
stdout.Reset()
2290+
stderr.Reset()
2291+
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())
2294+
}
2295+
2296+
stdout.Reset()
2297+
stderr.Reset()
2298+
code = run([]string{"--cwd", project, "plugin", "update", "missing"}, strings.NewReader(""), &stdout, &stderr)
2299+
if code != 1 || !strings.Contains(stderr.String(), "installed plugin missing was not found") {
2300+
t.Fatalf("missing exit=%d stdout=%q stderr=%q", code, stdout.String(), stderr.String())
2301+
}
2302+
}
2303+
22222304
func TestRunPluginEnableDisableCLI(t *testing.T) {
22232305
configHome := t.TempDir()
22242306
t.Setenv("CLAUDE_CONFIG_DIR", configHome)

docs/cc-100-roadmap.md

Lines changed: 2 additions & 2 deletions
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 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 enable|disable [--scope user] <plugin>``plugin disable --all` 现在复用 `internal/config` 用户 settings 写入 helper,更新 `enabledPlugins` 并覆盖基本参数冲突。
40+
M8 补充:CLI `plugin marketplace list` 现在可列出 settings 中已配置的 marketplace;`--json` 输出按名称排序的 source/repo/url/path/package/installLocation 结构,普通文本输出与无配置提示也已覆盖。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] <plugin>` 与 `plugin disable --all` 现在复用 `internal/config` 用户 settings 写入 helper,更新 `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

@@ -1442,7 +1442,7 @@ M7 补充:terminal input parser 和 configurable keybinding name parser 现在
14421442
- plugin manifest、marketplace、local install、URL catalog cache、git/github clone/update cache、npm pack/cache、update UI/background lifecycle。
14431443
- plugin hooks/agents/MCP,其中本地 plugin 同步工具 hook 已接入,剩余完整 plugin agent/MCP 与 hook UI/policy parity。
14441444

1445-
当前状态:已完成项目 skill discovery、目录式 `SKILL.md` prompt metadata loading、project legacy `.claude/commands` prompt command loading、command registry metadata/lookup/filter、agent-metadata strict plugin-only policy filtering、部分内置 slash command aliases/metadata、prompt expansion、基础 `Skill` tool inline 调用、本地项目 prompt skill 的基础 slash 调用接入、本地 prompt skill 的 command permissions attachment/current-turn 权限继承,本地 plugin command/skill/agent/MCP server/output style/hook 的 manifest discovery,本地 plugin 同步工具 hook 执行,headless `/plugin available [query]` 与 `/plugin marketplace plugins|search|show` 可浏览已配置 marketplace 插件并标注 available/installed/update 状态,CLI `plugin list --json --available` 可输出 installed/available marketplace JSON,headless `/plugin install <name>` 可从 settings/directory/file/URL catalog/git/github/npm cache 配置的 marketplace 来源复制插件到项目 `.claude/plugins` 并刷新 plugin MCP server app-state,headless `/plugin update [name]` 可刷新 marketplace cache 并替换已安装同名插件,headless `/help`/`/skills` 列表与单项详情,output style 系统提示注入,以及 `/clear` 基础 local command no-query 路径;仍缺 bundled/MCP/remote skills、forked skill/agent 执行、完整 local/local-jsx 实际执行、TUI `/help`/`/skills` 面板、权限 UI/SDK 展示、plugin marketplace TUI/background lifecycle、skill prompt shell injection 和完整 agents/MCP/output-style UI 接线。
1445+
当前状态:已完成项目 skill discovery、目录式 `SKILL.md` prompt metadata loading、project legacy `.claude/commands` prompt command loading、command registry metadata/lookup/filter、agent-metadata strict plugin-only policy filtering、部分内置 slash command aliases/metadata、prompt expansion、基础 `Skill` tool inline 调用、本地项目 prompt skill 的基础 slash 调用接入、本地 prompt skill 的 command permissions attachment/current-turn 权限继承,本地 plugin command/skill/agent/MCP server/output style/hook 的 manifest discovery,本地 plugin 同步工具 hook 执行,headless `/plugin available [query]` 与 `/plugin marketplace plugins|search|show` 可浏览已配置 marketplace 插件并标注 available/installed/update 状态,CLI `plugin list --json --available` 可输出 installed/available marketplace JSON,headless `/plugin install <name>` 可从 settings/directory/file/URL catalog/git/github/npm cache 配置的 marketplace 来源复制插件到项目 `.claude/plugins` 并刷新 plugin MCP server app-state,headless `/plugin update [name]` 和 CLI `plugin update --scope project <plugin>` 可复用共享更新 API 并替换已安装同名插件,headless `/help`/`/skills` 列表与单项详情,output style 系统提示注入,以及 `/clear` 基础 local command no-query 路径;仍缺 bundled/MCP/remote skills、forked skill/agent 执行、完整 local/local-jsx 实际执行、TUI `/help`/`/skills` 面板、权限 UI/SDK 展示、plugin marketplace TUI/background lifecycle、skill prompt shell injection 和完整 agents/MCP/output-style UI 接线。
14461446

14471447
### M9: MCP Platform
14481448

0 commit comments

Comments
 (0)