@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/LICENSE +21 -0
- package/README.md +161 -0
- package/README.zh.md +149 -0
- package/bin/curdx-flow.js +125 -0
- package/cli/README.md +87 -0
- package/cli/doctor.js +155 -0
- package/cli/init.js +296 -0
- package/cli/install.js +213 -0
- package/cli/protocols.js +124 -0
- package/cli/uninstall.js +238 -0
- package/cli/upgrade.js +75 -0
- package/cli/utils.js +337 -0
- package/package.json +35 -0
package/cli/doctor.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor command — external health check (no need to enter Claude Code).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
color,
|
|
9
|
+
log,
|
|
10
|
+
runSync,
|
|
11
|
+
claudeVersion,
|
|
12
|
+
listPlugins,
|
|
13
|
+
listMcps,
|
|
14
|
+
ensureClaudeMemRuntimes,
|
|
15
|
+
} from "./utils.js";
|
|
16
|
+
|
|
17
|
+
export async function doctor(args = []) {
|
|
18
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
19
|
+
|
|
20
|
+
log.title("🏥 CurDX-Flow Health Check");
|
|
21
|
+
|
|
22
|
+
let errors = 0;
|
|
23
|
+
let warnings = 0;
|
|
24
|
+
|
|
25
|
+
// ---------- claude CLI ----------
|
|
26
|
+
const cv = claudeVersion();
|
|
27
|
+
if (cv) {
|
|
28
|
+
log.ok(`claude CLI ${color.dim(cv)}`);
|
|
29
|
+
} else {
|
|
30
|
+
log.err("claude CLI not found (install Claude Code)");
|
|
31
|
+
errors++;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------- Node ----------
|
|
35
|
+
log.ok(`Node ${color.dim(process.version)}`);
|
|
36
|
+
|
|
37
|
+
// ---------- curdx-flow plugin ----------
|
|
38
|
+
const plugins = cv ? listPlugins() : [];
|
|
39
|
+
const curdx = plugins.find((p) => p.name === "curdx-flow");
|
|
40
|
+
if (curdx) {
|
|
41
|
+
if (curdx.status === "enabled") {
|
|
42
|
+
log.ok(`curdx-flow ${color.dim(`v${curdx.version} (enabled)`)}`);
|
|
43
|
+
} else {
|
|
44
|
+
log.err(`curdx-flow v${curdx.version} (${curdx.status})`);
|
|
45
|
+
errors++;
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
log.warn("curdx-flow not installed → run curdx-flow install");
|
|
49
|
+
warnings++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------- MCPs ----------
|
|
53
|
+
console.log(`\n${color.bold("MCP Servers:")}`);
|
|
54
|
+
const mcps = cv ? listMcps() : [];
|
|
55
|
+
const expectedMcps = ["context7", "sequential-thinking", "chrome-devtools"];
|
|
56
|
+
for (const m of expectedMcps) {
|
|
57
|
+
const found = mcps.find((x) => x.name === m);
|
|
58
|
+
if (found) {
|
|
59
|
+
log.ok(`${m.padEnd(22)} ${color.dim("auto-loaded")}`);
|
|
60
|
+
} else {
|
|
61
|
+
if (curdx) {
|
|
62
|
+
log.warn(`${m.padEnd(22)} not shown in claude mcp list (restart Claude Code may fix)`);
|
|
63
|
+
warnings++;
|
|
64
|
+
} else {
|
|
65
|
+
log.info(`${m.padEnd(22)} waiting for curdx-flow install`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------- Recommended plugins ----------
|
|
71
|
+
console.log(`\n${color.bold("Recommended plugins:")}`);
|
|
72
|
+
const recommended = [
|
|
73
|
+
{ name: "pua", installCmd: "claude plugin install pua@pua-skills" },
|
|
74
|
+
{ name: "claude-mem", installCmd: "claude plugin install claude-mem@thedotmack" },
|
|
75
|
+
{ name: "frontend-design", installCmd: "claude plugin install frontend-design@claude-plugins-official" },
|
|
76
|
+
];
|
|
77
|
+
let claudeMemEnabled = false;
|
|
78
|
+
for (const r of recommended) {
|
|
79
|
+
const p = plugins.find((x) => x.name === r.name);
|
|
80
|
+
if (p && p.status === "enabled") {
|
|
81
|
+
log.ok(`${r.name.padEnd(22)} ${color.dim(`v${p.version}`)}`);
|
|
82
|
+
if (r.name === "claude-mem") claudeMemEnabled = true;
|
|
83
|
+
} else if (p && p.status === "failed") {
|
|
84
|
+
log.err(`${r.name.padEnd(22)} load failed`);
|
|
85
|
+
errors++;
|
|
86
|
+
} else {
|
|
87
|
+
log.warn(`${r.name.padEnd(22)} not installed ${color.dim("(run: curdx-flow install --all)")}`);
|
|
88
|
+
warnings++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------- Runtime PATH guards (only if claude-mem is installed) ----------
|
|
93
|
+
if (claudeMemEnabled) {
|
|
94
|
+
console.log(`\n${color.bold("Runtime (claude-mem dependencies):")}`);
|
|
95
|
+
const rt = ensureClaudeMemRuntimes();
|
|
96
|
+
for (const [name, res] of Object.entries(rt)) {
|
|
97
|
+
if (res.status === "ok") {
|
|
98
|
+
log.ok(`${name.padEnd(22)} ${color.dim("visible on PATH")}`);
|
|
99
|
+
} else if (res.status === "linked") {
|
|
100
|
+
log.ok(
|
|
101
|
+
`${name.padEnd(22)} ${color.dim(`auto-linked ${res.link} → ${res.path}`)}`
|
|
102
|
+
);
|
|
103
|
+
} else if (res.status === "missing") {
|
|
104
|
+
log.warn(
|
|
105
|
+
`${name.padEnd(22)} not installed ${color.dim("(claude-mem will auto-install on next Claude Code session)")}`
|
|
106
|
+
);
|
|
107
|
+
warnings++;
|
|
108
|
+
} else if (res.status === "path-unwritable") {
|
|
109
|
+
const dir = res.path.split("/").slice(0, -1).join("/");
|
|
110
|
+
log.err(
|
|
111
|
+
`${name.padEnd(22)} installed but not on PATH ${color.dim(`(add export PATH="${dir}:$PATH" to your shell rc)`)}`
|
|
112
|
+
);
|
|
113
|
+
errors++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------- Project state ----------
|
|
119
|
+
console.log(`\n${color.bold("Local project:")}`);
|
|
120
|
+
const cwd = process.cwd();
|
|
121
|
+
const flowDir = path.join(cwd, ".flow");
|
|
122
|
+
try {
|
|
123
|
+
const stat = await fs.stat(flowDir);
|
|
124
|
+
if (stat.isDirectory()) {
|
|
125
|
+
log.ok(`.flow/ ${color.dim(cwd)}`);
|
|
126
|
+
// Check active spec
|
|
127
|
+
try {
|
|
128
|
+
const active = await fs.readFile(path.join(flowDir, ".active-spec"), "utf-8");
|
|
129
|
+
log.info(`Active spec ${color.cyan(active.trim())}`);
|
|
130
|
+
} catch {
|
|
131
|
+
log.info("Active spec (none)");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
log.info(`.flow/ not a curdx-flow project (run: curdx-flow init)`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------- Summary ----------
|
|
139
|
+
console.log();
|
|
140
|
+
if (errors > 0) {
|
|
141
|
+
console.log(color.red(`Summary: ${errors} error(s), ${warnings} warning(s)`));
|
|
142
|
+
console.log(color.dim("Fix errors and re-run curdx-flow doctor"));
|
|
143
|
+
process.exit(1);
|
|
144
|
+
} else if (warnings > 0) {
|
|
145
|
+
console.log(color.yellow(`Summary: ${warnings} warning(s). Usable, but worth addressing.`));
|
|
146
|
+
} else {
|
|
147
|
+
console.log(color.green("Summary: all healthy ✓"));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (verbose) {
|
|
151
|
+
console.log(`\n${color.bold("Details:")}`);
|
|
152
|
+
console.log(color.dim(` Plugins raw:`));
|
|
153
|
+
console.log(runSync("claude", ["plugin", "list"]).stdout);
|
|
154
|
+
}
|
|
155
|
+
}
|
package/cli/init.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init command — external version of /curdx-flow:init.
|
|
3
|
+
* Creates .flow/ structure in target directory.
|
|
4
|
+
*
|
|
5
|
+
* Uses templates from plugin cache if available, falls back to embedded templates.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import {
|
|
12
|
+
color,
|
|
13
|
+
log,
|
|
14
|
+
confirm,
|
|
15
|
+
pluginCacheDir,
|
|
16
|
+
has,
|
|
17
|
+
runSync,
|
|
18
|
+
} from "./utils.js";
|
|
19
|
+
|
|
20
|
+
export async function init(args = []) {
|
|
21
|
+
const force = args.includes("--force");
|
|
22
|
+
const target = args.find((a) => !a.startsWith("--")) || process.cwd();
|
|
23
|
+
const absTarget = path.resolve(target);
|
|
24
|
+
|
|
25
|
+
log.title(`📦 Initialize CurDX-Flow project`);
|
|
26
|
+
console.log(` Target: ${color.cyan(absTarget)}`);
|
|
27
|
+
|
|
28
|
+
// ---------- Check target exists ----------
|
|
29
|
+
try {
|
|
30
|
+
const stat = await fs.stat(absTarget);
|
|
31
|
+
if (!stat.isDirectory()) {
|
|
32
|
+
log.err(`Target is not a directory: ${absTarget}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
log.err(`Directory does not exist: ${absTarget}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ---------- Check existing .flow/ ----------
|
|
41
|
+
const flowDir = path.join(absTarget, ".flow");
|
|
42
|
+
if (existsSync(flowDir)) {
|
|
43
|
+
if (!force) {
|
|
44
|
+
log.warn(`.flow/ already exists`);
|
|
45
|
+
const ok = await confirm("Overwrite existing .flow/?", false);
|
|
46
|
+
if (!ok) {
|
|
47
|
+
log.info("Cancelled");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------- Find templates ----------
|
|
54
|
+
const tmplDir = await findLatestTemplatesDir();
|
|
55
|
+
|
|
56
|
+
// ---------- Create directory structure ----------
|
|
57
|
+
log.step(1, 4, "Creating .flow/ directory structure...");
|
|
58
|
+
const dirs = [
|
|
59
|
+
".flow",
|
|
60
|
+
".flow/specs",
|
|
61
|
+
".flow/_epics",
|
|
62
|
+
".flow/checkpoints",
|
|
63
|
+
".flow/threads",
|
|
64
|
+
".flow/seeds",
|
|
65
|
+
];
|
|
66
|
+
for (const d of dirs) {
|
|
67
|
+
await fs.mkdir(path.join(absTarget, d), { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
log.ok("Directories created");
|
|
70
|
+
|
|
71
|
+
// ---------- Generate files from templates ----------
|
|
72
|
+
log.step(2, 4, "Generating core files...");
|
|
73
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
74
|
+
const userName = gitConfig("user.name") || "you";
|
|
75
|
+
const projectName = path.basename(absTarget);
|
|
76
|
+
|
|
77
|
+
const files = [
|
|
78
|
+
{ tmpl: "PROJECT.md.tmpl", out: "PROJECT.md" },
|
|
79
|
+
{ tmpl: "CONTEXT.md.tmpl", out: "CONTEXT.md" },
|
|
80
|
+
{ tmpl: "STATE.md.tmpl", out: "STATE.md" },
|
|
81
|
+
{ tmpl: "ROADMAP.md.tmpl", out: "ROADMAP.md" },
|
|
82
|
+
{ tmpl: "config.json.tmpl", out: "config.json" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
const outPath = path.join(flowDir, f.out);
|
|
87
|
+
if (existsSync(outPath) && !force) {
|
|
88
|
+
log.info(` ${f.out} already exists, skipping`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let content;
|
|
93
|
+
if (tmplDir) {
|
|
94
|
+
try {
|
|
95
|
+
content = await fs.readFile(path.join(tmplDir, f.tmpl), "utf-8");
|
|
96
|
+
} catch {
|
|
97
|
+
content = embeddedTemplate(f.tmpl);
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
content = embeddedTemplate(f.tmpl);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Placeholder replacement
|
|
104
|
+
content = content
|
|
105
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, projectName)
|
|
106
|
+
.replace(/\{\{CREATED_DATE\}\}/g, today)
|
|
107
|
+
.replace(/\{\{USER_NAME\}\}/g, userName);
|
|
108
|
+
|
|
109
|
+
await fs.writeFile(outPath, content, "utf-8");
|
|
110
|
+
log.ok(` .flow/${f.out}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------- Update .gitignore ----------
|
|
114
|
+
log.step(3, 4, "Updating .gitignore...");
|
|
115
|
+
const giPath = path.join(absTarget, ".gitignore");
|
|
116
|
+
const giAdd = [
|
|
117
|
+
"",
|
|
118
|
+
"# CurDX-Flow runtime (auto-generated, do not commit)",
|
|
119
|
+
".flow/checkpoints/",
|
|
120
|
+
".flow/threads/",
|
|
121
|
+
".flow/seeds/",
|
|
122
|
+
".flow/.active-spec",
|
|
123
|
+
".flow/specs/*/.state.json",
|
|
124
|
+
".flow/specs/*/.progress.md",
|
|
125
|
+
".flow/_epics/*/.epic-state.json",
|
|
126
|
+
].join("\n");
|
|
127
|
+
|
|
128
|
+
let existingGi = "";
|
|
129
|
+
try {
|
|
130
|
+
existingGi = await fs.readFile(giPath, "utf-8");
|
|
131
|
+
} catch {
|
|
132
|
+
// no .gitignore yet
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!existingGi.includes("CurDX-Flow")) {
|
|
136
|
+
await fs.writeFile(giPath, existingGi + giAdd + "\n", "utf-8");
|
|
137
|
+
log.ok(".gitignore appended with CurDX-Flow ignores");
|
|
138
|
+
} else {
|
|
139
|
+
log.info(".gitignore already has CurDX-Flow rules, skipping");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------- Done ----------
|
|
143
|
+
log.blank();
|
|
144
|
+
log.step(4, 4, "Done");
|
|
145
|
+
log.ok(`CurDX-Flow project initialized: ${color.cyan(absTarget)}`);
|
|
146
|
+
|
|
147
|
+
console.log(`\n${color.bold("Next steps:")}\n`);
|
|
148
|
+
console.log(` 1. Edit ${color.cyan(".flow/PROJECT.md")} — fill in the project vision (~5 min)`);
|
|
149
|
+
console.log(` 2. Edit ${color.cyan(".flow/CONTEXT.md")} — your preferences`);
|
|
150
|
+
console.log(` 3. Enter Claude Code:`);
|
|
151
|
+
console.log(` ${color.cyan("cd " + absTarget)}`);
|
|
152
|
+
console.log(` ${color.cyan("claude")}`);
|
|
153
|
+
console.log(` ${color.cyan("/curdx-flow:start my-feature \"<description>\"")}`);
|
|
154
|
+
console.log();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------- Helpers ----------
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Find the latest installed templates directory in plugin cache.
|
|
161
|
+
* Returns null if plugin not installed.
|
|
162
|
+
*/
|
|
163
|
+
async function findLatestTemplatesDir() {
|
|
164
|
+
const pluginBase = pluginCacheDir();
|
|
165
|
+
try {
|
|
166
|
+
const versions = await fs.readdir(pluginBase);
|
|
167
|
+
// Pick last (lexicographically — semver works for simple cases)
|
|
168
|
+
const sorted = versions.sort();
|
|
169
|
+
for (let i = sorted.length - 1; i >= 0; i--) {
|
|
170
|
+
const tmplDir = path.join(pluginBase, sorted[i], "templates");
|
|
171
|
+
if (existsSync(tmplDir)) return tmplDir;
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function gitConfig(key) {
|
|
180
|
+
if (!has("git")) return null;
|
|
181
|
+
const r = runSync("git", ["config", "--get", key]);
|
|
182
|
+
return r.code === 0 ? r.stdout.trim() : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Embedded fallback templates — used when plugin cache not available.
|
|
187
|
+
* Keep these minimal; rich templates live in templates/*.tmpl.
|
|
188
|
+
*
|
|
189
|
+
* Note: these embedded templates are English. Users who want Chinese
|
|
190
|
+
* project state can edit the resulting files (or set lang preference
|
|
191
|
+
* in CONTEXT.md and use the rich templates from the plugin cache).
|
|
192
|
+
*/
|
|
193
|
+
function embeddedTemplate(name) {
|
|
194
|
+
const today = "{{CREATED_DATE}}";
|
|
195
|
+
const project = "{{PROJECT_NAME}}";
|
|
196
|
+
const user = "{{USER_NAME}}";
|
|
197
|
+
|
|
198
|
+
switch (name) {
|
|
199
|
+
case "PROJECT.md.tmpl":
|
|
200
|
+
return `# ${project}
|
|
201
|
+
|
|
202
|
+
> CurDX-Flow project vision
|
|
203
|
+
|
|
204
|
+
## One-line description
|
|
205
|
+
TODO: in one sentence — what is the project, who is it for, what problem does it solve
|
|
206
|
+
|
|
207
|
+
## Why this exists
|
|
208
|
+
TODO:
|
|
209
|
+
|
|
210
|
+
## Core users
|
|
211
|
+
TODO:
|
|
212
|
+
|
|
213
|
+
## Success criteria (verifiable metrics)
|
|
214
|
+
1. TODO:
|
|
215
|
+
2. TODO:
|
|
216
|
+
|
|
217
|
+
## Tech stack
|
|
218
|
+
TODO:
|
|
219
|
+
|
|
220
|
+
## Out of scope (Scope Guard)
|
|
221
|
+
- ✗ TODO:
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
_Generated by curdx-flow init on ${today}. Maintainer: ${user}_
|
|
226
|
+
`;
|
|
227
|
+
|
|
228
|
+
case "CONTEXT.md.tmpl":
|
|
229
|
+
return `# ${project} — User preferences
|
|
230
|
+
|
|
231
|
+
## Code style
|
|
232
|
+
- Indentation: TODO
|
|
233
|
+
- Quotes: TODO
|
|
234
|
+
- Naming: TODO
|
|
235
|
+
|
|
236
|
+
## Communication preferences
|
|
237
|
+
- Language: en
|
|
238
|
+
- Verbosity: balanced
|
|
239
|
+
|
|
240
|
+
## Tooling preferences
|
|
241
|
+
- Package manager: TODO
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
_Generated by curdx-flow init on ${today}_
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
case "STATE.md.tmpl":
|
|
249
|
+
return `# ${project} — Cross-session state
|
|
250
|
+
|
|
251
|
+
## Key decisions (Decisions)
|
|
252
|
+
None yet.
|
|
253
|
+
|
|
254
|
+
## Blockers
|
|
255
|
+
None yet.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
_Initialized on ${today}_
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
case "ROADMAP.md.tmpl":
|
|
263
|
+
return `# ${project} — Roadmap
|
|
264
|
+
|
|
265
|
+
## Current version: v0.1 (MVP)
|
|
266
|
+
|
|
267
|
+
**Goal**: TODO
|
|
268
|
+
|
|
269
|
+
### Success criteria
|
|
270
|
+
- [ ] TODO
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
_Initialized on ${today}_
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
case "config.json.tmpl":
|
|
278
|
+
return `{
|
|
279
|
+
"version": "1.0",
|
|
280
|
+
"mode": "standard",
|
|
281
|
+
"execution": {
|
|
282
|
+
"strategy": "auto",
|
|
283
|
+
"max_parallel": 5
|
|
284
|
+
},
|
|
285
|
+
"gates": {
|
|
286
|
+
"always_on": ["karpathy-gate", "verification-gate"],
|
|
287
|
+
"standard_mode": ["tdd-gate", "coverage-audit-gate"]
|
|
288
|
+
},
|
|
289
|
+
"created": "${today}"
|
|
290
|
+
}
|
|
291
|
+
`;
|
|
292
|
+
|
|
293
|
+
default:
|
|
294
|
+
return `# ${name}\n\n(template not found)\n`;
|
|
295
|
+
}
|
|
296
|
+
}
|
package/cli/install.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* install command — install curdx-flow plugin + optional recommended plugins.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
color,
|
|
7
|
+
log,
|
|
8
|
+
run,
|
|
9
|
+
has,
|
|
10
|
+
claudeVersion,
|
|
11
|
+
listPlugins,
|
|
12
|
+
multiSelect,
|
|
13
|
+
ensureClaudeMemRuntimes,
|
|
14
|
+
} from "./utils.js";
|
|
15
|
+
import { injectGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
|
|
16
|
+
|
|
17
|
+
// Recommended plugins with their marketplace source + install identifier
|
|
18
|
+
const RECOMMENDED = [
|
|
19
|
+
{
|
|
20
|
+
name: "pua",
|
|
21
|
+
marketplace: "tanweai/pua",
|
|
22
|
+
installSpec: "pua@pua-skills",
|
|
23
|
+
hint: "no-give-up + three red lines",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "claude-mem",
|
|
27
|
+
marketplace: "thedotmack/claude-mem",
|
|
28
|
+
installSpec: "claude-mem@thedotmack",
|
|
29
|
+
hint: "automatic cross-session memory",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "frontend-design",
|
|
33
|
+
marketplace: null, // already in default marketplace claude-plugins-official
|
|
34
|
+
installSpec: "frontend-design@claude-plugins-official",
|
|
35
|
+
hint: "Anthropic official UI skill",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export async function install(args = []) {
|
|
40
|
+
const all = args.includes("--all");
|
|
41
|
+
const noDeps = args.includes("--no-deps");
|
|
42
|
+
|
|
43
|
+
log.title("🚀 CurDX-Flow Installer");
|
|
44
|
+
|
|
45
|
+
// ---------- Step 1: Check claude CLI ----------
|
|
46
|
+
log.step(1, 4, "Checking claude CLI...");
|
|
47
|
+
const ver = claudeVersion();
|
|
48
|
+
if (!ver) {
|
|
49
|
+
log.err("claude CLI not found. Install Claude Code from https://code.claude.com first.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
log.ok(`claude CLI found (${ver})`);
|
|
53
|
+
|
|
54
|
+
// ---------- Step 2: Add marketplace ----------
|
|
55
|
+
log.blank();
|
|
56
|
+
log.step(2, 4, "Adding curdx-flow marketplace...");
|
|
57
|
+
const addRes = await run("claude", ["plugin", "marketplace", "add", "curdx/curdx-flow"], {
|
|
58
|
+
silent: true,
|
|
59
|
+
});
|
|
60
|
+
if (addRes.code !== 0 && !addRes.stderr.includes("already")) {
|
|
61
|
+
// Not a fatal error if already added
|
|
62
|
+
log.warn(`marketplace add output: ${addRes.stderr.trim() || addRes.stdout.trim()}`);
|
|
63
|
+
} else {
|
|
64
|
+
log.ok("curdx-flow-marketplace added");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------- Step 3: Install curdx-flow plugin ----------
|
|
68
|
+
log.blank();
|
|
69
|
+
log.step(3, 4, "Installing curdx-flow plugin (3 MCPs will auto-start)...");
|
|
70
|
+
const installed = listPlugins();
|
|
71
|
+
const already = installed.find((p) => p.name === "curdx-flow");
|
|
72
|
+
if (already) {
|
|
73
|
+
log.ok(`curdx-flow already installed (v${already.version}, ${already.status})`);
|
|
74
|
+
} else {
|
|
75
|
+
const r = await run(
|
|
76
|
+
"claude",
|
|
77
|
+
["plugin", "install", "curdx-flow@curdx-flow-marketplace"],
|
|
78
|
+
{ silent: true }
|
|
79
|
+
);
|
|
80
|
+
if (r.code !== 0) {
|
|
81
|
+
log.err(`Install failed: ${r.stderr.trim() || r.stdout.trim()}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
log.ok("curdx-flow installed");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------- Step 4: Recommended plugins ----------
|
|
88
|
+
log.blank();
|
|
89
|
+
log.step(4, 4, "Recommended plugins");
|
|
90
|
+
|
|
91
|
+
if (noDeps) {
|
|
92
|
+
log.info("Skipping recommended plugins (--no-deps)");
|
|
93
|
+
printNextSteps();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let toInstall;
|
|
98
|
+
if (all) {
|
|
99
|
+
toInstall = RECOMMENDED.map((r) => r.name);
|
|
100
|
+
log.info("--all mode: installing all recommended");
|
|
101
|
+
} else {
|
|
102
|
+
const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
|
|
103
|
+
const choices = RECOMMENDED.map((r) => ({
|
|
104
|
+
label: `${color.bold(r.name)}${currentlyInstalled.has(r.name) ? color.green(" (installed)") : ""}`,
|
|
105
|
+
value: r.name,
|
|
106
|
+
hint: r.hint,
|
|
107
|
+
}));
|
|
108
|
+
const defaults = RECOMMENDED
|
|
109
|
+
.map((r, i) => (currentlyInstalled.has(r.name) ? -1 : i))
|
|
110
|
+
.filter((i) => i >= 0);
|
|
111
|
+
|
|
112
|
+
toInstall = await multiSelect("Which recommended plugins to install?", choices, defaults);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!toInstall || toInstall.length === 0) {
|
|
116
|
+
log.info("No recommended plugins selected, skipping");
|
|
117
|
+
printNextSteps();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Install each
|
|
122
|
+
for (const pluginName of toInstall) {
|
|
123
|
+
const rec = RECOMMENDED.find((r) => r.name === pluginName);
|
|
124
|
+
log.blank();
|
|
125
|
+
console.log(` ${color.cyan("⏳")} Installing ${color.bold(rec.name)}...`);
|
|
126
|
+
|
|
127
|
+
// 1. Add marketplace (if needed)
|
|
128
|
+
if (rec.marketplace) {
|
|
129
|
+
const ma = await run(
|
|
130
|
+
"claude",
|
|
131
|
+
["plugin", "marketplace", "add", rec.marketplace],
|
|
132
|
+
{ silent: true }
|
|
133
|
+
);
|
|
134
|
+
if (ma.code !== 0 && !ma.stderr.includes("already")) {
|
|
135
|
+
log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
|
|
136
|
+
// Don't abort — may already exist
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 2. Install
|
|
141
|
+
const ir = await run("claude", ["plugin", "install", rec.installSpec], {
|
|
142
|
+
silent: true,
|
|
143
|
+
});
|
|
144
|
+
if (ir.code === 0) {
|
|
145
|
+
console.log(` ${color.green("✓")} ${rec.name} installed`);
|
|
146
|
+
|
|
147
|
+
// 3. Post-install hook for claude-mem: its .mcp.json hard-codes `bun`,
|
|
148
|
+
// but ~/.bun/bin is not on PATH when Claude Code spawns the MCP server.
|
|
149
|
+
// Auto-create a PATH-visible symlink to fix it.
|
|
150
|
+
if (rec.name === "claude-mem") {
|
|
151
|
+
const r = ensureClaudeMemRuntimes();
|
|
152
|
+
for (const [name, res] of Object.entries(r)) {
|
|
153
|
+
if (res.status === "linked") {
|
|
154
|
+
console.log(
|
|
155
|
+
` ${color.green("✓")} ${name} → PATH symlink created ${color.dim(`(${res.link} → ${res.path})`)}`
|
|
156
|
+
);
|
|
157
|
+
} else if (res.status === "missing") {
|
|
158
|
+
console.log(
|
|
159
|
+
` ${color.yellow("⚠")} ${name} not installed ${color.dim("(claude-mem will auto-install on first run; or run: curdx-flow doctor)")}`
|
|
160
|
+
);
|
|
161
|
+
} else if (res.status === "path-unwritable") {
|
|
162
|
+
console.log(
|
|
163
|
+
` ${color.yellow("⚠")} ${name} installed ${color.dim(`(${res.path}) but no writable PATH location — add export PATH=\"${res.path.split("/").slice(0,-1).join("/")}:$PATH\" to your shell rc`)}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
// status === "ok" → already on PATH, stay silent
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
console.log(
|
|
171
|
+
` ${color.red("✗")} ${rec.name} install failed: ${ir.stderr.trim().split("\n").pop()}`
|
|
172
|
+
);
|
|
173
|
+
console.log(
|
|
174
|
+
color.dim(
|
|
175
|
+
` Run manually: claude plugin install ${rec.installSpec}`
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------- Step 5: inject global protocols ----------
|
|
182
|
+
log.blank();
|
|
183
|
+
console.log(color.dim("Injecting global protocols into ~/.claude/CLAUDE.md..."));
|
|
184
|
+
try {
|
|
185
|
+
const r = injectGlobalProtocols();
|
|
186
|
+
if (r.action === "created") {
|
|
187
|
+
log.ok(`Global protocols injected ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
|
|
188
|
+
} else if (r.action === "upgraded") {
|
|
189
|
+
log.ok(`Global protocols upgraded ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
|
|
190
|
+
} else {
|
|
191
|
+
log.info(`Global protocols up to date ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
log.warn(`Protocol injection failed: ${err.message} ${color.dim("(non-blocking)")}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
printNextSteps();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function printNextSteps() {
|
|
201
|
+
console.log(`\n${color.bold("✅ Install complete")}\n`);
|
|
202
|
+
console.log(`${color.bold("Next steps")}:\n`);
|
|
203
|
+
console.log(` ${color.dim("# Verify health")}`);
|
|
204
|
+
console.log(` curdx-flow doctor\n`);
|
|
205
|
+
console.log(` ${color.dim("# Initialize .flow/ in your project")}`);
|
|
206
|
+
console.log(` cd ~/your-project && curdx-flow init\n`);
|
|
207
|
+
console.log(` ${color.dim("# Start using it (inside Claude Code)")}`);
|
|
208
|
+
console.log(` ${color.cyan("claude")}`);
|
|
209
|
+
console.log(` ${color.cyan("/curdx-flow:start my-feature \"<describe what to build>\"")}\n`);
|
|
210
|
+
console.log(
|
|
211
|
+
`${color.bold("Learn more")}: https://github.com/curdx/curdx-flow/blob/main/docs/getting-started.md\n`
|
|
212
|
+
);
|
|
213
|
+
}
|