@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 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 p8 from "@clack/prompts";
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 p7 from "@clack/prompts";
191
+ import * as p8 from "@clack/prompts";
170
192
 
171
193
  // src/flows/install.ts
172
- import * as p3 from "@clack/prompts";
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, log4) {
187
- log4.message(`$ ${cmd} ${args.join(" ")}`);
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
- log4.message(trimmed);
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((p9) => {
238
- const [name = p9.id, marketplace = ""] = p9.id.split("@");
264
+ pluginCache = arr.map((p10) => {
265
+ const [name = p10.id, marketplace = ""] = p10.id.split("@");
239
266
  return {
240
- id: p9.id,
267
+ id: p10.id,
241
268
  name,
242
269
  marketplace,
243
- version: p9.version,
244
- scope: p9.scope,
245
- enabled: p9.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((p9) => p9.id === id);
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((p9) => p9.id === id);
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 selectInteractive() {
521
- const states = await Promise.all(PKGS.map(async (pkg) => ({ pkg, installed: await pkg.isInstalled() })));
522
- const options = states.map(({ pkg, installed }) => ({
523
- value: pkg.id,
524
- label: `${pkg.name} ${pc.dim(`(${pkg.type})`)} ${installed ? pc.green(`\u2713 ${t("pkg.installed")}`) : pc.yellow(`\u2717 ${t("pkg.notInstalled")}`)}`,
525
- hint: pkg.description
526
- }));
527
- const initialValues = states.filter((s) => !s.installed).map((s) => s.pkg.id);
528
- const picked = await p3.multiselect({
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 (p3.isCancel(picked)) return null;
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 p3.log.warn(`Unknown id: ${id}`);
845
+ else p4.log.warn(`Unknown id: ${id}`);
545
846
  }
546
847
  return found;
547
848
  }
548
- async function runOne(pkg, opts, alreadyInstalled) {
549
- let reinstall = false;
550
- if (alreadyInstalled) {
551
- if (opts.yes) {
552
- reinstall = true;
553
- } else {
554
- const ans = await p3.confirm({
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 (p3.isCancel(ans) || ans === false) {
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
- p3.log.warn(t("install.prereqFail", { name: pkg.name, reason: r.reason }));
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 log4 = p3.taskLog({ title: t("install.starting", { name: pkg.name }) });
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
- log4.message(t("reinstall.uninstalling"));
581
- await pkg.uninstall({ log: log4, config, t });
582
- log4.message(t("reinstall.installing"));
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
- await pkg.install({ log: log4, config, t });
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
- log4.error(`${t("install.failed", { name: pkg.name })}
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
- p3.note(lines.join("\n"), t("install.summaryTitle"));
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 targets = explicit ? selectFromIds(opts) : await selectInteractive();
612
- if (targets === null) {
613
- p3.cancel(t("app.cancelled"));
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 installedMap = /* @__PURE__ */ new Map();
950
+ const stateMap = /* @__PURE__ */ new Map();
621
951
  await Promise.all(
622
- targets.map(async (pkg) => {
623
- installedMap.set(pkg.id, await pkg.isInstalled());
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 installed = installedMap.get(pkg.id) ?? false;
629
- results.push(await runOne(pkg, opts, installed));
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 p4 from "@clack/prompts";
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
- p4.log.warn(`Unknown id: ${id}`);
995
+ p5.log.warn(`Unknown id: ${id}`);
650
996
  continue;
651
997
  }
652
998
  if (!installed.some((x2) => x2.id === pkg.id)) {
653
- p4.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
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
- p4.log.info(t("uninstall.noneInstalled"));
1006
+ p5.log.info(t("uninstall.noneInstalled"));
661
1007
  return;
662
1008
  }
663
- const picked = await p4.multiselect({
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 (p4.isCancel(picked)) {
673
- p4.cancel(t("app.cancelled"));
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
- p4.log.info(t("install.nothingSelected"));
1025
+ p5.log.info(t("install.nothingSelected"));
680
1026
  return;
681
1027
  }
682
1028
  if (!opts.yes) {
683
- const ok2 = await p4.confirm({
1029
+ const ok2 = await p5.confirm({
684
1030
  message: t("uninstall.confirm", { count: targets.length }),
685
1031
  initialValue: false
686
1032
  });
687
- if (p4.isCancel(ok2) || ok2 === false) {
688
- p4.cancel(t("app.cancelled"));
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 log4 = p4.taskLog({ title: t("uninstall.starting", { name: pkg.name }) });
1040
+ const log5 = p5.taskLog({ title: t("uninstall.starting", { name: pkg.name }) });
695
1041
  try {
696
- await pkg.uninstall({ log: log4, config: {}, t });
697
- log4.success(t("uninstall.success", { name: pkg.name }));
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
- log4.error(`${t("uninstall.failed", { name: pkg.name })}
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
- p4.note(
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 p5 from "@clack/prompts";
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
- p5.log.info(t("update.noneInstalled"));
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
- p5.log.warn(`Unknown id: ${id}`);
1085
+ p6.log.warn(`Unknown id: ${id}`);
739
1086
  continue;
740
1087
  }
741
1088
  if (!installed.some((x2) => x2.id === pkg.id)) {
742
- p5.log.warn(`${pkg.name}: ${t("pkg.notInstalled")}`);
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 p5.multiselect({
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 (p5.isCancel(picked)) {
758
- p5.cancel(t("app.cancelled"));
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
- p5.log.info(t("install.nothingSelected"));
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
- p5.log.info(t("update.mcpAutoNote", { name: pkg.name }));
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
- p5.log.info(t("update.context7Note"));
1122
+ p6.log.info(t("update.context7Note"));
776
1123
  results.push({ id: pkg.id, status: "noop" });
777
1124
  continue;
778
1125
  }
779
- const log4 = p5.taskLog({ title: t("update.starting", { name: pkg.name }) });
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: log4, config: {}, t });
1129
+ await pkg.update({ log: log5, config: {}, t });
783
1130
  } else {
784
- await pkg.uninstall({ log: log4, config: {}, t });
785
- await pkg.install({ log: log4, config: {}, t });
1131
+ await pkg.uninstall({ log: log5, config: {}, t });
1132
+ await pkg.install({ log: log5, config: {}, t });
786
1133
  }
787
- log4.success(t("update.success", { name: pkg.name }));
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
- log4.error(`${t("update.failed", { name: pkg.name })}
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
- p5.note(
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 p6 from "@clack/prompts";
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
- id: pkg.id,
816
- name: pkg.name,
817
- type: pkg.type,
818
- installed: await pkg.isInstalled()
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
- p6.note([header, sep, ...rows].join("\n"), t("status.title"));
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 p7.select({
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 (p7.isCancel(action) || action === "exit") {
848
- p7.cancel(t("app.cancelled"));
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 reinstall plugins / MCP servers" },
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
- p8.intro(t("app.intro"));
1252
+ p9.intro(t("app.intro"));
885
1253
  const ids = collectPositional(args);
886
- await installFlow({ ids, all: Boolean(args.all), yes: Boolean(args.yes) });
887
- p8.outro(t("app.outro"));
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
- p8.intro(t("app.intro"));
1273
+ p9.intro(t("app.intro"));
900
1274
  const ids = collectPositional(args);
901
- await uninstallFlow({ ids, yes: Boolean(args.yes) });
902
- p8.outro(t("app.outro"));
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
- p8.intro(t("app.intro"));
1292
+ p9.intro(t("app.intro"));
915
1293
  const ids = collectPositional(args);
916
- await updateFlow({ ids, all: Boolean(args.all) });
917
- p8.outro(t("app.outro"));
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) p8.intro(t("app.intro"));
1310
+ if (!args.json) p9.intro(t("app.intro"));
929
1311
  await statusFlow({ json: Boolean(args.json) });
930
- if (!args.json) p8.outro(t("app.outro"));
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.1.0",
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
- p8.intro(t("app.intro"));
1354
+ p9.intro(t("app.intro"));
973
1355
  await mainMenu();
974
- p8.outro(t("app.outro"));
1356
+ p9.outro(t("app.outro"));
975
1357
  }
976
1358
  var argv = process.argv.slice(2);
977
1359
  var first = firstNonFlag(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Interactive installer for Claude Code plugins and MCP servers",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",