@curdx/flow 2.0.0-beta.8 → 2.0.0

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.
package/cli/install.js CHANGED
@@ -15,8 +15,20 @@ import {
15
15
  listPlugins,
16
16
  multiSelect,
17
17
  ensureClaudeMemRuntimes,
18
+ select,
19
+ intro,
20
+ outro,
21
+ readConfig,
22
+ writeConfig,
23
+ text,
24
+ note,
25
+ multiselectClack,
26
+ runSync,
27
+ VERSION,
18
28
  } from "./utils.js";
19
29
  import { injectGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
30
+ import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
31
+ import { readUserMcpConfig } from "./utils.js";
20
32
 
21
33
  // When installed via npm, this CLI file lives at <pkg-root>/cli/install.js.
22
34
  // The npm package bundles the full plugin body (.claude-plugin/, agents/,
@@ -28,38 +40,35 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
28
40
  const PKG_ROOT = dirname(__dirname);
29
41
  const LOCAL_MARKETPLACE_MANIFEST = join(PKG_ROOT, ".claude-plugin", "marketplace.json");
30
42
 
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
- {
52
- name: "chrome-devtools-mcp",
53
- marketplace: "ChromeDevTools/chrome-devtools-mcp",
54
- installSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
55
- hint: "Chrome DevTools + Puppeteer (Google official)",
56
- },
57
- ];
43
+ // Recommended plugins: single source of truth is cli/registry.js.
44
+ // See registry.js for the rationale — this list used to drift across
45
+ // install/uninstall/upgrade/doctor, producing the chrome-devtools-mcp
46
+ // orphan-plugin bug (installable but uninstallable).
47
+ const RECOMMENDED = RECOMMENDED_PLUGINS;
58
48
 
