@curdx/flow 2.0.6 → 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.
@@ -0,0 +1,251 @@
1
+ import { color, ensureClaudeMemRuntimes, listPlugins, log, multiselectClack, note, run, text, writeConfig } from "./utils.js";
2
+ import { BUNDLED_MCPS, RECOMMENDED_PLUGINS, REQUIRED_PLUGINS } from "./registry.js";
3
+ import { readUserMcpConfig } from "./utils.js";
4
+
5
+ const RECOMMENDED = RECOMMENDED_PLUGINS;
6
+
7
+ export async function installRequiredPlugins({ yes, language, config }) {
8
+ log.blank();
9
+ log.info("Installing required Claude Code plugins...");
10
+ for (const plugin of REQUIRED_PLUGINS) {
11
+ console.log(` ${color.cyan("▸")} Installing ${color.bold(plugin.name)}...`);
12
+ const ma = await run(
13
+ "claude",
14
+ ["plugin", "marketplace", "add", "--scope", plugin.scope, plugin.marketplaceSource],
15
+ { silent: true }
16
+ );
17
+ if (ma.code !== 0 && !ma.stderr.includes("already")) {
18
+ log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
19
+ }
20
+
21
+ const ir = await run(
22
+ "claude",
23
+ ["plugin", "install", "--scope", plugin.scope, plugin.installSpec],
24
+ { silent: true }
25
+ );
26
+ if (ir.code === 0) {
27
+ console.log(` ${color.green("✓")} ${plugin.name} installed`);
28
+
29
+ if (plugin.requiresConfig && plugin.configType === "apiKey" && !yes) {
30
+ await promptPluginConfig(plugin, language, config);
31
+ }
32
+ } else {
33
+ console.log(
34
+ ` ${color.red("✗")} ${plugin.name} install failed: ${ir.stderr.trim().split("\n").pop()}`
35
+ );
36
+ console.log(
37
+ color.dim(
38
+ ` Run manually: claude plugin marketplace add --scope ${plugin.scope} ${plugin.marketplaceSource}`
39
+ )
40
+ );
41
+ console.log(
42
+ color.dim(
43
+ ` Then: claude plugin install --scope ${plugin.scope} ${plugin.installSpec}`
44
+ )
45
+ );
46
+ }
47
+ }
48
+ }
49
+
50
+ export async function registerBundledMcps() {
51
+ log.blank();
52
+ log.info("Registering required MCP servers (user-level)...");
53
+ const existingUserMcps = readUserMcpConfig();
54
+ for (const mcp of BUNDLED_MCPS) {
55
+ if (mcp.preserveExisting && existingUserMcps.has(mcp.name)) {
56
+ const existing = existingUserMcps.get(mcp.name);
57
+ log.info(
58
+ ` ${mcp.name.padEnd(22)} ${color.dim(`already registered (${(existing.args || []).join(" ")}) — preserving`)}`
59
+ );
60
+ continue;
61
+ }
62
+ const r = await run(
63
+ "claude",
64
+ ["mcp", "add", "--scope", "user", mcp.name, "--", mcp.command, ...mcp.args],
65
+ { silent: true }
66
+ );
67
+ if (r.code === 0) {
68
+ log.ok(` ${mcp.name.padEnd(22)} ${color.dim("registered")}`);
69
+ } else if (r.stderr.includes("already exists")) {
70
+ log.info(` ${mcp.name.padEnd(22)} ${color.dim("already exists — skipped")}`);
71
+ } else {
72
+ log.warn(
73
+ ` ${mcp.name.padEnd(22)} registration failed: ${r.stderr.trim().split("\n").pop()}`
74
+ );
75
+ log.info(
76
+ ` Run manually: claude mcp add --scope user ${mcp.name} -- ${mcp.command} ${mcp.args.join(" ")}`
77
+ );
78
+ }
79
+ }
80
+ }
81
+
82
+ export async function installRecommendedPlugins({ all, yes, language }) {
83
+ log.blank();
84
+ log.step(4, 5, "Recommended plugins");
85
+
86
+ let toInstall;
87
+ if (all) {
88
+ toInstall = RECOMMENDED.map((r) => r.name);
89
+ log.info("--all mode: installing all recommended");
90
+ } else if (yes) {
91
+ const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
92
+ toInstall = RECOMMENDED
93
+ .filter((r) => !currentlyInstalled.has(r.name))
94
+ .map((r) => r.name);
95
+ log.info(`--yes mode: installing ${toInstall.length} recommended plugins`);
96
+ } else {
97
+ const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
98
+ const options = RECOMMENDED.map((r) => ({
99
+ value: r.name,
100
+ label: `${r.name}${currentlyInstalled.has(r.name) ? " (installed)" : ""}`,
101
+ hint: r.hint,
102
+ }));
103
+ const initialValues = RECOMMENDED.map((r) => r.name);
104
+
105
+ toInstall = await multiselectClack({
106
+ message: language === "zh"
107
+ ? "选择要安装的推荐插件(空格切换,回车确认)"
108
+ : "Select recommended plugins to install (space to toggle, enter to confirm)",
109
+ options,
110
+ initialValues,
111
+ required: false,
112
+ });
113
+ }
114
+
115
+ if (!toInstall || toInstall.length === 0) {
116
+ log.info("No recommended plugins selected, skipping");
117
+ return false;
118
+ }
119
+
120
+ for (const pluginName of toInstall) {
121
+ const rec = RECOMMENDED.find((r) => r.name === pluginName);
122
+ log.blank();
123
+ console.log(` ${color.cyan("▸")} Installing ${color.bold(rec.name)}...`);
124
+
125
+ if (rec.marketplaceSource) {
126
+ const ma = await run(
127
+ "claude",
128
+ ["plugin", "marketplace", "add", "--scope", rec.scope, rec.marketplaceSource],
129
+ { silent: true }
130
+ );
131
+ if (ma.code !== 0 && !ma.stderr.includes("already")) {
132
+ log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
133
+ }
134
+ }
135
+
136
+ const ir = await run("claude", ["plugin", "install", "--scope", rec.scope, rec.installSpec], {
137
+ silent: true,
138
+ });
139
+ if (ir.code === 0) {
140
+ console.log(` ${color.green("✓")} ${rec.name} installed`);
141
+
142
+ if (rec.postInstall === "claude-mem-runtimes") {
143
+ const r = ensureClaudeMemRuntimes();
144
+ for (const [name, res] of Object.entries(r)) {
145
+ if (res.status === "linked") {
146
+ console.log(
147
+ ` ${color.green("✓")} ${name} → PATH symlink created ${color.dim(`(${res.link} → ${res.path})`)}`
148
+ );
149
+ } else if (res.status === "missing") {
150
+ console.log(
151
+ ` ${color.yellow("⚠")} ${name} not installed ${color.dim("(claude-mem will auto-install on first run; or run: curdx-flow doctor)")}`
152
+ );
153
+ } else if (res.status === "path-unwritable") {
154
+ console.log(
155
+ ` ${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`)}`
156
+ );
157
+ }
158
+ }
159
+ }
160
+ } else {
161
+ console.log(
162
+ ` ${color.red("✗")} ${rec.name} install failed: ${ir.stderr.trim().split("\n").pop()}`
163
+ );
164
+ console.log(
165
+ color.dim(
166
+ ` Run manually: claude plugin install --scope ${rec.scope} ${rec.installSpec}`
167
+ )
168
+ );
169
+ }
170
+ }
171
+ return true;
172
+ }
173
+
174
+ async function promptPluginConfig(plugin, language, config) {
175
+ if (plugin.name !== "context7-plugin") return;
176
+
177
+ log.blank();
178
+ await note(
179
+ language === "zh"
180
+ ? "Context7 需要 API key 才能使用。\n获取 API key: https://console.upstash.com/context7"
181
+ : "Context7 requires an API key to function.\nGet your API key at: https://console.upstash.com/context7",
182
+ language === "zh" ? "配置 Context7" : "Configure Context7"
183
+ );
184
+
185
+ const apiKey = await text({
186
+ message: language === "zh"
187
+ ? "输入你的 Context7 API key(或按 Enter 跳过)"
188
+ : "Enter your Context7 API key (or press Enter to skip)",
189
+ placeholder: "ctx7sk-...",
190
+ validate: (value) => {
191
+ if (!value) return;
192
+ if (!value.startsWith("ctx7sk-") && !value.startsWith("ctx7_")) {
193
+ return language === "zh"
194
+ ? "API key 应该以 ctx7sk- 或 ctx7_ 开头"
195
+ : "API key should start with ctx7sk- or ctx7_";
196
+ }
197
+ },
198
+ });
199
+
200
+ if (apiKey) {
201
+ config.context7ApiKey = apiKey;
202
+ writeConfig(config);
203
+
204
+ const r = await run(
205
+ "claude",
206
+ ["mcp", "add", "--scope", "user", "context7", "--env", `CONTEXT7_API_KEY=${apiKey}`, "--", "npx", "-y", "@upstash/context7-mcp"],
207
+ { silent: true }
208
+ );
209
+
210
+ if (r.code === 0) {
211
+ log.ok(
212
+ language === "zh"
213
+ ? " Context7 API key 已配置"
214
+ : " Context7 API key configured"
215
+ );
216
+ } else if (r.stderr.includes("already exists")) {
217
+ await run("claude", ["mcp", "remove", "--scope", "user", "context7"], { silent: true });
218
+ const r2 = await run(
219
+ "claude",
220
+ ["mcp", "add", "--scope", "user", "context7", "--env", `CONTEXT7_API_KEY=${apiKey}`, "--", "npx", "-y", "@upstash/context7-mcp"],
221
+ { silent: true }
222
+ );
223
+ if (r2.code === 0) {
224
+ log.ok(
225
+ language === "zh"
226
+ ? " Context7 API key 已更新"
227
+ : " Context7 API key updated"
228
+ );
229
+ }
230
+ } else {
231
+ log.warn(
232
+ language === "zh"
233
+ ? ` Context7 MCP 配置失败: ${r.stderr.trim().split("\n").pop()}`
234
+ : ` Context7 MCP configuration failed: ${r.stderr.trim().split("\n").pop()}`
235
+ );
236
+ log.info(
237
+ color.dim(
238
+ language === "zh"
239
+ ? ` 手动运行: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- npx -y @upstash/context7-mcp`
240
+ : ` Run manually: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- npx -y @upstash/context7-mcp`
241
+ )
242
+ );
243
+ }
244
+ } else {
245
+ log.info(
246
+ language === "zh"
247
+ ? " 跳过 Context7 配置(稍后可运行 curdx-flow install 重新配置)"
248
+ : " Skipped Context7 configuration (run curdx-flow install later to reconfigure)"
249
+ );
250
+ }
251
+ }
@@ -0,0 +1,102 @@
1
+ import { color, log, run } from "./utils.js";
2
+
3
+ export async function addCurdxMarketplace({ marketplaceSource, marketplaceLabel, useOffline }) {
4
+ log.blank();
5
+ log.step(2, 5, `Adding curdx-flow marketplace from ${marketplaceLabel}...`);
6
+
7
+ // Remove any existing marketplace with the same name so we get a clean
8
+ // rebind to the chosen source. Errors are non-fatal (marketplace may
9
+ // simply not exist yet).
10
+ await run(
11
+ "claude",
12
+ ["plugin", "marketplace", "remove", "curdx-flow-marketplace"],
13
+ { silent: true }
14
+ );
15
+
16
+ const addRes = await run(
17
+ "claude",
18
+ ["plugin", "marketplace", "add", "--scope", "user", marketplaceSource],
19
+ { silent: true }
20
+ );
21
+ if (addRes.code !== 0 && !addRes.stderr.includes("already")) {
22
+ log.warn(`marketplace add output: ${addRes.stderr.trim() || addRes.stdout.trim()}`);
23
+ } else {
24
+ log.ok(
25
+ `curdx-flow-marketplace added ${color.dim(useOffline ? "(offline, no GitHub fetch)" : "(from GitHub)")}`
26
+ );
27
+ }
28
+ }
29
+
30
+ export async function installCurdxFlowPlugin({ prevCurdxFlow, shippedVersion }) {
31
+ log.blank();
32
+ log.step(3, 5, "Installing curdx-flow plugin...");
33
+
34
+ // Use the pre-Step-2 snapshot — by this point `claude plugin marketplace
35
+ // remove` has already evicted the plugin, so listPlugins() here would
36
+ // always return undefined for curdx-flow and we'd mis-report "installed"
37
+ // when we actually upgraded (the bug reported by @wdx's beta.14 log).
38
+ const already = prevCurdxFlow;
39
+
40
+ if (already && shippedVersion && already.version === shippedVersion) {
41
+ // Step 2 removes and re-adds the marketplace to rebind it to the current
42
+ // source. Claude Code removes plugins installed from that marketplace as
43
+ // part of `marketplace remove`, so even a same-version install must be
44
+ // re-registered here.
45
+ log.info(
46
+ `curdx-flow already at v${already.version}, re-registering...`
47
+ );
48
+ const r = await run(
49
+ "claude",
50
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
51
+ { silent: true }
52
+ );
53
+ if (r.code !== 0) {
54
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
55
+ process.exit(1);
56
+ }
57
+ log.ok(`curdx-flow re-registered at v${shippedVersion}`);
58
+ } else if (already && shippedVersion) {
59
+ log.info(
60
+ `curdx-flow v${already.version} → v${shippedVersion}, installing...`
61
+ );
62
+ const r = await run(
63
+ "claude",
64
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
65
+ { silent: true }
66
+ );
67
+ if (r.code !== 0) {
68
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
69
+ process.exit(1);
70
+ }
71
+ log.ok(`curdx-flow upgraded to v${shippedVersion}`);
72
+ } else if (already) {
73
+ log.info(
74
+ `curdx-flow v${already.version} detected, re-registering...`
75
+ );
76
+ const r = await run(
77
+ "claude",
78
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
79
+ { silent: true }
80
+ );
81
+ if (r.code !== 0) {
82
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
83
+ process.exit(1);
84
+ }
85
+ log.ok("curdx-flow re-registered");
86
+ } else {
87
+ const r = await run(
88
+ "claude",
89
+ ["plugin", "install", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
90
+ { silent: true }
91
+ );
92
+ if (r.code !== 0) {
93
+ log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
94
+ process.exit(1);
95
+ }
96
+ if (shippedVersion) {
97
+ log.ok(`curdx-flow v${shippedVersion} installed`);
98
+ } else {
99
+ log.ok("curdx-flow installed");
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,35 @@
1
+ import { log, readConfig, select, writeConfig } from "./utils.js";
2
+
3
+ export async function resolveInstallLanguage({ yes }) {
4
+ const config = readConfig();
5
+ let language = config.language;
6
+
7
+ if (!language && !yes) {
8
+ log.blank();
9
+ language = await select({
10
+ message: "Choose your preferred language / 选择语言",
11
+ options: [
12
+ {
13
+ value: "en",
14
+ label: "English",
15
+ hint: "CLI output and documentation in English",
16
+ },
17
+ {
18
+ value: "zh",
19
+ label: "简体中文",
20
+ hint: "CLI 输出和文档使用简体中文",
21
+ },
22
+ ],
23
+ initialValue: "en",
24
+ });
25
+ config.language = language;
26
+ writeConfig(config);
27
+ log.ok(`Language set to ${language === "zh" ? "简体中文" : "English"}`);
28
+ } else if (!language) {
29
+ language = "en";
30
+ config.language = language;
31
+ writeConfig(config);
32
+ }
33
+
34
+ return { language, config };
35
+ }
@@ -0,0 +1,25 @@
1
+ import { color, has } from "./utils.js";
2
+
3
+ export function printNextSteps() {
4
+ const cliOnPath = has("curdx-flow");
5
+ const cliCmd = cliOnPath ? "curdx-flow" : "npx @curdx/flow";
6
+
7
+ console.log(`\n${color.bold(`${color.green("✓")} Install complete`)}\n`);
8
+ console.log(`${color.bold("Restart Claude Code")} so the plugin registers all its commands and hooks.\n`);
9
+ console.log(`${color.bold("Next steps")}:\n`);
10
+ console.log(` ${color.dim("# Verify health")}`);
11
+ console.log(` ${cliCmd} doctor\n`);
12
+ console.log(` ${color.dim("# Inside any project, initialize and start a feature spec")}`);
13
+ console.log(` ${color.cyan("cd ~/your-project")}`);
14
+ console.log(` ${color.cyan("claude")}`);
15
+ console.log(` ${color.cyan("/curdx-flow:init")}`);
16
+ console.log(` ${color.cyan("/curdx-flow:start my-feature \"<one-line goal>\"")}\n`);
17
+ if (!cliOnPath) {
18
+ console.log(
19
+ `${color.dim("Tip: install the CLI globally for shorter commands —")} ${color.cyan("npm i -g @curdx/flow")}\n`
20
+ );
21
+ }
22
+ console.log(
23
+ `${color.bold("Learn more")}: https://github.com/curdx/curdx-flow/blob/main/docs/getting-started.md\n`
24
+ );
25
+ }
@@ -0,0 +1,9 @@
1
+ export function parseInstallOptions(args = []) {
2
+ return {
3
+ all: args.includes("--all"),
4
+ noDeps: args.includes("--no-deps"),
5
+ forceOnline: args.includes("--online") || args.includes("--from-github"),
6
+ yes: args.includes("--yes") || args.includes("-y"),
7
+ skipSelfUpdate: args.includes("--skip-self-update"),
8
+ };
9
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // When installed via npm, this CLI file lives at <pkg-root>/cli/*.js.
6
+ // The npm package bundles the full plugin body (.claude-plugin/, agents/,
7
+ // commands/, etc.) so install can register <pkg-root> as a local marketplace
8
+ // and avoid fetching anything from GitHub.
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ export const PKG_ROOT = dirname(__dirname);
11
+ export const LOCAL_MARKETPLACE_MANIFEST = join(
12
+ PKG_ROOT,
13
+ ".claude-plugin",
14
+ "marketplace.json"
15
+ );
16
+
17
+ export function shouldUseOfflineInstall({ forceOnline }) {
18
+ return !forceOnline && existsSync(LOCAL_MARKETPLACE_MANIFEST);
19
+ }
20
+
21
+ export function getMarketplaceSource(useOffline) {
22
+ return useOffline ? PKG_ROOT : "curdx/curdx-flow";
23
+ }
24
+
25
+ export function getMarketplaceLabel(useOffline) {
26
+ return useOffline
27
+ ? `local npm package (${PKG_ROOT})`
28
+ : "GitHub curdx/curdx-flow";
29
+ }
30
+
31
+ export function readShippedVersion() {
32
+ try {
33
+ const mf = JSON.parse(readFileSync(LOCAL_MARKETPLACE_MANIFEST, "utf-8"));
34
+ return mf?.metadata?.version || null;
35
+ } catch {
36
+ // marketplace not local (online install) or unreadable
37
+ return null;
38
+ }
39
+ }
@@ -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 { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
31
- import { readUserMcpConfig } from "./utils.js";
32
-
33
- // When installed via npm, this CLI file lives at <pkg-root>/cli/install.js.
34
- // The npm package bundles the full plugin body (.claude-plugin/, agents/,
35
- // commands/, etc.) so we can register <pkg-root> as a local marketplace
36
- // and avoid fetching anything from GitHub. This makes `curdx-flow install`
37
- // fast & offline-capable, particularly important for users behind restricted
38
- // network egress (great firewalls, air-gapped environments).
39
- const __dirname = dirname(fileURLToPath(import.meta.url));
40
- const PKG_ROOT = dirname(__dirname);
41
- const LOCAL_MARKETPLACE_MANIFEST = join(PKG_ROOT, ".claude-plugin", "marketplace.json");
42
-
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;
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.includes("--all");
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
- // Default to offline install when the npm package includes the full plugin
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 = 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
- }
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
- // ---------- Step 2: Add marketplace ----------
182
- log.blank();
183
- const marketplaceSource = useOffline ? PKG_ROOT : "curdx/curdx-flow";
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
- let shippedVersion = null;
219
- try {
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
- 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
- }
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
- 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
- }
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
- let toInstall;
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "CLI installer for CurdX-Flow — AI engineering workflow meta-framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {