@curdx/flow 1.1.1

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/utils.js ADDED
@@ -0,0 +1,337 @@
1
+ /**
2
+ * Shared utilities for curdx-flow CLI.
3
+ * Zero npm deps — only Node built-ins.
4
+ */
5
+
6
+ import { spawn, spawnSync } from "node:child_process";
7
+ import { createInterface } from "node:readline";
8
+
9
+ export const VERSION = "1.1.1";
10
+
11
+ // ---------- Color helpers (no chalk dep) ----------
12
+ const isTTY = process.stdout.isTTY && process.env.TERM !== "dumb";
13
+ const c = (code) => (s) => isTTY ? `\x1b[${code}m${s}\x1b[0m` : String(s);
14
+
15
+ export const color = {
16
+ red: c("31"),
17
+ green: c("32"),
18
+ yellow: c("33"),
19
+ blue: c("34"),
20
+ magenta: c("35"),
21
+ cyan: c("36"),
22
+ dim: c("2"),
23
+ bold: c("1"),
24
+ underline: c("4"),
25
+ };
26
+
27
+ // ---------- Logging helpers ----------
28
+ export const log = {
29
+ info: (msg) => console.log(`${color.cyan("ℹ")} ${msg}`),
30
+ ok: (msg) => console.log(`${color.green("✓")} ${msg}`),
31
+ warn: (msg) => console.log(`${color.yellow("⚠")} ${msg}`),
32
+ err: (msg) => console.error(`${color.red("✗")} ${msg}`),
33
+ step: (n, total, msg) =>
34
+ console.log(`${color.dim(`[${n}/${total}]`)} ${msg}`),
35
+ blank: () => console.log(""),
36
+ title: (msg) => console.log(`\n${color.bold(msg)}\n`),
37
+ };
38
+
39
+ // ---------- Run shell command ----------
40
+ /**
41
+ * Run a command, stream output live. Returns { code, stdout, stderr }.
42
+ */
43
+ export function run(cmd, args = [], opts = {}) {
44
+ return new Promise((resolve) => {
45
+ const child = spawn(cmd, args, {
46
+ stdio: opts.silent ? ["ignore", "pipe", "pipe"] : "inherit",
47
+ env: { ...process.env, ...opts.env },
48
+ cwd: opts.cwd || process.cwd(),
49
+ shell: false,
50
+ });
51
+
52
+ let stdout = "";
53
+ let stderr = "";
54
+ if (opts.silent) {
55
+ child.stdout.on("data", (d) => (stdout += d.toString()));
56
+ child.stderr.on("data", (d) => (stderr += d.toString()));
57
+ }
58
+
59
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
60
+ child.on("error", (err) => resolve({ code: -1, stdout: "", stderr: err.message }));
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Sync run — for quick checks (e.g. "which claude").
66
+ */
67
+ export function runSync(cmd, args = []) {
68
+ const res = spawnSync(cmd, args, { encoding: "utf-8", shell: false });
69
+ return {
70
+ code: res.status ?? -1,
71
+ stdout: res.stdout ?? "",
72
+ stderr: res.stderr ?? "",
73
+ };
74
+ }
75
+
76
+ // ---------- Check if a command exists ----------
77
+ export function has(cmd) {
78
+ const res = runSync("which", [cmd]);
79
+ return res.code === 0 && res.stdout.trim().length > 0;
80
+ }
81
+
82
+ // ---------- Interactive prompts (readline, no deps) ----------
83
+ /**
84
+ * Ask user a yes/no question. Default applies on empty input.
85
+ */
86
+ export function confirm(message, defaultYes = true) {
87
+ return new Promise((resolve) => {
88
+ const rl = createInterface({
89
+ input: process.stdin,
90
+ output: process.stdout,
91
+ });
92
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
93
+ rl.question(`${color.cyan("?")} ${message} ${color.dim(hint)} `, (ans) => {
94
+ rl.close();
95
+ const v = ans.trim().toLowerCase();
96
+ if (v === "") return resolve(defaultYes);
97
+ resolve(v === "y" || v === "yes");
98
+ });
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Ask user to pick from a list. Returns selected value or null if aborted.
104
+ */
105
+ export function select(message, choices, defaultIndex = 0) {
106
+ return new Promise((resolve) => {
107
+ console.log(`${color.cyan("?")} ${message}`);
108
+ choices.forEach((ch, i) => {
109
+ const marker = i === defaultIndex ? color.green("▸") : " ";
110
+ console.log(` ${marker} ${color.bold(String(i + 1))}. ${ch.label}`);
111
+ });
112
+
113
+ const rl = createInterface({
114
+ input: process.stdin,
115
+ output: process.stdout,
116
+ });
117
+ rl.question(
118
+ ` ${color.dim(`(default: ${defaultIndex + 1}, q to abort) `)}`,
119
+ (ans) => {
120
+ rl.close();
121
+ const v = ans.trim().toLowerCase();
122
+ if (v === "q") return resolve(null);
123
+ if (v === "") return resolve(choices[defaultIndex].value);
124
+ const n = parseInt(v, 10);
125
+ if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
126
+ return resolve(choices[n - 1].value);
127
+ }
128
+ console.log(color.yellow(" (invalid, using default)"));
129
+ resolve(choices[defaultIndex].value);
130
+ }
131
+ );
132
+ });
133
+ }
134
+
135
+ /**
136
+ * Multi-select (checkbox-style via comma-separated input).
137
+ * Returns array of selected values.
138
+ */
139
+ export function multiSelect(message, choices, defaults = null) {
140
+ return new Promise((resolve) => {
141
+ const defaultSet = new Set(
142
+ defaults ?? choices.map((_, i) => i)
143
+ );
144
+ console.log(`${color.cyan("?")} ${message}`);
145
+ choices.forEach((ch, i) => {
146
+ const checked = defaultSet.has(i)
147
+ ? color.green("[x]")
148
+ : color.dim("[ ]");
149
+ console.log(` ${checked} ${color.bold(String(i + 1))}. ${ch.label}${ch.hint ? color.dim(` — ${ch.hint}`) : ""}`);
150
+ });
151
+ console.log(
152
+ color.dim(
153
+ " (comma-separated selection, e.g. 1,3 | a=all | n=none | Enter=default)"
154
+ )
155
+ );
156
+
157
+ const rl = createInterface({
158
+ input: process.stdin,
159
+ output: process.stdout,
160
+ });
161
+ rl.question(` > `, (ans) => {
162
+ rl.close();
163
+ const v = ans.trim().toLowerCase();
164
+ let selected;
165
+ if (v === "") {
166
+ selected = [...defaultSet];
167
+ } else if (v === "a" || v === "all") {
168
+ selected = choices.map((_, i) => i);
169
+ } else if (v === "n" || v === "none") {
170
+ selected = [];
171
+ } else {
172
+ selected = v
173
+ .split(/[,\s]+/)
174
+ .map((x) => parseInt(x, 10) - 1)
175
+ .filter((i) => Number.isInteger(i) && i >= 0 && i < choices.length);
176
+ }
177
+ resolve(selected.map((i) => choices[i].value));
178
+ });
179
+ });
180
+ }
181
+
182
+ // ---------- Claude CLI helpers ----------
183
+ /** Get claude CLI version, or null if not installed. */
184
+ export function claudeVersion() {
185
+ if (!has("claude")) return null;
186
+ const res = runSync("claude", ["--version"]);
187
+ if (res.code !== 0) return null;
188
+ // Output like "2.1.114 (Claude Code)"
189
+ const m = res.stdout.match(/(\d+\.\d+\.\d+)/);
190
+ return m ? m[1] : res.stdout.trim().split("\n")[0];
191
+ }
192
+
193
+ /** List installed plugins via `claude plugin list`. Returns array of { name, version, status }. */
194
+ export function listPlugins() {
195
+ const res = runSync("claude", ["plugin", "list"]);
196
+ if (res.code !== 0) return [];
197
+ const out = res.stdout;
198
+ const plugins = [];
199
+ // Parse format like:
200
+ // ❯ curdx-flow@curdx-flow-marketplace
201
+ // Version: 1.1.1
202
+ // Scope: user
203
+ // Status: ✔ enabled
204
+ const blocks = out.split(/\n\s*❯\s*/).slice(1);
205
+ for (const block of blocks) {
206
+ const lines = block.split("\n");
207
+ const name = lines[0].trim().split("@")[0];
208
+ const version = (block.match(/Version:\s*(\S+)/) || [])[1];
209
+ const status = block.includes("✔") ? "enabled" : block.includes("✘") ? "failed" : "unknown";
210
+ plugins.push({ name, version, status, raw: block });
211
+ }
212
+ return plugins;
213
+ }
214
+
215
+ /** List MCPs via `claude mcp list`. Returns array of { name, status }. */
216
+ export function listMcps() {
217
+ const res = runSync("claude", ["mcp", "list"]);
218
+ if (res.code !== 0) return [];
219
+ const lines = res.stdout.split("\n");
220
+ const mcps = [];
221
+ for (const line of lines) {
222
+ // Rough parse — adjust if format differs
223
+ const m = line.match(/^\s*([a-z0-9-]+)\s*[:\-]/i);
224
+ if (m) mcps.push({ name: m[1], status: "registered" });
225
+ }
226
+ return mcps;
227
+ }
228
+
229
+ // ---------- Paths ----------
230
+ export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-flow-marketplace") {
231
+ return `${process.env.HOME}/.claude/plugins/cache/${marketplace}/${pluginName}`;
232
+ }
233
+
234
+ // ---------- Runtime PATH guards (bun / uv) ----------
235
+ // claude-mem hard-codes `command: "bun"` in its .mcp.json, but bun installs to
236
+ // ~/.bun/bin which is not on PATH when Claude Code spawns MCP servers
237
+ // (macOS non-interactive shells do not source .zshrc). This module provides
238
+ // detection + self-healing: create a symlink to the user-level bun install
239
+ // in a PATH-visible directory.
240
+
241
+ import { existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
242
+ import { join } from "node:path";
243
+
244
+ const HOME = process.env.HOME || "";
245
+
246
+ /** Candidate bun install locations (priority order) */
247
+ const BUN_CANDIDATES = [
248
+ join(HOME, ".bun", "bin", "bun"),
249
+ "/opt/homebrew/bin/bun",
250
+ "/usr/local/bin/bun",
251
+ "/home/linuxbrew/.linuxbrew/bin/bun",
252
+ ];
253
+
254
+ /** Candidate uv install locations */
255
+ const UV_CANDIDATES = [
256
+ join(HOME, ".local", "bin", "uv"),
257
+ join(HOME, ".cargo", "bin", "uv"),
258
+ "/opt/homebrew/bin/uv",
259
+ "/usr/local/bin/uv",
260
+ ];
261
+
262
+ /** PATH-visible directories where symlinks can be created (priority order; use if exists, else try to create) */
263
+ const SYMLINK_TARGET_DIRS = [
264
+ join(HOME, ".local", "bin"),
265
+ join(HOME, ".npm-global", "bin"),
266
+ ];
267
+
268
+ /** Find the absolute path of a runtime that actually exists */
269
+ function findRuntime(candidates) {
270
+ for (const p of candidates) if (existsSync(p)) return p;
271
+ return null;
272
+ }
273
+
274
+ /** Whether the current PATH can resolve this command */
275
+ function inPath(cmd) {
276
+ return has(cmd);
277
+ }
278
+
279
+ /** Find a writable PATH-visible directory for symlink creation */
280
+ function findSymlinkDir() {
281
+ const pathDirs = (process.env.PATH || "").split(":").filter(Boolean);
282
+ for (const d of SYMLINK_TARGET_DIRS) {
283
+ if (pathDirs.includes(d)) {
284
+ try {
285
+ if (!existsSync(d)) mkdirSync(d, { recursive: true });
286
+ return d;
287
+ } catch {
288
+ // continue
289
+ }
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+
295
+ /**
296
+ * Ensure cmd is resolvable on PATH. If it is installed but not visible
297
+ * on PATH, create a symlink automatically.
298
+ * @returns {{status:"ok"|"linked"|"missing"|"path-unwritable", path?:string, link?:string}}
299
+ */
300
+ export function ensureRuntimeInPath(cmd, candidates) {
301
+ if (inPath(cmd)) return { status: "ok" };
302
+
303
+ const realPath = findRuntime(candidates);
304
+ if (!realPath) return { status: "missing" };
305
+
306
+ const linkDir = findSymlinkDir();
307
+ if (!linkDir) return { status: "path-unwritable", path: realPath };
308
+
309
+ const linkPath = join(linkDir, cmd);
310
+ // If it already exists and points to the same target, return idempotently
311
+ if (existsSync(linkPath)) {
312
+ try {
313
+ const stat = lstatSync(linkPath);
314
+ if (stat.isSymbolicLink() && readlinkSync(linkPath) === realPath) {
315
+ return { status: "ok", path: realPath, link: linkPath };
316
+ }
317
+ // Old symlink/file points elsewhere — overwrite
318
+ unlinkSync(linkPath);
319
+ } catch {
320
+ // ignore
321
+ }
322
+ }
323
+ try {
324
+ symlinkSync(realPath, linkPath);
325
+ return { status: "linked", path: realPath, link: linkPath };
326
+ } catch (err) {
327
+ return { status: "path-unwritable", path: realPath };
328
+ }
329
+ }
330
+
331
+ /** One-shot: ensure both bun and uv (claude-mem's runtimes) are resolvable on PATH */
332
+ export function ensureClaudeMemRuntimes() {
333
+ return {
334
+ bun: ensureRuntimeInPath("bun", BUN_CANDIDATES),
335
+ uv: ensureRuntimeInPath("uv", UV_CANDIDATES),
336
+ };
337
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@curdx/flow",
3
+ "version": "1.1.1",
4
+ "description": "CLI installer for CurDX-Flow — AI engineering workflow meta-framework for Claude Code",
5
+ "type": "module",
6
+ "bin": {
7
+ "curdx-flow": "bin/curdx-flow.js"
8
+ },
9
+ "scripts": {
10
+ "prepublishOnly": "node bin/curdx-flow.js --version"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "cli/",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/curdx/curdx-flow.git"
23
+ },
24
+ "homepage": "https://github.com/curdx/curdx-flow",
25
+ "bugs": "https://github.com/curdx/curdx-flow/issues",
26
+ "license": "MIT",
27
+ "author": "wdx <bydongxin@gmail.com>",
28
+ "keywords": [
29
+ "claude-code",
30
+ "cli",
31
+ "installer",
32
+ "ai-engineering",
33
+ "curdx-flow"
34
+ ]
35
+ }