@curdx/flow 2.0.21 → 2.2.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 +25 -2
- package/.claude-plugin/plugin.json +10 -1
- package/CHANGELOG.md +106 -3
- package/README.md +3 -0
- package/README.zh.md +14 -5
- package/agent-preamble/preamble.md +3 -6
- package/agents/flow-qa-engineer.md +16 -15
- package/agents/flow-ui-researcher.md +2 -2
- package/agents/flow-verifier.md +3 -3
- package/bin/curdx-flow +5 -0
- package/cli/README.md +10 -9
- package/cli/install-bundled-mcps.js +37 -0
- package/cli/install-companions.js +19 -252
- package/cli/install-context7-config.js +97 -0
- package/cli/install-recommended-plugins.js +104 -0
- package/cli/install-required-plugins.js +57 -0
- package/cli/install-self-update.js +2 -91
- package/cli/install.js +12 -1
- package/cli/lib/claude.js +42 -11
- package/cli/lib/doctor-report.js +47 -8
- package/cli/lib/semver.js +95 -0
- package/cli/protocols-body.md +3 -2
- package/cli/utils.js +1 -0
- package/hooks/scripts/quick-mode-guard.sh +6 -7
- package/hooks/scripts/session-start.sh +6 -3
- package/knowledge/execution-strategies.md +5 -5
- package/knowledge/planning-reviews.md +2 -2
- package/knowledge/wave-execution.md +17 -17
- package/package.json +3 -3
- package/schemas/agent-frontmatter.schema.json +66 -0
- package/schemas/config.schema.json +24 -4
- package/schemas/gate-frontmatter.schema.json +30 -0
- package/schemas/hooks.schema.json +83 -0
- package/schemas/plugin-manifest.schema.json +66 -0
- package/schemas/skill-frontmatter.schema.json +72 -0
- package/schemas/spec-state.schema.json +7 -2
- package/skills/brownfield-index/SKILL.md +2 -1
- package/skills/browser-qa/SKILL.md +5 -4
- package/skills/debug/SKILL.md +105 -0
- package/skills/epic/SKILL.md +2 -1
- package/{commands/fast.md → skills/fast/SKILL.md} +2 -1
- package/{commands/help.md → skills/help/SKILL.md} +15 -5
- package/{commands/implement.md → skills/implement/SKILL.md} +14 -154
- package/skills/implement/references/wave-execution.md +162 -0
- package/{commands/init.md → skills/init/SKILL.md} +1 -0
- package/{commands/review.md → skills/review/SKILL.md} +38 -3
- package/skills/security-audit/SKILL.md +2 -1
- package/{commands/spec.md → skills/spec/SKILL.md} +4 -3
- package/{commands/start.md → skills/start/SKILL.md} +2 -1
- package/skills/ui-sketch/SKILL.md +2 -1
- package/skills/verify/SKILL.md +99 -0
- package/templates/CONTEXT.md.tmpl +1 -1
- package/templates/PROJECT.md.tmpl +1 -1
- package/templates/config.json.tmpl +6 -6
- package/templates/progress.md.tmpl +2 -2
- package/commands/debug.md +0 -199
- package/commands/verify.md +0 -142
|
@@ -1,252 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
} from "./
|
|
20
|
-
|
|
21
|
-
const RECOMMENDED = RECOMMENDED_PLUGINS;
|
|
22
|
-
const CONTEXT7_COMMAND = "npx";
|
|
23
|
-
const CONTEXT7_ARGS = ["-y", "@upstash/context7-mcp"];
|
|
24
|
-
|
|
25
|
-
export async function installRequiredPlugins({ yes, language, config }) {
|
|
26
|
-
log.blank();
|
|
27
|
-
log.info("Installing required Claude Code plugins...");
|
|
28
|
-
for (const plugin of REQUIRED_PLUGINS) {
|
|
29
|
-
console.log(` ${color.cyan("▸")} Installing ${color.bold(plugin.name)}...`);
|
|
30
|
-
const ma = await addPluginMarketplace(plugin);
|
|
31
|
-
if (ma.code !== 0 && !ma.stderr.includes("already")) {
|
|
32
|
-
log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const ir = await installPlugin(plugin);
|
|
36
|
-
if (ir.code === 0) {
|
|
37
|
-
console.log(` ${color.green("✓")} ${plugin.name} installed`);
|
|
38
|
-
|
|
39
|
-
if (plugin.requiresConfig && plugin.configType === "apiKey" && !yes) {
|
|
40
|
-
await promptPluginConfig(plugin, language, config);
|
|
41
|
-
}
|
|
42
|
-
} else {
|
|
43
|
-
console.log(
|
|
44
|
-
` ${color.red("✗")} ${plugin.name} install failed: ${resultLastLine(ir)}`
|
|
45
|
-
);
|
|
46
|
-
console.log(
|
|
47
|
-
color.dim(
|
|
48
|
-
` Run manually: claude plugin marketplace add --scope ${plugin.scope} ${plugin.marketplaceSource}`
|
|
49
|
-
)
|
|
50
|
-
);
|
|
51
|
-
console.log(
|
|
52
|
-
color.dim(
|
|
53
|
-
` Then: claude plugin install --scope ${plugin.scope} ${plugin.installSpec}`
|
|
54
|
-
)
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export async function registerBundledMcps() {
|
|
61
|
-
log.blank();
|
|
62
|
-
log.info("Registering required MCP servers (user-level)...");
|
|
63
|
-
const existingUserMcps = readUserMcpConfig();
|
|
64
|
-
for (const mcp of BUNDLED_MCPS) {
|
|
65
|
-
if (mcp.preserveExisting && existingUserMcps.has(mcp.name)) {
|
|
66
|
-
const existing = existingUserMcps.get(mcp.name);
|
|
67
|
-
log.info(
|
|
68
|
-
` ${mcp.name.padEnd(22)} ${color.dim(`already registered (${(existing.args || []).join(" ")}) — preserving`)}`
|
|
69
|
-
);
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
const r = await addMcp(mcp);
|
|
73
|
-
if (r.code === 0) {
|
|
74
|
-
log.ok(` ${mcp.name.padEnd(22)} ${color.dim("registered")}`);
|
|
75
|
-
} else if (r.stderr.includes("already exists")) {
|
|
76
|
-
log.info(` ${mcp.name.padEnd(22)} ${color.dim("already exists — skipped")}`);
|
|
77
|
-
} else {
|
|
78
|
-
log.warn(
|
|
79
|
-
` ${mcp.name.padEnd(22)} registration failed: ${resultLastLine(r)}`
|
|
80
|
-
);
|
|
81
|
-
log.info(
|
|
82
|
-
` Run manually: claude mcp add --scope user ${mcp.name} -- ${mcp.command} ${mcp.args.join(" ")}`
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function installRecommendedPlugins({ all, yes, language }) {
|
|
89
|
-
log.blank();
|
|
90
|
-
log.step(4, 5, "Recommended plugins");
|
|
91
|
-
|
|
92
|
-
let toInstall;
|
|
93
|
-
if (all) {
|
|
94
|
-
toInstall = RECOMMENDED.map((r) => r.name);
|
|
95
|
-
log.info("--all mode: installing all recommended");
|
|
96
|
-
} else if (yes) {
|
|
97
|
-
const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
|
|
98
|
-
toInstall = RECOMMENDED
|
|
99
|
-
.filter((r) => !currentlyInstalled.has(r.name))
|
|
100
|
-
.map((r) => r.name);
|
|
101
|
-
log.info(`--yes mode: installing ${toInstall.length} recommended plugins`);
|
|
102
|
-
} else {
|
|
103
|
-
const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
|
|
104
|
-
const options = RECOMMENDED.map((r) => ({
|
|
105
|
-
value: r.name,
|
|
106
|
-
label: `${r.name}${currentlyInstalled.has(r.name) ? " (installed)" : ""}`,
|
|
107
|
-
hint: r.hint,
|
|
108
|
-
}));
|
|
109
|
-
const initialValues = RECOMMENDED.map((r) => r.name);
|
|
110
|
-
|
|
111
|
-
toInstall = await multiselectClack({
|
|
112
|
-
message: language === "zh"
|
|
113
|
-
? "选择要安装的推荐插件(空格切换,回车确认)"
|
|
114
|
-
: "Select recommended plugins to install (space to toggle, enter to confirm)",
|
|
115
|
-
options,
|
|
116
|
-
initialValues,
|
|
117
|
-
required: false,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!toInstall || toInstall.length === 0) {
|
|
122
|
-
log.info("No recommended plugins selected, skipping");
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
for (const pluginName of toInstall) {
|
|
127
|
-
const rec = RECOMMENDED.find((r) => r.name === pluginName);
|
|
128
|
-
log.blank();
|
|
129
|
-
console.log(` ${color.cyan("▸")} Installing ${color.bold(rec.name)}...`);
|
|
130
|
-
|
|
131
|
-
if (rec.marketplaceSource) {
|
|
132
|
-
const ma = await addPluginMarketplace(rec);
|
|
133
|
-
if (ma.code !== 0 && !ma.stderr.includes("already")) {
|
|
134
|
-
log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const ir = await installPlugin(rec);
|
|
139
|
-
if (ir.code === 0) {
|
|
140
|
-
console.log(` ${color.green("✓")} ${rec.name} installed`);
|
|
141
|
-
|
|
142
|
-
if (rec.postInstall === "claude-mem-runtimes") {
|
|
143
|
-
const r = ensureClaudeMemRuntimes();
|
|
144
|
-
for (const [name, res] of Object.entries(r)) {
|
|
145
|
-
if (res.status === "linked") {
|
|
146
|
-
console.log(
|
|
147
|
-
` ${color.green("✓")} ${name} → PATH symlink created ${color.dim(`(${res.link} → ${res.path})`)}`
|
|
148
|
-
);
|
|
149
|
-
} else if (res.status === "missing") {
|
|
150
|
-
console.log(
|
|
151
|
-
` ${color.yellow("⚠")} ${name} not installed ${color.dim("(claude-mem will auto-install on first run; or run: curdx-flow doctor)")}`
|
|
152
|
-
);
|
|
153
|
-
} else if (res.status === "path-unwritable") {
|
|
154
|
-
console.log(
|
|
155
|
-
` ${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`)}`
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
console.log(
|
|
162
|
-
` ${color.red("✗")} ${rec.name} install failed: ${resultLastLine(ir)}`
|
|
163
|
-
);
|
|
164
|
-
console.log(
|
|
165
|
-
color.dim(
|
|
166
|
-
` Run manually: claude plugin install --scope ${rec.scope} ${rec.installSpec}`
|
|
167
|
-
)
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return true;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function promptPluginConfig(plugin, language, config) {
|
|
175
|
-
if (plugin.name !== "context7-plugin") return;
|
|
176
|
-
|
|
177
|
-
log.blank();
|
|
178
|
-
await note(
|
|
179
|
-
language === "zh"
|
|
180
|
-
? "Context7 需要 API key 才能使用。\n获取 API key: https://console.upstash.com/context7"
|
|
181
|
-
: "Context7 requires an API key to function.\nGet your API key at: https://console.upstash.com/context7",
|
|
182
|
-
language === "zh" ? "配置 Context7" : "Configure Context7"
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
const apiKey = await text({
|
|
186
|
-
message: language === "zh"
|
|
187
|
-
? "输入你的 Context7 API key(或按 Enter 跳过)"
|
|
188
|
-
: "Enter your Context7 API key (or press Enter to skip)",
|
|
189
|
-
placeholder: "ctx7sk-...",
|
|
190
|
-
validate: (value) => {
|
|
191
|
-
if (!value) return;
|
|
192
|
-
if (!value.startsWith("ctx7sk-") && !value.startsWith("ctx7_")) {
|
|
193
|
-
return language === "zh"
|
|
194
|
-
? "API key 应该以 ctx7sk- 或 ctx7_ 开头"
|
|
195
|
-
: "API key should start with ctx7sk- or ctx7_";
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
if (apiKey) {
|
|
201
|
-
config.context7ApiKey = apiKey;
|
|
202
|
-
writeConfig(config);
|
|
203
|
-
|
|
204
|
-
const r = await addMcp(buildContext7Mcp(apiKey));
|
|
205
|
-
|
|
206
|
-
if (r.code === 0) {
|
|
207
|
-
log.ok(
|
|
208
|
-
language === "zh"
|
|
209
|
-
? " Context7 API key 已配置"
|
|
210
|
-
: " Context7 API key configured"
|
|
211
|
-
);
|
|
212
|
-
} else if (r.stderr.includes("already exists")) {
|
|
213
|
-
await removeMcp({ name: "context7" });
|
|
214
|
-
const r2 = await addMcp(buildContext7Mcp(apiKey));
|
|
215
|
-
if (r2.code === 0) {
|
|
216
|
-
log.ok(
|
|
217
|
-
language === "zh"
|
|
218
|
-
? " Context7 API key 已更新"
|
|
219
|
-
: " Context7 API key updated"
|
|
220
|
-
);
|
|
221
|
-
}
|
|
222
|
-
} else {
|
|
223
|
-
log.warn(
|
|
224
|
-
language === "zh"
|
|
225
|
-
? ` Context7 MCP 配置失败: ${resultLastLine(r)}`
|
|
226
|
-
: ` Context7 MCP configuration failed: ${resultLastLine(r)}`
|
|
227
|
-
);
|
|
228
|
-
log.info(
|
|
229
|
-
color.dim(
|
|
230
|
-
language === "zh"
|
|
231
|
-
? ` 手动运行: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- ${CONTEXT7_COMMAND} ${CONTEXT7_ARGS.join(" ")}`
|
|
232
|
-
: ` Run manually: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- ${CONTEXT7_COMMAND} ${CONTEXT7_ARGS.join(" ")}`
|
|
233
|
-
)
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
|
-
} else {
|
|
237
|
-
log.info(
|
|
238
|
-
language === "zh"
|
|
239
|
-
? " 跳过 Context7 配置(稍后可运行 curdx-flow install 重新配置)"
|
|
240
|
-
: " Skipped Context7 configuration (run curdx-flow install later to reconfigure)"
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function buildContext7Mcp(apiKey) {
|
|
246
|
-
return {
|
|
247
|
-
name: "context7",
|
|
248
|
-
env: [`CONTEXT7_API_KEY=${apiKey}`],
|
|
249
|
-
command: CONTEXT7_COMMAND,
|
|
250
|
-
args: CONTEXT7_ARGS,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Re-export barrel preserving the pre-Stage-7 import surface. New callers
|
|
3
|
+
* should import directly from the concern-specific modules below instead
|
|
4
|
+
* of going through this barrel. Kept in place so existing imports in
|
|
5
|
+
* cli/install.js and cli/install-workflow.js keep working unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Concern split (see docs/refactor-plan.md Stage 7):
|
|
8
|
+
* install-required-plugins.js — required Claude Code companion plugins
|
|
9
|
+
* install-bundled-mcps.js — user-level MCP registration
|
|
10
|
+
* install-recommended-plugins.js — optional recommended plugins
|
|
11
|
+
* install-context7-config.js — context7 API-key prompt (private to required)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
addRequiredPluginMarketplaces,
|
|
16
|
+
installRequiredPlugins,
|
|
17
|
+
} from "./install-required-plugins.js";
|
|
18
|
+
export { registerBundledMcps } from "./install-bundled-mcps.js";
|
|
19
|
+
export { installRecommendedPlugins } from "./install-recommended-plugins.js";
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context7 API-key configuration during install. Private to the
|
|
3
|
+
* required-plugins concern; not re-exported from install-companions.js.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { addMcp, removeMcp } from "./lib/claude-ops.js";
|
|
7
|
+
import {
|
|
8
|
+
color,
|
|
9
|
+
log,
|
|
10
|
+
note,
|
|
11
|
+
resultLastLine,
|
|
12
|
+
text,
|
|
13
|
+
writeConfig,
|
|
14
|
+
} from "./utils.js";
|
|
15
|
+
|
|
16
|
+
const CONTEXT7_COMMAND = "npx";
|
|
17
|
+
const CONTEXT7_ARGS = ["-y", "@upstash/context7-mcp"];
|
|
18
|
+
|
|
19
|
+
export async function installContext7Config(plugin, language, config) {
|
|
20
|
+
if (plugin.name !== "context7-plugin") return;
|
|
21
|
+
|
|
22
|
+
log.blank();
|
|
23
|
+
await note(
|
|
24
|
+
language === "zh"
|
|
25
|
+
? "Context7 需要 API key 才能使用。\n获取 API key: https://console.upstash.com/context7"
|
|
26
|
+
: "Context7 requires an API key to function.\nGet your API key at: https://console.upstash.com/context7",
|
|
27
|
+
language === "zh" ? "配置 Context7" : "Configure Context7"
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const apiKey = await text({
|
|
31
|
+
message: language === "zh"
|
|
32
|
+
? "输入你的 Context7 API key(或按 Enter 跳过)"
|
|
33
|
+
: "Enter your Context7 API key (or press Enter to skip)",
|
|
34
|
+
placeholder: "ctx7sk-...",
|
|
35
|
+
validate: (value) => {
|
|
36
|
+
if (!value) return;
|
|
37
|
+
if (!value.startsWith("ctx7sk-") && !value.startsWith("ctx7_")) {
|
|
38
|
+
return language === "zh"
|
|
39
|
+
? "API key 应该以 ctx7sk- 或 ctx7_ 开头"
|
|
40
|
+
: "API key should start with ctx7sk- or ctx7_";
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (apiKey) {
|
|
46
|
+
config.context7ApiKey = apiKey;
|
|
47
|
+
writeConfig(config);
|
|
48
|
+
|
|
49
|
+
const r = await addMcp(buildContext7Mcp(apiKey));
|
|
50
|
+
|
|
51
|
+
if (r.code === 0) {
|
|
52
|
+
log.ok(
|
|
53
|
+
language === "zh"
|
|
54
|
+
? " Context7 API key 已配置"
|
|
55
|
+
: " Context7 API key configured"
|
|
56
|
+
);
|
|
57
|
+
} else if (r.stderr.includes("already exists")) {
|
|
58
|
+
await removeMcp({ name: "context7" });
|
|
59
|
+
const r2 = await addMcp(buildContext7Mcp(apiKey));
|
|
60
|
+
if (r2.code === 0) {
|
|
61
|
+
log.ok(
|
|
62
|
+
language === "zh"
|
|
63
|
+
? " Context7 API key 已更新"
|
|
64
|
+
: " Context7 API key updated"
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
log.warn(
|
|
69
|
+
language === "zh"
|
|
70
|
+
? ` Context7 MCP 配置失败: ${resultLastLine(r)}`
|
|
71
|
+
: ` Context7 MCP configuration failed: ${resultLastLine(r)}`
|
|
72
|
+
);
|
|
73
|
+
log.info(
|
|
74
|
+
color.dim(
|
|
75
|
+
language === "zh"
|
|
76
|
+
? ` 手动运行: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- ${CONTEXT7_COMMAND} ${CONTEXT7_ARGS.join(" ")}`
|
|
77
|
+
: ` Run manually: claude mcp add --scope user context7 --env CONTEXT7_API_KEY=${apiKey} -- ${CONTEXT7_COMMAND} ${CONTEXT7_ARGS.join(" ")}`
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
log.info(
|
|
83
|
+
language === "zh"
|
|
84
|
+
? " 跳过 Context7 配置(稍后可运行 curdx-flow install 重新配置)"
|
|
85
|
+
: " Skipped Context7 configuration (run curdx-flow install later to reconfigure)"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildContext7Mcp(apiKey) {
|
|
91
|
+
return {
|
|
92
|
+
name: "context7",
|
|
93
|
+
env: [`CONTEXT7_API_KEY=${apiKey}`],
|
|
94
|
+
command: CONTEXT7_COMMAND,
|
|
95
|
+
args: CONTEXT7_ARGS,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install optional recommended companion plugins. List source-of-truth:
|
|
3
|
+
* cli/registry.js RECOMMENDED_PLUGINS. Handles --all / --yes / interactive
|
|
4
|
+
* multiselect and the claude-mem bun/uv PATH symlink post-install step.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { addPluginMarketplace, installPlugin } from "./lib/claude-ops.js";
|
|
8
|
+
import { RECOMMENDED_PLUGINS } from "./registry.js";
|
|
9
|
+
import {
|
|
10
|
+
color,
|
|
11
|
+
ensureClaudeMemRuntimes,
|
|
12
|
+
listPlugins,
|
|
13
|
+
log,
|
|
14
|
+
multiselectClack,
|
|
15
|
+
resultLastLine,
|
|
16
|
+
} from "./utils.js";
|
|
17
|
+
|
|
18
|
+
const RECOMMENDED = RECOMMENDED_PLUGINS;
|
|
19
|
+
|
|
20
|
+
export async function installRecommendedPlugins({ all, yes, language }) {
|
|
21
|
+
log.blank();
|
|
22
|
+
log.step(4, 5, "Recommended plugins");
|
|
23
|
+
|
|
24
|
+
let toInstall;
|
|
25
|
+
if (all) {
|
|
26
|
+
toInstall = RECOMMENDED.map((r) => r.name);
|
|
27
|
+
log.info("--all mode: installing all recommended");
|
|
28
|
+
} else if (yes) {
|
|
29
|
+
const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
|
|
30
|
+
toInstall = RECOMMENDED
|
|
31
|
+
.filter((r) => !currentlyInstalled.has(r.name))
|
|
32
|
+
.map((r) => r.name);
|
|
33
|
+
log.info(`--yes mode: installing ${toInstall.length} recommended plugins`);
|
|
34
|
+
} else {
|
|
35
|
+
const currentlyInstalled = new Set(listPlugins().map((p) => p.name));
|
|
36
|
+
const options = RECOMMENDED.map((r) => ({
|
|
37
|
+
value: r.name,
|
|
38
|
+
label: `${r.name}${currentlyInstalled.has(r.name) ? " (installed)" : ""}`,
|
|
39
|
+
hint: r.hint,
|
|
40
|
+
}));
|
|
41
|
+
const initialValues = RECOMMENDED.map((r) => r.name);
|
|
42
|
+
|
|
43
|
+
toInstall = await multiselectClack({
|
|
44
|
+
message: language === "zh"
|
|
45
|
+
? "选择要安装的推荐插件(空格切换,回车确认)"
|
|
46
|
+
: "Select recommended plugins to install (space to toggle, enter to confirm)",
|
|
47
|
+
options,
|
|
48
|
+
initialValues,
|
|
49
|
+
required: false,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!toInstall || toInstall.length === 0) {
|
|
54
|
+
log.info("No recommended plugins selected, skipping");
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const pluginName of toInstall) {
|
|
59
|
+
const rec = RECOMMENDED.find((r) => r.name === pluginName);
|
|
60
|
+
log.blank();
|
|
61
|
+
console.log(` ${color.cyan("▸")} Installing ${color.bold(rec.name)}...`);
|
|
62
|
+
|
|
63
|
+
if (rec.marketplaceSource) {
|
|
64
|
+
const ma = await addPluginMarketplace(rec);
|
|
65
|
+
if (ma.code !== 0 && !ma.stderr.includes("already")) {
|
|
66
|
+
log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ir = await installPlugin(rec);
|
|
71
|
+
if (ir.code === 0) {
|
|
72
|
+
console.log(` ${color.green("✓")} ${rec.name} installed`);
|
|
73
|
+
|
|
74
|
+
if (rec.postInstall === "claude-mem-runtimes") {
|
|
75
|
+
const r = ensureClaudeMemRuntimes();
|
|
76
|
+
for (const [name, res] of Object.entries(r)) {
|
|
77
|
+
if (res.status === "linked") {
|
|
78
|
+
console.log(
|
|
79
|
+
` ${color.green("✓")} ${name} → PATH symlink created ${color.dim(`(${res.link} → ${res.path})`)}`
|
|
80
|
+
);
|
|
81
|
+
} else if (res.status === "missing") {
|
|
82
|
+
console.log(
|
|
83
|
+
` ${color.yellow("⚠")} ${name} not installed ${color.dim("(claude-mem will auto-install on first run; or run: curdx-flow doctor)")}`
|
|
84
|
+
);
|
|
85
|
+
} else if (res.status === "path-unwritable") {
|
|
86
|
+
console.log(
|
|
87
|
+
` ${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`)}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
console.log(
|
|
94
|
+
` ${color.red("✗")} ${rec.name} install failed: ${resultLastLine(ir)}`
|
|
95
|
+
);
|
|
96
|
+
console.log(
|
|
97
|
+
color.dim(
|
|
98
|
+
` Run manually: claude plugin install --scope ${rec.scope} ${rec.installSpec}`
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install required Claude Code companion plugins (today: context7-plugin).
|
|
3
|
+
* Post-install, dispatches Context7 API-key configuration if applicable.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { addPluginMarketplace, installPlugin } from "./lib/claude-ops.js";
|
|
7
|
+
import { REQUIRED_PLUGINS } from "./registry.js";
|
|
8
|
+
import { color, log, resultLastLine } from "./utils.js";
|
|
9
|
+
import { installContext7Config } from "./install-context7-config.js";
|
|
10
|
+
|
|
11
|
+
export async function addRequiredPluginMarketplaces({ logWarnings = true } = {}) {
|
|
12
|
+
for (const plugin of REQUIRED_PLUGINS) {
|
|
13
|
+
const ma = await addPluginMarketplace(plugin);
|
|
14
|
+
if (ma.code !== 0 && !ma.stderr.includes("already") && logWarnings) {
|
|
15
|
+
log.warn(` marketplace add warning: ${ma.stderr.trim().split("\n")[0]}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function installRequiredPlugins({
|
|
21
|
+
yes,
|
|
22
|
+
language,
|
|
23
|
+
config,
|
|
24
|
+
skipMarketplaceAdd = false,
|
|
25
|
+
}) {
|
|
26
|
+
log.blank();
|
|
27
|
+
log.info("Installing required Claude Code plugins...");
|
|
28
|
+
if (!skipMarketplaceAdd) {
|
|
29
|
+
await addRequiredPluginMarketplaces();
|
|
30
|
+
}
|
|
31
|
+
for (const plugin of REQUIRED_PLUGINS) {
|
|
32
|
+
console.log(` ${color.cyan("▸")} Installing ${color.bold(plugin.name)}...`);
|
|
33
|
+
|
|
34
|
+
const ir = await installPlugin(plugin);
|
|
35
|
+
if (ir.code === 0) {
|
|
36
|
+
console.log(` ${color.green("✓")} ${plugin.name} installed`);
|
|
37
|
+
|
|
38
|
+
if (plugin.requiresConfig && plugin.configType === "apiKey" && !yes) {
|
|
39
|
+
await installContext7Config(plugin, language, config);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
console.log(
|
|
43
|
+
` ${color.red("✗")} ${plugin.name} install failed: ${resultLastLine(ir)}`
|
|
44
|
+
);
|
|
45
|
+
console.log(
|
|
46
|
+
color.dim(
|
|
47
|
+
` Run manually: claude plugin marketplace add --scope ${plugin.scope} ${plugin.marketplaceSource}`
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
console.log(
|
|
51
|
+
color.dim(
|
|
52
|
+
` Then: claude plugin install --scope ${plugin.scope} ${plugin.installSpec}`
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -2,98 +2,9 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { log, run, runSync, VERSION } from "./utils.js";
|
|
5
|
+
import { compareVersions, isVersionNewer } from "./lib/semver.js";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
return /^\d+$/.test(token) ? Number(token) : token;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function parseVersion(version) {
|
|
11
|
-
const normalized = String(version || "").trim().replace(/^v/i, "");
|
|
12
|
-
const [coreRaw = "0", prereleaseRaw] = normalized.split("-", 2);
|
|
13
|
-
const core = coreRaw.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
14
|
-
const prerelease = prereleaseRaw
|
|
15
|
-
? prereleaseRaw.split(/[.-]/).filter(Boolean).map(normalizeVersionToken)
|
|
16
|
-
: [];
|
|
17
|
-
|
|
18
|
-
return { core, prerelease };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function compareIdentifier(left, right) {
|
|
22
|
-
if (left === right) {
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const leftIsNumber = typeof left === "number";
|
|
27
|
-
const rightIsNumber = typeof right === "number";
|
|
28
|
-
|
|
29
|
-
if (leftIsNumber && rightIsNumber) {
|
|
30
|
-
return left > right ? 1 : -1;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (leftIsNumber) {
|
|
34
|
-
return -1;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (rightIsNumber) {
|
|
38
|
-
return 1;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return left > right ? 1 : -1;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function compareVersions(leftVersion, rightVersion) {
|
|
45
|
-
const left = parseVersion(leftVersion);
|
|
46
|
-
const right = parseVersion(rightVersion);
|
|
47
|
-
const coreLength = Math.max(left.core.length, right.core.length);
|
|
48
|
-
|
|
49
|
-
for (let index = 0; index < coreLength; index += 1) {
|
|
50
|
-
const leftPart = left.core[index] ?? 0;
|
|
51
|
-
const rightPart = right.core[index] ?? 0;
|
|
52
|
-
if (leftPart !== rightPart) {
|
|
53
|
-
return leftPart > rightPart ? 1 : -1;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const leftHasPrerelease = left.prerelease.length > 0;
|
|
58
|
-
const rightHasPrerelease = right.prerelease.length > 0;
|
|
59
|
-
|
|
60
|
-
if (!leftHasPrerelease && !rightHasPrerelease) {
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!leftHasPrerelease) {
|
|
65
|
-
return 1;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!rightHasPrerelease) {
|
|
69
|
-
return -1;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const prereleaseLength = Math.max(left.prerelease.length, right.prerelease.length);
|
|
73
|
-
for (let index = 0; index < prereleaseLength; index += 1) {
|
|
74
|
-
const leftPart = left.prerelease[index];
|
|
75
|
-
const rightPart = right.prerelease[index];
|
|
76
|
-
|
|
77
|
-
if (leftPart === undefined) {
|
|
78
|
-
return -1;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (rightPart === undefined) {
|
|
82
|
-
return 1;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const comparison = compareIdentifier(leftPart, rightPart);
|
|
86
|
-
if (comparison !== 0) {
|
|
87
|
-
return comparison;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return 0;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export function isVersionNewer(latestVersion, currentVersion) {
|
|
95
|
-
return compareVersions(latestVersion, currentVersion) > 0;
|
|
96
|
-
}
|
|
7
|
+
export { compareVersions, isVersionNewer };
|
|
97
8
|
|
|
98
9
|
/**
|
|
99
10
|
* Check for CLI updates and auto-update if available.
|
package/cli/install.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
+
addRequiredPluginMarketplaces,
|
|
6
7
|
installRequiredPlugins,
|
|
7
8
|
registerBundledMcps,
|
|
8
9
|
} from "./install-companions.js";
|
|
@@ -60,6 +61,11 @@ export async function install(args = []) {
|
|
|
60
61
|
|
|
61
62
|
await addCurdxMarketplace(context);
|
|
62
63
|
|
|
64
|
+
// Claude Code resolves plugin dependencies during install. Register
|
|
65
|
+
// required companion marketplaces before installing curdx-flow so its
|
|
66
|
+
// cross-marketplace dependency on Context7 can be satisfied immediately.
|
|
67
|
+
await addRequiredPluginMarketplaces({ logWarnings: false });
|
|
68
|
+
|
|
63
69
|
// ---------- Step 3: Install curdx-flow plugin ----------
|
|
64
70
|
// Read the version the marketplace is shipping so we can decide whether an
|
|
65
71
|
// already-installed plugin needs an update (same name but stale version
|
|
@@ -68,7 +74,12 @@ export async function install(args = []) {
|
|
|
68
74
|
await installCurdxFlowPlugin({ prevCurdxFlow, shippedVersion });
|
|
69
75
|
|
|
70
76
|
// ---------- Step 3.5: Install required plugins + register user-level MCPs ----------
|
|
71
|
-
await installRequiredPlugins({
|
|
77
|
+
await installRequiredPlugins({
|
|
78
|
+
yes: context.yes,
|
|
79
|
+
language,
|
|
80
|
+
config,
|
|
81
|
+
skipMarketplaceAdd: true,
|
|
82
|
+
});
|
|
72
83
|
|
|
73
84
|
// Beta.12: direct MCPs migrated from plugin.json bundling. See cli/registry.js
|
|
74
85
|
// for the rationale. Context7 now uses Upstash's official plugin instead.
|