@curdx/flow 2.0.0-beta.9 → 2.0.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +4 -20
- package/CHANGELOG.md +81 -0
- package/README.md +6 -3
- package/README.zh.md +18 -18
- package/bin/curdx-flow.js +30 -1
- package/cli/README.md +8 -6
- package/cli/doctor.js +90 -15
- package/cli/install.js +425 -32
- package/cli/protocols-body.md +21 -0
- package/cli/protocols.js +20 -29
- package/cli/registry.js +64 -14
- package/cli/uninstall.js +101 -7
- package/cli/upgrade.js +1 -1
- package/cli/utils.js +321 -61
- package/commands/implement.md +3 -3
- package/commands/init.md +14 -3
- package/commands/start.md +34 -12
- package/hooks/hooks.json +2 -3
- package/hooks/scripts/inject-karpathy.sh +8 -5
- package/package.json +8 -4
- package/skills/brownfield-index/SKILL.md +1 -1
- package/skills/browser-qa/SKILL.md +1 -1
- package/skills/epic/SKILL.md +1 -1
- package/skills/security-audit/SKILL.md +1 -1
- package/skills/ui-sketch/SKILL.md +1 -1
package/cli/utils.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared utilities for curdx-flow CLI.
|
|
3
|
-
* Zero npm deps — only Node built-ins.
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
import { spawn, spawnSync } from "node:child_process";
|
|
7
6
|
import { createInterface } from "node:readline";
|
|
8
|
-
import { readFileSync } from "node:fs";
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync } from "node:fs";
|
|
9
8
|
import { fileURLToPath } from "node:url";
|
|
10
9
|
import { dirname, join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
11
|
|
|
12
12
|
// Read version dynamically from package.json so `curdx-flow --version` always
|
|
13
13
|
// reflects the installed package version (avoids drift after npm version bumps).
|
|
@@ -88,7 +88,154 @@ export function has(cmd) {
|
|
|
88
88
|
return res.code === 0 && res.stdout.trim().length > 0;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
// ----------
|
|
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) ----------
|
|
92
239
|
/**
|
|
93
240
|
* Ask user a yes/no question. Default applies on empty input.
|
|
94
241
|
*/
|
|
@@ -108,39 +255,6 @@ export function confirm(message, defaultYes = true) {
|
|
|
108
255
|
});
|
|
109
256
|
}
|
|
110
257
|
|
|
111
|
-
/**
|
|
112
|
-
* Ask user to pick from a list. Returns selected value or null if aborted.
|
|
113
|
-
*/
|
|
114
|
-
export function select(message, choices, defaultIndex = 0) {
|
|
115
|
-
return new Promise((resolve) => {
|
|
116
|
-
console.log(`${color.cyan("?")} ${message}`);
|
|
117
|
-
choices.forEach((ch, i) => {
|
|
118
|
-
const marker = i === defaultIndex ? color.green("▸") : " ";
|
|
119
|
-
console.log(` ${marker} ${color.bold(String(i + 1))}. ${ch.label}`);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const rl = createInterface({
|
|
123
|
-
input: process.stdin,
|
|
124
|
-
output: process.stdout,
|
|
125
|
-
});
|
|
126
|
-
rl.question(
|
|
127
|
-
` ${color.dim(`(default: ${defaultIndex + 1}, q to abort) `)}`,
|
|
128
|
-
(ans) => {
|
|
129
|
-
rl.close();
|
|
130
|
-
const v = ans.trim().toLowerCase();
|
|
131
|
-
if (v === "q") return resolve(null);
|
|
132
|
-
if (v === "") return resolve(choices[defaultIndex].value);
|
|
133
|
-
const n = parseInt(v, 10);
|
|
134
|
-
if (Number.isInteger(n) && n >= 1 && n <= choices.length) {
|
|
135
|
-
return resolve(choices[n - 1].value);
|
|
136
|
-
}
|
|
137
|
-
console.log(color.yellow(" (invalid, using default)"));
|
|
138
|
-
resolve(choices[defaultIndex].value);
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
258
|
/**
|
|
145
259
|
* Multi-select (checkbox-style via comma-separated input).
|
|
146
260
|
* Returns array of selected values.
|
|
@@ -199,47 +313,190 @@ export function claudeVersion() {
|
|
|
199
313
|
return m ? m[1] : res.stdout.trim().split("\n")[0];
|
|
200
314
|
}
|
|
201
315
|
|
|
202
|
-
/**
|
|
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
|
+
*
|
|
322
|
+
* Returns array of { id, name, marketplaceId, version, status, scope }.
|
|
323
|
+
*/
|
|
203
324
|
export function listPlugins() {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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.
|
|
209
346
|
// ❯ curdx-flow@curdx-flow-marketplace
|
|
210
347
|
// Version: 1.1.1
|
|
211
|
-
// Scope: user
|
|
212
348
|
// Status: ✔ enabled
|
|
213
|
-
|
|
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);
|
|
214
354
|
for (const block of blocks) {
|
|
215
355
|
const lines = block.split("\n");
|
|
216
|
-
const
|
|
356
|
+
const id = lines[0].trim();
|
|
357
|
+
const name = id.split("@")[0];
|
|
217
358
|
const version = (block.match(/Version:\s*(\S+)/) || [])[1];
|
|
218
|
-
const status = block.includes("✔")
|
|
219
|
-
|
|
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 });
|
|
220
365
|
}
|
|
221
366
|
return plugins;
|
|
222
367
|
}
|
|
223
368
|
|
|
224
|
-
/**
|
|
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
|
+
*/
|
|
225
449
|
export function listMcps() {
|
|
226
450
|
const res = runSync("claude", ["mcp", "list"]);
|
|
227
451
|
if (res.code !== 0) return [];
|
|
228
|
-
|
|
452
|
+
return parseMcpList(res.stdout);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Exported for testing against a fixed input. */
|
|
456
|
+
export function parseMcpList(output) {
|
|
229
457
|
const mcps = [];
|
|
230
|
-
for (const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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 });
|
|
234
496
|
}
|
|
235
497
|
return mcps;
|
|
236
498
|
}
|
|
237
499
|
|
|
238
|
-
// ---------- Paths ----------
|
|
239
|
-
export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-flow-marketplace") {
|
|
240
|
-
return `${process.env.HOME}/.claude/plugins/cache/${marketplace}/${pluginName}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
500
|
// ---------- Runtime PATH guards (bun / uv) ----------
|
|
244
501
|
// claude-mem hard-codes `command: "bun"` in its .mcp.json, but bun installs to
|
|
245
502
|
// ~/.bun/bin which is not on PATH when Claude Code spawns MCP servers
|
|
@@ -247,10 +504,13 @@ export function pluginCacheDir(pluginName = "curdx-flow", marketplace = "curdx-f
|
|
|
247
504
|
// detection + self-healing: create a symlink to the user-level bun install
|
|
248
505
|
// in a PATH-visible directory.
|
|
249
506
|
|
|
250
|
-
|
|
251
|
-
//
|
|
507
|
+
// Note: existsSync, mkdirSync, symlinkSync, lstatSync, unlinkSync, readlinkSync, homedir, join
|
|
508
|
+
// are already imported at the top of this file.
|
|
252
509
|
|
|
253
|
-
|
|
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();
|
|
254
514
|
|
|
255
515
|
/** Candidate bun install locations (priority order) */
|
|
256
516
|
const BUN_CANDIDATES = [
|
package/commands/implement.md
CHANGED
|
@@ -15,7 +15,7 @@ Execute spec tasks per tasks.md. Select the best execution strategy based on arg
|
|
|
15
15
|
## Step 1: Preflight Checks
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
[ ! -d ".flow" ] && { echo "
|
|
18
|
+
[ ! -d ".flow" ] && { echo "✗ Not a CurDX-Flow project. Run /curdx-flow:init first"; exit 1; }
|
|
19
19
|
|
|
20
20
|
ARGS="$ARGUMENTS"
|
|
21
21
|
SPEC_NAME=""
|
|
@@ -35,10 +35,10 @@ for arg in $ARGS; do
|
|
|
35
35
|
done
|
|
36
36
|
|
|
37
37
|
[ -z "$SPEC_NAME" ] && SPEC_NAME=$(cat .flow/.active-spec 2>/dev/null)
|
|
38
|
-
[ -z "$SPEC_NAME" ] && { echo "
|
|
38
|
+
[ -z "$SPEC_NAME" ] && { echo "✗ No active spec. Run /curdx-flow:start first"; exit 1; }
|
|
39
39
|
|
|
40
40
|
DIR=".flow/specs/$SPEC_NAME"
|
|
41
|
-
[ ! -f "$DIR/tasks.md" ] && { echo "
|
|
41
|
+
[ ! -f "$DIR/tasks.md" ] && { echo "✗ Missing tasks.md. Run /curdx-flow:spec first (or /curdx-flow:spec --phase=tasks to rebuild just the tasks phase)"; exit 1; }
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
## Step 2: Parse Task Characteristics from tasks.md
|
package/commands/init.md
CHANGED
|
@@ -71,9 +71,20 @@ Append (if not already present):
|
|
|
71
71
|
|
|
72
72
|
### Step 5: Health Check
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
Do NOT shell out to a new terminal for this step — you are already inside
|
|
75
|
+
Claude Code. Verify inline via the information the plugin already has:
|
|
76
|
+
|
|
77
|
+
- Read `~/.claude/plugins/data/curdx-flow/.deps-checked` (optional — the
|
|
78
|
+
SessionStart hook already refreshes this once per day).
|
|
79
|
+
- If the user asks for the full report, suggest they run
|
|
80
|
+
`npx @curdx/flow doctor` in a separate terminal — don't try to spawn
|
|
81
|
+
it from inside the Claude Code session (output won't render cleanly
|
|
82
|
+
and the user has to alt-tab to see it).
|
|
83
|
+
|
|
84
|
+
Items the CLI doctor covers (for user reference):
|
|
85
|
+
- 2 bundled MCPs (context7 / sequential-thinking) — visible in `claude mcp list`
|
|
86
|
+
- 4 recommended plugins (pua / claude-mem / frontend-design / chrome-devtools-mcp)
|
|
87
|
+
- Runtime PATH guards for `bun` / `uv` (relevant only when claude-mem is installed)
|
|
77
88
|
|
|
78
89
|
### Step 6: Prompt Next Steps
|
|
79
90
|
|
package/commands/start.md
CHANGED
|
@@ -32,18 +32,40 @@ Entry point for every feature. Works in four modes depending on flags and existi
|
|
|
32
32
|
|
|
33
33
|
## Flag parsing
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
**Do not shell-split `$ARGUMENTS`.** It is a user-supplied string that may
|
|
36
|
+
contain quoted substrings with spaces, `$`-signs, or embedded quotes.
|
|
37
|
+
`xargs`, naive `awk`, and `sed`-based quote stripping all mis-parse at
|
|
38
|
+
least one of those cases (e.g. `my-feature "Fix user's login bug"` breaks
|
|
39
|
+
`xargs: unmatched quote`). Parse the string as a model task instead:
|
|
40
|
+
|
|
41
|
+
1. **Flags** (order-independent, each is self-delimited):
|
|
42
|
+
- `--resume` / `--list` — boolean presence
|
|
43
|
+
- `--mode=<fast|standard|enterprise>` — value after `=`
|
|
44
|
+
Detect each with a single regex over the full `$ARGUMENTS` string and
|
|
45
|
+
remove the matched span from your working copy. Flags not in the list
|
|
46
|
+
above are errors — surface them to the user.
|
|
47
|
+
|
|
48
|
+
2. **Positional args** (after flags removed):
|
|
49
|
+
- First whitespace-separated token → `SPEC_NAME` (kebab-case `[a-z0-9-]+`).
|
|
50
|
+
- Remainder of the string, trimmed and with one layer of outer `"..."`
|
|
51
|
+
or `'...'` quotes stripped → `GOAL`. Preserve inner quotes as-is.
|
|
52
|
+
|
|
53
|
+
3. If `SPEC_NAME` does not match `^[a-z0-9][a-z0-9-]*$` (per
|
|
54
|
+
`schemas/spec-state.schema.json`), stop and ask the user to pick a
|
|
55
|
+
valid kebab-case name.
|
|
56
|
+
|
|
57
|
+
Mode must be `fast`, `standard`, or `enterprise`. Invalid → default to
|
|
58
|
+
`standard` with a warning.
|
|
59
|
+
|
|
60
|
+
Example inputs and their parse:
|
|
61
|
+
|
|
62
|
+
| `$ARGUMENTS` | SPEC_NAME | GOAL | flags |
|
|
63
|
+
|-------------------------------------------------|--------------|-------------------------------|---------------|
|
|
64
|
+
| `my-feature "Add JWT auth"` | `my-feature` | `Add JWT auth` | — |
|
|
65
|
+
| `my-feature --mode=fast "Add JWT auth"` | `my-feature` | `Add JWT auth` | mode=fast |
|
|
66
|
+
| `my-feature "Fix user's login bug"` | `my-feature` | `Fix user's login bug` | — |
|
|
67
|
+
| `--list` | — | — | list=true |
|
|
68
|
+
| `--resume` | — | — | resume=true |
|
|
47
69
|
|
|
48
70
|
## Branch logic
|
|
49
71
|
|
package/hooks/hooks.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# CurDX-Flow
|
|
2
|
+
# CurDX-Flow SessionStart baseline injection
|
|
3
3
|
# Injects the L1 baseline (Karpathy 4 principles + mandatory tool rules + 3 red lines)
|
|
4
|
-
#
|
|
4
|
+
# as additionalContext at every session boot (startup, /clear, post-compact).
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# Wired under SessionStart rather than InstructionsLoaded because Claude Code's
|
|
7
|
+
# InstructionsLoaded event is observability-only — its hook schema rejects
|
|
8
|
+
# hookSpecificOutput / additionalContext. SessionStart with matcher
|
|
9
|
+
# "startup|clear|compact" gives the same "baseline is always on, even after
|
|
10
|
+
# compaction" property while staying within the supported schema.
|
|
8
11
|
|
|
9
12
|
set -u
|
|
10
13
|
|
|
@@ -46,7 +49,7 @@ CONTEXT='## CurDX-Flow Mind Baseline (L1 — always on)
|
|
|
46
49
|
# Emit JSON with safe encoding
|
|
47
50
|
if command -v python3 >/dev/null 2>&1; then
|
|
48
51
|
ESCAPED="$(printf '%s' "$CONTEXT" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))')"
|
|
49
|
-
printf '{"hookSpecificOutput":{"hookEventName":"
|
|
52
|
+
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":%s}}\n' "$ESCAPED"
|
|
50
53
|
fi
|
|
51
54
|
|
|
52
55
|
exit 0
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@curdx/flow",
|
|
3
|
-
"version": "2.0.0
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI installer for CurDX-Flow — AI engineering workflow meta-framework for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"flow": "bin/curdx-flow.js",
|
|
8
7
|
"curdx-flow": "bin/curdx-flow.js"
|
|
9
8
|
},
|
|
10
9
|
"scripts": {
|
|
11
|
-
"
|
|
10
|
+
"test": "node --test test/*.test.js",
|
|
11
|
+
"prepublishOnly": "node --test test/*.test.js && node bin/curdx-flow.js --version"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"bin/",
|
|
@@ -44,5 +44,9 @@
|
|
|
44
44
|
"installer",
|
|
45
45
|
"ai-engineering",
|
|
46
46
|
"curdx-flow"
|
|
47
|
-
]
|
|
47
|
+
],
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@clack/prompts": "^0.8.2",
|
|
50
|
+
"picocolors": "^1.1.1"
|
|
51
|
+
}
|
|
48
52
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: brownfield-index
|
|
3
|
-
description: Invoke when the user is new to an unfamiliar / legacy / brownfield codebase and wants a structural understanding — module map, component inventory, API surface, data flow. Triggers on "legacy code", "brownfield", "unfamiliar", "new to this code", "new to this project", "just joined", "inherited codebase", "explore codebase", "understand structure", "index code", "map modules", "tour", "onboard", "what is this project"
|
|
3
|
+
description: Invoke when the user is new to an unfamiliar / legacy / brownfield codebase and wants a structural understanding — module map, component inventory, API surface, data flow. Triggers on "legacy code", "brownfield", "unfamiliar", "new to this code", "new to this project", "just joined", "inherited codebase", "explore codebase", "understand structure", "index code", "map modules", "tour", "onboard", "what is this project".
|
|
4
4
|
allowed-tools: [Read, Grep, Glob, Bash]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: browser-qa
|
|
3
|
-
description: Invoke when the user wants to test a UI/frontend in a real browser — accessibility, performance, console errors, network traffic, visual regression. Triggers on "browser test", "test in browser", "UI test", "e2e test", "frontend test", "accessibility", "a11y", "WCAG", "lighthouse", "performance audit", "console error", "network request", "cross-browser", "responsive", "mobile test", "visual regression", "screenshot"
|
|
3
|
+
description: Invoke when the user wants to test a UI/frontend in a real browser — accessibility, performance, console errors, network traffic, visual regression. Triggers on "browser test", "test in browser", "UI test", "e2e test", "frontend test", "accessibility", "a11y", "WCAG", "lighthouse", "performance audit", "console error", "network request", "cross-browser", "responsive", "mobile test", "visual regression", "screenshot".
|
|
4
4
|
allowed-tools: [Read, Write, Bash, Grep, Glob, WebFetch]
|
|
5
5
|
---
|
|
6
6
|
|
package/skills/epic/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: epic
|
|
3
|
-
description: Invoke when user wants to break a large feature into multiple smaller specs with a dependency graph. Triggers on "epic", "big feature", "too big", "decompose", "break down", "break into", "split into", "multi-spec", "multiple features", "sub-features", "vertical slice", "parent feature", "large scope", "
|
|
3
|
+
description: Invoke when user wants to break a large feature into multiple smaller specs with a dependency graph. Triggers on "epic", "big feature", "too big", "decompose", "break down", "break into", "split into", "multi-spec", "multiple features", "sub-features", "vertical slice", "parent feature", "large scope", "won't fit in one sprint", "needs splitting".
|
|
4
4
|
allowed-tools: [Read, Write, Grep, Glob, Bash]
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-audit
|
|
3
|
-
description: Invoke when the user wants a security review — OWASP Top 10, STRIDE threat modeling, credential handling, injection, secrets, sensitive data handling. Triggers on "security", "auth", "authentication", "credential", "password", "secret", "API key", "token", "OWASP", "STRIDE", "CVE", "vulnerability", "injection", "XSS", "CSRF", "SSRF", "SQL injection", "hardcoded secret", "sensitive data", "leak", "
|
|
3
|
+
description: Invoke when the user wants a security review — OWASP Top 10, STRIDE threat modeling, credential handling, injection, secrets, sensitive data handling. Triggers on "security", "auth", "authentication", "credential", "password", "secret", "API key", "token", "OWASP", "STRIDE", "CVE", "vulnerability", "injection", "XSS", "CSRF", "SSRF", "SQL injection", "hardcoded secret", "sensitive data", "leak", "will my API key leak", "is this safe".
|
|
4
4
|
allowed-tools: [Read, Grep, Glob, Bash, WebSearch]
|
|
5
5
|
---
|
|
6
6
|
|