@curdx/flow 2.0.0-beta.9 → 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,9 +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";
20
- import { RECOMMENDED_PLUGINS } from "./registry.js";
30
+ import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
31
+ import { readUserMcpConfig } from "./utils.js";
21
32
 
22
33
  // When installed via npm, this CLI file lives at <pkg-root>/cli/install.js.
23
34
  // The npm package bundles the full plugin body (.claude-plugin/, agents/,
@@ -39,6 +50,25 @@ export async function install(args = []) {
39
50
  const all = args.includes("--all");
40
51
  const noDeps = args.includes("--no-deps");
41
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
+ }
42
72
 
43
73
  // Default to offline install when the npm package includes the full plugin
44
74
  // body (since 1.1.5). Fall back to GitHub only if the local manifest is
@@ -46,10 +76,48 @@ export async function install(args = []) {
46
76
  // or the user explicitly passes --online.
47
77
  const useOffline = !forceOnline && existsSync(LOCAL_MARKETPLACE_MANIFEST);
48
78
 
49
- 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
+ }
50
117
 
51
118
  // ---------- Step 1: Check claude CLI ----------
52
- log.step(1, 4, "Checking claude CLI...");
119
+ log.blank();
120
+ log.step(1, 5, "Checking claude CLI...");
53
121
  const ver = claudeVersion();
