@curdx/flow 2.0.0-beta.1 → 2.0.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +3 -10
  3. package/CHANGELOG.md +61 -0
  4. package/README.zh.md +2 -2
  5. package/agent-preamble/preamble.md +81 -11
  6. package/agents/flow-adversary.md +40 -55
  7. package/agents/flow-architect.md +23 -10
  8. package/agents/flow-debugger.md +2 -2
  9. package/agents/flow-edge-hunter.md +20 -6
  10. package/agents/flow-executor.md +3 -3
  11. package/agents/flow-planner.md +51 -48
  12. package/agents/flow-product-designer.md +14 -1
  13. package/agents/flow-qa-engineer.md +1 -1
  14. package/agents/flow-researcher.md +17 -2
  15. package/agents/flow-reviewer.md +5 -1
  16. package/agents/flow-security-auditor.md +1 -1
  17. package/agents/flow-triage-analyst.md +1 -1
  18. package/agents/flow-ui-researcher.md +2 -2
  19. package/agents/flow-ux-designer.md +1 -1
  20. package/agents/flow-verifier.md +47 -14
  21. package/bin/curdx-flow.js +13 -1
  22. package/cli/doctor.js +73 -13
  23. package/cli/install.js +62 -36
  24. package/cli/protocols.js +63 -10
  25. package/cli/registry.js +73 -0
  26. package/cli/uninstall.js +9 -11
  27. package/cli/upgrade.js +6 -10
  28. package/cli/utils.js +150 -56
  29. package/commands/fast.md +1 -1
  30. package/commands/implement.md +4 -4
  31. package/commands/init.md +14 -3
  32. package/commands/review.md +14 -5
  33. package/commands/spec.md +26 -2
  34. package/commands/start.md +47 -17
  35. package/commands/verify.md +13 -0
  36. package/gates/adversarial-review-gate.md +19 -19
  37. package/gates/devex-gate.md +4 -5
  38. package/gates/edge-case-gate.md +1 -1
  39. package/hooks/hooks.json +0 -11
  40. package/hooks/scripts/quick-mode-guard.sh +12 -9
  41. package/hooks/scripts/session-start.sh +1 -1
  42. package/hooks/scripts/stop-watcher.sh +25 -15
  43. package/knowledge/execution-strategies.md +6 -5
  44. package/knowledge/spec-driven-development.md +8 -7
  45. package/knowledge/two-stage-review.md +4 -3
  46. package/package.json +4 -2
  47. package/skills/brownfield-index/SKILL.md +62 -0
  48. package/skills/browser-qa/SKILL.md +50 -0
  49. package/skills/epic/SKILL.md +68 -0
  50. package/skills/security-audit/SKILL.md +50 -0
  51. package/skills/ui-sketch/SKILL.md +49 -0
  52. package/templates/config.json.tmpl +1 -1
  53. package/templates/design.md.tmpl +32 -112
  54. package/templates/requirements.md.tmpl +25 -43
  55. package/templates/research.md.tmpl +37 -68
  56. package/templates/tasks.md.tmpl +27 -84
  57. package/hooks/scripts/fail-tracker.sh +0 -31
package/cli/install.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * install command — install curdx-flow plugin + optional recommended plugins.
3
3
  */
4
4
 
