@curdx/flow 2.0.7 → 2.0.9

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,12 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // Read version dynamically from package.json so `curdx-flow --version` always
6
+ // reflects the installed package version.
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkgJson = JSON.parse(
9
+ readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")
10
+ );
11
+
12
+ export const VERSION = pkgJson.version;
package/cli/router.js ADDED
@@ -0,0 +1,49 @@
1
+ import { doctor } from "./doctor.js";
2
+ import { install } from "./install.js";
3
+ import { uninstall } from "./uninstall.js";
4
+ import { upgrade } from "./upgrade.js";
5
+ import { printHelp } from "./help.js";
6
+ import { VERSION, color } from "./utils.js";
7
+
8
+ export async function runCli(args = process.argv.slice(2)) {
9
+ const cmd = args[0];
10
+ const rest = args.slice(1);
11
+
12
+ if (cmd === "--version" || cmd === "-v") {
13
+ console.log(VERSION);
14
+ return;
15
+ }
16
+
17
+ if (!cmd || cmd === "--help" || cmd === "-h") {
18
+ printHelp();
19
+ return;
20
+ }
21
+
22
+ try {
23
+ switch (cmd) {
24
+ case "install":
25
+ await install(rest);
26
+ break;
27
+ case "doctor":
28
+ await doctor(rest);
29
+ break;
30
+ case "upgrade":
31
+ await upgrade(rest);
32
+ break;
33
+ case "uninstall":
34
+ case "remove":
35
+ await uninstall(rest);
36
+ break;
37
+ default:
38
+ console.error(color.red(`Unknown command: ${cmd}`));
39
+ console.error(`Run ${color.cyan("npx @curdx/flow --help")} for CLI usage.`);
40
+ process.exit(1);
41
+ }
42
+ } catch (err) {
43
+ console.error(color.red(`\n✗ ${err.message || err}`));
44
+ if (process.env.CURDX_DEBUG) {
45
+ console.error(err.stack || "");
46
+ }
47
+ process.exit(1);
48
+ }
49
+ }
package/cli/utils.js CHANGED
@@ -1,606 +1,32 @@
1
1
  /**
2
2
  * Shared utilities for curdx-flow CLI.
3
- */
4
-
5
- import { spawn, spawnSync } from "node:child_process";
6
- import { createInterface } from "node:readline";
7
- import { readFileSync, writeFileSync, existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
8
- import { fileURLToPath } from "node:url";
9
- import { dirname, join } from "node:path";
10
- import { homedir } from "node:os";
11
-
12
- // Read version dynamically from package.json so `curdx-flow --version` always
13
- // reflects the installed package version (avoids drift after npm version bumps).
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const pkgJson = JSON.parse(
16
- readFileSync(join(__dirname, "..", "package.json"), "utf-8")
17
- );
18
- export const VERSION = pkgJson.version;
19
-
20
- // ---------- Color helpers (no chalk dep) ----------
21
- const isTTY = process.stdout.isTTY && process.env.TERM !== "dumb";
22
- const c = (code) => (s) => isTTY ? `\x1b[${code}m${s}\x1b[0m` : String(s);
23
-
24
- export const color = {
25
- red: c("31"),
26
- green: c("32"),
27
- yellow: c("33"),
28
- blue: c("34"),
29
- magenta: c("35"),
30
- cyan: c("36"),
31
- dim: c("2"),
32
- bold: c("1"),
33
- underline: c("4"),
34
- };
35
-
36
- // ---------- Logging helpers ----------
37
- export const log = {
38
- info: (msg) => console.log(`${color.cyan("ℹ")} ${msg}`),
39
- ok: (msg) => console.log(`${color.green("✓")} ${msg}`),
40
- warn: (msg) => console.log(`${color.yellow("⚠")} ${msg}`),
41
- err: (msg) => console.error(`${color.red("✗")} ${msg}`),
42
- step: (n, total, msg) =>
43
- console.log(`${color.dim(`[${n}/${total}]`)} ${msg}`),
44
- blank: () => console.log(""),
45
- title: (msg) => console.log(`\n${color.bold(msg)}\n`),
46
- };
47
-
48
- // ---------- Run shell command ----------
49
- /**
50
- * Run a command, stream output live. Returns { code, stdout, stderr }.
51
- */
52
- export function run(cmd, args = [], opts = {}) {
53
- return new Promise((resolve) => {
54
- const child = spawn(cmd, args, {
55
- stdio: opts.silent ? ["ignore", "pipe", "pipe"] : "inherit",
56
- env: { ...process.env, ...opts.env },
57
- cwd: opts.cwd || process.cwd(),
58
- shell: false,
59
- });
60
-
61
- let stdout = "";
62
- let stderr = "";
63
- if (opts.silent) {
64
- child.stdout.on("data", (d) => (stdout += d.toString()));
65
- child.stderr.on("data", (d) => (stderr += d.toString()));
66
- }
67
-
68
- child.on("close", (code) => resolve({ code, stdout, stderr }));
69
- child.on("error", (err) => resolve({ code: -1, stdout: "", stderr: err.message }));
70
- });
71
- }
72
-
73
- /**
74
- * Sync run — for quick checks (e.g. "which claude").
75
- */
76
- export function runSync(cmd, args = []) {
77
- const res = spawnSync(cmd, args, { encoding: "utf-8", shell: false });
78
- return {
79
- code: res.status ?? -1,
80
- stdout: res.stdout ?? "",
81
- stderr: res.stderr ?? "",
82
- };
83
- }
84
-
85
- // ---------- Check if a command exists ----------
86
- export function has(cmd) {
87
- const res = runSync("which", [cmd]);
88
- return res.code === 0 && res.stdout.trim().length > 0;
89
- }
90
-
91
- // ---------- @clack/prompts wrappers ----------
92
- let _clack = null;
93
-
94
- /**
95
- * Lazy-load @clack/prompts (ESM module)
96
- */
97
- async function getClack() {
98
- if (!_clack) {
99
- _clack = await import("@clack/prompts");
100
- }
101
- return _clack;
102
- }
103
-
104
- /**
105
- * Handle user cancellation gracefully
106
- */
107
- async function handleCancel(value, message = "Operation cancelled") {
108
- const clack = await getClack();
109
- if (clack.isCancel(value)) {
110
- clack.cancel(message);
111
- process.exit(0);
112
- }
113
- return false;
114
- }
115
-
116
- /**
117
- * Single-select prompt with arrow key navigation
118
- * @param {Object} options
119
- * @param {string} options.message - Question to ask
120
- * @param {Array} options.options - Array of {value, label, hint?}
121
- * @param {any} [options.initialValue] - Default selected value
122
- * @returns {Promise<any>} Selected value
123
- */
124
- export async function select(options) {
125
- const clack = await getClack();
126
- const result = await clack.select({
127
- message: options.message,
128
- options: options.options,
129
- initialValue: options.initialValue,
130
- });
131
- await handleCancel(result);
132
- return result;
133
- }
134
-
135
- /**
136
- * Multi-select prompt with checkboxes (arrow keys + space to toggle)
137
- * @param {Object} options
138
- * @param {string} options.message - Question to ask
139
- * @param {Array} options.options - Array of {value, label, hint?}
140
- * @param {Array} [options.initialValues] - Default selected values
141
- * @param {boolean} [options.required] - Whether at least one must be selected
142
- * @returns {Promise<Array>} Array of selected values
143
- */
144
- export async function multiselectClack(options) {
145
- const clack = await getClack();
146
- const result = await clack.multiselect({
147
- message: options.message,
148
- options: options.options,
149
- initialValues: options.initialValues || [],
150
- required: options.required !== undefined ? options.required : false,
151
- });
152
- await handleCancel(result);
153
- return result;
154
- }
155
-
156
- /**
157
- * Text input prompt with validation
158
- * @param {Object} options
159
- * @param {string} options.message - Question to ask
160
- * @param {string} [options.placeholder] - Placeholder text
161
- * @param {string} [options.defaultValue] - Default value
162
- * @param {Function} [options.validate] - Validation function (return string for error, undefined for success)
163
- * @returns {Promise<string>} User input
164
- */
165
- export async function text(options) {
166
- const clack = await getClack();
167
- const result = await clack.text({
168
- message: options.message,
169
- placeholder: options.placeholder,
170
- defaultValue: options.defaultValue,
171
- validate: options.validate,
172
- });
173
- await handleCancel(result);
174
- return result;
175
- }
176
-
177
- /**
178
- * Spinner for async operations
179
- * @returns {Promise<Object>} Spinner controller
180
- */
181
- export async function spinner() {
182
- const clack = await getClack();
183
- return clack.spinner();
184
- }
185
-
186
- /**
187
- * Display intro message
188
- */
189
- export async function intro(message) {
190
- const clack = await getClack();
191
- clack.intro(message);
192
- }
193
-
194
- /**
195
- * Display outro message
196
- */
197
- export async function outro(message) {
198
- const clack = await getClack();
199
- clack.outro(message);
200
- }
201
-
202
- /**
203
- * Display a note/info box
204
- */
205
- export async function note(message, title) {
206
- const clack = await getClack();
207
- clack.note(message, title);
208
- }
209
-
210
- // ---------- Config file helpers ----------
211
- const CONFIG_DIR = join(homedir(), ".claude");
212
- const CONFIG_FILE = join(CONFIG_DIR, "curdx-flow-config.json");
213
-
214
- /**
215
- * Read curdx-flow config from ~/.claude/curdx-flow-config.json
216
- */
217
- export function readConfig() {
218
- if (!existsSync(CONFIG_FILE)) {
219
- return {};
220
- }
221
- try {
222
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
223
- } catch {
224
- return {};
225
- }
226
- }
227
-
228
- /**
229
- * Write curdx-flow config to ~/.claude/curdx-flow-config.json
230
- */
231
- export function writeConfig(config) {
232
- if (!existsSync(CONFIG_DIR)) {
233
- mkdirSync(CONFIG_DIR, { recursive: true });
234
- }
235
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
236
- }
237
-
238
- // ---------- Interactive prompts (readline, legacy) ----------
239
- /**
240
- * Ask user a yes/no question. Default applies on empty input.
241
- */
242
- export function confirm(message, defaultYes = true) {
243
- return new Promise((resolve) => {
244
- const rl = createInterface({
245
- input: process.stdin,
246
- output: process.stdout,
247
- });
248
- const hint = defaultYes ? "[Y/n]" : "[y/N]";
249
- rl.question(`${color.cyan("?")} ${message} ${color.dim(hint)} `, (ans) => {
250
- rl.close();
251
- const v = ans.trim().toLowerCase();
252
- if (v === "") return resolve(defaultYes);
253
- resolve(v === "y" || v === "yes");
254
- });
255
- });
256
- }
257
-
258
- /**
259
- * Multi-select (checkbox-style via comma-separated input).
260
- * Returns array of selected values.
261
- */
262
- export function multiSelect(message, choices, defaults = null) {
263
- return new Promise((resolve) => {
264
- const defaultSet = new Set(
265
- defaults ?? choices.map((_, i) => i)
266
- );
267
- console.log(`${color.cyan("?")} ${message}`);
268
- choices.forEach((ch, i) => {
269
- const checked = defaultSet.has(i)
270
- ? color.green("[x]")
271
- : color.dim("[ ]");
272
- console.log(` ${checked} ${color.bold(String(i + 1))}. ${ch.label}${ch.hint ? color.dim(` — ${ch.hint}`) : ""}`);
273
- });
274
- console.log(
275
- color.dim(
276
- " (comma-separated selection, e.g. 1,3 | a=all | n=none | Enter=default)"
277
- )
278
- );
279
-
280
- const rl = createInterface({
281
- input: process.stdin,
282
- output: process.stdout,
283
- });
284
- rl.question(` > `, (ans) => {
285
- rl.close();
286
- const v = ans.trim().toLowerCase();
287
- let selected;
288
- if (v === "") {
289
- selected = [...defaultSet];
290
- } else if (v === "a" || v === "all") {
291
- selected = choices.map((_, i) => i);
292
- } else if (v === "n" || v === "none") {
293
- selected = [];
294
- } else {
295
- selected = v
296
- .split(/[,\s]+/)
297
- .map((x) => parseInt(x, 10) - 1)
298
- .filter((i) => Number.isInteger(i) && i >= 0 && i < choices.length);
299
- }
300
- resolve(selected.map((i) => choices[i].value));
301
- });
302
- });
303
- }
304
-
305
- // ---------- Claude CLI helpers ----------
306
- /** Get claude CLI version, or null if not installed. */
307
- export function claudeVersion() {
308
- if (!has("claude")) return null;
309
- const res = runSync("claude", ["--version"]);
310
- if (res.code !== 0) return null;
311
- // Output like "2.1.114 (Claude Code)"
312
- const m = res.stdout.match(/(\d+\.\d+\.\d+)/);
313
- return m ? m[1] : res.stdout.trim().split("\n")[0];
314
- }
315
-
316
- /**
317
- * List installed plugins. Prefers the structured `claude plugin list --json`
318
- * output (stable machine-readable format; confirmed present in claude
319
- * 2.1.117+). Falls back to parsing the human-readable stream-text output
320
- * for older CLI versions, but warns that parser is brittle.
321
3
  *
322
- * Returns array of { id, name, marketplaceId, version, status, scope }.
323
- */
324
- export function listPlugins() {
325
- // Preferred: structured JSON output.
326
- const j = runSync("claude", ["plugin", "list", "--json"]);
327
- if (j.code === 0 && j.stdout.trim().startsWith("[")) {
328
- try {
329
- const arr = JSON.parse(j.stdout);
330
- return arr.map((p) => ({
331
- // id has form "name@marketplace" — name is stable for dedup/lookup.
332
- id: String(p.id || ""),
333
- name: String(p.id || "").split("@")[0],
334
- marketplaceId: String(p.id || "").split("@")[1] || undefined,
335
- version: p.version,
336
- status: p.enabled === false ? "disabled" : "enabled",
337
- scope: p.scope,
338
- raw: JSON.stringify(p),
339
- }));
340
- } catch {
341
- // JSON parse failed — fall through to legacy text parser.
342
- }
343
- }
344
-
345
- // Legacy fallback: parse the human-readable format.
346
- // ❯ curdx-flow@curdx-flow-marketplace
347
- // Version: 1.1.1
348
- // Status: ✔ enabled
349
- // Fragile — matches unicode markers. Kept only for older claude CLIs.
350
- const res = runSync("claude", ["plugin", "list"]);
351
- if (res.code !== 0) return [];
352
- const plugins = [];
353
- const blocks = res.stdout.split(/\n\s*❯\s*/).slice(1);
354
- for (const block of blocks) {
355
- const lines = block.split("\n");
356
- const id = lines[0].trim();
357
- const name = id.split("@")[0];
358
- const version = (block.match(/Version:\s*(\S+)/) || [])[1];
359
- const status = block.includes("✔")
360
- ? "enabled"
361
- : block.includes("✘")
362
- ? "failed"
363
- : "unknown";
364
- plugins.push({ id, name, marketplaceId: id.split("@")[1], version, status, raw: block });
365
- }
366
- return plugins;
367
- }
368
-
369
- /**
370
- * List configured Claude Code plugin marketplaces.
371
- * Returns array of { name, source, repo, path } when `--json` is supported.
372
- */
373
- export function listPluginMarketplaces() {
374
- const j = runSync("claude", ["plugin", "marketplace", "list", "--json"]);
375
- if (j.code === 0 && j.stdout.trim().startsWith("[")) {
376
- try {
377
- return JSON.parse(j.stdout);
378
- } catch {
379
- return [];
380
- }
381
- }
382
- return [];
383
- }
384
-
385
- /**
386
- * Read the user-level MCP registrations from ~/.claude.json. These are the
387
- * MCPs the user added manually via `claude mcp add …` — distinct from
388
- * plugin-bundled MCPs (which live in plugin.json).
389
- *
390
- * Returns a Map keyed by server name with the raw config object. Returns
391
- * an empty Map if the file is missing / unreadable / has no mcpServers
392
- * section — all of which are normal states and not errors.
393
- */
394
- export function readUserMcpConfig() {
395
- try {
396
- const path = join(HOME, ".claude.json");
397
- if (!existsSync(path)) return new Map();
398
- const cfg = JSON.parse(readFileSync(path, "utf-8"));
399
- const servers = cfg?.mcpServers || {};
400
- return new Map(Object.entries(servers));
401
- } catch {
402
- return new Map();
403
- }
404
- }
405
-
406
- /**
407
- * Given the output of listMcps() and a user-level MCP config map, find
408
- * MCPs that are registered BOTH as user-level AND as plugin-bundled.
409
- * The plugin-bundled form shows up as `plugin:<plugin>:<name>` in
410
- * listMcps output, so a user-level "context7" and a plugin-level
411
- * "plugin:curdx-flow:context7" are a duplicate pair.
412
- *
413
- * Returns array of { name, userConfig, pluginEntry }.
414
- */
415
- export function findDuplicateMcps(mcps, userConfig) {
416
- const duplicates = [];
417
- for (const m of mcps) {
418
- // Only look at plugin-prefixed entries — they're the reference for
419
- // what's bundled. Check if user has their own non-prefixed version.
420
- if (m.plugin && userConfig.has(m.name)) {
421
- duplicates.push({
422
- name: m.name,
423
- userConfig: userConfig.get(m.name),
424
- pluginEntry: m,
425
- });
426
- }
427
- }
428
- return duplicates;
429
- }
430
-
431
- /**
432
- * List MCP servers registered with the `claude` CLI. Returns array of
433
- * { name, plugin, fullName, status, command }
434
- * where `plugin` is set when the MCP came from a plugin (real name is
435
- * `plugin:<plugin>:<mcp>`), `name` is the trailing segment, and `fullName`
436
- * is the original as reported by claude.
437
- *
438
- * Fixture captured from `claude mcp list` (2.1.117):
439
- * Checking MCP server health…
440
- *
441
- * plugin:curdx-flow:context7: npx -y @upstash/context7-mcp@latest - ✓ Connected
442
- * context7: npx -y @upstash/context7-mcp --api-key ... - ✓ Connected
443
- * claude.ai Gmail: https://gmailmcp... - ✓ Connected
444
- *
445
- * `claude mcp list --json` does not exist on 2.1.117 (verified), so this
446
- * parser is the primary path. It is fixture-tested in test/utils.test.js
447
- * so format regressions get caught in CI.
448
- */
449
- export function listMcps() {
450
- const res = runSync("claude", ["mcp", "list"]);
451
- if (res.code !== 0) return [];
452
- return parseMcpList(res.stdout);
453
- }
454
-
455
- /** Exported for testing against a fixed input. */
456
- export function parseMcpList(output) {
457
- const mcps = [];
458
- for (const raw of output.split("\n")) {
459
- const line = raw.trimEnd();
460
- if (!line) continue;
461
- // skip the health-check header line
462
- if (line.startsWith("Checking") || line.startsWith("checking")) continue;
463
- // Expected format: "<fullName>: <command-or-url> - <status>"
464
- // fullName may itself contain colons when prefixed with "plugin:<p>:<m>".
465
- // Match from the end to find the status sentinel " - ", then split off
466
- // the name at the first ": " after the identifier prefix.
467
- const statusSplit = line.lastIndexOf(" - ");
468
- if (statusSplit === -1) continue;
469
- const statusRaw = line.slice(statusSplit + 3).trim();
470
- const beforeStatus = line.slice(0, statusSplit);
471
- // Find the first ": " that separates name from command. Note the space
472
- // after the colon — this disambiguates from the colons inside
473
- // "plugin:foo:bar".
474
- const nameSplit = beforeStatus.indexOf(": ");
475
- if (nameSplit === -1) continue;
476
- const fullName = beforeStatus.slice(0, nameSplit).trim();
477
- const command = beforeStatus.slice(nameSplit + 2).trim();
478
-
479
- let plugin = null;
480
- let name = fullName;
481
- if (fullName.startsWith("plugin:")) {
482
- const parts = fullName.split(":");
483
- if (parts.length >= 3) {
484
- plugin = parts[1];
485
- name = parts.slice(2).join(":");
486
- }
487
- }
488
-
489
- const status = /Connected|✓/.test(statusRaw)
490
- ? "connected"
491
- : /Failed|✗/.test(statusRaw)
492
- ? "failed"
493
- : "unknown";
494
-
495
- mcps.push({ name, plugin, fullName, status, command });
496
- }
497
- return mcps;
498
- }
499
-
500
- // ---------- Runtime PATH guards (bun / uv) ----------
501
- // claude-mem hard-codes `command: "bun"` in its .mcp.json, but bun installs to
502
- // ~/.bun/bin which is not on PATH when Claude Code spawns MCP servers
503
- // (macOS non-interactive shells do not source .zshrc). This module provides
504
- // detection + self-healing: create a symlink to the user-level bun install
505
- // in a PATH-visible directory.
506
-
507
- // Note: existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync, homedir, join
508
- // are already imported at the top of this file.
509
-
510
- // os.homedir() is sourced from the OS-level user record and works even
511
- // when $HOME is empty (non-login shells, some CI containers). See the
512
- // same rationale in cli/protocols.js.
513
- const HOME = homedir();
514
-
515
- /** Candidate bun install locations (priority order) */
516
- const BUN_CANDIDATES = [
517
- join(HOME, ".bun", "bin", "bun"),
518
- "/opt/homebrew/bin/bun",
519
- "/usr/local/bin/bun",
520
- "/home/linuxbrew/.linuxbrew/bin/bun",
521
- ];
522
-
523
- /** Candidate uv install locations */
524
- const UV_CANDIDATES = [
525
- join(HOME, ".local", "bin", "uv"),
526
- join(HOME, ".cargo", "bin", "uv"),
527
- "/opt/homebrew/bin/uv",
528
- "/usr/local/bin/uv",
529
- ];
530
-
531
- /** PATH-visible directories where symlinks can be created (priority order; use if exists, else try to create) */
532
- const SYMLINK_TARGET_DIRS = [
533
- join(HOME, ".local", "bin"),
534
- join(HOME, ".npm-global", "bin"),
535
- ];
536
-
537
- /** Find the absolute path of a runtime that actually exists */
538
- function findRuntime(candidates) {
539
- for (const p of candidates) if (existsSync(p)) return p;
540
- return null;
541
- }
542
-
543
- /** Whether the current PATH can resolve this command */
544
- function inPath(cmd) {
545
- return has(cmd);
546
- }
547
-
548
- /** Find a writable PATH-visible directory for symlink creation */
549
- function findSymlinkDir() {
550
- const pathDirs = (process.env.PATH || "").split(":").filter(Boolean);
551
- for (const d of SYMLINK_TARGET_DIRS) {
552
- if (pathDirs.includes(d)) {
553
- try {
554
- if (!existsSync(d)) mkdirSync(d, { recursive: true });
555
- return d;
556
- } catch {
557
- // continue
558
- }
559
- }
560
- }
561
- return null;
562
- }
563
-
564
- /**
565
- * Ensure cmd is resolvable on PATH. If it is installed but not visible
566
- * on PATH, create a symlink automatically.
567
- * @returns {{status:"ok"|"linked"|"missing"|"path-unwritable", path?:string, link?:string}}
568
- */
569
- export function ensureRuntimeInPath(cmd, candidates) {
570
- if (inPath(cmd)) return { status: "ok" };
571
-
572
- const realPath = findRuntime(candidates);
573
- if (!realPath) return { status: "missing" };
574
-
575
- const linkDir = findSymlinkDir();
576
- if (!linkDir) return { status: "path-unwritable", path: realPath };
577
-
578
- const linkPath = join(linkDir, cmd);
579
- // If it already exists and points to the same target, return idempotently
580
- if (existsSync(linkPath)) {
581
- try {
582
- const stat = lstatSync(linkPath);
583
- if (stat.isSymbolicLink() && readlinkSync(linkPath) === realPath) {
584
- return { status: "ok", path: realPath, link: linkPath };
585
- }
586
- // Old symlink/file points elsewhere — overwrite
587
- unlinkSync(linkPath);
588
- } catch {
589
- // ignore
590
- }
591
- }
592
- try {
593
- symlinkSync(realPath, linkPath);
594
- return { status: "linked", path: realPath, link: linkPath };
595
- } catch (err) {
596
- return { status: "path-unwritable", path: realPath };
597
- }
598
- }
599
-
600
- /** One-shot: ensure both bun and uv (claude-mem's runtimes) are resolvable on PATH */
601
- export function ensureClaudeMemRuntimes() {
602
- return {
603
- bun: ensureRuntimeInPath("bun", BUN_CANDIDATES),
604
- uv: ensureRuntimeInPath("uv", UV_CANDIDATES),
605
- };
606
- }
4
+ * This module is kept as the stable public import surface for existing CLI
5
+ * commands. Implementations live under cli/lib/ so each concern stays small.
6
+ */
7
+
8
+ export { VERSION } from "./lib/version.js";
9
+ export { color, log } from "./lib/logging.js";
10
+ export { has, run, runSync } from "./lib/process.js";
11
+ export {
12
+ confirm,
13
+ intro,
14
+ multiSelect,
15
+ multiselectClack,
16
+ note,
17
+ outro,
18
+ select,
19
+ spinner,
20
+ text,
21
+ } from "./lib/prompts.js";
22
+ export { readConfig, writeConfig } from "./lib/config.js";
23
+ export {
24
+ claudeVersion,
25
+ findDuplicateMcps,
26
+ listMcps,
27
+ listPluginMarketplaces,
28
+ listPlugins,
29
+ parseMcpList,
30
+ readUserMcpConfig,
31
+ } from "./lib/claude.js";
32
+ export { ensureClaudeMemRuntimes, ensureRuntimeInPath } from "./lib/runtime.js";