59
49
  export async function install(args = []) {
60
50
  const all = args.includes("--all");
61
51
  const noDeps = args.includes("--no-deps");
62
52
  const forceOnline = args.includes("--online") || args.includes("--from-github");
53
+ const yes = args.includes("--yes") || args.includes("-y");
54
+ const skipSelfUpdate = args.includes("--skip-self-update");
55
+
56
+ // ---------- Step 0: Self-update check ----------
57
+ if (!skipSelfUpdate) {
58
+ const updateResult = await checkAndUpdateSelf();
59
+ if (updateResult.updated) {
60
+ // CLI was updated, re-exec with same args
61
+ log.info("Restarting with updated version...");
62
+ const { spawn } = await import("node:child_process");
63
+ const child = spawn("curdx-flow", ["install", ...args, "--skip-self-update"], {
64
+ stdio: "inherit",
65
+ shell: false,
66
+ });
67
+ return new Promise((resolve) => {
68
+ child.on("close", (code) => process.exit(code || 0));
69
+ });
70
+ }
71
+ }
63
72
 
64
73
  // Default to offline install when the npm package includes the full plugin
65
74
  // body (since 1.1.5). Fall back to GitHub only if the local manifest is
@@ -67,10 +76,48 @@ export async function install(args = []) {
67
76
  // or the user explicitly passes --online.
68
77
  const useOffline = !forceOnline && existsSync(LOCAL_MARKETPLACE_MANIFEST);
69
78
 
70
- log.title("🚀 CurDX-Flow Installer");
79
+ // Use @clack intro only in interactive TTY mode
80
+ if (process.stdout.isTTY && !yes) {
81
+ await intro("🚀 CurDX-Flow Installer");
82
+ } else {
83
+ log.title("🚀 CurDX-Flow Installer");
84
+ }
85
+
86
+ // ---------- Step 0: Language selection ----------
87
+ const config = readConfig();
88
+ let language = config.language;
89
+
90
+ if (!language && !yes) {
91
+ log.blank();
92
+ language = await select({
93
+ message: "Choose your preferred language / 选择语言",
94
+ options: [
95
+ {
96
+ value: "en",
97
+ label: "English",
98
+ hint: "CLI output and documentation in English"
99
+ },
100
+ {
101
+ value: "zh",
102
+ label: "简体中文",
103
+ hint: "CLI 输出和文档使用简体中文"
104
+ },
105
+ ],
106
+ initialValue: "en",
107
+ });
108
+ config.language = language;
109
+ writeConfig(config);
110
+ log.ok(`Language set to ${language === "zh" ? "简体中文" : "English"}`);
111
+ } else if (!language) {
112
+ // --yes mode, default to English
113
+ language = "en";
114
+ config.language = language;
115
+ writeConfig(config);
116
+ }
71
117
 
72
118
  // ---------- Step 1: Check claude CLI ----------
73
- log.step(1, 4, "Checking claude CLI...");
119
+ log.blank();
120
+ log.step(1, 5, "Checking claude CLI...");
74
121
  const ver = claudeVersion();
75
122
  if (!ver) {
76
123
  log.err("claude CLI not found. Install Claude Code from https://code.claude.com first.");
@@ -78,13 +125,66 @@ export async function install(args = []) {
78
125
  }
79
126
  log.ok(`claude CLI found (${ver})`);
80
127
 
128
+ // Snapshot curdx-flow's pre-install version BEFORE Step 2 touches the
129
+ // marketplace. Step 2 does `claude plugin marketplace remove` + `add` to
130
+ // rebind to the current source, and the remove side-effect also drops
131
+ // any plugins installed from that marketplace. If we only call
132
+ // listPlugins() in Step 3, curdx-flow is already gone from the list and
133
+ // we can't tell a fresh install apart from an upgrade — the Step 3
134
+ // output then incorrectly says "installed" for both cases.
135
+ const prevCurdxFlow = listPlugins().find((p) => p.name === "curdx-flow");
136
+
137
+ // ---------- Step 1.5: Existing installation action menu ----------
138
+ if (prevCurdxFlow && !yes) {
139
+ log.blank();
140
+ const action = await select({
141
+ message: language === "zh"
142
+ ? "检测到已安装的版本。如何继续?"
143
+ : "Existing installation detected. How would you like to proceed?",
144
+ options: [
145
+ {
146
+ value: "upgrade",
147
+ label: language === "zh" ? "升级到最新版本" : "Upgrade to latest version",
148
+ hint: language === "zh"
149
+ ? `当前: v${prevCurdxFlow.version}`
150
+ : `Current: v${prevCurdxFlow.version}`
151
+ },
152
+ {
153
+ value: "reinstall",
154
+ label: language === "zh" ? "重新安装(保留配置)" : "Reinstall (preserve config)",
155
+ },
156
+ {
157
+ value: "reconfigure",
158
+ label: language === "zh" ? "重新配置插件" : "Reconfigure plugins",
159
+ },
160
+ {
161
+ value: "cancel",
162
+ label: language === "zh" ? "取消" : "Cancel",
163
+ },
164
+ ],
165
+ initialValue: "upgrade",
166
+ });
167
+
168
+ if (action === "cancel") {
169
+ log.info(language === "zh" ? "安装已取消" : "Installation cancelled");
170
+ process.exit(0);
171
+ }
172
+
173
+ if (action === "reconfigure") {
174
+ log.info(language === "zh"
175
+ ? "重新配置模式:将重新提示插件选择和 API key 配置"
176
+ : "Reconfigure mode: will re-prompt for plugin selection and API key configuration");
177
+ // Continue with normal flow but force prompts
178
+ }
179
+ }
180
+
81
181
  // ---------- Step 2: Add marketplace ----------
82
182
  log.blank();
83
183
  const marketplaceSource = useOffline ? PKG_ROOT : "curdx/curdx-flow";
84
184
  const marketplaceLabel = useOffline
85
185
  ? `local npm package (${PKG_ROOT})`
86
186
  : "GitHub curdx/curdx-flow";
87
- log.step(2, 4, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
187
+ log.step(2, 5, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
88
188
 
89
189
  // Remove any existing marketplace with the same name so we get a clean
90
190
  // rebind to the chosen source. Errors are non-fatal (marketplace may
@@ -97,7 +197,7 @@ export async function install(args = []) {
97
197
 
98
198
  const addRes = await run(
99
199
  "claude",
100
- ["plugin", "marketplace", "add", marketplaceSource],
200
+ ["plugin", "marketplace", "add", "--scope", "user", marketplaceSource],
101
201
  { silent: true }
102
202
  );
103
203
  if (addRes.code !== 0 && !addRes.stderr.includes("already")) {
@@ -111,7 +211,7 @@ export async function install(args = []) {
111
211
 
112
212
  // ---------- Step 3: Install curdx-flow plugin ----------
113
213
  log.blank();
114
- log.step(3, 4, "Installing curdx-flow plugin (2 MCPs will auto-start)...");
214
+ log.step(3, 5, "Installing curdx-flow plugin...");
115
215
  // Read the version the marketplace is shipping so we can decide whether an
116
216
  // already-installed plugin needs an update (same name but stale version
117
217
  // previously silently skipped the upgrade — caused the beta.1 → beta.7 drift).
@@ -125,41 +225,157 @@ export async function install(args = []) {
125
225
  // marketplace not local (online install) or unreadable — fall through
126
226
  }
127
227
 
128
- const installed = listPlugins();
129
- const already = installed.find((p) => p.name === "curdx-flow");
130
- if (already && shippedVersion && already.version !== shippedVersion) {
228
+ // Use the pre-Step-2 snapshot — by this point `claude plugin marketplace
229
+ // remove` has already evicted the plugin, so listPlugins() here would
230
+ // always return undefined for curdx-flow and we'd mis-report "installed"
231
+ // when we actually upgraded (the bug reported by @wdx's beta.14 log).
232
+ const already = prevCurdxFlow;
233
+
234
+ if (already && shippedVersion && already.version === shippedVersion) {
235
+ // Step 2 removes and re-adds the marketplace to rebind it to the current
236
+ // source. Claude Code removes plugins installed from that marketplace as
237
+ // part of `marketplace remove`, so even a same-version install must be
238
+ // re-registered here.
131
239
  log.info(
132
- `curdx-flow installed at v${already.version}, marketplace ships v${shippedVersion} — updating...`
240
+ `curdx-flow already at v${already.version}, re-registering...`
133
241
  );
134
242
  const r = await run(
135
243
  "claude",
136
- ["plugin", "update", "curdx-flow@curdx-flow-marketplace"],
244
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
137
245
  { silent: true }
138
246
  );
139
247
  if (r.code !== 0) {
140
- log.warn(`Update returned non-zero: ${r.stderr.trim() || r.stdout.trim()}`);
141
- log.info(`If the version stays on v${already.version}, run: claude plugin uninstall curdx-flow@curdx-flow-marketplace && retry`);
142
- } else {
143
- log.ok(`curdx-flow updated to v${shippedVersion}`);
248
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
249
+ process.exit(1);
144
250
  }
251
+ log.ok(`curdx-flow re-registered at v${shippedVersion}`);
252
+ } else if (already && shippedVersion) {
253
+ // Existing install, different version — frame as an upgrade.
254
+ log.info(
255
+ `curdx-flow v${already.version} → v${shippedVersion}, installing...`
256
+ );
257
+ const r = await run(
258
+ "claude",
259
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
260
+ { silent: true }
261
+ );
262
+ if (r.code !== 0) {
263
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
264
+ process.exit(1);
265
+ }
266
+ log.ok(`curdx-flow upgraded to v${shippedVersion}`);
145
267
  } else if (already) {
146
- log.ok(`curdx-flow already installed (v${already.version}, ${already.status})`);
268
+ // shippedVersion unknown (e.g. online install) — best we can do is report
269
+ // the previous version and let `claude plugin install` idempotently
270
+ // re-register.
271
+ log.info(
272
+ `curdx-flow v${already.version} detected, re-registering...`
273
+ );
274
+ const r = await run(
275
+ "claude",
276
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
277
+ { silent: true }
278
+ );
279
+ if (r.code !== 0) {
280
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
281
+ process.exit(1);
282
+ }
283
+ log.ok(`curdx-flow re-registered`);
147
284
  } else {
148
285
  const r = await run(
149
286
  "claude",
150
- ["plugin", "install", "curdx-flow@curdx-flow-marketplace"],
287
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
151
288
  { silent: true }
152
289
  );
153
290
  if (r.code !== 0) {
154
291
  log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
155
292
  process.exit(1);
156
293
  }
157
- log.ok("curdx-flow installed");
294
+ if (shippedVersion) {
295
+ log.ok(`curdx-flow v${shippedVersion} installed`);
296
+ } else {
297
+ log.ok("curdx-flow installed");
298
+ }
299
+ }
300
+
301
+ // ---------- Step 3.5: Install required plugins + register user-level MCPs ----------
302
+ log.blank();
303
+ log.info("Installing required Claude Code plugins...");
304
+ for (const plugin of REQUIRED_PLUGINS) {
305
+ console.log(` ${color.cyan("▸")} Installing ${color.bold(plugin.name)}...`);
306
+ const ma = await run(
307
+ "claude",
308
+ ["plugin", "marketplace", "add", "--scope", plugin.scope, plugin.marketplaceSource],
309
+ { silent: true }
310
+ );
311
+ if (ma.code !== 0 && !ma.stderr.includes("already")) {
312
+ log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
313
+ }
314
+
315
+ const ir = await run(
316
+ "claude",
317
+ ["plugin", "install", "--scope", plugin.scope, plugin.installSpec],
318
+ { silent: true }
319
+ );
320
+ if (ir.code === 0) {
321
+ console.log(` ${color.green("✓")} ${plugin.name} installed`);
322
+
323
+ // Post-install: API key configuration
324
+ if (plugin.requiresConfig && plugin.configType === "apiKey" && !yes) {
325
+ await promptPluginConfig(plugin, language, config);
326
+ }
327
+ } else {
328
+ console.log(
329
+ ` ${color.red("✗")} ${plugin.name} install failed: ${ir.stderr.trim().split("\n").pop()}`
330
+ );
331
+ console.log(
332
+ color.dim(
333
+ ` Run manually: claude plugin marketplace add --scope ${plugin.scope} ${plugin.marketplaceSource}`
334
+ )
335
+ );
336
+ console.log(
337
+ color.dim(
338
+ ` Then: claude plugin install --scope ${plugin.scope} ${plugin.installSpec}`
339
+ )
340
+ );
341
+ }
342
+ }
343
+
344
+ // Beta.12: direct MCPs migrated from plugin.json bundling. See cli/registry.js
345
+ // for the rationale. Context7 now uses Upstash's official plugin instead.
346
+ log.blank();
347
+ log.info("Registering required MCP servers (user-level)...");
348
+ const existingUserMcps = readUserMcpConfig();
349
+ for (const mcp of BUNDLED_MCPS) {
350
+ if (mcp.preserveExisting && existingUserMcps.has(mcp.name)) {
351
+ const existing = existingUserMcps.get(mcp.name);
352
+ log.info(
353
+ ` ${mcp.name.padEnd(22)} ${color.dim(`already registered (${(existing.args || []).join(" ")}) — preserving`)}`
354
+ );
355
+ continue;
356
+ }
357
+ const r = await run(
358
+ "claude",
359
+ ["mcp", "add", "--scope", "user", mcp.name, "--", mcp.command, ...mcp.args],
360
+ { silent: true }
361
+ );
362
+ if (r.code === 0) {
363
+ log.ok(` ${mcp.name.padEnd(22)} ${color.dim("registered")}`);
364
+ } else if (r.stderr.includes("already exists")) {
365
+ log.info(` ${mcp.name.padEnd(22)} ${color.dim("already exists — skipped")}`);
366
+ } else {
367
+ log.warn(
368
+ ` ${mcp.name.padEnd(22)} registration failed: ${r.stderr.trim().split("\n").pop()}`
369
+ );
370
+ log.info(
371
+ ` Run manually: claude mcp add --scope user ${mcp.name} -- ${mcp.command} ${mcp.args.join(" ")}`
372
+ );
373
+ }
158
374
  }
159
375
 
160
376
  // ---------- Step 4: Recommended plugins ----------
161
377
  log.blank();
162
- log.step(4, 4, "Recommended plugins");
378
+ log.step(4, 5, "Recommended plugins");
163
379
 
164
380
  if (noDeps) {
165
381
  log.info("Skipping recommended plugins (--no-deps)");
@@ -171,18 +387,32 @@ export async function install(args = []) {
171
387
  if (all) {
172
388
  toInstall = RECOMMENDED.map((r) => r.name);
173
389
  log.info("--all mode: installing all recommended");
390
+ } else if (yes) {
391
+ // --yes mode: install all not-yet-installed plugins
392
+ const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
393
+ toInstall = RECOMMENDED
394
+ .filter((r) => !currentlyInstalled.has(r.name))
395
+ .map((r) => r.name);
396
+ log.info(`--yes mode: installing ${toInstall.length} recommended plugins`);
174
397
  } else {
175
398
  const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
176
- const choices = RECOMMENDED.map((r) => ({
177
- label: `${color.bold(r.name)}${currentlyInstalled.has(r.name) ? color.green(" (installed)") : ""}`,
399
+ const options = RECOMMENDED.map((r) => ({
178
400
  value: r.name,
401
+ label: `${r.name}${currentlyInstalled.has(r.name) ? " (installed)" : ""}`,
179
402
  hint: r.hint,
180
403
  }));
181
- const defaults = RECOMMENDED
182
- .map((r, i) => (currentlyInstalled.has(r.name) ? -1 : i))
183
- .filter((i) => i >= 0);
404
+ const initialValues = RECOMMENDED
405
+ .filter((r) => !currentlyInstalled.has(r.name))
406
+ .map((r) => r.name);
184
407
 
185
- toInstall = await multiSelect("Which recommended plugins to install?", choices, defaults);
408
+ toInstall = await multiselectClack({
409
+ message: language === "zh"
410
+ ? "选择要安装的推荐插件(空格切换,回车确认)"
411
+ : "Select recommended plugins to install (space to toggle, enter to confirm)",
412
+ options,
413
+ initialValues,
414
+ required: false,
415
+ });
186
416
  }
187
417
 
188
418
  if (!toInstall || toInstall.length === 0) {
@@ -195,13 +425,13 @@ export async function install(args = []) {
195
425
  for (const pluginName of toInstall) {
196
426
  const rec = RECOMMENDED.find((r) => r.name === pluginName);
197
427
  log.blank();
198
- console.log(` ${color.cyan("")} Installing ${color.bold(rec.name)}...`);
428
+ console.log(` ${color.cyan("")} Installing ${color.bold(rec.name)}...`);
199
429
 
200
430
  // 1. Add marketplace (if needed)
201
- if (rec.marketplace) {
431
+ if (rec.marketplaceSource) {
202
432
  const ma = await run(
203
433
  "claude",
204
- ["plugin", "marketplace", "add", rec.marketplace],
434
+ ["plugin", "marketplace", "add", "--scope", rec.scope, rec.marketplaceSource],
205
435
  { silent: true }
206
436
  );
207
437
  if (ma.code !== 0 && !ma.stderr.includes("already")) {
@@ -211,7 +441,7 @@ export async function install(args = []) {
211
441
  }
212
442
 
213
443
  // 2. Install
214
- const ir = await run("claude", ["plugin", "install", rec.installSpec], {
444
+ const ir = await run("claude", ["plugin", "install", "--scope", rec.scope, rec.installSpec], {
215
445
  silent: true,
216
446
  });
217
447
  if (ir.code === 0) {
@@ -220,7 +450,7 @@ export async function install(args = []) {
220
450
  // 3. Post-install hook for claude-mem: its .mcp.json hard-codes `bun`,
221
451
  // but ~/.bun/bin is not on PATH when Claude Code spawns the MCP server.
222
452
  // Auto-create a PATH-visible symlink to fix it.
223
- if (rec.name === "claude-mem") {
453
+ if (rec.postInstall === "claude-mem-runtimes") {
224
454
  const r = ensureClaudeMemRuntimes();
225
455
  for (const [name, res] of Object.entries(r)) {
226
456
  if (res.status === "linked") {
@@ -245,7 +475,7 @@ export async function install(args = []) {
245
475
  );
246
476
  console.log(
247
477
  color.dim(
248
- ` Run manually: claude plugin install ${rec.installSpec}`
478
+ ` Run manually: claude plugin install --scope ${rec.scope} ${rec.installSpec}`
249
479
  )
250
480
  );
251
481
  }
@@ -253,11 +483,13 @@ export async function install(args = []) {
253
483
 
254
484
  // ---------- Step 5: inject global protocols ----------
255
485
  log.blank();
256
- console.log(color.dim("Injecting global protocols into ~/.claude/CLAUDE.md..."));
486
+ log.step(5, 5, "Injecting global protocols into ~/.claude/CLAUDE.md...");
257
487
  try {
258
488
  const r = injectGlobalProtocols();
259
489
  if (r.action === "created") {
260
490
  log.ok(`Global protocols injected ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
491
+ } else if (r.action === "appended") {
492
+ log.ok(`Global protocols appended ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
261
493
  } else if (r.action === "upgraded") {
262
494
  log.ok(`Global protocols upgraded ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
263
495
  } else {
@@ -270,13 +502,155 @@ export async function install(args = []) {
270
502
  printNextSteps();
271
503
  }
272
504
 
505
+ /**
506
+ * Check for CLI updates and auto-update if available
507
+ * Returns { updated: boolean, version?: string }
508
+ */
509
+ async function checkAndUpdateSelf() {
510
+ try {
511
+ // Check if globally installed
512
+ const globalPath = runSync("npm", ["root", "-g"]).stdout.trim();
513
+ const installedPath = join(globalPath, "@curdx/flow");
514
+
515
+ if (!existsSync(installedPath)) {
516
+ // Not globally installed, skip update
517
+ return { updated: false };
518
+ }
519
+
520
+ // Check npm registry for latest version
521
+ log.info("Checking for CLI updates...");
522
+ const res = runSync("npm", ["view", "@curdx/flow", "version"]);
523
+
524
+ if (res.code !== 0) {
525
+ // Registry check failed, continue with current version
526
+ return { updated: false };
527
+ }
528
+
529
+ const latestVersion = res.stdout.trim();
530
+ const currentVersion = VERSION;
531
+
532
+ if (latestVersion === currentVersion) {
533
+ log.ok(`CLI is up to date (v${currentVersion})`);
534
+ return { updated: false };
535
+ }
536
+
537
+ // Compare versions (simple string comparison works for semver)
538
+ if (latestVersion > currentVersion) {
539
+ log.info(`New version available: v${currentVersion} → v${latestVersion}`);
540
+ log.info("Updating CLI...");
541
+
542
+ const updateRes = await run("npm", ["install", "-g", "@curdx/flow@latest"], {
543
+ silent: false,
544
+ });
545
+
546
+ if (updateRes.code === 0) {
547
+ log.ok(`CLI updated to v${latestVersion}`);
548
+ return { updated: true, version: latestVersion };
549
+ } else {
550
+ log.warn("CLI update failed, continuing with current version");
551
+ return { updated: false };
552
+ }
553
+ }
554
+
555
+ return { updated: false };
556
+ } catch (err) {
557
+ // Update check failed, continue silently
558
+ return { updated: false };
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Prompt for plugin-specific configuration (e.g., API keys)
564
+ */
565
+ async function promptPluginConfig(plugin, language, config) {
566
+ if (plugin.name === "context7-plugin") {
567
+ log.blank();
568
+ await note(
569
+ language === "zh"
570
+ ? "Context7 需要 API key 才能使用。\n获取 API key: https://console.upstash.com/context7"
571
+ : "Context7 requires an API key to function.\nGet your API key at: https://console.upstash.com/context7",
572
+ language === "zh" ? "配置 Context7" : "Configure Context7"
573
+ );
574
+
575
+ const apiKey = await text({
576
+ message: language === "zh"
577
+ ? "输入你的 Context7 API key(或按 Enter 跳过)"
578
+ : "Enter your Context7 API key (or press Enter to skip)",
579
+ placeholder: "ctx7_...",
580
+ validate: (value) => {
581
+ if (!value) return; // Allow skip
582
+ if (!value.startsWith("ctx7_")) {
583
+ return language === "zh"
584
+ ? "API key 应该以 ctx7_ 开头"
585
+ : "API key should start with ctx7_";
586
+ }
587
+ },
588
+ });
589
+
590
+ if (apiKey) {
591
+ // Save to config for future reference
592
+ config.context7ApiKey = apiKey;
593
+ writeConfig(config);
594
+
595
+ // Add to MCP config with environment variable
596
+ const r = await run(
597
+ "claude",
598
+ ["mcp", "add", "--scope", "user", "context7", "--env", `CONTEXT7_API_KEY=${apiKey}`, "--", "npx", "-y", "@upstash/context7-mcp"],
599
+ { silent: true }
600
+ );
601
+
602
+ if (r.code === 0) {
603
+ log.ok(
604
+ language === "zh"
605
+ ? " Context7 API key 已配置"
606
+ : " Context7 API key configured"
607
+ );
608
+ } else if (r.stderr.includes("already exists")) {
609
+ // Update existing MCP server
610
+ await run("claude", ["mcp", "remove", "--scope", "user", "context7"], { silent: true });
611
+ const r2 = await run(
612
+ "claude",
613
+ ["mcp", "add", "--scope", "user", "context7", "--env", `CONTEXT7_API_KEY=${apiKey}`, "--", "npx", "-y", "@upstash/context7-mcp"],
614
+ { silent: true }
615
+ );
616
+ if (r2.code === 0) {
617
+ log.ok(
618
+ language === "zh"
619
+ ? " Context7 API key 已更新"
620
+ : " Context7 API key updated"
621
+ );
622
+ }
623
+ } else {
624
+ log.warn(
625
+ language === "zh"
626
+ ? ` Context7 MCP 配置失败: ${r.stderr.trim().split("\n").pop()}`
627
+ : ` Context7 MCP configuration failed: ${r.stderr.trim().split("\n").pop()}`
628
+ );
629
+ log.info(
630
+ color.dim(
631
+ language === "zh"
632
+ ? ` 手动运行: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- npx -y @upstash/context7-mcp`
633
+ : ` Run manually: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- npx -y @upstash/context7-mcp`
634
+ )
635
+ );
636
+ }
637
+ } else {
638
+ log.info(
639
+ language === "zh"
640
+ ? " 跳过 Context7 配置(稍后可运行 curdx-flow install 重新配置)"
641
+ : " Skipped Context7 configuration (run curdx-flow install later to reconfigure)"
642
+ );
643
+ }
644
+ }
645
+ }
646
+
273
647
  function printNextSteps() {
274
648
  // Detect whether the CLI is globally installed (curdx-flow on PATH) or
275
649
  // the user ran us via npx. Tell them the right invocation each time.
276
650
  const cliOnPath = has("curdx-flow");
277
651
  const cliCmd = cliOnPath ? "curdx-flow" : "npx @curdx/flow";
278
652
 
279
- console.log(`\n${color.bold(" Install complete")}\n`);
653
+ console.log(`\n${color.bold(`${color.green("✓")} Install complete`)}\n`);
280
654
  console.log(`${color.bold("Restart Claude Code")} so the plugin registers all its commands and hooks.\n`);
281
655
  console.log(`${color.bold("Next steps")}:\n`);
282
656
  console.log(` ${color.dim("# Verify health")}`);
@@ -0,0 +1,21 @@
1
+ ## Global Protocols (curdx-flow)
2
+
3
+ All operations MUST strictly follow these system constraints:
4
+
5
+ ### Language separation
6
+ - **Tool / persistence layer = English**: commit messages, code, comments, file names, function names, PR descriptions, CLI log output, error messages thrown by code, and any artifact persisted to the repository or shown in a developer terminal.
7
+ - **Conversational layer = Simplified Chinese**: chat replies, explanations and reasoning shown directly to the human in a conversation interface (e.g. Claude Code chat).
8
+
9
+ Rationale: English in the persistence/tool layer aligns with developer-tool industry norms (npm/git/cargo are all English) and keeps the codebase internationally collaborable. Chinese in the conversational layer matches the user's language preference. Mixing the two (e.g. Chinese commit messages, Chinese CLI log output) is a violation.
10
+
11
+ ### Discovery & reasoning
12
+ - **Library / framework / API questions**: query `context7` MCP first. Do not rely on training memory.
13
+ - **Planning / design / architecture review / epic decomposition**: use `sequential-thinking` MCP with at least 5 thoughts.
14
+ - **Cross-session memory**: query `claude-mem` MCP at task start when available.
15
+
16
+ ### Three red lines (inherited from pua)
17
+ 1. **Closed loop**: claiming "done"? Provide evidence (build output / passing tests / curl result).
18
+ 2. **Fact-driven**: before saying "probably an env issue", verify it. Unverified attribution = blame-shifting.
19
+ 3. **Exhaust everything**: before saying "I cannot", complete the systematic 4-stage debugging.
20
+
21
+ > Source: curdx-flow CLI installer (`curdx-flow install`). Remove with: `curdx-flow uninstall --purge`.