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