@curdx/flow 2.0.17 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/uninstall.js CHANGED
@@ -9,7 +9,6 @@ import { homedir } from "node:os";
9
9
  import {
10
10
  color,
11
11
  log,
12
- run,
13
12
  resultLastLine,
14
13
  resultOutput,
15
14
  confirm,
@@ -20,28 +19,15 @@ import {
20
19
  import { removeGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
21
20
  import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
22
21
  import {
23
- mcpRemoveArgs,
24
- pluginMarketplaceRemoveArgs,
25
- pluginUninstallArgs,
26
- } from "./lib/claude-commands.js";
22
+ removeMcp,
23
+ removePluginMarketplace,
24
+ uninstallPlugin,
25
+ } from "./lib/claude-ops.js";
27
26
 
28
27
  const HOME = homedir();
29
28
 
30
- // Pull uninstall-relevant subset from the single registry. See registry.js.
31
- const RECOMMENDED = RECOMMENDED_PLUGINS.map(({ name, uninstallSpec, uninstallArgs, marketplaceId, scope }) => ({
32
- name,
33
- uninstallSpec,
34
- uninstallArgs: uninstallArgs || [],
35
- marketplaceId,
36
- scope,
37
- }));
38
- const REQUIRED = REQUIRED_PLUGINS.map(({ name, uninstallSpec, uninstallArgs, marketplaceId, scope }) => ({
39
- name,
40
- uninstallSpec,
41
- uninstallArgs: uninstallArgs || [],
42
- marketplaceId,
43
- scope,
44
- }));
29
+ const RECOMMENDED = RECOMMENDED_PLUGINS.map(toUninstallTarget);
30
+ const REQUIRED = REQUIRED_PLUGINS.map(toUninstallTarget);
45
31
 
46
32
  // Symlinks created by install.js (only cleaned with --purge)
47
33
  const MANAGED_SYMLINKS = [
@@ -56,164 +42,175 @@ export async function uninstall(args = []) {
56
42
 
57
43
  log.title("🗑️ CurdX-Flow Uninstaller");
58
44
 
59
- // ---------- Step 1: claude CLI present? ----------
60
45
  const cv = claudeVersion();
61
46
  if (!cv) {
62
47
  log.err("claude CLI not found, cannot uninstall plugin.");
63
48
  process.exit(1);
64
49
  }
65
50
 
66
- // ---------- Step 2: confirmation ----------
67
- if (!yes) {
68
- const ok = await confirm(
69
- `This will uninstall the ${color.bold("curdx-flow")} plugin. Continue?`,
70
- false
71
- );
72
- if (!ok) {
73
- log.info("Cancelled");
74
- return;
75
- }
51
+ if (!(await confirmUninstall({ yes }))) {
52
+ log.info("Cancelled");
53
+ return;
76
54
  }
77
55
 
78
- // ---------- Step 3: uninstall curdx-flow plugin ----------
56
+ await uninstallCurdxFlowPlugin();
57
+ await maybeUninstallRecommendedPlugins({ yes, keepRecommended });
58
+ await maybeRemoveBundledMcps({ yes, keepRecommended });
59
+ await maybeUninstallRequiredPlugins({ yes });
60
+ await maybePurgeRuntimeArtifacts({ purge });
61
+ removeProtocols();
62
+ await maybeRemoveProjectState({ yes });
63
+
64
+ printSummary({ purge });
65
+ }
66
+
67
+ async function confirmUninstall({ yes }) {
68
+ if (yes) return true;
69
+ return confirm(
70
+ `This will uninstall the ${color.bold("curdx-flow")} plugin. Continue?`,
71
+ false
72
+ );
73
+ }
74
+
75
+ async function uninstallCurdxFlowPlugin() {
79
76
  log.blank();
80
77
  log.step(1, 4, "Uninstalling curdx-flow plugin...");
81
- const installed = listPlugins();
82
- const curdx = installed.find((p) => p.name === "curdx-flow");
78
+ const curdx = listPlugins().find((plugin) => plugin.name === "curdx-flow");
83
79
  if (!curdx) {
84
80
  log.info("curdx-flow not installed, skipping");
85
- } else {
86
- const r = await run(
87
- "claude",
88
- pluginUninstallArgs({
89
- scope: "user",
90
- uninstallSpec: "curdx-flow@curdx-flow-marketplace",
91
- }),
92
- { silent: true }
93
- );
94
- if (r.code === 0) {
95
- log.ok("curdx-flow uninstalled");
96
- } else {
97
- log.err(`Uninstall failed: ${resultOutput(r)}`);
98
- }
81
+ return;
99
82
  }
100
83
 
101
- // ---------- Step 4: optionally uninstall recommended ----------
84
+ const result = await uninstallPlugin({
85
+ scope: "user",
86
+ uninstallSpec: "curdx-flow@curdx-flow-marketplace",
87
+ });
88
+ if (result.code === 0) {
89
+ log.ok("curdx-flow uninstalled");
90
+ return;
91
+ }
92
+
93
+ log.err(`Uninstall failed: ${resultOutput(result)}`);
94
+ }
95
+
96
+ async function maybeUninstallRecommendedPlugins({ yes, keepRecommended }) {
102
97
  log.blank();
103
98
  log.step(2, 4, "Recommended plugins");
104
99
  if (keepRecommended) {
105
100
  log.info("Keeping recommended plugins (--keep-recommended)");
106
- } else {
107
- const currentlyInstalled = listPlugins();
108
- const presentRecs = RECOMMENDED.filter((r) =>
109
- currentlyInstalled.find((p) => p.name === r.name)
101
+ return;
102
+ }
103
+
104
+ const currentlyInstalled = listPlugins();
105
+ const present = RECOMMENDED.filter((entry) =>
106
+ currentlyInstalled.some((plugin) => plugin.name === entry.name)
107
+ );
108
+ if (present.length === 0) {
109
+ log.info("No installed recommended plugins");
110
+ return;
111
+ }
112
+
113
+ const selected = await selectPluginsToRemove({ yes, present });
114
+ for (const name of selected) {
115
+ const entry = present.find((plugin) => plugin.name === name);
116
+ if (!entry) continue;
117
+ await uninstallNamedPlugin(entry);
118
+ }
119
+ }
120
+
121
+ async function selectPluginsToRemove({ yes, present }) {
122
+ if (yes) {
123
+ log.info(
124
+ color.dim("--yes mode: keeping recommended plugins (use --purge to remove them)")
110
125
  );
126
+ return [];
127
+ }
111
128
 
112
- if (presentRecs.length === 0) {
113
- log.info("No installed recommended plugins");
114
- } else {
115
- let toRemove;
116
- if (yes) {
117
- toRemove = []; // Default to none — user must opt in explicitly
118
- log.info(
119
- color.dim("--yes mode: keeping recommended plugins (use --purge to remove them)")
120
- );
121
- } else {
122
- const choices = presentRecs.map((r) => ({
123
- label: color.bold(r.name),
124
- value: r.name,
125
- hint: "",
126
- }));
127
- toRemove = await multiSelect(
128
- "Which recommended plugins to also uninstall? (default: none)",
129
- choices,
130
- [] // default: nothing checked
131
- );
132
- }
129
+ const choices = present.map((entry) => ({
130
+ label: color.bold(entry.name),
131
+ value: entry.name,
132
+ hint: "",
133
+ }));
134
+ return multiSelect(
135
+ "Which recommended plugins to also uninstall? (default: none)",
136
+ choices,
137
+ []
138
+ );
139
+ }
133
140
 
134
- for (const name of toRemove) {
135
- const rec = presentRecs.find((r) => r.name === name);
136
- log.blank();
137
- console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(rec.name)}...`);
138
- const r = await run(
139
- "claude",
140
- pluginUninstallArgs(rec),
141
- { silent: true }
142
- );
143
- if (r.code === 0) {
144
- console.log(` ${color.green("✓")} ${rec.name} uninstalled`);
145
- } else {
146
- console.log(
147
- ` ${color.red("✗")} ${rec.name} uninstall failed: ${resultLastLine(r)}`
148
- );
149
- }
150
- }
151
- }
141
+ async function uninstallNamedPlugin(entry) {
142
+ log.blank();
143
+ console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
144
+ const result = await uninstallPlugin(entry);
145
+ if (result.code === 0) {
146
+ console.log(` ${color.green("")} ${entry.name} uninstalled`);
147
+ return;
152
148
  }
153
149
 
154
- // ---------- Step 4.5: optionally remove user-level MCPs ----------
155
- // Starting beta.12, the install command registers context7 +
156
- // sequential-thinking at user-level (not plugin-bundled). Ask before
157
- // removing because the user may have customised args (e.g. --api-key)
158
- // or still be using these MCPs outside curdx-flow.
150
+ console.log(
151
+ ` ${color.red("✗")} ${entry.name} uninstall failed: ${resultLastLine(result)}`
152
+ );
153
+ }
154
+
155
+ async function maybeRemoveBundledMcps({ yes, keepRecommended }) {
159
156
  log.blank();
160
157
  log.info("Required MCP servers (context7, sequential-thinking)");
161
158
  if (keepRecommended || yes) {
162
159
  log.info(
163
160
  color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
164
161
  );
165
- } else {
166
- const removeMcps = await confirm(
167
- `Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((m) => m.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
168
- false
169
- );
170
- if (removeMcps) {
171
- for (const mcp of BUNDLED_MCPS) {
172
- const r = await run("claude", mcpRemoveArgs({ name: mcp.name }), {
173
- silent: true,
174
- });
175
- if (r.code === 0) {
176
- log.ok(` ${mcp.name.padEnd(22)} removed`);
177
- } else {
178
- log.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
179
- }
180
- }
162
+ return;
163
+ }
164
+
165
+ const removeMcps = await confirm(
166
+ `Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((mcp) => mcp.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
167
+ false
168
+ );
169
+ if (!removeMcps) {
170
+ log.info("Keeping user-level MCPs");
171
+ return;
172
+ }
173
+
174
+ for (const mcp of BUNDLED_MCPS) {
175
+ const result = await removeMcp({ name: mcp.name });
176
+ if (result.code === 0) {
177
+ log.ok(` ${mcp.name.padEnd(22)} removed`);
181
178
  } else {
182
- log.info("Keeping user-level MCPs");
179
+ log.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
183
180
  }
184
181
  }
182
+ }
185
183
 
186
- // ---------- Step 4.75: uninstall required companion plugins ----------
184
+ async function maybeUninstallRequiredPlugins({ yes }) {
187
185
  log.blank();
188
186
  log.info("Required companion plugins");
189
187
  if (yes) {
190
188
  log.info(
191
189
  color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
192
190
  );
193
- } else {
194
- const removeRequired = await confirm(
195
- `Remove required companion plugins (${REQUIRED.map((p) => p.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
196
- false
197
- );
198
- if (removeRequired) {
199
- for (const plugin of REQUIRED) {
200
- const r = await run(
201
- "claude",
202
- pluginUninstallArgs(plugin),
203
- { silent: true }
204
- );
205
- if (r.code === 0) {
206
- log.ok(` ${plugin.name.padEnd(22)} uninstalled`);
207
- } else {
208
- log.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
209
- }
210
- }
191
+ return;
192
+ }
193
+
194
+ const removeRequired = await confirm(
195
+ `Remove required companion plugins (${REQUIRED.map((plugin) => plugin.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
196
+ false
197
+ );
198
+ if (!removeRequired) {
199
+ log.info("Keeping required companion plugins");
200
+ return;
201
+ }
202
+
203
+ for (const plugin of REQUIRED) {
204
+ const result = await uninstallPlugin(plugin);
205
+ if (result.code === 0) {
206
+ log.ok(` ${plugin.name.padEnd(22)} uninstalled`);
211
207
  } else {
212
- log.info("Keeping required companion plugins");
208
+ log.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
213
209
  }
214
210
  }
211
+ }
215
212
 
216
- // ---------- Step 5: cleanup symlinks (only with --purge) ----------
213
+ async function maybePurgeRuntimeArtifacts({ purge }) {
217
214
  log.blank();
218
215
  log.step(3, 4, "Runtime symlinks and marketplaces");
219
216
  if (!purge) {
@@ -223,116 +220,139 @@ export async function uninstall(args = []) {
223
220
  log.info(
224
221
  color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
225
222
  );
226
- } else {
227
- const marketplaceIds = [
228
- ...new Set(
229
- RECOMMENDED
230
- .concat(REQUIRED)
231
- .map((r) => r.marketplaceId)
232
- .filter((id) => id && id !== "claude-plugins-official")
233
- ),
234
- ];
235
- for (const marketplaceId of marketplaceIds) {
236
- const r = await run(
237
- "claude",
238
- pluginMarketplaceRemoveArgs(marketplaceId),
239
- { silent: true }
240
- );
241
- if (r.code === 0) {
242
- log.ok(`Removed marketplace ${marketplaceId}`);
243
- } else if (!r.stderr.includes("not found")) {
244
- log.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(r)}`);
245
- }
223
+ return;
224
+ }
225
+
226
+ await purgeManagedMarketplaces();
227
+ removeManagedSymlinks();
228
+ }
229
+
230
+ async function purgeManagedMarketplaces() {
231
+ const marketplaceIds = [
232
+ ...new Set(
233
+ RECOMMENDED
234
+ .concat(REQUIRED)
235
+ .map((entry) => entry.marketplaceId)
236
+ .filter((id) => id && id !== "claude-plugins-official")
237
+ ),
238
+ ];
239
+
240
+ for (const marketplaceId of marketplaceIds) {
241
+ const result = await removePluginMarketplace(marketplaceId);
242
+ if (result.code === 0) {
243
+ log.ok(`Removed marketplace ${marketplaceId}`);
244
+ } else if (!result.stderr.includes("not found")) {
245
+ log.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(result)}`);
246
246
  }
247
+ }
248
+ }
247
249
 
248
- for (const link of MANAGED_SYMLINKS) {
249
- if (!existsSync(link) && !isBrokenSymlink(link)) {
250
+ function removeManagedSymlinks() {
251
+ for (const link of MANAGED_SYMLINKS) {
252
+ if (!existsSync(link) && !isBrokenSymlink(link)) {
253
+ continue;
254
+ }
255
+ try {
256
+ const stat = lstatSync(link);
257
+ if (!stat.isSymbolicLink()) {
258
+ log.warn(
259
+ `${link} is not a symlink (likely a real file placed by the user), skipping`
260
+ );
250
261
  continue;
251
262
  }
252
- try {
253
- const stat = lstatSync(link);
254
- if (!stat.isSymbolicLink()) {
255
- log.warn(
256
- `${link} is not a symlink (likely a real file placed by the user), skipping`
257
- );
258
- continue;
259
- }
260
- const target = readlinkSync(link);
261
- unlinkSync(link);
262
- log.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
263
- } catch (err) {
264
- log.warn(`Failed to remove ${link}: ${err.message}`);
265
- }
263
+ const target = readlinkSync(link);
264
+ unlinkSync(link);
265
+ log.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
266
+ } catch (err) {
267
+ log.warn(`Failed to remove ${link}: ${err.message}`);
266
268
  }
267
269
  }
270
+ }
268
271
 
269
- // ---------- Step 5.5: remove global protocols block ----------
272
+ function removeProtocols() {
270
273
  log.blank();
271
274
  console.log(color.dim("Removing global protocols from ~/.claude/CLAUDE.md..."));
272
275
  try {
273
- const r = removeGlobalProtocols();
274
- if (r.action === "removed") {
276
+ const result = removeGlobalProtocols();
277
+ if (result.action === "removed") {
275
278
  log.ok(`Global protocols removed ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
276
- } else if (r.action === "not-present") {
277
- log.info(`Global protocols not present, skipping`);
279
+ } else if (result.action === "not-present") {
280
+ log.info("Global protocols not present, skipping");
278
281
  } else {
279
- log.info(`~/.claude/CLAUDE.md does not exist, skipping`);
282
+ log.info("~/.claude/CLAUDE.md does not exist, skipping");
280
283
  }
281
284
  } catch (err) {
282
285
  log.warn(`Protocol removal failed: ${err.message}`);
283
286
  }
287
+ }
284
288
 
285
- // ---------- Step 6: project .flow/ ----------
289
+ async function maybeRemoveProjectState({ yes }) {
286
290
  log.blank();
287
291
  log.step(4, 4, "Project state directory");
288
292
  const flowDir = join(process.cwd(), ".flow");
289
293
  if (!existsSync(flowDir)) {
290
294
  log.info(".flow/ does not exist, skipping");
291
- } else if (yes) {
295
+ return;
296
+ }
297
+
298
+ if (yes) {
292
299
  log.info(
293
300
  color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
294
301
  );
295
- } else {
296
- const ok = await confirm(
297
- `${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
298
- false
299
- );
300
- if (ok) {
301
- try {
302
- rmSync(flowDir, { recursive: true, force: true });
303
- log.ok(`Removed ${flowDir}`);
304
- } catch (err) {
305
- log.err(`Removal failed: ${err.message}`);
306
- }
307
- } else {
308
- log.info("Keeping .flow/");
309
- }
302
+ return;
303
+ }
304
+
305
+ const ok = await confirm(
306
+ `${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
307
+ false
308
+ );
309
+ if (!ok) {
310
+ log.info("Keeping .flow/");
311
+ return;
310
312
  }
311
313
 
312
- // ---------- Summary ----------
314
+ try {
315
+ rmSync(flowDir, { recursive: true, force: true });
316
+ log.ok(`Removed ${flowDir}`);
317
+ } catch (err) {
318
+ log.err(`Removal failed: ${err.message}`);
319
+ }
320
+ }
321
+
322
+ function printSummary({ purge }) {
313
323
  log.blank();
314
324
  console.log(color.bold("✅ Uninstall complete"));
315
- if (!purge) {
316
- console.log(
317
- color.dim(
318
- `\nArtifacts kept:\n` +
319
- ` - ~/.local/bin/bun, ~/.local/bin/uv (symlinks; use --purge to remove)\n` +
320
- ` - bun/uv binaries themselves (~/.bun/bin/bun, ~/.local/bin/uv real installs)\n` +
321
- ` - claude-mem data (~/.claude-mem/)\n` +
322
- ` - claude marketplace cache`
323
- )
324
- );
325
- console.log(
326
- color.dim(
327
- `\nFully purge: ${color.cyan("curdx-flow uninstall --purge")}`
328
- )
329
- );
330
- }
325
+ if (purge) return;
326
+
327
+ console.log(
328
+ color.dim(
329
+ `\nArtifacts kept:\n` +
330
+ ` - ~/.local/bin/bun, ~/.local/bin/uv (symlinks; use --purge to remove)\n` +
331
+ ` - bun/uv binaries themselves (~/.bun/bin/bun, ~/.local/bin/uv real installs)\n` +
332
+ ` - claude-mem data (~/.claude-mem/)\n` +
333
+ ` - claude marketplace cache`
334
+ )
335
+ );
336
+ console.log(
337
+ color.dim(
338
+ `\nFully purge: ${color.cyan("curdx-flow uninstall --purge")}`
339
+ )
340
+ );
341
+ }
342
+
343
+ function toUninstallTarget(entry) {
344
+ return {
345
+ name: entry.name,
346
+ uninstallSpec: entry.uninstallSpec,
347
+ uninstallArgs: entry.uninstallArgs || [],
348
+ marketplaceId: entry.marketplaceId,
349
+ scope: entry.scope,
350
+ };
331
351
  }
332
352
 
333
- function isBrokenSymlink(p) {
353
+ function isBrokenSymlink(pathname) {
334
354
  try {
335
- return lstatSync(p).isSymbolicLink();
355
+ return lstatSync(pathname).isSymbolicLink();
336
356
  } catch {
337
357
  return false;
338
358
  }
package/cli/upgrade.js CHANGED
@@ -2,15 +2,15 @@
2
2
  * upgrade command — update curdx-flow + recommended plugins to latest.
3
3
  */
4
4
 
5
- import { color, log, run, listPlugins, claudeVersion, resultLastLine } from "./utils.js";
5
+ import { color, log, listPlugins, claudeVersion, resultLastLine } from "./utils.js";
6
6
  import {
7
7
  PLUGINS_TO_UPDATE,
8
8
  MARKETPLACES_TO_REFRESH,
9
9
  } from "./registry.js";
10
10
  import {
11
- pluginMarketplaceUpdateArgs,
12
- pluginUpdateArgs,
13
- } from "./lib/claude-commands.js";
11
+ updatePlugin,
12
+ updatePluginMarketplace,
13
+ } from "./lib/claude-ops.js";
14
14
 
15
15
  export async function upgrade(args = []) {
16
16
  log.title("⬆️ CurdX-Flow upgrade");
@@ -23,11 +23,7 @@ export async function upgrade(args = []) {
23
23
  // Refresh marketplaces first (derived from cli/registry.js)
24
24
  log.step(1, 2, "Refreshing marketplaces...");
25
25
  for (const mp of MARKETPLACES_TO_REFRESH) {
26
- const r = await run(
27
- "claude",
28
- pluginMarketplaceUpdateArgs(mp),
29
- { silent: true }
30
- );
26
+ const r = await updatePluginMarketplace(mp);
31
27
  if (r.code === 0) {
32
28
  log.ok(` ${mp}`);
33
29
  } else {
@@ -51,7 +47,7 @@ export async function upgrade(args = []) {
51
47
  continue;
52
48
  }
53
49
 
54
- const r = await run("claude", pluginUpdateArgs({ spec }), { silent: true });
50
+ const r = await updatePlugin(spec);
55
51
  if (r.code === 0) {
56
52
  const updated = r.stdout.includes("updated from");
57
53
  if (updated) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "2.0.17",
3
+ "version": "2.0.18",
4
4
  "description": "CLI installer for CurdX-Flow — AI engineering workflow meta-framework for Claude Code",
5
5
  "type": "module",
6
6
  "bin": {