@curdx/flow 3.1.0 → 3.3.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/CHANGELOG.md +32 -0
- package/README.md +22 -0
- package/dist/index.mjs +501 -119
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@curdx/flow` are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/) and the project follows [Semantic Versioning](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## 3.3.0 — 2026-04-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **CLAUDE.md sync** — every successful `install` / `update` / `uninstall` now rewrites a small managed block in `~/.claude/CLAUDE.md` so Claude Code has session-start knowledge of which tools are installed and when to use each. The block lives between `<!-- BEGIN @curdx/flow v1 -->` / `<!-- END @curdx/flow v1 -->` markers; everything outside is preserved verbatim. Uninstalling all managed items removes the block entirely.
|
|
10
|
+
- **`Pkg.whenToUse` and `Pkg.slashNamespace`** — two new optional registry fields. `whenToUse` is the English trigger fragment shown in the CLAUDE.md "Available tools/plugins" list (e.g. "auto-fires on 2+ failures..."). `slashNamespace` is the slash invocation pattern (e.g. `/pua:*`) — only set on plugins that expose one. Both populated for the six bundled items, sourced from each upstream's own documentation.
|
|
11
|
+
- **Conditional Rules section** — the block's `Rules:` lines are emitted only for currently-installed tools, so the block never advises Claude to use a tool that isn't there. The "plan first" rule names whichever planners (`sequential-thinking`, `claude-mem`) are installed.
|
|
12
|
+
- **`--no-claude-md` flag and `CURDX_FLOW_NO_CLAUDE_MD` env var** — opt out of the CLAUDE.md sync (CI, locked-down filesystems, or users who prefer to manage CLAUDE.md by hand).
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
|
|
16
|
+
- Sync is **safe by default**: writes are atomic (tmp + `fs.rename`), partial CLAUDE.md changes are impossible, and a failed sync prints a warning but never aborts a successful install.
|
|
17
|
+
- Forward-compatible: the BEGIN/END regex matches any `v\d+` suffix, so a future `v2` block format will silently replace any pre-existing `v1` block.
|
|
18
|
+
- Block content is always English regardless of `--lang`. CLAUDE.md's audience is Claude itself; English keeps instructions stable and avoids diff churn from alternating language runs.
|
|
19
|
+
|
|
20
|
+
## 3.2.0 — 2026-04-26
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **Version-aware install** — `flow install` now detects already-installed items with newer versions available upstream and presents a third state `↑ installed v3.0.0 → v3.2.3 available` in the multiselect. Items with updates are pre-selected by default alongside not-installed items, so a single Enter ships "install missing + upgrade outdated".
|
|
25
|
+
- **Smart dispatch** — selected items route to the right operation:
|
|
26
|
+
- not installed → `install` (full)
|
|
27
|
+
- update available → `update` (incremental, via `claude plugin update <id>`)
|
|
28
|
+
- already installed but selected → reinstall confirmation prompt (uninstall + install)
|
|
29
|
+
- **Marketplace cache refresh** — install flow runs `claude plugin marketplace update <name>` for each pkg's marketplace before reading `latestVersion`. Skipped per-marketplace if its cache mtime is within 1 hour. New flag `--no-refresh` to opt out entirely (CI / offline use).
|
|
30
|
+
- **`flow status --json` enriched** — now includes `installedVersion`, `latestVersion`, and `updateAvailable` fields for each item, so external scripts can detect upgrade candidates without parsing the multiselect UI.
|
|
31
|
+
- **`Pkg.installedVersion` / `Pkg.latestVersion` / `Pkg.marketplaces`** — optional methods on the registry interface. Implemented for `pua` and `claude-mem` (the two items whose marketplaces declare `version` in `.claude-plugin/marketplace.json`). Other items gracefully fall back to the boolean installed/not-installed display when versions aren't available.
|
|
32
|
+
|
|
33
|
+
### Notes
|
|
34
|
+
|
|
35
|
+
Of the 6 bundled items, only `pua` and `claude-mem` expose comparable versions. `chrome-devtools-mcp` and `frontend-design` (Anthropic official marketplace) don't declare `version` in marketplace metadata and so always render as "installed" without version. Both MCP servers (`sequential-thinking`, `context7`) have no installed-version concept (`npx -y` auto-fetches latest each launch / remote HTTP) and behave the same way.
|
|
36
|
+
|
|
5
37
|
## 3.1.0 — 2026-04-26
|
|
6
38
|
|
|
7
39
|
Major rewrite preserving the same goal (one-command installer for Claude Code plugins and MCP servers) with a cleaner internal architecture and broader coverage.
|
package/README.md
CHANGED
|
@@ -34,6 +34,28 @@ npx @curdx/flow --lang en # override language
|
|
|
34
34
|
| `sequential-thinking` | mcp | `@modelcontextprotocol/server-sequential-thinking` |
|
|
35
35
|
| `context7` | mcp | HTTP — `https://mcp.context7.com/mcp` (optional API key) |
|
|
36
36
|
|
|
37
|
+
## What it writes to your filesystem
|
|
38
|
+
|
|
39
|
+
After every successful `install` / `update` / `uninstall`, flow keeps a short managed block in your global `~/.claude/CLAUDE.md` so Claude Code knows at session start which tools are installed and when to use them. The block looks like:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
<!-- BEGIN @curdx/flow v1 -->
|
|
43
|
+
## Tool Usage
|
|
44
|
+
|
|
45
|
+
Available tools/plugins:
|
|
46
|
+
- pua (v3.0.0) — `/pua:*` — auto-fires on 2+ failures or user frustration; ...
|
|
47
|
+
- ...
|
|
48
|
+
|
|
49
|
+
Rules:
|
|
50
|
+
- Do not call every tool by default; ...
|
|
51
|
+
- ...
|
|
52
|
+
|
|
53
|
+
Run `npx @curdx/flow` to install / update / uninstall.
|
|
54
|
+
<!-- END @curdx/flow v1 -->
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Anything outside the BEGIN/END markers is preserved verbatim — flow only ever rewrites or removes the block itself. Uninstalling all managed items removes the block entirely. Pass `--no-claude-md` (or set `CURDX_FLOW_NO_CLAUDE_MD=1`) to opt out.
|
|
58
|
+
|
|
37
59
|
## Requirements
|
|
38
60
|
|
|
39
61
|
- Node.js >= 20.12
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import * as
|
|
4
|
+
import * as p9 from "@clack/prompts";
|
|
5
5
|
import { defineCommand, runMain } from "citty";
|
|
6
6
|
|
|
7
7
|
// src/ui/language.ts
|
|
@@ -24,6 +24,12 @@ var messages = {
|
|
|
24
24
|
"pkg.installed": "\u5DF2\u5B89\u88C5",
|
|
25
25
|
"pkg.notInstalled": "\u672A\u5B89\u88C5",
|
|
26
26
|
"pkg.unknown": "\u672A\u77E5",
|
|
27
|
+
"pkg.upToDateWithVersion": "\u5DF2\u5B89\u88C5 v{version}",
|
|
28
|
+
"pkg.updateAvailable": "\u5DF2\u5B89\u88C5 v{current} \u2192 v{latest} \u53EF\u7528",
|
|
29
|
+
"marketplace.refreshing": "\u5237\u65B0 marketplace \u7F13\u5B58\u2026",
|
|
30
|
+
"marketplace.refreshed": "\u5DF2\u5237\u65B0 {count} \u4E2A marketplace",
|
|
31
|
+
"marketplace.refreshSkipped": "marketplace \u7F13\u5B58\u4ECD\u662F\u65B0\u9C9C\u7684\uFF0C\u8DF3\u8FC7\u5237\u65B0",
|
|
32
|
+
"install.updating": '\u66F4\u65B0 "{name}" \u5230 v{version}',
|
|
27
33
|
"install.selectPrompt": "\u52FE\u9009\u8981\u5B89\u88C5 / \u91CD\u88C5\u7684\u6761\u76EE\uFF08\u9ED8\u8BA4\u52FE\u9009\u672A\u5B89\u88C5\u7684\uFF09",
|
|
28
34
|
"install.nothingSelected": "\u6CA1\u6709\u9009\u62E9\u4EFB\u4F55\u6761\u76EE\uFF0C\u5DF2\u9000\u51FA\u3002",
|
|
29
35
|
"install.confirmReinstall": '"{name}" \u5DF2\u5B89\u88C5\uFF0C\u662F\u5426\u91CD\u65B0\u5B89\u88C5\uFF08\u5148\u5378\u8F7D\u518D\u5B89\u88C5\uFF09\uFF1F',
|
|
@@ -60,7 +66,12 @@ var messages = {
|
|
|
60
66
|
"chrome.prereqNode": "\u9700\u8981 Node.js >= 20.19\uFF0C\u5F53\u524D\u7248\u672C {current}",
|
|
61
67
|
"chrome.prereqChrome": "\u9700\u8981\u672C\u673A\u5DF2\u5B89\u88C5 Chrome\uFF08chrome-devtools-mcp \u4F1A\u8C03\u7528\u672C\u5730\u6D4F\u89C8\u5668\uFF09",
|
|
62
68
|
"reinstall.uninstalling": "\u5148\u5378\u8F7D\u65E7\u7248\u672C\u2026",
|
|
63
|
-
"reinstall.installing": "\u5B89\u88C5\u65B0\u7248\u672C\u2026"
|
|
69
|
+
"reinstall.installing": "\u5B89\u88C5\u65B0\u7248\u672C\u2026",
|
|
70
|
+
"claudeMd.synced": "CLAUDE.md \u5DF2\u66F4\u65B0\uFF08{path}\uFF09",
|
|
71
|
+
"claudeMd.unchanged": "CLAUDE.md \u5DF2\u662F\u6700\u65B0",
|
|
72
|
+
"claudeMd.removed": "\u5DF2\u4ECE CLAUDE.md \u79FB\u9664 @curdx/flow \u533A\u5757",
|
|
73
|
+
"claudeMd.skipped": "\u5DF2\u8DF3\u8FC7 CLAUDE.md \u540C\u6B65\uFF08--no-claude-md\uFF09",
|
|
74
|
+
"claudeMd.failed": "CLAUDE.md \u540C\u6B65\u5931\u8D25\uFF1A{error}"
|
|
64
75
|
};
|
|
65
76
|
var zh_default = messages;
|
|
66
77
|
|
|
@@ -81,6 +92,12 @@ var messages2 = {
|
|
|
81
92
|
"pkg.installed": "installed",
|
|
82
93
|
"pkg.notInstalled": "not installed",
|
|
83
94
|
"pkg.unknown": "unknown",
|
|
95
|
+
"pkg.upToDateWithVersion": "installed v{version}",
|
|
96
|
+
"pkg.updateAvailable": "v{current} \u2192 v{latest} available",
|
|
97
|
+
"marketplace.refreshing": "Refreshing marketplace caches\u2026",
|
|
98
|
+
"marketplace.refreshed": "Refreshed {count} marketplace(s)",
|
|
99
|
+
"marketplace.refreshSkipped": "Marketplace caches are fresh, skipping refresh",
|
|
100
|
+
"install.updating": 'Updating "{name}" to v{version}',
|
|
84
101
|
"install.selectPrompt": "Select items to install / reinstall (not-installed are pre-selected)",
|
|
85
102
|
"install.nothingSelected": "Nothing selected. Exiting.",
|
|
86
103
|
"install.confirmReinstall": '"{name}" is already installed. Reinstall (uninstall then install)?',
|
|
@@ -117,7 +134,12 @@ var messages2 = {
|
|
|
117
134
|
"chrome.prereqNode": "Requires Node.js >= 20.19 (current: {current})",
|
|
118
135
|
"chrome.prereqChrome": "Requires Chrome installed locally (chrome-devtools-mcp drives the local browser)",
|
|
119
136
|
"reinstall.uninstalling": "Uninstalling old version\u2026",
|
|
120
|
-
"reinstall.installing": "Installing new version\u2026"
|
|
137
|
+
"reinstall.installing": "Installing new version\u2026",
|
|
138
|
+
"claudeMd.synced": "CLAUDE.md updated ({path})",
|
|
139
|
+
"claudeMd.unchanged": "CLAUDE.md already up to date",
|
|
140
|
+
"claudeMd.removed": "Removed @curdx/flow block from CLAUDE.md",
|
|
141
|
+
"claudeMd.skipped": "Skipped CLAUDE.md sync (--no-claude-md)",
|
|
142
|
+
"claudeMd.failed": "CLAUDE.md sync failed: {error}"
|
|
121
143
|
};
|
|
122
144
|
var en_default = messages2;
|
|
123
145
|
|
|
@@ -166,12 +188,17 @@ async function initLanguage(override) {
|
|
|
166
188
|
}
|
|
167
189
|
|
|
168
190
|
// src/ui/menu.ts
|
|
169
|
-
import * as
|
|
191
|
+
import * as p8 from "@clack/prompts";
|
|
170
192
|
|
|
171
193
|
// src/flows/install.ts
|
|
172
|
-
import * as
|
|
194
|
+
import * as p4 from "@clack/prompts";
|
|
173
195
|
import pc from "picocolors";
|
|
174
196
|
|
|
197
|
+
// src/runner/state.ts
|
|
198
|
+
import { promises as fs } from "fs";
|
|
199
|
+
import path from "path";
|
|
200
|
+
import os from "os";
|
|
201
|
+
|
|
175
202
|
// src/runner/exec.ts
|
|
176
203
|
import { x } from "tinyexec";
|
|
177
204
|
async function run(cmd, args) {
|
|
@@ -183,15 +210,15 @@ async function run(cmd, args) {
|
|
|
183
210
|
stderr: result.stderr
|
|
184
211
|
};
|
|
185
212
|
}
|
|
186
|
-
async function runStreaming(cmd, args,
|
|
187
|
-
|
|
213
|
+
async function runStreaming(cmd, args, log5) {
|
|
214
|
+
log5.message(`$ ${cmd} ${args.join(" ")}`);
|
|
188
215
|
const proc = x(cmd, args, { throwOnError: false });
|
|
189
216
|
let stdout = "";
|
|
190
217
|
for await (const line of proc) {
|
|
191
218
|
const trimmed = line.replace(/\r?\n$/, "");
|
|
192
219
|
if (trimmed.length > 0) {
|
|
193
220
|
stdout += trimmed + "\n";
|
|
194
|
-
|
|
221
|
+
log5.message(trimmed);
|
|
195
222
|
}
|
|
196
223
|
}
|
|
197
224
|
const finished = await proc;
|
|
@@ -234,15 +261,15 @@ async function listPlugins(force = false) {
|
|
|
234
261
|
}
|
|
235
262
|
try {
|
|
236
263
|
const arr = JSON.parse(res.stdout);
|
|
237
|
-
pluginCache = arr.map((
|
|
238
|
-
const [name =
|
|
264
|
+
pluginCache = arr.map((p10) => {
|
|
265
|
+
const [name = p10.id, marketplace = ""] = p10.id.split("@");
|
|
239
266
|
return {
|
|
240
|
-
id:
|
|
267
|
+
id: p10.id,
|
|
241
268
|
name,
|
|
242
269
|
marketplace,
|
|
243
|
-
version:
|
|
244
|
-
scope:
|
|
245
|
-
enabled:
|
|
270
|
+
version: p10.version,
|
|
271
|
+
scope: p10.scope,
|
|
272
|
+
enabled: p10.enabled
|
|
246
273
|
};
|
|
247
274
|
});
|
|
248
275
|
} catch {
|
|
@@ -288,7 +315,7 @@ async function listMcp(force = false) {
|
|
|
288
315
|
}
|
|
289
316
|
async function isPluginInstalled(id) {
|
|
290
317
|
const list = await listPlugins();
|
|
291
|
-
return list.some((
|
|
318
|
+
return list.some((p10) => p10.id === id);
|
|
292
319
|
}
|
|
293
320
|
async function isMarketplaceAdded(name) {
|
|
294
321
|
const list = await listMarketplaces();
|
|
@@ -298,6 +325,55 @@ async function isMcpInstalled(name) {
|
|
|
298
325
|
const list = await listMcp();
|
|
299
326
|
return list.some((m) => m.name === name);
|
|
300
327
|
}
|
|
328
|
+
async function findPlugin(id) {
|
|
329
|
+
const list = await listPlugins();
|
|
330
|
+
return list.find((p10) => p10.id === id);
|
|
331
|
+
}
|
|
332
|
+
var marketplaceJsonCache = /* @__PURE__ */ new Map();
|
|
333
|
+
function marketplaceDir(name) {
|
|
334
|
+
return path.join(os.homedir(), ".claude", "plugins", "marketplaces", name);
|
|
335
|
+
}
|
|
336
|
+
async function readMarketplaceJson(name) {
|
|
337
|
+
if (marketplaceJsonCache.has(name)) return marketplaceJsonCache.get(name) ?? null;
|
|
338
|
+
const file = path.join(marketplaceDir(name), ".claude-plugin", "marketplace.json");
|
|
339
|
+
try {
|
|
340
|
+
const raw = await fs.readFile(file, "utf8");
|
|
341
|
+
const parsed = JSON.parse(raw);
|
|
342
|
+
marketplaceJsonCache.set(name, parsed);
|
|
343
|
+
return parsed;
|
|
344
|
+
} catch {
|
|
345
|
+
marketplaceJsonCache.set(name, null);
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
async function getMarketplacePluginVersion(marketplaceName, pluginName) {
|
|
350
|
+
const m = await readMarketplaceJson(marketplaceName);
|
|
351
|
+
if (!m?.plugins) return null;
|
|
352
|
+
const entry = m.plugins.find((p10) => p10.name === pluginName);
|
|
353
|
+
return entry?.version ?? null;
|
|
354
|
+
}
|
|
355
|
+
var REFRESH_TTL_MS = 60 * 60 * 1e3;
|
|
356
|
+
async function shouldSkipRefresh(name) {
|
|
357
|
+
try {
|
|
358
|
+
const stat = await fs.stat(marketplaceDir(name));
|
|
359
|
+
return Date.now() - stat.mtimeMs < REFRESH_TTL_MS;
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async function refreshMarketplaces(names) {
|
|
365
|
+
const unique = [...new Set(names)];
|
|
366
|
+
const toRefresh = [];
|
|
367
|
+
for (const name of unique) {
|
|
368
|
+
if (!await shouldSkipRefresh(name)) toRefresh.push(name);
|
|
369
|
+
}
|
|
370
|
+
if (toRefresh.length === 0) return [];
|
|
371
|
+
await Promise.all(
|
|
372
|
+
toRefresh.map((name) => run("claude", ["plugin", "marketplace", "update", name]))
|
|
373
|
+
);
|
|
374
|
+
for (const name of toRefresh) marketplaceJsonCache.delete(name);
|
|
375
|
+
return toRefresh;
|
|
376
|
+
}
|
|
301
377
|
|
|
302
378
|
// src/registry/plugins/_helpers.ts
|
|
303
379
|
async function ensureMarketplace(marketplaceName, marketplaceSource, ctx) {
|
|
@@ -325,6 +401,7 @@ async function updatePluginById(pluginId, ctx) {
|
|
|
325
401
|
|
|
326
402
|
// src/registry/plugins/pua.ts
|
|
327
403
|
var PLUGIN_ID = "pua@pua-skills";
|
|
404
|
+
var PLUGIN_NAME = "pua";
|
|
328
405
|
var MARKETPLACE_NAME = "pua-skills";
|
|
329
406
|
var MARKETPLACE_SOURCE = "tanweai/pua";
|
|
330
407
|
var pua = {
|
|
@@ -332,7 +409,16 @@ var pua = {
|
|
|
332
409
|
name: "pua",
|
|
333
410
|
description: "tanweai/pua \u2014 Chinese Claude Code skills bundle",
|
|
334
411
|
type: "plugin",
|
|
412
|
+
slashNamespace: "/pua:*",
|
|
413
|
+
whenToUse: "auto-fires on 2+ failures or user frustration; sub-modes p7 / p9 / pro / loop. Skip on first-attempt failures or when a known fix is executing.",
|
|
414
|
+
marketplaces: () => [MARKETPLACE_NAME],
|
|
335
415
|
isInstalled: () => isPluginInstalled(PLUGIN_ID),
|
|
416
|
+
installedVersion: async () => {
|
|
417
|
+
const p10 = await findPlugin(PLUGIN_ID);
|
|
418
|
+
const v = p10?.version;
|
|
419
|
+
return v && v !== "unknown" ? v : null;
|
|
420
|
+
},
|
|
421
|
+
latestVersion: () => getMarketplacePluginVersion(MARKETPLACE_NAME, PLUGIN_NAME),
|
|
336
422
|
install: async (ctx) => {
|
|
337
423
|
await ensureMarketplace(MARKETPLACE_NAME, MARKETPLACE_SOURCE, ctx);
|
|
338
424
|
await installPluginById(PLUGIN_ID, ctx);
|
|
@@ -344,6 +430,7 @@ var pua_default = pua;
|
|
|
344
430
|
|
|
345
431
|
// src/registry/plugins/claude-mem.ts
|
|
346
432
|
var PLUGIN_ID2 = "claude-mem@thedotmack";
|
|
433
|
+
var PLUGIN_NAME2 = "claude-mem";
|
|
347
434
|
var MARKETPLACE_NAME2 = "thedotmack";
|
|
348
435
|
var MARKETPLACE_SOURCE2 = "thedotmack/claude-mem";
|
|
349
436
|
var claudeMem = {
|
|
@@ -351,7 +438,16 @@ var claudeMem = {
|
|
|
351
438
|
name: "claude-mem",
|
|
352
439
|
description: "thedotmack/claude-mem \u2014 persistent cross-session memory for Claude Code",
|
|
353
440
|
type: "plugin",
|
|
441
|
+
slashNamespace: "/claude-mem:*",
|
|
442
|
+
whenToUse: 'for cross-session memory search ("did we solve this before?"), phased planning (`make-plan`), or phased execution (`do`).',
|
|
443
|
+
marketplaces: () => [MARKETPLACE_NAME2],
|
|
354
444
|
isInstalled: () => isPluginInstalled(PLUGIN_ID2),
|
|
445
|
+
installedVersion: async () => {
|
|
446
|
+
const p10 = await findPlugin(PLUGIN_ID2);
|
|
447
|
+
const v = p10?.version;
|
|
448
|
+
return v && v !== "unknown" ? v : null;
|
|
449
|
+
},
|
|
450
|
+
latestVersion: () => getMarketplacePluginVersion(MARKETPLACE_NAME2, PLUGIN_NAME2),
|
|
355
451
|
install: async (ctx) => {
|
|
356
452
|
await ensureMarketplace(MARKETPLACE_NAME2, MARKETPLACE_SOURCE2, ctx);
|
|
357
453
|
await installPluginById(PLUGIN_ID2, ctx);
|
|
@@ -379,6 +475,7 @@ var chromeDevtoolsMcp = {
|
|
|
379
475
|
name: "chrome-devtools-mcp",
|
|
380
476
|
description: "ChromeDevTools/chrome-devtools-mcp \u2014 drive a real Chrome from Claude Code",
|
|
381
477
|
type: "plugin",
|
|
478
|
+
whenToUse: "when debugging code that runs in a browser: perf traces, network / console inspection, DOM / CSS issues. Prefer snapshot over screenshot.",
|
|
382
479
|
prereqCheck: async (t2) => {
|
|
383
480
|
const major = Number(process.versions.node.split(".")[0] ?? "0");
|
|
384
481
|
const minor = Number(process.versions.node.split(".")[1] ?? "0");
|
|
@@ -407,6 +504,7 @@ var frontendDesign = {
|
|
|
407
504
|
name: "frontend-design",
|
|
408
505
|
description: "Anthropic official \u2014 UI/frontend design helpers",
|
|
409
506
|
type: "plugin",
|
|
507
|
+
whenToUse: "auto-fires when building UI / web components / pages. Best where visual personality matters (landing, marketing, portfolio).",
|
|
410
508
|
isInstalled: () => isPluginInstalled(PLUGIN_ID4),
|
|
411
509
|
install: (ctx) => installPluginById(PLUGIN_ID4, ctx),
|
|
412
510
|
uninstall: (ctx) => uninstallPluginById(PLUGIN_ID4, ctx),
|
|
@@ -421,6 +519,7 @@ var sequentialThinking = {
|
|
|
421
519
|
name: "sequential-thinking",
|
|
422
520
|
description: "modelcontextprotocol/server-sequential-thinking \u2014 structured reasoning helper",
|
|
423
521
|
type: "mcp",
|
|
522
|
+
whenToUse: "for complex multi-step problems where assumptions may shift (architecture comparison, risk-assessed migrations, prod-only debugging). Skip for simple queries.",
|
|
424
523
|
isInstalled: () => isMcpInstalled(MCP_NAME),
|
|
425
524
|
install: async (ctx) => {
|
|
426
525
|
const r = await runStreaming(
|
|
@@ -461,6 +560,7 @@ var context7 = {
|
|
|
461
560
|
name: "context7",
|
|
462
561
|
description: "upstash/context7 \u2014 up-to-date docs from any library (HTTP MCP, optional API key)",
|
|
463
562
|
type: "mcp",
|
|
563
|
+
whenToUse: "for any library / SDK / framework / API / Claude Code docs lookup. Use instead of web search.",
|
|
464
564
|
isInstalled: () => isMcpInstalled(MCP_NAME2),
|
|
465
565
|
configPrompts: async ({ t: t2 }) => {
|
|
466
566
|
p2.note(`${t2("context7.dashboardHint")}
|
|
@@ -513,25 +613,226 @@ var PKGS = [
|
|
|
513
613
|
context7_default
|
|
514
614
|
];
|
|
515
615
|
function findPkg(id) {
|
|
516
|
-
return PKGS.find((
|
|
616
|
+
return PKGS.find((p10) => p10.id === id);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/runner/claudeMd.ts
|
|
620
|
+
import { promises as fs2 } from "fs";
|
|
621
|
+
import path2 from "path";
|
|
622
|
+
import os2 from "os";
|
|
623
|
+
import * as p3 from "@clack/prompts";
|
|
624
|
+
var BEGIN_MARKER = "<!-- BEGIN @curdx/flow v1 -->";
|
|
625
|
+
var END_MARKER = "<!-- END @curdx/flow v1 -->";
|
|
626
|
+
var BLOCK_RE = /<!-- BEGIN @curdx\/flow v\d+[^>]*-->[\s\S]*?<!-- END @curdx\/flow v\d+ -->/;
|
|
627
|
+
function claudeMdPath() {
|
|
628
|
+
return path2.join(os2.homedir(), ".claude", "CLAUDE.md");
|
|
629
|
+
}
|
|
630
|
+
function renderItemLine(item) {
|
|
631
|
+
let line = `- ${item.name}`;
|
|
632
|
+
if (item.version) line += ` (v${item.version})`;
|
|
633
|
+
if (item.slashNamespace) line += ` \u2014 \`${item.slashNamespace}\``;
|
|
634
|
+
if (item.whenToUse) line += ` \u2014 ${item.whenToUse}`;
|
|
635
|
+
return line;
|
|
636
|
+
}
|
|
637
|
+
var ALWAYS_ON_RULES = [
|
|
638
|
+
"Do not call every tool by default; pick by the trigger condition above.",
|
|
639
|
+
"For first-attempt failures or simple edits, skip extra tools."
|
|
640
|
+
];
|
|
641
|
+
function buildConditionalRules(installedIds) {
|
|
642
|
+
const out = [];
|
|
643
|
+
const planners = [];
|
|
644
|
+
if (installedIds.has("sequential-thinking")) planners.push("sequential-thinking");
|
|
645
|
+
if (installedIds.has("claude-mem")) planners.push("claude-mem `make-plan`");
|
|
646
|
+
if (planners.length > 0) {
|
|
647
|
+
out.push(`For complex / risky changes, plan first (${planners.join(" or ")}).`);
|
|
648
|
+
}
|
|
649
|
+
if (installedIds.has("context7")) {
|
|
650
|
+
out.push("For library / SDK lookups, prefer context7 over web search.");
|
|
651
|
+
}
|
|
652
|
+
if (installedIds.has("chrome-devtools-mcp")) {
|
|
653
|
+
out.push("For browser-rendered behavior, verify in chrome-devtools-mcp instead of guessing.");
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
function renderBlock(items) {
|
|
658
|
+
const installedIds = new Set(items.map((i) => i.id));
|
|
659
|
+
const rules = [...ALWAYS_ON_RULES, ...buildConditionalRules(installedIds)];
|
|
660
|
+
return [
|
|
661
|
+
BEGIN_MARKER,
|
|
662
|
+
"## Tool Usage",
|
|
663
|
+
"",
|
|
664
|
+
"Available tools/plugins:",
|
|
665
|
+
...items.map(renderItemLine),
|
|
666
|
+
"",
|
|
667
|
+
"Rules:",
|
|
668
|
+
...rules.map((r) => `- ${r}`),
|
|
669
|
+
"",
|
|
670
|
+
"Run `npx @curdx/flow` to install / update / uninstall.",
|
|
671
|
+
END_MARKER
|
|
672
|
+
].join("\n");
|
|
673
|
+
}
|
|
674
|
+
function withEol(s, eol) {
|
|
675
|
+
return eol === "\n" ? s : s.split("\n").join(eol);
|
|
676
|
+
}
|
|
677
|
+
function ensureSingleTrailingNewline(s, eol) {
|
|
678
|
+
if (s.length === 0) return s;
|
|
679
|
+
return s.replace(/[\r\n]+$/, "") + eol;
|
|
680
|
+
}
|
|
681
|
+
function upsertBlock(existing, blockBody, eol) {
|
|
682
|
+
const block = withEol(blockBody, eol);
|
|
683
|
+
if (BLOCK_RE.test(existing)) {
|
|
684
|
+
return existing.replace(BLOCK_RE, block);
|
|
685
|
+
}
|
|
686
|
+
if (existing.length === 0) {
|
|
687
|
+
return block + eol;
|
|
688
|
+
}
|
|
689
|
+
const trimmed = existing.replace(/[\r\n\s]+$/, "");
|
|
690
|
+
return trimmed + eol + eol + block + eol;
|
|
691
|
+
}
|
|
692
|
+
function removeBlock(existing, eol) {
|
|
693
|
+
if (!BLOCK_RE.test(existing)) return existing;
|
|
694
|
+
let next = existing.replace(BLOCK_RE, "");
|
|
695
|
+
const tripleEol = new RegExp(`(?:\\r?\\n){3,}`, "g");
|
|
696
|
+
next = next.replace(tripleEol, eol + eol);
|
|
697
|
+
if (next.replace(/[\s\r\n]/g, "").length === 0) return "";
|
|
698
|
+
return ensureSingleTrailingNewline(next, eol);
|
|
699
|
+
}
|
|
700
|
+
async function pkgToItem(pkg) {
|
|
701
|
+
let version;
|
|
702
|
+
if (pkg.installedVersion) {
|
|
703
|
+
const v = await pkg.installedVersion();
|
|
704
|
+
if (v) version = v;
|
|
705
|
+
}
|
|
706
|
+
return {
|
|
707
|
+
id: pkg.id,
|
|
708
|
+
name: pkg.name,
|
|
709
|
+
type: pkg.type,
|
|
710
|
+
version,
|
|
711
|
+
whenToUse: pkg.whenToUse,
|
|
712
|
+
slashNamespace: pkg.slashNamespace
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async function collectInstalledItems() {
|
|
716
|
+
await Promise.all([listPlugins(true), listMcp(true)]);
|
|
717
|
+
const items = [];
|
|
718
|
+
for (const pkg of PKGS) {
|
|
719
|
+
if (await pkg.isInstalled()) {
|
|
720
|
+
items.push(await pkgToItem(pkg));
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
items.sort((a, b) => {
|
|
724
|
+
if (a.type !== b.type) return a.type === "plugin" ? -1 : 1;
|
|
725
|
+
return a.name.localeCompare(b.name);
|
|
726
|
+
});
|
|
727
|
+
return items;
|
|
728
|
+
}
|
|
729
|
+
async function syncClaudeMd(opts) {
|
|
730
|
+
const file = claudeMdPath();
|
|
731
|
+
if (opts?.skip) return { status: "skipped", path: file };
|
|
732
|
+
try {
|
|
733
|
+
const items = await collectInstalledItems();
|
|
734
|
+
let existing = "";
|
|
735
|
+
let existed = true;
|
|
736
|
+
try {
|
|
737
|
+
existing = await fs2.readFile(file, "utf8");
|
|
738
|
+
} catch (err) {
|
|
739
|
+
if (err.code === "ENOENT") {
|
|
740
|
+
existed = false;
|
|
741
|
+
} else {
|
|
742
|
+
throw err;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const eol = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
746
|
+
const hadBlock = BLOCK_RE.test(existing);
|
|
747
|
+
let next;
|
|
748
|
+
if (items.length === 0) {
|
|
749
|
+
if (!hadBlock) {
|
|
750
|
+
return { status: "unchanged", path: file };
|
|
751
|
+
}
|
|
752
|
+
next = removeBlock(existing, eol);
|
|
753
|
+
} else {
|
|
754
|
+
next = upsertBlock(existing, renderBlock(items), eol);
|
|
755
|
+
}
|
|
756
|
+
if (next === existing) {
|
|
757
|
+
return { status: "unchanged", path: file };
|
|
758
|
+
}
|
|
759
|
+
await fs2.mkdir(path2.dirname(file), { recursive: true });
|
|
760
|
+
const tmp = `${file}.tmp.${process.pid}`;
|
|
761
|
+
await fs2.writeFile(tmp, next, "utf8");
|
|
762
|
+
await fs2.rename(tmp, file);
|
|
763
|
+
if (!existed) return { status: "created", path: file };
|
|
764
|
+
if (hadBlock && items.length === 0) return { status: "removed", path: file };
|
|
765
|
+
return { status: "updated", path: file };
|
|
766
|
+
} catch (err) {
|
|
767
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
768
|
+
return { status: "failed", path: file, error: msg };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function syncFromState(opts) {
|
|
772
|
+
const r = await syncClaudeMd(opts);
|
|
773
|
+
switch (r.status) {
|
|
774
|
+
case "skipped":
|
|
775
|
+
p3.log.info(t("claudeMd.skipped"));
|
|
776
|
+
return;
|
|
777
|
+
case "unchanged":
|
|
778
|
+
p3.log.info(t("claudeMd.unchanged"));
|
|
779
|
+
return;
|
|
780
|
+
case "created":
|
|
781
|
+
case "updated":
|
|
782
|
+
p3.log.info(t("claudeMd.synced", { path: r.path }));
|
|
783
|
+
return;
|
|
784
|
+
case "removed":
|
|
785
|
+
p3.log.info(t("claudeMd.removed"));
|
|
786
|
+
return;
|
|
787
|
+
case "failed":
|
|
788
|
+
p3.log.warn(t("claudeMd.failed", { error: r.error ?? "unknown" }));
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
517
791
|
}
|
|
518
792
|
|
|
519
793
|
// src/flows/install.ts
|
|
520
|
-
async function
|
|
521
|
-
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
794
|
+
async function deriveState(pkg) {
|
|
795
|
+
if (!await pkg.isInstalled()) return { kind: "not_installed" };
|
|
796
|
+
const [installed, latest] = await Promise.all([
|
|
797
|
+
pkg.installedVersion?.() ?? Promise.resolve(null),
|
|
798
|
+
pkg.latestVersion?.() ?? Promise.resolve(null)
|
|
799
|
+
]);
|
|
800
|
+
if (installed && latest && installed !== latest) {
|
|
801
|
+
return { kind: "update_available", current: installed, latest };
|
|
802
|
+
}
|
|
803
|
+
return { kind: "up_to_date", version: installed };
|
|
804
|
+
}
|
|
805
|
+
function stateLabel(pkg, s) {
|
|
806
|
+
const head = `${pkg.name} ${pc.dim(`(${pkg.type})`)}`;
|
|
807
|
+
switch (s.kind) {
|
|
808
|
+
case "not_installed":
|
|
809
|
+
return `${head} ${pc.yellow(`\u2717 ${t("pkg.notInstalled")}`)}`;
|
|
810
|
+
case "up_to_date":
|
|
811
|
+
return `${head} ${pc.green(
|
|
812
|
+
s.version ? `\u2713 ${t("pkg.upToDateWithVersion", { version: s.version })}` : `\u2713 ${t("pkg.installed")}`
|
|
813
|
+
)}`;
|
|
814
|
+
case "update_available":
|
|
815
|
+
return `${head} ${pc.cyan(
|
|
816
|
+
`\u2191 ${t("pkg.updateAvailable", { current: s.current, latest: s.latest })}`
|
|
817
|
+
)}`;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function selectInteractive(states) {
|
|
821
|
+
const options = PKGS.map((pkg) => {
|
|
822
|
+
const s = states.get(pkg.id);
|
|
823
|
+
return { value: pkg.id, label: stateLabel(pkg, s), hint: pkg.description };
|
|
824
|
+
});
|
|
825
|
+
const initialValues = PKGS.filter((pkg) => {
|
|
826
|
+
const s = states.get(pkg.id);
|
|
827
|
+
return s.kind === "not_installed" || s.kind === "update_available";
|
|
828
|
+
}).map((pkg) => pkg.id);
|
|
829
|
+
const picked = await p4.multiselect({
|
|
529
830
|
message: t("install.selectPrompt"),
|
|
530
831
|
options,
|
|
531
832
|
initialValues,
|
|
532
833
|
required: false
|
|
533
834
|
});
|
|
534
|
-
if (
|
|
835
|
+
if (p4.isCancel(picked)) return null;
|
|
535
836
|
return picked.map((id) => findPkg(id)).filter((x2) => Boolean(x2));
|
|
536
837
|
}
|
|
537
838
|
function selectFromIds(opts) {
|
|
@@ -541,52 +842,70 @@ function selectFromIds(opts) {
|
|
|
541
842
|
for (const id of opts.ids) {
|
|
542
843
|
const pkg = findPkg(id);
|
|
543
844
|
if (pkg) found.push(pkg);
|
|
544
|
-
else
|
|
845
|
+
else p4.log.warn(`Unknown id: ${id}`);
|
|
545
846
|
}
|
|
546
847
|
return found;
|
|
547
848
|
}
|
|
548
|
-
async function runOne(pkg,
|
|
549
|
-
let
|
|
550
|
-
if (
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
849
|
+
async function runOne(pkg, state, opts) {
|
|
850
|
+
let mode;
|
|
851
|
+
if (state.kind === "not_installed") {
|
|
852
|
+
mode = "install";
|
|
853
|
+
} else if (state.kind === "update_available") {
|
|
854
|
+
mode = "update";
|
|
855
|
+
} else {
|
|
856
|
+
if (!opts.yes) {
|
|
857
|
+
const ans = await p4.confirm({
|
|
555
858
|
message: t("install.confirmReinstall", { name: pkg.name }),
|
|
556
859
|
initialValue: false
|
|
557
860
|
});
|
|
558
|
-
if (
|
|
861
|
+
if (p4.isCancel(ans) || ans === false) {
|
|
559
862
|
return { id: pkg.id, status: "skip", message: t("install.skippedReinstall", { name: pkg.name }) };
|
|
560
863
|
}
|
|
561
|
-
reinstall = true;
|
|
562
864
|
}
|
|
865
|
+
mode = "reinstall";
|
|
563
866
|
}
|
|
564
867
|
if (pkg.prereqCheck) {
|
|
565
868
|
const r = await pkg.prereqCheck(t);
|
|
566
869
|
if (!r.ok) {
|
|
567
|
-
|
|
870
|
+
p4.log.warn(t("install.prereqFail", { name: pkg.name, reason: r.reason }));
|
|
568
871
|
return { id: pkg.id, status: "skip", message: r.reason };
|
|
569
872
|
}
|
|
570
873
|
}
|
|
571
874
|
let config = {};
|
|
572
|
-
if (pkg.configPrompts) {
|
|
875
|
+
if (pkg.configPrompts && mode !== "update") {
|
|
573
876
|
const cfg = await pkg.configPrompts({ t });
|
|
574
877
|
if (cfg === null) return { id: pkg.id, status: "skip", message: t("app.cancelled") };
|
|
575
878
|
config = cfg;
|
|
576
879
|
}
|
|
577
|
-
const
|
|
880
|
+
const titleKey = mode === "update" ? "install.updating" : "install.starting";
|
|
881
|
+
const titleVars = { name: pkg.name };
|
|
882
|
+
if (mode === "update" && state.kind === "update_available") {
|
|
883
|
+
titleVars["version"] = state.latest;
|
|
884
|
+
}
|
|
885
|
+
const log5 = p4.taskLog({ title: t(titleKey, titleVars) });
|
|
578
886
|
try {
|
|
579
|
-
if (reinstall) {
|
|
580
|
-
|
|
581
|
-
await pkg.uninstall({ log:
|
|
582
|
-
|
|
887
|
+
if (mode === "reinstall") {
|
|
888
|
+
log5.message(t("reinstall.uninstalling"));
|
|
889
|
+
await pkg.uninstall({ log: log5, config, t });
|
|
890
|
+
log5.message(t("reinstall.installing"));
|
|
891
|
+
await pkg.install({ log: log5, config, t });
|
|
892
|
+
} else if (mode === "update") {
|
|
893
|
+
if (pkg.update) {
|
|
894
|
+
await pkg.update({ log: log5, config, t });
|
|
895
|
+
} else {
|
|
896
|
+
log5.message(t("reinstall.uninstalling"));
|
|
897
|
+
await pkg.uninstall({ log: log5, config, t });
|
|
898
|
+
log5.message(t("reinstall.installing"));
|
|
899
|
+
await pkg.install({ log: log5, config, t });
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
await pkg.install({ log: log5, config, t });
|
|
583
903
|
}
|
|
584
|
-
|
|
585
|
-
log4.success(t("install.success", { name: pkg.name }));
|
|
904
|
+
log5.success(t("install.success", { name: pkg.name }));
|
|
586
905
|
return { id: pkg.id, status: "ok" };
|
|
587
906
|
} catch (err) {
|
|
588
907
|
const msg = err instanceof Error ? err.message : String(err);
|
|
589
|
-
|
|
908
|
+
log5.error(`${t("install.failed", { name: pkg.name })}
|
|
590
909
|
${msg}`);
|
|
591
910
|
return { id: pkg.id, status: "fail", message: msg };
|
|
592
911
|
}
|
|
@@ -604,35 +923,62 @@ function summarize(results) {
|
|
|
604
923
|
...skip.map((r) => ` ${pc.yellow("-")} ${r.id}${r.message ? pc.dim(` (${r.message})`) : ""}`),
|
|
605
924
|
...fail.map((r) => ` ${pc.red("\u2717")} ${r.id}${r.message ? pc.dim(` (${r.message.split("\n")[0]})`) : ""}`)
|
|
606
925
|
];
|
|
607
|
-
|
|
926
|
+
p4.note(lines.join("\n"), t("install.summaryTitle"));
|
|
927
|
+
}
|
|
928
|
+
async function maybeRefreshMarketplaces(opts) {
|
|
929
|
+
if (opts.noRefresh) return;
|
|
930
|
+
const names = /* @__PURE__ */ new Set();
|
|
931
|
+
for (const pkg of PKGS) {
|
|
932
|
+
if (pkg.marketplaces) for (const n of pkg.marketplaces()) names.add(n);
|
|
933
|
+
}
|
|
934
|
+
if (names.size === 0) return;
|
|
935
|
+
const sp = p4.spinner();
|
|
936
|
+
sp.start(t("marketplace.refreshing"));
|
|
937
|
+
const refreshed = await refreshMarketplaces([...names]);
|
|
938
|
+
sp.stop(
|
|
939
|
+
refreshed.length > 0 ? t("marketplace.refreshed", { count: refreshed.length }) : t("marketplace.refreshSkipped")
|
|
940
|
+
);
|
|
608
941
|
}
|
|
609
942
|
async function installFlow(opts = {}) {
|
|
943
|
+
await maybeRefreshMarketplaces(opts);
|
|
610
944
|
const explicit = opts.all || opts.ids && opts.ids.length > 0;
|
|
611
|
-
const
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (targets.length === 0) {
|
|
617
|
-
p3.log.info(t("install.nothingSelected"));
|
|
945
|
+
const candidates = explicit ? selectFromIds(opts) : [...PKGS];
|
|
946
|
+
if (candidates.length === 0) {
|
|
947
|
+
p4.log.info(t("install.nothingSelected"));
|
|
618
948
|
return;
|
|
619
949
|
}
|
|
620
|
-
const
|
|
950
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
621
951
|
await Promise.all(
|
|
622
|
-
|
|
623
|
-
|
|
952
|
+
candidates.map(async (pkg) => {
|
|
953
|
+
stateMap.set(pkg.id, await deriveState(pkg));
|
|
624
954
|
})
|
|
625
955
|
);
|
|
956
|
+
let targets;
|
|
957
|
+
if (explicit) {
|
|
958
|
+
targets = candidates;
|
|
959
|
+
} else {
|
|
960
|
+
const picked = await selectInteractive(stateMap);
|
|
961
|
+
if (picked === null) {
|
|
962
|
+
p4.cancel(t("app.cancelled"));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
targets = picked;
|
|
966
|
+
}
|
|
967
|
+
if (targets.length === 0) {
|
|
968
|
+
p4.log.info(t("install.nothingSelected"));
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
626
971
|
const results = [];
|
|
627
972
|
for (const pkg of targets) {
|
|
628
|
-
const
|
|
629
|
-
results.push(await runOne(pkg,
|
|
973
|
+
const state = stateMap.get(pkg.id) ?? { kind: "not_installed" };
|
|
974
|
+
results.push(await runOne(pkg, state, opts));
|
|
630
975
|
}
|
|
631
976
|
summarize(results);
|
|
977
|
+
await syncFromState({ skip: opts.noClaudeMd });
|
|
632
978
|
}
|
|
633
979
|
|
|
634
980
|
// src/flows/uninstall.ts
|
|
635
|
-
import * as
|
|
981
|
+
import * as p5 from "@clack/prompts";
|
|
636
982
|
import pc2 from "picocolors";
|
|
637
983
|
async function getInstalled() {
|
|
638
984
|
const states = await Promise.all(PKGS.map(async (pkg) => ({ pkg, installed: await pkg.isInstalled() })));
|
|
@@ -646,21 +992,21 @@ async function uninstallFlow(opts = {}) {
|
|
|
646
992
|
for (const id of opts.ids) {
|
|
647
993
|
const pkg = findPkg(id);
|
|
648
994
|
if (!pkg) {
|
|
649
|
-
|
|
995
|
+
p5.log.warn(`Unknown id: ${id}`);
|
|
650
996
|
continue;
|
|
651
997
|
}
|
|
652
998
|
if (!installed.some((x2) => x2.id === pkg.id)) {
|
|
653
|
-
|
|
999
|
+
p5.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
|
|
654
1000
|
continue;
|
|
655
1001
|
}
|
|
656
1002
|
targets.push(pkg);
|
|
657
1003
|
}
|
|
658
1004
|
} else {
|
|
659
1005
|
if (installed.length === 0) {
|
|
660
|
-
|
|
1006
|
+
p5.log.info(t("uninstall.noneInstalled"));
|
|
661
1007
|
return;
|
|
662
1008
|
}
|
|
663
|
-
const picked = await
|
|
1009
|
+
const picked = await p5.multiselect({
|
|
664
1010
|
message: t("uninstall.selectPrompt"),
|
|
665
1011
|
options: installed.map((pkg) => ({
|
|
666
1012
|
value: pkg.id,
|
|
@@ -669,53 +1015,54 @@ async function uninstallFlow(opts = {}) {
|
|
|
669
1015
|
})),
|
|
670
1016
|
required: false
|
|
671
1017
|
});
|
|
672
|
-
if (
|
|
673
|
-
|
|
1018
|
+
if (p5.isCancel(picked)) {
|
|
1019
|
+
p5.cancel(t("app.cancelled"));
|
|
674
1020
|
return;
|
|
675
1021
|
}
|
|
676
1022
|
targets = picked.map((id) => findPkg(id)).filter((x2) => Boolean(x2));
|
|
677
1023
|
}
|
|
678
1024
|
if (targets.length === 0) {
|
|
679
|
-
|
|
1025
|
+
p5.log.info(t("install.nothingSelected"));
|
|
680
1026
|
return;
|
|
681
1027
|
}
|
|
682
1028
|
if (!opts.yes) {
|
|
683
|
-
const ok2 = await
|
|
1029
|
+
const ok2 = await p5.confirm({
|
|
684
1030
|
message: t("uninstall.confirm", { count: targets.length }),
|
|
685
1031
|
initialValue: false
|
|
686
1032
|
});
|
|
687
|
-
if (
|
|
688
|
-
|
|
1033
|
+
if (p5.isCancel(ok2) || ok2 === false) {
|
|
1034
|
+
p5.cancel(t("app.cancelled"));
|
|
689
1035
|
return;
|
|
690
1036
|
}
|
|
691
1037
|
}
|
|
692
1038
|
const results = [];
|
|
693
1039
|
for (const pkg of targets) {
|
|
694
|
-
const
|
|
1040
|
+
const log5 = p5.taskLog({ title: t("uninstall.starting", { name: pkg.name }) });
|
|
695
1041
|
try {
|
|
696
|
-
await pkg.uninstall({ log:
|
|
697
|
-
|
|
1042
|
+
await pkg.uninstall({ log: log5, config: {}, t });
|
|
1043
|
+
log5.success(t("uninstall.success", { name: pkg.name }));
|
|
698
1044
|
results.push({ id: pkg.id, status: "ok" });
|
|
699
1045
|
} catch (err) {
|
|
700
1046
|
const msg = err instanceof Error ? err.message : String(err);
|
|
701
|
-
|
|
1047
|
+
log5.error(`${t("uninstall.failed", { name: pkg.name })}
|
|
702
1048
|
${msg}`);
|
|
703
1049
|
results.push({ id: pkg.id, status: "fail", message: msg });
|
|
704
1050
|
}
|
|
705
1051
|
}
|
|
706
1052
|
const ok = results.filter((r) => r.status === "ok").length;
|
|
707
1053
|
const fail = results.filter((r) => r.status === "fail").length;
|
|
708
|
-
|
|
1054
|
+
p5.note(
|
|
709
1055
|
[
|
|
710
1056
|
pc2.green(t("install.summaryOk", { count: ok })),
|
|
711
1057
|
pc2.red(t("install.summaryFail", { count: fail }))
|
|
712
1058
|
].join("\n"),
|
|
713
1059
|
t("install.summaryTitle")
|
|
714
1060
|
);
|
|
1061
|
+
await syncFromState({ skip: opts.noClaudeMd });
|
|
715
1062
|
}
|
|
716
1063
|
|
|
717
1064
|
// src/flows/update.ts
|
|
718
|
-
import * as
|
|
1065
|
+
import * as p6 from "@clack/prompts";
|
|
719
1066
|
import pc3 from "picocolors";
|
|
720
1067
|
async function getInstalled2() {
|
|
721
1068
|
const states = await Promise.all(PKGS.map(async (pkg) => ({ pkg, installed: await pkg.isInstalled() })));
|
|
@@ -724,7 +1071,7 @@ async function getInstalled2() {
|
|
|
724
1071
|
async function updateFlow(opts = {}) {
|
|
725
1072
|
const installed = await getInstalled2();
|
|
726
1073
|
if (installed.length === 0) {
|
|
727
|
-
|
|
1074
|
+
p6.log.info(t("update.noneInstalled"));
|
|
728
1075
|
return;
|
|
729
1076
|
}
|
|
730
1077
|
let targets;
|
|
@@ -735,17 +1082,17 @@ async function updateFlow(opts = {}) {
|
|
|
735
1082
|
for (const id of opts.ids) {
|
|
736
1083
|
const pkg = findPkg(id);
|
|
737
1084
|
if (!pkg) {
|
|
738
|
-
|
|
1085
|
+
p6.log.warn(`Unknown id: ${id}`);
|
|
739
1086
|
continue;
|
|
740
1087
|
}
|
|
741
1088
|
if (!installed.some((x2) => x2.id === pkg.id)) {
|
|
742
|
-
|
|
1089
|
+
p6.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
|
|
743
1090
|
continue;
|
|
744
1091
|
}
|
|
745
1092
|
targets.push(pkg);
|
|
746
1093
|
}
|
|
747
1094
|
} else {
|
|
748
|
-
const picked = await
|
|
1095
|
+
const picked = await p6.multiselect({
|
|
749
1096
|
message: t("update.selectPrompt"),
|
|
750
1097
|
options: installed.map((pkg) => ({
|
|
751
1098
|
value: pkg.id,
|
|
@@ -754,41 +1101,41 @@ async function updateFlow(opts = {}) {
|
|
|
754
1101
|
})),
|
|
755
1102
|
required: false
|
|
756
1103
|
});
|
|
757
|
-
if (
|
|
758
|
-
|
|
1104
|
+
if (p6.isCancel(picked)) {
|
|
1105
|
+
p6.cancel(t("app.cancelled"));
|
|
759
1106
|
return;
|
|
760
1107
|
}
|
|
761
1108
|
targets = picked.map((id) => findPkg(id)).filter((x2) => Boolean(x2));
|
|
762
1109
|
}
|
|
763
1110
|
if (targets.length === 0) {
|
|
764
|
-
|
|
1111
|
+
p6.log.info(t("install.nothingSelected"));
|
|
765
1112
|
return;
|
|
766
1113
|
}
|
|
767
1114
|
const results = [];
|
|
768
1115
|
for (const pkg of targets) {
|
|
769
1116
|
if (pkg.id === "sequential-thinking") {
|
|
770
|
-
|
|
1117
|
+
p6.log.info(t("update.mcpAutoNote", { name: pkg.name }));
|
|
771
1118
|
results.push({ id: pkg.id, status: "noop" });
|
|
772
1119
|
continue;
|
|
773
1120
|
}
|
|
774
1121
|
if (pkg.id === "context7") {
|
|
775
|
-
|
|
1122
|
+
p6.log.info(t("update.context7Note"));
|
|
776
1123
|
results.push({ id: pkg.id, status: "noop" });
|
|
777
1124
|
continue;
|
|
778
1125
|
}
|
|
779
|
-
const
|
|
1126
|
+
const log5 = p6.taskLog({ title: t("update.starting", { name: pkg.name }) });
|
|
780
1127
|
try {
|
|
781
1128
|
if (pkg.update) {
|
|
782
|
-
await pkg.update({ log:
|
|
1129
|
+
await pkg.update({ log: log5, config: {}, t });
|
|
783
1130
|
} else {
|
|
784
|
-
await pkg.uninstall({ log:
|
|
785
|
-
await pkg.install({ log:
|
|
1131
|
+
await pkg.uninstall({ log: log5, config: {}, t });
|
|
1132
|
+
await pkg.install({ log: log5, config: {}, t });
|
|
786
1133
|
}
|
|
787
|
-
|
|
1134
|
+
log5.success(t("update.success", { name: pkg.name }));
|
|
788
1135
|
results.push({ id: pkg.id, status: "ok" });
|
|
789
1136
|
} catch (err) {
|
|
790
1137
|
const msg = err instanceof Error ? err.message : String(err);
|
|
791
|
-
|
|
1138
|
+
log5.error(`${t("update.failed", { name: pkg.name })}
|
|
792
1139
|
${msg}`);
|
|
793
1140
|
results.push({ id: pkg.id, status: "fail", message: msg });
|
|
794
1141
|
}
|
|
@@ -796,7 +1143,7 @@ ${msg}`);
|
|
|
796
1143
|
const ok = results.filter((r) => r.status === "ok").length;
|
|
797
1144
|
const fail = results.filter((r) => r.status === "fail").length;
|
|
798
1145
|
const noop = results.filter((r) => r.status === "noop").length;
|
|
799
|
-
|
|
1146
|
+
p6.note(
|
|
800
1147
|
[
|
|
801
1148
|
pc3.green(t("install.summaryOk", { count: ok })),
|
|
802
1149
|
pc3.red(t("install.summaryFail", { count: fail })),
|
|
@@ -804,19 +1151,31 @@ ${msg}`);
|
|
|
804
1151
|
].join("\n"),
|
|
805
1152
|
t("install.summaryTitle")
|
|
806
1153
|
);
|
|
1154
|
+
await syncFromState({ skip: opts.noClaudeMd });
|
|
807
1155
|
}
|
|
808
1156
|
|
|
809
1157
|
// src/flows/status.ts
|
|
810
|
-
import * as
|
|
1158
|
+
import * as p7 from "@clack/prompts";
|
|
811
1159
|
import pc4 from "picocolors";
|
|
812
1160
|
async function statusFlow(opts = {}) {
|
|
813
1161
|
const states = await Promise.all(
|
|
814
|
-
PKGS.map(async (pkg) =>
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
1162
|
+
PKGS.map(async (pkg) => {
|
|
1163
|
+
const installed = await pkg.isInstalled();
|
|
1164
|
+
const installedVersion = installed && pkg.installedVersion ? await pkg.installedVersion() : null;
|
|
1165
|
+
const latestVersion = pkg.latestVersion ? await pkg.latestVersion() : null;
|
|
1166
|
+
const updateAvailable = Boolean(
|
|
1167
|
+
installed && installedVersion && latestVersion && installedVersion !== latestVersion
|
|
1168
|
+
);
|
|
1169
|
+
return {
|
|
1170
|
+
id: pkg.id,
|
|
1171
|
+
name: pkg.name,
|
|
1172
|
+
type: pkg.type,
|
|
1173
|
+
installed,
|
|
1174
|
+
installedVersion,
|
|
1175
|
+
latestVersion,
|
|
1176
|
+
updateAvailable
|
|
1177
|
+
};
|
|
1178
|
+
})
|
|
820
1179
|
);
|
|
821
1180
|
if (opts.json) {
|
|
822
1181
|
process.stdout.write(JSON.stringify(states, null, 2) + "\n");
|
|
@@ -829,12 +1188,12 @@ async function statusFlow(opts = {}) {
|
|
|
829
1188
|
const rows = states.map(
|
|
830
1189
|
(s) => `${s.name.padEnd(nameW)} ${s.type.padEnd(typeW)} ${s.installed ? pc4.green(`\u2713 ${t("pkg.installed")}`) : pc4.yellow(`\u2717 ${t("pkg.notInstalled")}`)}`
|
|
831
1190
|
);
|
|
832
|
-
|
|
1191
|
+
p7.note([header, sep, ...rows].join("\n"), t("status.title"));
|
|
833
1192
|
}
|
|
834
1193
|
|
|
835
1194
|
// src/ui/menu.ts
|
|
836
1195
|
async function mainMenu() {
|
|
837
|
-
const action = await
|
|
1196
|
+
const action = await p8.select({
|
|
838
1197
|
message: t("menu.title"),
|
|
839
1198
|
options: [
|
|
840
1199
|
{ value: "install", label: t("menu.install") },
|
|
@@ -844,8 +1203,8 @@ async function mainMenu() {
|
|
|
844
1203
|
{ value: "exit", label: t("menu.exit") }
|
|
845
1204
|
]
|
|
846
1205
|
});
|
|
847
|
-
if (
|
|
848
|
-
|
|
1206
|
+
if (p8.isCancel(action) || action === "exit") {
|
|
1207
|
+
p8.cancel(t("app.cancelled"));
|
|
849
1208
|
return;
|
|
850
1209
|
}
|
|
851
1210
|
switch (action) {
|
|
@@ -868,23 +1227,38 @@ async function mainMenu() {
|
|
|
868
1227
|
function parseLang(v) {
|
|
869
1228
|
return v === "zh" || v === "en" ? v : void 0;
|
|
870
1229
|
}
|
|
1230
|
+
function noClaudeMdFromArgs(args) {
|
|
1231
|
+
if (args["no-claude-md"]) return true;
|
|
1232
|
+
return Boolean(process.env["CURDX_FLOW_NO_CLAUDE_MD"]);
|
|
1233
|
+
}
|
|
871
1234
|
var sharedArgs = {
|
|
872
|
-
lang: { type: "string", description: "Override language: zh or en" }
|
|
1235
|
+
lang: { type: "string", description: "Override language: zh or en" },
|
|
1236
|
+
"no-claude-md": {
|
|
1237
|
+
type: "boolean",
|
|
1238
|
+
description: "Skip syncing the @curdx/flow block in ~/.claude/CLAUDE.md"
|
|
1239
|
+
}
|
|
873
1240
|
};
|
|
874
1241
|
var installCmd = defineCommand({
|
|
875
|
-
meta: { name: "install", description: "Install or
|
|
1242
|
+
meta: { name: "install", description: "Install, reinstall, or update plugins / MCP servers" },
|
|
876
1243
|
args: {
|
|
877
1244
|
...sharedArgs,
|
|
878
1245
|
all: { type: "boolean", description: "Install all known items" },
|
|
879
1246
|
yes: { type: "boolean", description: "Skip reinstall confirmation (assume yes)" },
|
|
1247
|
+
"no-refresh": { type: "boolean", description: "Skip refreshing marketplace caches" },
|
|
880
1248
|
ids: { type: "positional", required: false, description: "Item ids", default: "" }
|
|
881
1249
|
},
|
|
882
1250
|
async run({ args }) {
|
|
883
1251
|
await initLanguage(parseLang(args.lang));
|
|
884
|
-
|
|
1252
|
+
p9.intro(t("app.intro"));
|
|
885
1253
|
const ids = collectPositional(args);
|
|
886
|
-
await installFlow({
|
|
887
|
-
|
|
1254
|
+
await installFlow({
|
|
1255
|
+
ids,
|
|
1256
|
+
all: Boolean(args.all),
|
|
1257
|
+
yes: Boolean(args.yes),
|
|
1258
|
+
noRefresh: Boolean(args["no-refresh"]),
|
|
1259
|
+
noClaudeMd: noClaudeMdFromArgs(args)
|
|
1260
|
+
});
|
|
1261
|
+
p9.outro(t("app.outro"));
|
|
888
1262
|
}
|
|
889
1263
|
});
|
|
890
1264
|
var uninstallCmd = defineCommand({
|
|
@@ -896,10 +1270,14 @@ var uninstallCmd = defineCommand({
|
|
|
896
1270
|
},
|
|
897
1271
|
async run({ args }) {
|
|
898
1272
|
await initLanguage(parseLang(args.lang));
|
|
899
|
-
|
|
1273
|
+
p9.intro(t("app.intro"));
|
|
900
1274
|
const ids = collectPositional(args);
|
|
901
|
-
await uninstallFlow({
|
|
902
|
-
|
|
1275
|
+
await uninstallFlow({
|
|
1276
|
+
ids,
|
|
1277
|
+
yes: Boolean(args.yes),
|
|
1278
|
+
noClaudeMd: noClaudeMdFromArgs(args)
|
|
1279
|
+
});
|
|
1280
|
+
p9.outro(t("app.outro"));
|
|
903
1281
|
}
|
|
904
1282
|
});
|
|
905
1283
|
var updateCmd = defineCommand({
|
|
@@ -911,10 +1289,14 @@ var updateCmd = defineCommand({
|
|
|
911
1289
|
},
|
|
912
1290
|
async run({ args }) {
|
|
913
1291
|
await initLanguage(parseLang(args.lang));
|
|
914
|
-
|
|
1292
|
+
p9.intro(t("app.intro"));
|
|
915
1293
|
const ids = collectPositional(args);
|
|
916
|
-
await updateFlow({
|
|
917
|
-
|
|
1294
|
+
await updateFlow({
|
|
1295
|
+
ids,
|
|
1296
|
+
all: Boolean(args.all),
|
|
1297
|
+
noClaudeMd: noClaudeMdFromArgs(args)
|
|
1298
|
+
});
|
|
1299
|
+
p9.outro(t("app.outro"));
|
|
918
1300
|
}
|
|
919
1301
|
});
|
|
920
1302
|
var statusCmd = defineCommand({
|
|
@@ -925,16 +1307,16 @@ var statusCmd = defineCommand({
|
|
|
925
1307
|
},
|
|
926
1308
|
async run({ args }) {
|
|
927
1309
|
await initLanguage(parseLang(args.lang));
|
|
928
|
-
if (!args.json)
|
|
1310
|
+
if (!args.json) p9.intro(t("app.intro"));
|
|
929
1311
|
await statusFlow({ json: Boolean(args.json) });
|
|
930
|
-
if (!args.json)
|
|
1312
|
+
if (!args.json) p9.outro(t("app.outro"));
|
|
931
1313
|
}
|
|
932
1314
|
});
|
|
933
1315
|
var SUBCOMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "update", "status"]);
|
|
934
1316
|
var root = defineCommand({
|
|
935
1317
|
meta: {
|
|
936
1318
|
name: "@curdx/flow",
|
|
937
|
-
version: "3.
|
|
1319
|
+
version: "3.3.0",
|
|
938
1320
|
description: "Interactive installer for Claude Code plugins and MCP servers"
|
|
939
1321
|
},
|
|
940
1322
|
args: sharedArgs,
|
|
@@ -969,9 +1351,9 @@ async function runInteractive(argv2) {
|
|
|
969
1351
|
else if (argv2[i]?.startsWith("--lang=")) lang = parseLang(argv2[i].slice("--lang=".length));
|
|
970
1352
|
}
|
|
971
1353
|
await initLanguage(lang);
|
|
972
|
-
|
|
1354
|
+
p9.intro(t("app.intro"));
|
|
973
1355
|
await mainMenu();
|
|
974
|
-
|
|
1356
|
+
p9.outro(t("app.outro"));
|
|
975
1357
|
}
|
|
976
1358
|
var argv = process.argv.slice(2);
|
|
977
1359
|
var first = firstNonFlag(argv);
|