@curdx/flow 2.0.18 → 2.0.20

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,209 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import { installRecommendedPlugins } from "./install-companions.js";
4
+ import { printNextSteps } from "./install-next-steps.js";
5
+ import { parseInstallOptions } from "./install-options.js";
6
+ import {
7
+ getMarketplaceLabel,
8
+ getMarketplaceSource,
9
+ shouldUseOfflineInstall,
10
+ } from "./install-paths.js";
11
+ import { injectGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
12
+ import { checkAndUpdateSelf } from "./install-self-update.js";
13
+ import {
14
+ claudeVersion,
15
+ color,
16
+ intro,
17
+ listPlugins,
18
+ log,
19
+ select,
20
+ } from "./utils.js";
21
+
22
+ export const INSTALL_STEP_COUNT = 5;
23
+
24
+ export function createInstallContext(args = []) {
25
+ const options = parseInstallOptions(args);
26
+ const useOffline = shouldUseOfflineInstall({ forceOnline: options.forceOnline });
27
+
28
+ return {
29
+ ...options,
30
+ args,
31
+ useOffline,
32
+ marketplaceSource: getMarketplaceSource(useOffline),
33
+ marketplaceLabel: getMarketplaceLabel(useOffline),
34
+ };
35
+ }
36
+
37
+ export async function maybeRestartWithUpdatedCli(
38
+ context,
39
+ {
40
+ checkAndUpdateSelfImpl = checkAndUpdateSelf,
41
+ spawnImpl = spawn,
42
+ logImpl = log,
43
+ exitImpl = process.exit,
44
+ } = {}
45
+ ) {
46
+ if (context.skipSelfUpdate) {
47
+ return false;
48
+ }
49
+
50
+ const updateResult = await checkAndUpdateSelfImpl();
51
+ if (!updateResult.updated) {
52
+ return false;
53
+ }
54
+
55
+ logImpl.info("Restarting with updated version...");
56
+ const child = spawnImpl(
57
+ "curdx-flow",
58
+ ["install", ...context.args, "--skip-self-update"],
59
+ {
60
+ stdio: "inherit",
61
+ shell: false,
62
+ }
63
+ );
64
+
65
+ await new Promise((resolve) => {
66
+ child.on("close", (code) => {
67
+ exitImpl(code || 0);
68
+ resolve();
69
+ });
70
+ });
71
+
72
+ return true;
73
+ }
74
+
75
+ export async function renderInstallIntro(
76
+ { yes },
77
+ { introImpl = intro, logImpl = log, isTTY = process.stdout.isTTY } = {}
78
+ ) {
79
+ if (isTTY && !yes) {
80
+ await introImpl("🚀 CurdX-Flow Installer");
81
+ return;
82
+ }
83
+
84
+ logImpl.title("🚀 CurdX-Flow Installer");
85
+ }
86
+
87
+ export function ensureClaudeCliAvailable(
88
+ { logImpl = log, claudeVersionImpl = claudeVersion, exitImpl = process.exit } = {}
89
+ ) {
90
+ logImpl.blank();
91
+ logImpl.step(1, INSTALL_STEP_COUNT, "Checking claude CLI...");
92
+
93
+ const version = claudeVersionImpl();
94
+ if (!version) {
95
+ logImpl.err(
96
+ "claude CLI not found. Install Claude Code from https://code.claude.com first."
97
+ );
98
+ exitImpl(1);
99
+ return null;
100
+ }
101
+
102
+ logImpl.ok(`claude CLI found (${version})`);
103
+ return version;
104
+ }
105
+
106
+ export function snapshotCurdxFlowInstall({ listPluginsImpl = listPlugins } = {}) {
107
+ return listPluginsImpl().find((plugin) => plugin.name === "curdx-flow");
108
+ }
109
+
110
+ export async function resolveExistingInstallationAction(
111
+ { prevCurdxFlow, yes, language },
112
+ { selectImpl = select, logImpl = log, exitImpl = process.exit } = {}
113
+ ) {
114
+ if (!prevCurdxFlow || yes) {
115
+ return { action: null, continueInstall: true };
116
+ }
117
+
118
+ logImpl.blank();
119
+ const action = await selectImpl({
120
+ message: language === "zh"
121
+ ? "检测到已安装的版本。如何继续?"
122
+ : "Existing installation detected. How would you like to proceed?",
123
+ options: [
124
+ {
125
+ value: "upgrade",
126
+ label: language === "zh" ? "升级到最新版本" : "Upgrade to latest version",
127
+ hint: language === "zh"
128
+ ? `当前: v${prevCurdxFlow.version}`
129
+ : `Current: v${prevCurdxFlow.version}`,
130
+ },
131
+ {
132
+ value: "reinstall",
133
+ label: language === "zh" ? "重新安装(保留配置)" : "Reinstall (preserve config)",
134
+ },
135
+ {
136
+ value: "reconfigure",
137
+ label: language === "zh" ? "重新配置插件" : "Reconfigure plugins",
138
+ },
139
+ {
140
+ value: "cancel",
141
+ label: language === "zh" ? "取消" : "Cancel",
142
+ },
143
+ ],
144
+ initialValue: "upgrade",
145
+ });
146
+
147
+ if (action === "cancel") {
148
+ logImpl.info(language === "zh" ? "安装已取消" : "Installation cancelled");
149
+ exitImpl(0);
150
+ return { action, continueInstall: false };
151
+ }
152
+
153
+ if (action === "reconfigure") {
154
+ logImpl.info(
155
+ language === "zh"
156
+ ? "重新配置模式:将重新提示插件选择和 API key 配置"
157
+ : "Reconfigure mode: will re-prompt for plugin selection and API key configuration"
158
+ );
159
+ }
160
+
161
+ return { action, continueInstall: true };
162
+ }
163
+
164
+ export async function runRecommendedPluginsStep(
165
+ { noDeps, all, yes, language },
166
+ {
167
+ logImpl = log,
168
+ installRecommendedPluginsImpl = installRecommendedPlugins,
169
+ printNextStepsImpl = printNextSteps,
170
+ } = {}
171
+ ) {
172
+ if (noDeps) {
173
+ logImpl.blank();
174
+ logImpl.step(4, INSTALL_STEP_COUNT, "Recommended plugins");
175
+ logImpl.info("Skipping recommended plugins (--no-deps)");
176
+ printNextStepsImpl();
177
+ return false;
178
+ }
179
+
180
+ const installedRecommended = await installRecommendedPluginsImpl({ all, yes, language });
181
+ if (!installedRecommended) {
182
+ printNextStepsImpl();
183
+ return false;
184
+ }
185
+
186
+ return true;
187
+ }
188
+
189
+ export function injectGlobalProtocolsStep(
190
+ { logImpl = log, injectGlobalProtocolsImpl = injectGlobalProtocols } = {}
191
+ ) {
192
+ logImpl.blank();
193
+ logImpl.step(5, INSTALL_STEP_COUNT, "Injecting global protocols into ~/.claude/CLAUDE.md...");
194
+
195
+ try {
196
+ const result = injectGlobalProtocolsImpl();
197
+ if (result.action === "created") {
198
+ logImpl.ok(`Global protocols injected ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
199
+ } else if (result.action === "appended") {
200
+ logImpl.ok(`Global protocols appended ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
201
+ } else if (result.action === "upgraded") {
202
+ logImpl.ok(`Global protocols upgraded ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
203
+ } else {
204
+ logImpl.info(`Global protocols up to date ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
205
+ }
206
+ } catch (err) {
207
+ logImpl.warn(`Protocol injection failed: ${err.message} ${color.dim("(non-blocking)")}`);
208
+ }
209
+ }
package/cli/install.js CHANGED
@@ -3,16 +3,6 @@
3
3
  */
4
4
 
5
5
  import {
6
- color,
7
- log,
8
- claudeVersion,
9
- listPlugins,
10
- select,
11
- intro,
12
- } from "./utils.js";
13
- import { injectGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
14
- import {
15
- installRecommendedPlugins,
16
6
  installRequiredPlugins,
17
7
  registerBundledMcps,
18
8
  } from "./install-companions.js";
@@ -22,56 +12,32 @@ import {
22
12
  } from "./install-curdx-plugin.js";
23
13
  import { resolveInstallLanguage } from "./install-language.js";
24
14
  import { printNextSteps } from "./install-next-steps.js";
25
- import { parseInstallOptions } from "./install-options.js";
15
+ import { readShippedVersion } from "./install-paths.js";
26
16
  import {
27
- getMarketplaceLabel,
28
- getMarketplaceSource,
29
- readShippedVersion,
30
- shouldUseOfflineInstall,
31
- } from "./install-paths.js";
32
- import { checkAndUpdateSelf } from "./install-self-update.js";
17
+ createInstallContext,
18
+ ensureClaudeCliAvailable,
19
+ injectGlobalProtocolsStep,
20
+ maybeRestartWithUpdatedCli,
21
+ renderInstallIntro,
22
+ resolveExistingInstallationAction,
23
+ runRecommendedPluginsStep,
24
+ snapshotCurdxFlowInstall,
25
+ } from "./install-workflow.js";
33
26
 
34
27
  export async function install(args = []) {
35
- const { all, noDeps, forceOnline, yes, skipSelfUpdate } = parseInstallOptions(args);
28
+ const context = createInstallContext(args);
36
29
 
37
- // ---------- Step 0: Self-update check ----------
38
- if (!skipSelfUpdate) {
39
- const updateResult = await checkAndUpdateSelf();
40
- if (updateResult.updated) {
41
- // CLI was updated, re-exec with same args
42
- log.info("Restarting with updated version...");
43
- const { spawn } = await import("node:child_process");
44
- const child = spawn("curdx-flow", ["install", ...args, "--skip-self-update"], {
45
- stdio: "inherit",
46
- shell: false,
47
- });
48
- return new Promise((resolve) => {
49
- child.on("close", (code) => process.exit(code || 0));
50
- });
51
- }
30
+ if (await maybeRestartWithUpdatedCli(context)) {
31
+ return;
52
32
  }
53
33
 
54
- const useOffline = shouldUseOfflineInstall({ forceOnline });
55
-
56
- // Use @clack intro only in interactive TTY mode
57
- if (process.stdout.isTTY && !yes) {
58
- await intro("🚀 CurdX-Flow Installer");
59
- } else {
60
- log.title("🚀 CurdX-Flow Installer");
61
- }
34
+ await renderInstallIntro(context);
62
35
 
63
36
  // ---------- Step 0: Language selection ----------
64
- const { language, config } = await resolveInstallLanguage({ yes });
37
+ const { language, config } = await resolveInstallLanguage({ yes: context.yes });
65
38
 
66
39
  // ---------- Step 1: Check claude CLI ----------
67
- log.blank();
68
- log.step(1, 5, "Checking claude CLI...");
69
- const ver = claudeVersion();
70
- if (!ver) {
71
- log.err("claude CLI not found. Install Claude Code from https://code.claude.com first.");
72
- process.exit(1);
73
- }
74
- log.ok(`claude CLI found (${ver})`);
40
+ ensureClaudeCliAvailable();
75
41
 
76
42
  // Snapshot curdx-flow's pre-install version BEFORE Step 2 touches the
77
43
  // marketplace. Step 2 does `claude plugin marketplace remove` + `add` to
@@ -80,55 +46,19 @@ export async function install(args = []) {
80
46
  // listPlugins() in Step 3, curdx-flow is already gone from the list and
81
47
  // we can't tell a fresh install apart from an upgrade — the Step 3
82
48
  // output then incorrectly says "installed" for both cases.
83
- const prevCurdxFlow = listPlugins().find((p) => p.name === "curdx-flow");
49
+ const prevCurdxFlow = snapshotCurdxFlowInstall();
84
50
 
85
51
  // ---------- Step 1.5: Existing installation action menu ----------
86
- if (prevCurdxFlow && !yes) {
87
- log.blank();
88
- const action = await select({
89
- message: language === "zh"
90
- ? "检测到已安装的版本。如何继续?"
91
- : "Existing installation detected. How would you like to proceed?",
92
- options: [
93
- {
94
- value: "upgrade",
95
- label: language === "zh" ? "升级到最新版本" : "Upgrade to latest version",
96
- hint: language === "zh"
97
- ? `当前: v${prevCurdxFlow.version}`
98
- : `Current: v${prevCurdxFlow.version}`
99
- },
100
- {
101
- value: "reinstall",
102
- label: language === "zh" ? "重新安装(保留配置)" : "Reinstall (preserve config)",
103
- },
104
- {
105
- value: "reconfigure",
106
- label: language === "zh" ? "重新配置插件" : "Reconfigure plugins",
107
- },
108
- {
109
- value: "cancel",
110
- label: language === "zh" ? "取消" : "Cancel",
111
- },
112
- ],
113
- initialValue: "upgrade",
114
- });
115
-
116
- if (action === "cancel") {
117
- log.info(language === "zh" ? "安装已取消" : "Installation cancelled");
118
- process.exit(0);
119
- }
120
-
121
- if (action === "reconfigure") {
122
- log.info(language === "zh"
123
- ? "重新配置模式:将重新提示插件选择和 API key 配置"
124
- : "Reconfigure mode: will re-prompt for plugin selection and API key configuration");
125
- // Continue with normal flow but force prompts
126
- }
52
+ const existingInstall = await resolveExistingInstallationAction({
53
+ prevCurdxFlow,
54
+ yes: context.yes,
55
+ language,
56
+ });
57
+ if (!existingInstall.continueInstall) {
58
+ return;
127
59
  }
128
60
 
129
- const marketplaceSource = getMarketplaceSource(useOffline);
130
- const marketplaceLabel = getMarketplaceLabel(useOffline);
131
- await addCurdxMarketplace({ marketplaceSource, marketplaceLabel, useOffline });
61
+ await addCurdxMarketplace(context);
132
62
 
133
63
  // ---------- Step 3: Install curdx-flow plugin ----------
134
64
  // Read the version the marketplace is shipping so we can decide whether an
@@ -138,43 +68,23 @@ export async function install(args = []) {
138
68
  await installCurdxFlowPlugin({ prevCurdxFlow, shippedVersion });
139
69
 
140
70
  // ---------- Step 3.5: Install required plugins + register user-level MCPs ----------
141
- await installRequiredPlugins({ yes, language, config });
71
+ await installRequiredPlugins({ yes: context.yes, language, config });
142
72
 
143
73
  // Beta.12: direct MCPs migrated from plugin.json bundling. See cli/registry.js
144
74
  // for the rationale. Context7 now uses Upstash's official plugin instead.
145
75
  await registerBundledMcps();
146
76
 
147
77
  // ---------- Step 4: Recommended plugins ----------
148
- if (noDeps) {
149
- log.blank();
150
- log.step(4, 5, "Recommended plugins");
151
- log.info("Skipping recommended plugins (--no-deps)");
152
- printNextSteps();
153
- return;
154
- }
155
- const installedRecommended = await installRecommendedPlugins({ all, yes, language });
78
+ const installedRecommended = await runRecommendedPluginsStep({
79
+ noDeps: context.noDeps,
80
+ all: context.all,
81
+ yes: context.yes,
82
+ language,
83
+ });
156
84
  if (!installedRecommended) {
157
- printNextSteps();
158
85
  return;
159
86
  }
160
87
 
161
- // ---------- Step 5: inject global protocols ----------
162
- log.blank();
163
- log.step(5, 5, "Injecting global protocols into ~/.claude/CLAUDE.md...");
164
- try {
165
- const r = injectGlobalProtocols();
166
- if (r.action === "created") {
167
- log.ok(`Global protocols injected ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
168
- } else if (r.action === "appended") {
169
- log.ok(`Global protocols appended ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
170
- } else if (r.action === "upgraded") {
171
- log.ok(`Global protocols upgraded ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
172
- } else {
173
- log.info(`Global protocols up to date ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
174
- }
175
- } catch (err) {
176
- log.warn(`Protocol injection failed: ${err.message} ${color.dim("(non-blocking)")}`);
177
- }
178
-
88
+ injectGlobalProtocolsStep();
179
89
  printNextSteps();
180
90
  }
@@ -0,0 +1,146 @@
1
+ import { GLOBAL_CLAUDE_MD, removeGlobalProtocols } from "./protocols.js";
2
+ import {
3
+ claudeVersion,
4
+ color,
5
+ confirm,
6
+ listPlugins,
7
+ log,
8
+ multiSelect,
9
+ } from "./utils.js";
10
+
11
+ export const UNINSTALL_STEP_COUNT = 4;
12
+
13
+ export function createUninstallContext(args = []) {
14
+ return {
15
+ yes: args.includes("--yes") || args.includes("-y"),
16
+ purge: args.includes("--purge"),
17
+ keepRecommended: args.includes("--keep-recommended"),
18
+ };
19
+ }
20
+
21
+ export function ensureClaudeCliAvailableForUninstall(
22
+ { claudeVersionImpl = claudeVersion, logImpl = log, exitImpl = process.exit } = {}
23
+ ) {
24
+ const version = claudeVersionImpl();
25
+ if (!version) {
26
+ logImpl.err("claude CLI not found, cannot uninstall plugin.");
27
+ exitImpl(1);
28
+ return null;
29
+ }
30
+
31
+ return version;
32
+ }
33
+
34
+ export async function confirmUninstallStep(
35
+ { yes },
36
+ { confirmImpl = confirm, logImpl = log } = {}
37
+ ) {
38
+ if (yes) {
39
+ return true;
40
+ }
41
+
42
+ const confirmed = await confirmImpl(
43
+ `This will uninstall the ${color.bold("curdx-flow")} plugin. Continue?`,
44
+ false
45
+ );
46
+ if (!confirmed) {
47
+ logImpl.info("Cancelled");
48
+ }
49
+ return confirmed;
50
+ }
51
+
52
+ export function getInstalledTargets(entries, { listPluginsImpl = listPlugins } = {}) {
53
+ const installedNames = new Set(listPluginsImpl().map((plugin) => plugin.name));
54
+ return entries.filter((entry) => installedNames.has(entry.name));
55
+ }
56
+
57
+ export async function selectRecommendedPluginsToRemove(
58
+ { yes, present },
59
+ { multiSelectImpl = multiSelect, logImpl = log } = {}
60
+ ) {
61
+ if (present.length === 0) {
62
+ return [];
63
+ }
64
+
65
+ if (yes) {
66
+ logImpl.info(
67
+ color.dim("--yes mode: keeping recommended plugins (use --purge to remove them)")
68
+ );
69
+ return [];
70
+ }
71
+
72
+ const choices = present.map((entry) => ({
73
+ label: color.bold(entry.name),
74
+ value: entry.name,
75
+ hint: "",
76
+ }));
77
+
78
+ return multiSelectImpl(
79
+ "Which recommended plugins to also uninstall? (default: none)",
80
+ choices,
81
+ []
82
+ );
83
+ }
84
+
85
+ export function shouldKeepBundledMcps({ yes, keepRecommended }) {
86
+ if (!yes && !keepRecommended) {
87
+ return false;
88
+ }
89
+
90
+ return true;
91
+ }
92
+
93
+ export function shouldKeepRequiredPlugins({ yes }) {
94
+ return yes;
95
+ }
96
+
97
+ export function getManagedMarketplaceIds(entries) {
98
+ return [
99
+ ...new Set(
100
+ entries
101
+ .map((entry) => entry.marketplaceId)
102
+ .filter((id) => id && id !== "claude-plugins-official")
103
+ ),
104
+ ];
105
+ }
106
+
107
+ export function removeProtocolsStep(
108
+ { logImpl = log, removeGlobalProtocolsImpl = removeGlobalProtocols } = {}
109
+ ) {
110
+ logImpl.blank();
111
+ console.log(color.dim("Removing global protocols from ~/.claude/CLAUDE.md..."));
112
+
113
+ try {
114
+ const result = removeGlobalProtocolsImpl();
115
+ if (result.action === "removed") {
116
+ logImpl.ok(`Global protocols removed ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
117
+ } else if (result.action === "not-present") {
118
+ logImpl.info("Global protocols not present, skipping");
119
+ } else {
120
+ logImpl.info("~/.claude/CLAUDE.md does not exist, skipping");
121
+ }
122
+ } catch (err) {
123
+ logImpl.warn(`Protocol removal failed: ${err.message}`);
124
+ }
125
+ }
126
+
127
+ export function printUninstallSummary({ purge }, { logImpl = log } = {}) {
128
+ logImpl.blank();
129
+ console.log(color.bold("✅ Uninstall complete"));
130
+ if (purge) {
131
+ return;
132
+ }
133
+
134
+ console.log(
135
+ color.dim(
136
+ `\nArtifacts kept:\n` +
137
+ ` - ~/.local/bin/bun, ~/.local/bin/uv (symlinks; use --purge to remove)\n` +
138
+ ` - bun/uv binaries themselves (~/.bun/bin/bun, ~/.local/bin/uv real installs)\n` +
139
+ ` - claude-mem data (~/.claude-mem/)\n` +
140
+ ` - claude marketplace cache`
141
+ )
142
+ );
143
+ console.log(
144
+ color.dim(`\nFully purge: ${color.cyan("curdx-flow uninstall --purge")}`)
145
+ );
146
+ }