54
122
  if (!ver) {
55
123
  log.err("claude CLI not found. Install Claude Code from https://code.claude.com first.");
@@ -57,13 +125,66 @@ export async function install(args = []) {
57
125
  }
58
126
  log.ok(`claude CLI found (${ver})`);
59
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
+
60
181
  // ---------- Step 2: Add marketplace ----------
61
182
  log.blank();
62
183
  const marketplaceSource = useOffline ? PKG_ROOT : "curdx/curdx-flow";
63
184
  const marketplaceLabel = useOffline
64
185
  ? `local npm package (${PKG_ROOT})`
65
186
  : "GitHub curdx/curdx-flow";
66
- log.step(2, 4, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
187
+ log.step(2, 5, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
67
188
 
68
189
  // Remove any existing marketplace with the same name so we get a clean
69
190
  // rebind to the chosen source. Errors are non-fatal (marketplace may
@@ -76,7 +197,7 @@ export async function install(args = []) {
76
197
 
77
198
  const addRes = await run(
78
199
  "claude",
79
- ["plugin", "marketplace", "add", marketplaceSource],
200
+ ["plugin", "marketplace", "add", "--scope", "user", marketplaceSource],
80
201
  { silent: true }
81
202
  );
82
203
  if (addRes.code !== 0 && !addRes.stderr.includes("already")) {
@@ -90,7 +211,7 @@ export async function install(args = []) {
90
211
 
91
212
  // ---------- Step 3: Install curdx-flow plugin ----------
92
213
  log.blank();
93
- log.step(3, 4, "Installing curdx-flow plugin (2 MCPs will auto-start)...");
214
+ log.step(3, 5, "Installing curdx-flow plugin...");
94
215
  // Read the version the marketplace is shipping so we can decide whether an
95
216
  // already-installed plugin needs an update (same name but stale version
96
217
  // previously silently skipped the upgrade — caused the beta.1 → beta.7 drift).
@@ -104,41 +225,157 @@ export async function install(args = []) {
104
225
  // marketplace not local (online install) or unreadable — fall through
105
226
  }
106
227
 
107
- const installed = listPlugins();
108
- const already = installed.find((p) => p.name === "curdx-flow");
109
- 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.
110
239
  log.info(
111
- `curdx-flow installed at v${already.version}, marketplace ships v${shippedVersion} — updating...`
240
+ `curdx-flow already at v${already.version}, re-registering...`
112
241
  );
113
242
  const r = await run(
114
243
  "claude",
115
- ["plugin", "update", "curdx-flow@curdx-flow-marketplace"],
244
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
116
245
  { silent: true }
117
246
  );
118
247
  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}`);
248
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
249
+ process.exit(1);
123
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}`);
124
267
  } else if (already) {
125
- 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`);
126
284
  } else {
127
285
  const r = await run(
128
286
  "claude",
129
- ["plugin", "install", "curdx-flow@curdx-flow-marketplace"],
287
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
130
288
  { silent: true }
131
289
  );
132
290
  if (r.code !== 0) {
133
291
  log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
134
292
  process.exit(1);
135
293
  }
136
- 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
+ }
137
374
  }
138
375
 
139
376
  // ---------- Step 4: Recommended plugins ----------
140
377
  log.blank();
141
- log.step(4, 4, "Recommended plugins");
378
+ log.step(4, 5, "Recommended plugins");
142
379
 
143
380
  if (noDeps) {
144
381
  log.info("Skipping recommended plugins (--no-deps)");
@@ -150,18 +387,32 @@ export async function install(args = []) {
150
387
  if (all) {
151
388
  toInstall = RECOMMENDED.map((r) => r.name);
152
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`);
153
397
  } else {
154
398
  const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
155
- const choices = RECOMMENDED.map((r) => ({
156
- label: `${color.bold(r.name)}${currentlyInstalled.has(r.name) ? color.green(" (installed)") : ""}`,
399
+ const options = RECOMMENDED.map((r) => ({
157
400
  value: r.name,
401
+ label: `${r.name}${currentlyInstalled.has(r.name) ? " (installed)" : ""}`,
158
402
  hint: r.hint,
159
403
  }));
160
- const defaults = RECOMMENDED
161
- .map((r, i) => (currentlyInstalled.has(r.name) ? -1 : i))
162
- .filter((i) => i >= 0);
404
+ const initialValues = RECOMMENDED
405
+ .filter((r) => !currentlyInstalled.has(r.name))
406
+ .map((r) => r.name);
163
407
 
164
- 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
+ });
165
416
  }
166
417
 
167
418
  if (!toInstall || toInstall.length === 0) {
@@ -174,13 +425,13 @@ export async function install(args = []) {
174
425
  for (const pluginName of toInstall) {
175
426
  const rec = RECOMMENDED.find((r) => r.name === pluginName);
176
427
  log.blank();
177
- console.log(` ${color.cyan("")} Installing ${color.bold(rec.name)}...`);
428
+ console.log(` ${color.cyan("")} Installing ${color.bold(rec.name)}...`);
178
429
 
179
430
  // 1. Add marketplace (if needed)
180
- if (rec.marketplace) {
431
+ if (rec.marketplaceSource) {
181
432
  const ma = await run(
182
433
  "claude",
183
- ["plugin", "marketplace", "add", rec.marketplace],
434
+ ["plugin", "marketplace", "add", "--scope", rec.scope, rec.marketplaceSource],
184
435
  { silent: true }
185
436
  );
186
437
  if (ma.code !== 0 && !ma.stderr.includes("already")) {
@@ -190,7 +441,7 @@ export async function install(args = []) {
190
441
  }
191
442
 
192
443
  // 2. Install
193
- const ir = await run("claude", ["plugin", "install", rec.installSpec], {
444
+ const ir = await run("claude", ["plugin", "install", "--scope", rec.scope, rec.installSpec], {
194
445
  silent: true,
195
446
  });
196
447
  if (ir.code === 0) {
@@ -224,7 +475,7 @@ export async function install(args = []) {
224
475
  );
225
476
  console.log(
226
477
  color.dim(
227
- ` Run manually: claude plugin install ${rec.installSpec}`
478
+ ` Run manually: claude plugin install --scope ${rec.scope} ${rec.installSpec}`
228
479
  )
229
480
  );
230
481
  }
@@ -232,7 +483,7 @@ export async function install(args = []) {
232
483
 
233
484
  // ---------- Step 5: inject global protocols ----------
234
485
  log.blank();
235
- console.log(color.dim("Injecting global protocols into ~/.claude/CLAUDE.md..."));
486
+ log.step(5, 5, "Injecting global protocols into ~/.claude/CLAUDE.md...");
236
487
  try {
237
488
  const r = injectGlobalProtocols();
238
489
  if (r.action === "created") {
@@ -251,13 +502,155 @@ export async function install(args = []) {
251
502
  printNextSteps();
252
503
  }
253
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
+
254
647
  function printNextSteps() {
255
648
  // Detect whether the CLI is globally installed (curdx-flow on PATH) or
256
649
  // the user ran us via npx. Tell them the right invocation each time.
257
650
  const cliOnPath = has("curdx-flow");
258
651
  const cliCmd = cliOnPath ? "curdx-flow" : "npx @curdx/flow";
259
652
 
260
- console.log(`\n${color.bold(" Install complete")}\n`);
653
+ console.log(`\n${color.bold(`${color.green("✓")} Install complete`)}\n`);
261
654
  console.log(`${color.bold("Restart Claude Code")} so the plugin registers all its commands and hooks.\n`);
262
655
  console.log(`${color.bold("Next steps")}:\n`);
263
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`.
package/cli/protocols.js CHANGED
@@ -14,41 +14,32 @@ import {
14
14
  unlinkSync,
15
15
  } from "node:fs";
16
16
  import { join, dirname } from "node:path";
17
-
18
- const HOME = process.env.HOME || "";
17
+ import { homedir } from "node:os";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ // Use os.homedir() instead of process.env.HOME — HOME can be empty inside
21
+ // non-login shells (CI containers, some spawned child envs), which would
22
+ // resolve GLOBAL_CLAUDE_MD to "/.claude/CLAUDE.md" (filesystem root) and
23
+ // cause mkdir/writeFileSync to fail with EACCES. homedir() falls back to
24
+ // the effective user's passwd entry on POSIX and USERPROFILE on Windows.
25
+ const HOME = homedir();
19
26
  export const GLOBAL_CLAUDE_MD = join(HOME, ".claude", "CLAUDE.md");
20
27
 
21
28
  const SENTINEL_BEGIN =
22
29
  "<!-- BEGIN curdx-flow protocols (auto-managed; do not edit between sentinels) -->";
23
30
  const SENTINEL_END = "<!-- END curdx-flow protocols -->";
24
31
 
25
- // Protocol block is itself in English it's instructions for the model,
26
- // not user-facing prose. User chat output is still Chinese per the rule below.
27
- const PROTOCOL_BODY = `
28
- ## Global Protocols (curdx-flow)
29
-
30
- All operations MUST strictly follow these system constraints:
31
-
32
- ### Language separation
33
- - **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.
34
- - **Conversational layer = Simplified Chinese**: chat replies, explanations and reasoning shown directly to the human in a conversation interface (e.g. Claude Code chat).
35
-
36
- 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.
37
-
38
- ### Discovery & reasoning
39
- - **Library / framework / API questions**: query \`context7\` MCP first. Do not rely on training memory.
40
- - **Planning / design / architecture review / epic decomposition**: use \`sequential-thinking\` MCP with at least 5 thoughts.
41
- - **Cross-session memory**: query \`claude-mem\` MCP at task start when available.
42
-
43
- ### Three red lines (inherited from pua)
44
- 1. **Closed loop**: claiming "done"? Provide evidence (build output / passing tests / curl result).
45
- 2. **Fact-driven**: before saying "probably an env issue", verify it. Unverified attribution = blame-shifting.
46
- 3. **Exhaust everything**: before saying "I cannot", complete the systematic 4-stage debugging.
47
-
48
- > Source: curdx-flow CLI installer (\`curdx-flow install\`). Remove with: \`curdx-flow uninstall --purge\`.
49
- `;
50
-
51
- const FULL_BLOCK = `${SENTINEL_BEGIN}\n${PROTOCOL_BODY.trim()}\n${SENTINEL_END}`;
32
+ // Protocol body lives in a sibling markdown file so it keeps markdown tooling
33
+ // (preview, lint, prettier) and avoids backtick-escaping noise inside a JS
34
+ // template literal. The body itself is English — it's instructions for the
35
+ // model, not user-facing prose.
36
+ const __dirname = dirname(fileURLToPath(import.meta.url));
37
+ const PROTOCOL_BODY = readFileSync(
38
+ join(__dirname, "protocols-body.md"),
39
+ "utf-8"
40
+ ).trim();
41
+
42
+ const FULL_BLOCK = `${SENTINEL_BEGIN}\n${PROTOCOL_BODY}\n${SENTINEL_END}`;
52
43
 
53
44
  /**
54
45
  * Read existing CLAUDE.md content; return "" if missing.