@curdx/flow 2.0.0-beta.9 → 2.0.1
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +4 -20
- package/CHANGELOG.md +81 -0
- package/README.md +6 -3
- package/README.zh.md +18 -18
- package/bin/curdx-flow.js +30 -1
- package/cli/README.md +8 -6
- package/cli/doctor.js +90 -15
- package/cli/install.js +425 -32
- package/cli/protocols-body.md +21 -0
- package/cli/protocols.js +20 -29
- package/cli/registry.js +64 -14
- package/cli/uninstall.js +101 -7
- package/cli/upgrade.js +1 -1
- package/cli/utils.js +321 -61
- package/commands/implement.md +3 -3
- package/commands/init.md +14 -3
- package/commands/start.md +34 -12
- package/hooks/hooks.json +2 -3
- package/hooks/scripts/inject-karpathy.sh +8 -5
- package/package.json +8 -4
- package/skills/brownfield-index/SKILL.md +1 -1
- package/skills/browser-qa/SKILL.md +1 -1
- package/skills/epic/SKILL.md +1 -1
- package/skills/security-audit/SKILL.md +1 -1
- package/skills/ui-sketch/SKILL.md +1 -1
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
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
240
|
+
`curdx-flow already at v${already.version}, re-registering...`
|
|
112
241
|
);
|
|
113
242
|
const r = await run(
|
|
114
243
|
"claude",
|
|
115
|
-
["plugin", "
|
|
244
|
+
["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
|
|
116
245
|
{ silent: true }
|
|
117
246
|
);
|
|
118
247
|
if (r.code !== 0) {
|
|
119
|
-
log.
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
161
|
-
.
|
|
162
|
-
.
|
|
404
|
+
const initialValues = RECOMMENDED
|
|
405
|
+
.filter((r) => !currentlyInstalled.has(r.name))
|
|
406
|
+
.map((r) => r.name);
|
|
163
407
|
|
|
164
|
-
toInstall = await
|
|
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("
|
|
428
|
+
console.log(` ${color.cyan("▸")} Installing ${color.bold(rec.name)}...`);
|
|
178
429
|
|
|
179
430
|
// 1. Add marketplace (if needed)
|
|
180
|
-
if (rec.
|
|
431
|
+
if (rec.marketplaceSource) {
|
|
181
432
|
const ma = await run(
|
|
182
433
|
"claude",
|
|
183
|
-
["plugin", "marketplace", "add", rec.
|
|
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
|
-
|
|
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: "ctx7sk-...",
|
|
580
|
+
validate: (value) => {
|
|
581
|
+
if (!value) return; // Allow skip
|
|
582
|
+
if (!value.startsWith("ctx7sk-") && !value.startsWith("ctx7_")) {
|
|
583
|
+
return language === "zh"
|
|
584
|
+
? "API key 应该以 ctx7sk- 或 ctx7_ 开头"
|
|
585
|
+
: "API key should start with ctx7sk- or 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("
|
|
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
|
-
|
|
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
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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.
|