@curdx/flow 2.0.4 → 2.0.7
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/plugin.json +1 -1
- package/agents/flow-architect.md +38 -18
- package/agents/flow-planner.md +32 -0
- package/agents/flow-product-designer.md +31 -0
- package/agents/flow-researcher.md +30 -0
- package/cli/install-companions.js +251 -0
- package/cli/install-curdx-plugin.js +102 -0
- package/cli/install-language.js +35 -0
- package/cli/install-next-steps.js +25 -0
- package/cli/install-options.js +9 -0
- package/cli/install-paths.js +39 -0
- package/cli/install-self-update.js +57 -0
- package/cli/install.js +33 -523
- package/commands/spec.md +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { log, run, runSync, VERSION } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check for CLI updates and auto-update if available.
|
|
8
|
+
* @returns {Promise<{updated: boolean, version?: string}>}
|
|
9
|
+
*/
|
|
10
|
+
export async function checkAndUpdateSelf() {
|
|
11
|
+
try {
|
|
12
|
+
const globalPath = runSync("npm", ["root", "-g"]).stdout.trim();
|
|
13
|
+
const installedPath = join(globalPath, "@curdx/flow");
|
|
14
|
+
|
|
15
|
+
if (!existsSync(installedPath)) {
|
|
16
|
+
return { updated: false };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
log.info("Checking for CLI updates...");
|
|
20
|
+
const res = runSync("npm", ["view", "@curdx/flow", "version"]);
|
|
21
|
+
|
|
22
|
+
if (res.code !== 0) {
|
|
23
|
+
return { updated: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const latestVersion = res.stdout.trim();
|
|
27
|
+
const currentVersion = VERSION;
|
|
28
|
+
|
|
29
|
+
if (latestVersion === currentVersion) {
|
|
30
|
+
log.ok(`CLI is up to date (v${currentVersion})`);
|
|
31
|
+
return { updated: false };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Existing behavior used simple string comparison; keep it unchanged for
|
|
35
|
+
// this structural refactor.
|
|
36
|
+
if (latestVersion > currentVersion) {
|
|
37
|
+
log.info(`New version available: v${currentVersion} → v${latestVersion}`);
|
|
38
|
+
log.info("Updating CLI...");
|
|
39
|
+
|
|
40
|
+
const updateRes = await run("npm", ["install", "-g", "@curdx/flow@latest"], {
|
|
41
|
+
silent: false,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (updateRes.code === 0) {
|
|
45
|
+
log.ok(`CLI updated to v${latestVersion}`);
|
|
46
|
+
return { updated: true, version: latestVersion };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log.warn("CLI update failed, continuing with current version");
|
|
50
|
+
return { updated: false };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { updated: false };
|
|
54
|
+
} catch {
|
|
55
|
+
return { updated: false };
|
|
56
|
+
}
|
|
57
|
+
}
|
package/cli/install.js
CHANGED
|
@@ -2,56 +2,37 @@
|
|
|
2
2
|
* install command — install curdx-flow plugin + optional recommended plugins.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { dirname, join } from "node:path";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
|
|
9
5
|
import {
|
|
10
6
|
color,
|
|
11
7
|
log,
|
|
12
|
-
run,
|
|
13
|
-
has,
|
|
14
8
|
claudeVersion,
|
|
15
9
|
listPlugins,
|
|
16
|
-
multiSelect,
|
|
17
|
-
ensureClaudeMemRuntimes,
|
|
18
10
|
select,
|
|
19
11
|
intro,
|
|
20
|
-
outro,
|
|
21
|
-
readConfig,
|
|
22
|
-
writeConfig,
|
|
23
|
-
text,
|
|
24
|
-
note,
|
|
25
|
-
multiselectClack,
|
|
26
|
-
runSync,
|
|
27
|
-
VERSION,
|
|
28
12
|
} from "./utils.js";
|
|
29
13
|
import { injectGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
14
|
+
import {
|
|
15
|
+
installRecommendedPlugins,
|
|
16
|
+
installRequiredPlugins,
|
|
17
|
+
registerBundledMcps,
|
|
18
|
+
} from "./install-companions.js";
|
|
19
|
+
import {
|
|
20
|
+
addCurdxMarketplace,
|
|
21
|
+
installCurdxFlowPlugin,
|
|
22
|
+
} from "./install-curdx-plugin.js";
|
|
23
|
+
import { resolveInstallLanguage } from "./install-language.js";
|
|
24
|
+
import { printNextSteps } from "./install-next-steps.js";
|
|
25
|
+
import { parseInstallOptions } from "./install-options.js";
|
|
26
|
+
import {
|
|
27
|
+
getMarketplaceLabel,
|
|
28
|
+
getMarketplaceSource,
|
|
29
|
+
readShippedVersion,
|
|
30
|
+
shouldUseOfflineInstall,
|
|
31
|
+
} from "./install-paths.js";
|
|
32
|
+
import { checkAndUpdateSelf } from "./install-self-update.js";
|
|
48
33
|
|
|
49
34
|
export async function install(args = []) {
|
|
50
|
-
const all = args
|
|
51
|
-
const noDeps = args.includes("--no-deps");
|
|
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");
|
|
35
|
+
const { all, noDeps, forceOnline, yes, skipSelfUpdate } = parseInstallOptions(args);
|
|
55
36
|
|
|
56
37
|
// ---------- Step 0: Self-update check ----------
|
|
57
38
|
if (!skipSelfUpdate) {
|
|
@@ -70,11 +51,7 @@ export async function install(args = []) {
|
|
|
70
51
|
}
|
|
71
52
|
}
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
// body (since 1.1.5). Fall back to GitHub only if the local manifest is
|
|
75
|
-
// absent (i.e. running this CLI from an older bundle without plugin body)
|
|
76
|
-
// or the user explicitly passes --online.
|
|
77
|
-
const useOffline = !forceOnline && existsSync(LOCAL_MARKETPLACE_MANIFEST);
|
|
54
|
+
const useOffline = shouldUseOfflineInstall({ forceOnline });
|
|
78
55
|
|
|
79
56
|
// Use @clack intro only in interactive TTY mode
|
|
80
57
|
if (process.stdout.isTTY && !yes) {
|
|
@@ -84,36 +61,7 @@ export async function install(args = []) {
|
|
|
84
61
|
}
|
|
85
62
|
|
|
86
63
|
// ---------- Step 0: Language selection ----------
|
|
87
|
-
const config =
|
|
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
|
-
}
|
|
64
|
+
const { language, config } = await resolveInstallLanguage({ yes });
|
|
117
65
|
|
|
118
66
|
// ---------- Step 1: Check claude CLI ----------
|
|
119
67
|
log.blank();
|
|
@@ -178,308 +126,38 @@ export async function install(args = []) {
|
|
|
178
126
|
}
|
|
179
127
|
}
|
|
180
128
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const marketplaceLabel = useOffline
|
|
185
|
-
? `local npm package (${PKG_ROOT})`
|
|
186
|
-
: "GitHub curdx/curdx-flow";
|
|
187
|
-
log.step(2, 5, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
|
|
188
|
-
|
|
189
|
-
// Remove any existing marketplace with the same name so we get a clean
|
|
190
|
-
// rebind to the chosen source. Errors are non-fatal (marketplace may
|
|
191
|
-
// simply not exist yet).
|
|
192
|
-
await run(
|
|
193
|
-
"claude",
|
|
194
|
-
["plugin", "marketplace", "remove", "curdx-flow-marketplace"],
|
|
195
|
-
{ silent: true }
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
const addRes = await run(
|
|
199
|
-
"claude",
|
|
200
|
-
["plugin", "marketplace", "add", "--scope", "user", marketplaceSource],
|
|
201
|
-
{ silent: true }
|
|
202
|
-
);
|
|
203
|
-
if (addRes.code !== 0 && !addRes.stderr.includes("already")) {
|
|
204
|
-
// Not a fatal error if already added
|
|
205
|
-
log.warn(`marketplace add output: ${addRes.stderr.trim() || addRes.stdout.trim()}`);
|
|
206
|
-
} else {
|
|
207
|
-
log.ok(
|
|
208
|
-
`curdx-flow-marketplace added ${color.dim(useOffline ? "(offline, no GitHub fetch)" : "(from GitHub)")}`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
129
|
+
const marketplaceSource = getMarketplaceSource(useOffline);
|
|
130
|
+
const marketplaceLabel = getMarketplaceLabel(useOffline);
|
|
131
|
+
await addCurdxMarketplace({ marketplaceSource, marketplaceLabel, useOffline });
|
|
211
132
|
|
|
212
133
|
// ---------- Step 3: Install curdx-flow plugin ----------
|
|
213
|
-
log.blank();
|
|
214
|
-
log.step(3, 5, "Installing curdx-flow plugin...");
|
|
215
134
|
// Read the version the marketplace is shipping so we can decide whether an
|
|
216
135
|
// already-installed plugin needs an update (same name but stale version
|
|
217
136
|
// previously silently skipped the upgrade — caused the beta.1 → beta.7 drift).
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const mf = JSON.parse(
|
|
221
|
-
readFileSync(LOCAL_MARKETPLACE_MANIFEST, "utf-8")
|
|
222
|
-
);
|
|
223
|
-
shippedVersion = mf?.metadata?.version || null;
|
|
224
|
-
} catch {
|
|
225
|
-
// marketplace not local (online install) or unreadable — fall through
|
|
226
|
-
}
|
|
227
|
-
|
|
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.
|
|
239
|
-
log.info(
|
|
240
|
-
`curdx-flow already at v${already.version}, re-registering...`
|
|
241
|
-
);
|
|
242
|
-
const r = await run(
|
|
243
|
-
"claude",
|
|
244
|
-
["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
|
|
245
|
-
{ silent: true }
|
|
246
|
-
);
|
|
247
|
-
if (r.code !== 0) {
|
|
248
|
-
log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
|
|
249
|
-
process.exit(1);
|
|
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}`);
|
|
267
|
-
} else if (already) {
|
|
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`);
|
|
284
|
-
} else {
|
|
285
|
-
const r = await run(
|
|
286
|
-
"claude",
|
|
287
|
-
["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
|
|
288
|
-
{ silent: true }
|
|
289
|
-
);
|
|
290
|
-
if (r.code !== 0) {
|
|
291
|
-
log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
|
|
292
|
-
process.exit(1);
|
|
293
|
-
}
|
|
294
|
-
if (shippedVersion) {
|
|
295
|
-
log.ok(`curdx-flow v${shippedVersion} installed`);
|
|
296
|
-
} else {
|
|
297
|
-
log.ok("curdx-flow installed");
|
|
298
|
-
}
|
|
299
|
-
}
|
|
137
|
+
const shippedVersion = readShippedVersion();
|
|
138
|
+
await installCurdxFlowPlugin({ prevCurdxFlow, shippedVersion });
|
|
300
139
|
|
|
301
140
|
// ---------- Step 3.5: Install required plugins + register user-level MCPs ----------
|
|
302
|
-
|
|
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
|
-
}
|
|
141
|
+
await installRequiredPlugins({ yes, language, config });
|
|
343
142
|
|
|
344
143
|
// Beta.12: direct MCPs migrated from plugin.json bundling. See cli/registry.js
|
|
345
144
|
// for the rationale. Context7 now uses Upstash's official plugin instead.
|
|
346
|
-
|
|
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
|
-
}
|
|
374
|
-
}
|
|
145
|
+
await registerBundledMcps();
|
|
375
146
|
|
|
376
147
|
// ---------- Step 4: Recommended plugins ----------
|
|
377
|
-
log.blank();
|
|
378
|
-
log.step(4, 5, "Recommended plugins");
|
|
379
|
-
|
|
380
148
|
if (noDeps) {
|
|
149
|
+
log.blank();
|
|
150
|
+
log.step(4, 5, "Recommended plugins");
|
|
381
151
|
log.info("Skipping recommended plugins (--no-deps)");
|
|
382
152
|
printNextSteps();
|
|
383
153
|
return;
|
|
384
154
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
if (all) {
|
|
388
|
-
toInstall = RECOMMENDED.map((r) => r.name);
|
|
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`);
|
|
397
|
-
} else {
|
|
398
|
-
const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
|
|
399
|
-
const options = RECOMMENDED.map((r) => ({
|
|
400
|
-
value: r.name,
|
|
401
|
-
label: `${r.name}${currentlyInstalled.has(r.name) ? " (installed)" : ""}`,
|
|
402
|
-
hint: r.hint,
|
|
403
|
-
}));
|
|
404
|
-
// Default to ALL plugins (user can deselect what they don't want)
|
|
405
|
-
const initialValues = RECOMMENDED.map((r) => r.name);
|
|
406
|
-
|
|
407
|
-
toInstall = await multiselectClack({
|
|
408
|
-
message: language === "zh"
|
|
409
|
-
? "选择要安装的推荐插件(空格切换,回车确认)"
|
|
410
|
-
: "Select recommended plugins to install (space to toggle, enter to confirm)",
|
|
411
|
-
options,
|
|
412
|
-
initialValues,
|
|
413
|
-
required: false,
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (!toInstall || toInstall.length === 0) {
|
|
418
|
-
log.info("No recommended plugins selected, skipping");
|
|
155
|
+
const installedRecommended = await installRecommendedPlugins({ all, yes, language });
|
|
156
|
+
if (!installedRecommended) {
|
|
419
157
|
printNextSteps();
|
|
420
158
|
return;
|
|
421
159
|
}
|
|
422
160
|
|
|
423
|
-
// Install each
|
|
424
|
-
for (const pluginName of toInstall) {
|
|
425
|
-
const rec = RECOMMENDED.find((r) => r.name === pluginName);
|
|
426
|
-
log.blank();
|
|
427
|
-
console.log(` ${color.cyan("▸")} Installing ${color.bold(rec.name)}...`);
|
|
428
|
-
|
|
429
|
-
// 1. Add marketplace (if needed)
|
|
430
|
-
if (rec.marketplaceSource) {
|
|
431
|
-
const ma = await run(
|
|
432
|
-
"claude",
|
|
433
|
-
["plugin", "marketplace", "add", "--scope", rec.scope, rec.marketplaceSource],
|
|
434
|
-
{ silent: true }
|
|
435
|
-
);
|
|
436
|
-
if (ma.code !== 0 && !ma.stderr.includes("already")) {
|
|
437
|
-
log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
|
|
438
|
-
// Don't abort — may already exist
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// 2. Install
|
|
443
|
-
const ir = await run("claude", ["plugin", "install", "--scope", rec.scope, rec.installSpec], {
|
|
444
|
-
silent: true,
|
|
445
|
-
});
|
|
446
|
-
if (ir.code === 0) {
|
|
447
|
-
console.log(` ${color.green("✓")} ${rec.name} installed`);
|
|
448
|
-
|
|
449
|
-
// 3. Post-install hook for claude-mem: its .mcp.json hard-codes `bun`,
|
|
450
|
-
// but ~/.bun/bin is not on PATH when Claude Code spawns the MCP server.
|
|
451
|
-
// Auto-create a PATH-visible symlink to fix it.
|
|
452
|
-
if (rec.postInstall === "claude-mem-runtimes") {
|
|
453
|
-
const r = ensureClaudeMemRuntimes();
|
|
454
|
-
for (const [name, res] of Object.entries(r)) {
|
|
455
|
-
if (res.status === "linked") {
|
|
456
|
-
console.log(
|
|
457
|
-
` ${color.green("✓")} ${name} → PATH symlink created ${color.dim(`(${res.link} → ${res.path})`)}`
|
|
458
|
-
);
|
|
459
|
-
} else if (res.status === "missing") {
|
|
460
|
-
console.log(
|
|
461
|
-
` ${color.yellow("⚠")} ${name} not installed ${color.dim("(claude-mem will auto-install on first run; or run: curdx-flow doctor)")}`
|
|
462
|
-
);
|
|
463
|
-
} else if (res.status === "path-unwritable") {
|
|
464
|
-
console.log(
|
|
465
|
-
` ${color.yellow("⚠")} ${name} installed ${color.dim(`(${res.path}) but no writable PATH location — add export PATH=\"${res.path.split("/").slice(0,-1).join("/")}:$PATH\" to your shell rc`)}`
|
|
466
|
-
);
|
|
467
|
-
}
|
|
468
|
-
// status === "ok" → already on PATH, stay silent
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
console.log(
|
|
473
|
-
` ${color.red("✗")} ${rec.name} install failed: ${ir.stderr.trim().split("\n").pop()}`
|
|
474
|
-
);
|
|
475
|
-
console.log(
|
|
476
|
-
color.dim(
|
|
477
|
-
` Run manually: claude plugin install --scope ${rec.scope} ${rec.installSpec}`
|
|
478
|
-
)
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
161
|
// ---------- Step 5: inject global protocols ----------
|
|
484
162
|
log.blank();
|
|
485
163
|
log.step(5, 5, "Injecting global protocols into ~/.claude/CLAUDE.md...");
|
|
@@ -500,171 +178,3 @@ export async function install(args = []) {
|
|
|
500
178
|
|
|
501
179
|
printNextSteps();
|
|
502
180
|
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Check for CLI updates and auto-update if available
|
|
506
|
-
* Returns { updated: boolean, version?: string }
|
|
507
|
-
*/
|
|
508
|
-
async function checkAndUpdateSelf() {
|
|
509
|
-
try {
|
|
510
|
-
// Check if globally installed
|
|
511
|
-
const globalPath = runSync("npm", ["root", "-g"]).stdout.trim();
|
|
512
|
-
const installedPath = join(globalPath, "@curdx/flow");
|
|
513
|
-
|
|
514
|
-
if (!existsSync(installedPath)) {
|
|
515
|
-
// Not globally installed, skip update
|
|
516
|
-
return { updated: false };
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Check npm registry for latest version
|
|
520
|
-
log.info("Checking for CLI updates...");
|
|
521
|
-
const res = runSync("npm", ["view", "@curdx/flow", "version"]);
|
|
522
|
-
|
|
523
|
-
if (res.code !== 0) {
|
|
524
|
-
// Registry check failed, continue with current version
|
|
525
|
-
return { updated: false };
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const latestVersion = res.stdout.trim();
|
|
529
|
-
const currentVersion = VERSION;
|
|
530
|
-
|
|
531
|
-
if (latestVersion === currentVersion) {
|
|
532
|
-
log.ok(`CLI is up to date (v${currentVersion})`);
|
|
533
|
-
return { updated: false };
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Compare versions (simple string comparison works for semver)
|
|
537
|
-
if (latestVersion > currentVersion) {
|
|
538
|
-
log.info(`New version available: v${currentVersion} → v${latestVersion}`);
|
|
539
|
-
log.info("Updating CLI...");
|
|
540
|
-
|
|
541
|
-
const updateRes = await run("npm", ["install", "-g", "@curdx/flow@latest"], {
|
|
542
|
-
silent: false,
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
if (updateRes.code === 0) {
|
|
546
|
-
log.ok(`CLI updated to v${latestVersion}`);
|
|
547
|
-
return { updated: true, version: latestVersion };
|
|
548
|
-
} else {
|
|
549
|
-
log.warn("CLI update failed, continuing with current version");
|
|
550
|
-
return { updated: false };
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return { updated: false };
|
|
555
|
-
} catch (err) {
|
|
556
|
-
// Update check failed, continue silently
|
|
557
|
-
return { updated: false };
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Prompt for plugin-specific configuration (e.g., API keys)
|
|
563
|
-
*/
|
|
564
|
-
async function promptPluginConfig(plugin, language, config) {
|
|
565
|
-
if (plugin.name === "context7-plugin") {
|
|
566
|
-
log.blank();
|
|
567
|
-
await note(
|
|
568
|
-
language === "zh"
|
|
569
|
-
? "Context7 需要 API key 才能使用。\n获取 API key: https://console.upstash.com/context7"
|
|
570
|
-
: "Context7 requires an API key to function.\nGet your API key at: https://console.upstash.com/context7",
|
|
571
|
-
language === "zh" ? "配置 Context7" : "Configure Context7"
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
const apiKey = await text({
|
|
575
|
-
message: language === "zh"
|
|
576
|
-
? "输入你的 Context7 API key(或按 Enter 跳过)"
|
|
577
|
-
: "Enter your Context7 API key (or press Enter to skip)",
|
|
578
|
-
placeholder: "ctx7sk-...",
|
|
579
|
-
validate: (value) => {
|
|
580
|
-
if (!value) return; // Allow skip
|
|
581
|
-
if (!value.startsWith("ctx7sk-") && !value.startsWith("ctx7_")) {
|
|
582
|
-
return language === "zh"
|
|
583
|
-
? "API key 应该以 ctx7sk- 或 ctx7_ 开头"
|
|
584
|
-
: "API key should start with ctx7sk- or ctx7_";
|
|
585
|
-
}
|
|
586
|
-
},
|
|
587
|
-
});
|
|
588
|
-
|
|
589
|
-
if (apiKey) {
|
|
590
|
-
// Save to config for future reference
|
|
591
|
-
config.context7ApiKey = apiKey;
|
|
592
|
-
writeConfig(config);
|
|
593
|
-
|
|
594
|
-
// Add to MCP config with environment variable
|
|
595
|
-
const r = await run(
|
|
596
|
-
"claude",
|
|
597
|
-
["mcp", "add", "--scope", "user", "context7", "--env", `CONTEXT7_API_KEY=${apiKey}`, "--", "npx", "-y", "@upstash/context7-mcp"],
|
|
598
|
-
{ silent: true }
|
|
599
|
-
);
|
|
600
|
-
|
|
601
|
-
if (r.code === 0) {
|
|
602
|
-
log.ok(
|
|
603
|
-
language === "zh"
|
|
604
|
-
? " Context7 API key 已配置"
|
|
605
|
-
: " Context7 API key configured"
|
|
606
|
-
);
|
|
607
|
-
} else if (r.stderr.includes("already exists")) {
|
|
608
|
-
// Update existing MCP server
|
|
609
|
-
await run("claude", ["mcp", "remove", "--scope", "user", "context7"], { silent: true });
|
|
610
|
-
const r2 = await run(
|
|
611
|
-
"claude",
|
|
612
|
-
["mcp", "add", "--scope", "user", "context7", "--env", `CONTEXT7_API_KEY=${apiKey}`, "--", "npx", "-y", "@upstash/context7-mcp"],
|
|
613
|
-
{ silent: true }
|
|
614
|
-
);
|
|
615
|
-
if (r2.code === 0) {
|
|
616
|
-
log.ok(
|
|
617
|
-
language === "zh"
|
|
618
|
-
? " Context7 API key 已更新"
|
|
619
|
-
: " Context7 API key updated"
|
|
620
|
-
);
|
|
621
|
-
}
|
|
622
|
-
} else {
|
|
623
|
-
log.warn(
|
|
624
|
-
language === "zh"
|
|
625
|
-
? ` Context7 MCP 配置失败: ${r.stderr.trim().split("\n").pop()}`
|
|
626
|
-
: ` Context7 MCP configuration failed: ${r.stderr.trim().split("\n").pop()}`
|
|
627
|
-
);
|
|
628
|
-
log.info(
|
|
629
|
-
color.dim(
|
|
630
|
-
language === "zh"
|
|
631
|
-
? ` 手动运行: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- npx -y @upstash/context7-mcp`
|
|
632
|
-
: ` Run manually: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- npx -y @upstash/context7-mcp`
|
|
633
|
-
)
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
} else {
|
|
637
|
-
log.info(
|
|
638
|
-
language === "zh"
|
|
639
|
-
? " 跳过 Context7 配置(稍后可运行 curdx-flow install 重新配置)"
|
|
640
|
-
: " Skipped Context7 configuration (run curdx-flow install later to reconfigure)"
|
|
641
|
-
);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function printNextSteps() {
|
|
647
|
-
// Detect whether the CLI is globally installed (curdx-flow on PATH) or
|
|
648
|
-
// the user ran us via npx. Tell them the right invocation each time.
|
|
649
|
-
const cliOnPath = has("curdx-flow");
|
|
650
|
-
const cliCmd = cliOnPath ? "curdx-flow" : "npx @curdx/flow";
|
|
651
|
-
|
|
652
|
-
console.log(`\n${color.bold(`${color.green("✓")} Install complete`)}\n`);
|
|
653
|
-
console.log(`${color.bold("Restart Claude Code")} so the plugin registers all its commands and hooks.\n`);
|
|
654
|
-
console.log(`${color.bold("Next steps")}:\n`);
|
|
655
|
-
console.log(` ${color.dim("# Verify health")}`);
|
|
656
|
-
console.log(` ${cliCmd} doctor\n`);
|
|
657
|
-
console.log(` ${color.dim("# Inside any project, initialize and start a feature spec")}`);
|
|
658
|
-
console.log(` ${color.cyan("cd ~/your-project")}`);
|
|
659
|
-
console.log(` ${color.cyan("claude")}`);
|
|
660
|
-
console.log(` ${color.cyan("/curdx-flow:init")}`);
|
|
661
|
-
console.log(` ${color.cyan("/curdx-flow:start my-feature \"<one-line goal>\"")}\n`);
|
|
662
|
-
if (!cliOnPath) {
|
|
663
|
-
console.log(
|
|
664
|
-
`${color.dim("Tip: install the CLI globally for shorter commands —")} ${color.cyan("npm i -g @curdx/flow")}\n`
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
console.log(
|
|
668
|
-
`${color.bold("Learn more")}: https://github.com/curdx/curdx-flow/blob/main/docs/getting-started.md\n`
|
|
669
|
-
);
|
|
670
|
-
}
|