5
- import { existsSync } from "node:fs";
5
+ import { existsSync, readFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { fileURLToPath } from "node:url";
8
8
 
@@ -17,6 +17,7 @@ import {
17
17
  ensureClaudeMemRuntimes,
18
18
  } from "./utils.js";
19
19
  import { injectGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
20
+ import { RECOMMENDED_PLUGINS } from "./registry.js";
20
21
 
21
22
  // When installed via npm, this CLI file lives at <pkg-root>/cli/install.js.
22
23
  // The npm package bundles the full plugin body (.claude-plugin/, agents/,
@@ -28,27 +29,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
28
29
  const PKG_ROOT = dirname(__dirname);
29
30
  const LOCAL_MARKETPLACE_MANIFEST = join(PKG_ROOT, ".claude-plugin", "marketplace.json");
30
31
 
31
- // Recommended plugins with their marketplace source + install identifier
32
- const RECOMMENDED = [
33
- {
34
- name: "pua",
35
- marketplace: "tanweai/pua",
36
- installSpec: "pua@pua-skills",
37
- hint: "no-give-up + three red lines",
38
- },
39
- {
40
- name: "claude-mem",
41
- marketplace: "thedotmack/claude-mem",
42
- installSpec: "claude-mem@thedotmack",
43
- hint: "automatic cross-session memory",
44
- },
45
- {
46
- name: "frontend-design",
47
- marketplace: null, // already in default marketplace claude-plugins-official
48
- installSpec: "frontend-design@claude-plugins-official",
49
- hint: "Anthropic official UI skill",
50
- },
51
- ];
32
+ // Recommended plugins: single source of truth is cli/registry.js.
33
+ // See registry.js for the rationale — this list used to drift across
34
+ // install/uninstall/upgrade/doctor, producing the chrome-devtools-mcp
35
+ // orphan-plugin bug (installable but uninstallable).
36
+ const RECOMMENDED = RECOMMENDED_PLUGINS;
52
37
 
53
38
  export async function install(args = []) {
54
39
  const all = args.includes("--all");
@@ -64,7 +49,7 @@ export async function install(args = []) {
64
49
  log.title("🚀 CurDX-Flow Installer");
65
50
 
66
51
  // ---------- Step 1: Check claude CLI ----------
67
- log.step(1, 4, "Checking claude CLI...");
52
+ log.step(1, 5, "Checking claude CLI...");
68
53
  const ver = claudeVersion();
69
54
  if (!ver) {
70
55
  log.err("claude CLI not found. Install Claude Code from https://code.claude.com first.");
@@ -78,7 +63,7 @@ export async function install(args = []) {
78
63
  const marketplaceLabel = useOffline
79
64
  ? `local npm package (${PKG_ROOT})`
80
65
  : "GitHub curdx/curdx-flow";
81
- log.step(2, 4, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
66
+ log.step(2, 5, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
82
67
 
83
68
  // Remove any existing marketplace with the same name so we get a clean
84
69
  // rebind to the chosen source. Errors are non-fatal (marketplace may
@@ -105,10 +90,38 @@ export async function install(args = []) {
105
90
 
106
91
  // ---------- Step 3: Install curdx-flow plugin ----------
107
92
  log.blank();
108
- log.step(3, 4, "Installing curdx-flow plugin (3 MCPs will auto-start)...");
93
+ log.step(3, 5, "Installing curdx-flow plugin (2 MCPs will auto-start)...");
94
+ // Read the version the marketplace is shipping so we can decide whether an
95
+ // already-installed plugin needs an update (same name but stale version
96
+ // previously silently skipped the upgrade — caused the beta.1 → beta.7 drift).
97
+ let shippedVersion = null;
98
+ try {
99
+ const mf = JSON.parse(
100
+ readFileSync(LOCAL_MARKETPLACE_MANIFEST, "utf-8")
101
+ );
102
+ shippedVersion = mf?.metadata?.version || null;
103
+ } catch {
104
+ // marketplace not local (online install) or unreadable — fall through
105
+ }
106
+
109
107
  const installed = listPlugins();
110
108
  const already = installed.find((p) => p.name === "curdx-flow");
111
- if (already) {
109
+ if (already && shippedVersion && already.version !== shippedVersion) {
110
+ log.info(
111
+ `curdx-flow installed at v${already.version}, marketplace ships v${shippedVersion} — updating...`
112
+ );
113
+ const r = await run(
114
+ "claude",
115
+ ["plugin", "update", "curdx-flow@curdx-flow-marketplace"],
116
+ { silent: true }
117
+ );
118
+ if (r.code !== 0) {
119
+ log.warn(`Update returned non-zero: ${r.stderr.trim() || r.stdout.trim()}`);
120
+ log.info(`If the version stays on v${already.version}, run: claude plugin uninstall curdx-flow@curdx-flow-marketplace && retry`);
121
+ } else {
122
+ log.ok(`curdx-flow updated to v${shippedVersion}`);
123
+ }
124
+ } else if (already) {
112
125
  log.ok(`curdx-flow already installed (v${already.version}, ${already.status})`);
113
126
  } else {
114
127
  const r = await run(
@@ -125,7 +138,7 @@ export async function install(args = []) {
125
138
 
126
139
  // ---------- Step 4: Recommended plugins ----------
127
140
  log.blank();
128
- log.step(4, 4, "Recommended plugins");
141
+ log.step(4, 5, "Recommended plugins");
129
142
 
130
143
  if (noDeps) {
131
144
  log.info("Skipping recommended plugins (--no-deps)");
@@ -161,7 +174,7 @@ export async function install(args = []) {
161
174
  for (const pluginName of toInstall) {
162
175
  const rec = RECOMMENDED.find((r) => r.name === pluginName);
163
176
  log.blank();
164
- console.log(` ${color.cyan("")} Installing ${color.bold(rec.name)}...`);
177
+ console.log(` ${color.cyan("")} Installing ${color.bold(rec.name)}...`);
165
178
 
166
179
  // 1. Add marketplace (if needed)
167
180
  if (rec.marketplace) {
@@ -186,7 +199,7 @@ export async function install(args = []) {
186
199
  // 3. Post-install hook for claude-mem: its .mcp.json hard-codes `bun`,
187
200
  // but ~/.bun/bin is not on PATH when Claude Code spawns the MCP server.
188
201
  // Auto-create a PATH-visible symlink to fix it.
189
- if (rec.name === "claude-mem") {
202
+ if (rec.postInstall === "claude-mem-runtimes") {
190
203
  const r = ensureClaudeMemRuntimes();
191
204
  for (const [name, res] of Object.entries(r)) {
192
205
  if (res.status === "linked") {
@@ -219,11 +232,13 @@ export async function install(args = []) {
219
232
 
220
233
  // ---------- Step 5: inject global protocols ----------
221
234
  log.blank();
222
- console.log(color.dim("Injecting global protocols into ~/.claude/CLAUDE.md..."));
235
+ log.step(5, 5, "Injecting global protocols into ~/.claude/CLAUDE.md...");
223
236
  try {
224
237
  const r = injectGlobalProtocols();
225
238
  if (r.action === "created") {
226
239
  log.ok(`Global protocols injected ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
240
+ } else if (r.action === "appended") {
241
+ log.ok(`Global protocols appended ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
227
242
  } else if (r.action === "upgraded") {
228
243
  log.ok(`Global protocols upgraded ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
229
244
  } else {
@@ -237,15 +252,26 @@ export async function install(args = []) {
237
252
  }
238
253
 
239
254
  function printNextSteps() {
240
- console.log(`\n${color.bold("✅ Install complete")}\n`);
255
+ // Detect whether the CLI is globally installed (curdx-flow on PATH) or
256
+ // the user ran us via npx. Tell them the right invocation each time.
257
+ const cliOnPath = has("curdx-flow");
258
+ const cliCmd = cliOnPath ? "curdx-flow" : "npx @curdx/flow";
259
+
260
+ console.log(`\n${color.bold(`${color.green("✓")} Install complete`)}\n`);
261
+ console.log(`${color.bold("Restart Claude Code")} so the plugin registers all its commands and hooks.\n`);
241
262
  console.log(`${color.bold("Next steps")}:\n`);
242
263
  console.log(` ${color.dim("# Verify health")}`);
243
- console.log(` curdx-flow doctor\n`);
244
- console.log(` ${color.dim("# Initialize .flow/ in your project")}`);
245
- console.log(` cd ~/your-project && curdx-flow init\n`);
246
- console.log(` ${color.dim("# Start using it (inside Claude Code)")}`);
264
+ console.log(` ${cliCmd} doctor\n`);
265
+ console.log(` ${color.dim("# Inside any project, initialize and start a feature spec")}`);
266
+ console.log(` ${color.cyan("cd ~/your-project")}`);
247
267
  console.log(` ${color.cyan("claude")}`);
248
- console.log(` ${color.cyan("/curdx-flow:start my-feature \"<describe what to build>\"")}\n`);
268
+ console.log(` ${color.cyan("/curdx-flow:init")}`);
269
+ console.log(` ${color.cyan("/curdx-flow:start my-feature \"<one-line goal>\"")}\n`);
270
+ if (!cliOnPath) {
271
+ console.log(
272
+ `${color.dim("Tip: install the CLI globally for shorter commands —")} ${color.cyan("npm i -g @curdx/flow")}\n`
273
+ );
274
+ }
249
275
  console.log(
250
276
  `${color.bold("Learn more")}: https://github.com/curdx/curdx-flow/blob/main/docs/getting-started.md\n`
251
277
  );
package/cli/protocols.js CHANGED
@@ -5,10 +5,23 @@
5
5
  * and reversible (uninstall removes it cleanly without touching user content).
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
8
+ import {
9
+ readFileSync,
10
+ writeFileSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ renameSync,
14
+ unlinkSync,
15
+ } from "node:fs";
9
16
  import { join, dirname } from "node:path";
10
-
11
- const HOME = process.env.HOME || "";
17
+ import { homedir } from "node:os";
18
+
19
+ // Use os.homedir() instead of process.env.HOME — HOME can be empty inside
20
+ // non-login shells (CI containers, some spawned child envs), which would
21
+ // resolve GLOBAL_CLAUDE_MD to "/.claude/CLAUDE.md" (filesystem root) and
22
+ // cause mkdir/writeFileSync to fail with EACCES. homedir() falls back to
23
+ // the effective user's passwd entry on POSIX and USERPROFILE on Windows.
24
+ const HOME = homedir();
12
25
  export const GLOBAL_CLAUDE_MD = join(HOME, ".claude", "CLAUDE.md");
13
26
 
14
27
  const SENTINEL_BEGIN =
@@ -53,16 +66,56 @@ function readGlobalMd() {
53
66
 
54
67
  /**
55
68
  * Locate the sentinel block in the content.
56
- * Returns { start, end } indices into content, or null if not found.
69
+ * Returns { start, end } indices into content, `null` if neither sentinel is
70
+ * present, or throws if the block is corrupted (begin without matching end).
71
+ * The throw is intentional — previously the corrupted case silently returned
72
+ * null, so the next run would append a SECOND block, producing drift.
57
73
  */
58
74
  function findBlock(content) {
59
75
  const start = content.indexOf(SENTINEL_BEGIN);
60
- if (start === -1) return null;
76
+ if (start === -1) {
77
+ // Also check for a dangling END without BEGIN — that is also corrupted.
78
+ if (content.indexOf(SENTINEL_END) !== -1) {
79
+ throw new Error(
80
+ `Corrupted protocol block in ${GLOBAL_CLAUDE_MD}: END sentinel found without BEGIN. ` +
81
+ `Manually inspect the file and remove the dangling END line, then re-run.`
82
+ );
83
+ }
84
+ return null;
85
+ }
61
86
  const endIdx = content.indexOf(SENTINEL_END, start);
62
- if (endIdx === -1) return null;
87
+ if (endIdx === -1) {
88
+ throw new Error(
89
+ `Corrupted protocol block in ${GLOBAL_CLAUDE_MD}: BEGIN sentinel found without END. ` +
90
+ `Manually remove the orphan BEGIN line (or restore the END), then re-run.`
91
+ );
92
+ }
63
93
  return { start, end: endIdx + SENTINEL_END.length };
64
94
  }
65
95
 
96
+ /**
97
+ * Write `content` to `path` atomically: write to a sibling temp file first,
98
+ * then rename. This prevents a half-written CLAUDE.md if the process is
99
+ * interrupted mid-write, and avoids races between concurrent install /
100
+ * uninstall invocations.
101
+ */
102
+ function atomicWrite(path, content) {
103
+ const tmp = `${path}.curdx-flow.tmp.${process.pid}`;
104
+ try {
105
+ writeFileSync(tmp, content, "utf-8");
106
+ renameSync(tmp, path);
107
+ } catch (err) {
108
+ // Best-effort cleanup of the temp file; swallow errors here since we
109
+ // are already re-throwing the real failure.
110
+ try {
111
+ if (existsSync(tmp)) unlinkSync(tmp);
112
+ } catch {
113
+ // ignore
114
+ }
115
+ throw err;
116
+ }
117
+ }
118
+
66
119
  /**
67
120
  * Inject (or upgrade) the protocol block in ~/.claude/CLAUDE.md.
68
121
  * @returns {{action:"created"|"upgraded"|"unchanged", path:string}}
@@ -81,8 +134,8 @@ export function injectGlobalProtocols() {
81
134
  ? (existing.endsWith("\n") ? "\n" : "\n\n")
82
135
  : "";
83
136
  const next = existing + sep + FULL_BLOCK + "\n";
84
- writeFileSync(path, next, "utf-8");
85
- return { action: existing.length === 0 ? "created" : "created", path };
137
+ atomicWrite(path, next);
138
+ return { action: existing.length === 0 ? "created" : "appended", path };
86
139
  }
87
140
 
88
141
  // Replace existing block (handle upgrade-in-place)
@@ -92,7 +145,7 @@ export function injectGlobalProtocols() {
92
145
  }
93
146
  const next =
94
147
  existing.slice(0, block.start) + FULL_BLOCK + existing.slice(block.end);
95
- writeFileSync(path, next, "utf-8");
148
+ atomicWrite(path, next);
96
149
  return { action: "upgraded", path };
97
150
  }
98
151
 
@@ -119,6 +172,6 @@ export function removeGlobalProtocols() {
119
172
  }
120
173
 
121
174
  const next = existing.slice(0, start) + existing.slice(end);
122
- writeFileSync(path, next, "utf-8");
175
+ atomicWrite(path, next);
123
176
  return { action: "removed", path };
124
177
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Single source of truth for recommended companion plugins.
3
+ *
4
+ * Background: before this file existed, the list of recommended plugins lived
5
+ * in FOUR independent places (install.js, uninstall.js, upgrade.js,
6
+ * doctor.js). They drifted — chrome-devtools-mcp was added to install.js
7
+ * during the beta.8 MCP decoupling but forgotten in the other three,
8
+ * making it installable but uninstallable. This registry exists so adding
9
+ * or removing a plugin is a one-file change.
10
+ *
11
+ * Every consumer pulls what it needs via property access:
12
+ * - install.js → marketplace + installSpec + hint (+ optional postInstall)
13
+ * - uninstall.js → uninstallSpec
14
+ * - upgrade.js → installSpec (for `claude plugin update`) + marketplaceId
15
+ * - doctor.js → name + installSpec (for manual recovery hints)
16
+ */
17
+
18
+ export const RECOMMENDED_PLUGINS = [
19
+ {
20
+ name: "pua",
21
+ marketplace: "tanweai/pua",
22
+ marketplaceId: "pua",
23
+ installSpec: "pua@pua-skills",
24
+ uninstallSpec: "pua@pua-skills",
25
+ hint: "no-give-up + three red lines",
26
+ },
27
+ {
28
+ name: "claude-mem",
29
+ marketplace: "thedotmack/claude-mem",
30
+ marketplaceId: "thedotmack",
31
+ installSpec: "claude-mem@thedotmack",
32
+ uninstallSpec: "claude-mem@thedotmack",
33
+ hint: "automatic cross-session memory",
34
+ postInstall: "claude-mem-runtimes",
35
+ },
36
+ {
37
+ name: "frontend-design",
38
+ // Already in default marketplace claude-plugins-official, no add needed
39
+ marketplace: null,
40
+ marketplaceId: "claude-plugins-official",
41
+ installSpec: "frontend-design@claude-plugins-official",
42
+ uninstallSpec: "frontend-design@claude-plugins-official",
43
+ hint: "Anthropic official UI skill",
44
+ },
45
+ {
46
+ name: "chrome-devtools-mcp",
47
+ marketplace: "ChromeDevTools/chrome-devtools-mcp",
48
+ marketplaceId: "chrome-devtools-plugins",
49
+ installSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
50
+ uninstallSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
51
+ hint: "Chrome DevTools + Puppeteer (Google official)",
52
+ },
53
+ ];
54
+
55
+ /**
56
+ * Marketplaces to refresh during `upgrade`. Derived from RECOMMENDED_PLUGINS
57
+ * plus the curdx-flow marketplace itself.
58
+ */
59
+ export const MARKETPLACES_TO_REFRESH = [
60
+ "curdx-flow-marketplace",
61
+ ...RECOMMENDED_PLUGINS
62
+ .filter((p) => p.marketplaceId && p.marketplaceId !== "claude-plugins-official")
63
+ .map((p) => p.marketplaceId),
64
+ ];
65
+
66
+ /**
67
+ * Plugin install specs to update during `upgrade` — includes curdx-flow
68
+ * itself plus every recommended plugin.
69
+ */
70
+ export const PLUGINS_TO_UPDATE = [
71
+ "curdx-flow@curdx-flow-marketplace",
72
+ ...RECOMMENDED_PLUGINS.map((p) => p.installSpec),
73
+ ];
package/cli/uninstall.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
6
6
  import { join } from "node:path";
7
+ import { homedir } from "node:os";
7
8
 
8
9
  import {
9
10
  color,
@@ -15,18 +16,15 @@ import {
15
16
  listPlugins,
16
17
  } from "./utils.js";
17
18
  import { removeGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
19
+ import { RECOMMENDED_PLUGINS } from "./registry.js";
18
20
 
19
- const HOME = process.env.HOME || "";
21
+ const HOME = homedir();
20
22
 
21
- // Keep aligned with install.js
22
- const RECOMMENDED = [
23
- { name: "pua", uninstallSpec: "pua@pua-skills" },
24
- { name: "claude-mem", uninstallSpec: "claude-mem@thedotmack" },
25
- {
26
- name: "frontend-design",
27
- uninstallSpec: "frontend-design@claude-plugins-official",
28
- },
29
- ];
23
+ // Pull uninstall-relevant subset from the single registry. See registry.js.
24
+ const RECOMMENDED = RECOMMENDED_PLUGINS.map(({ name, uninstallSpec }) => ({
25
+ name,
26
+ uninstallSpec,
27
+ }));
30
28
 
31
29
  // Symlinks created by install.js (only cleaned with --purge)
32
30
  const MANAGED_SYMLINKS = [
@@ -116,7 +114,7 @@ export async function uninstall(args = []) {
116
114
  for (const name of toRemove) {
117
115
  const rec = presentRecs.find((r) => r.name === name);
118
116
  log.blank();
119
- console.log(` ${color.cyan("")} Uninstalling ${color.bold(rec.name)}...`);
117
+ console.log(` ${color.cyan("")} Uninstalling ${color.bold(rec.name)}...`);
120
118
  const r = await run(
121
119
  "claude",
122
120
  ["plugin", "uninstall", rec.uninstallSpec],
package/cli/upgrade.js CHANGED
@@ -3,13 +3,10 @@
3
3
  */
4
4
 
5
5
  import { color, log, run, listPlugins, claudeVersion } from "./utils.js";
6
-
7
- const PLUGINS_TO_UPDATE = [
8
- "curdx-flow@curdx-flow-marketplace",
9
- "pua@pua-skills",
10
- "claude-mem@thedotmack",
11
- "frontend-design@claude-plugins-official",
12
- ];
6
+ import {
7
+ PLUGINS_TO_UPDATE,
8
+ MARKETPLACES_TO_REFRESH,
9
+ } from "./registry.js";
13
10
 
14
11
  export async function upgrade(args = []) {
15
12
  log.title("⬆️ CurDX-Flow upgrade");
@@ -19,10 +16,9 @@ export async function upgrade(args = []) {
19
16
  process.exit(1);
20
17
  }
21
18
 
22
- // Refresh marketplaces first
19
+ // Refresh marketplaces first (derived from cli/registry.js)
23
20
  log.step(1, 2, "Refreshing marketplaces...");
24
- const marketplaces = ["curdx-flow-marketplace", "pua", "thedotmack"];
25
- for (const mp of marketplaces) {
21
+ for (const mp of MARKETPLACES_TO_REFRESH) {
26
22
  const r = await run(
27
23
  "claude",
28
24
  ["plugin", "marketplace", "update", mp],