@curdx/flow 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
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.2.0 — 2026-04-26
6
+
7
+ ### Added
8
+
9
+ - **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".
10
+ - **Smart dispatch** — selected items route to the right operation:
11
+ - not installed → `install` (full)
12
+ - update available → `update` (incremental, via `claude plugin update <id>`)
13
+ - already installed but selected → reinstall confirmation prompt (uninstall + install)
14
+ - **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).
15
+ - **`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.
16
+ - **`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.
17
+
18
+ ### Notes
19
+
20
+ 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.
21
+
5
22
  ## 3.1.0 — 2026-04-26
6
23
 
7
24
  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/dist/index.mjs CHANGED
@@ -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',
@@ -81,6 +87,12 @@ var messages2 = {
81
87
  "pkg.installed": "installed",
82
88
  "pkg.notInstalled": "not installed",
83
89
  "pkg.unknown": "unknown",
90
+ "pkg.upToDateWithVersion": "installed v{version}",
91
+ "pkg.updateAvailable": "v{current} \u2192 v{latest} available",
92
+ "marketplace.refreshing": "Refreshing marketplace caches\u2026",
93
+ "marketplace.refreshed": "Refreshed {count} marketplace(s)",
94
+ "marketplace.refreshSkipped": "Marketplace caches are fresh, skipping refresh",
95
+ "install.updating": 'Updating "{name}" to v{version}',
84
96
  "install.selectPrompt": "Select items to install / reinstall (not-installed are pre-selected)",
85
97
  "install.nothingSelected": "Nothing selected. Exiting.",
86
98
  "install.confirmReinstall": '"{name}" is already installed. Reinstall (uninstall then install)?',
@@ -172,6 +184,11 @@ import * as p7 from "@clack/prompts";
172
184
  import * as p3 from "@clack/prompts";
173
185
  import pc from "picocolors";
174
186
 
187
+ // src/runner/state.ts
188
+ import { promises as fs } from "fs";
189
+ import path from "path";
190
+ import os from "os";
191
+
175
192
  // src/runner/exec.ts
176
193
  import { x } from "tinyexec";
177
194
  async function run(cmd, args) {
@@ -298,6 +315,55 @@ async function isMcpInstalled(name) {
298
315
  const list = await listMcp();
299
316
  return list.some((m) => m.name === name);
300
317
  }
318
+ async function findPlugin(id) {
319
+ const list = await listPlugins();
320
+ return list.find((p9) => p9.id === id);
321
+ }
322
+ var marketplaceJsonCache = /* @__PURE__ */ new Map();
323
+ function marketplaceDir(name) {
324
+ return path.join(os.homedir(), ".claude", "plugins", "marketplaces", name);
325
+ }
326
+ async function readMarketplaceJson(name) {
327
+ if (marketplaceJsonCache.has(name)) return marketplaceJsonCache.get(name) ?? null;
328
+ const file = path.join(marketplaceDir(name), ".claude-plugin", "marketplace.json");
329
+ try {
330
+ const raw = await fs.readFile(file, "utf8");
331
+ const parsed = JSON.parse(raw);
332
+ marketplaceJsonCache.set(name, parsed);
333
+ return parsed;
334
+ } catch {
335
+ marketplaceJsonCache.set(name, null);
336
+ return null;
337
+ }
338
+ }
339
+ async function getMarketplacePluginVersion(marketplaceName, pluginName) {
340
+ const m = await readMarketplaceJson(marketplaceName);
341
+ if (!m?.plugins) return null;
342
+ const entry = m.plugins.find((p9) => p9.name === pluginName);
343
+ return entry?.version ?? null;
344
+ }
345
+ var REFRESH_TTL_MS = 60 * 60 * 1e3;
346
+ async function shouldSkipRefresh(name) {
347
+ try {
348
+ const stat = await fs.stat(marketplaceDir(name));
349
+ return Date.now() - stat.mtimeMs < REFRESH_TTL_MS;
350
+ } catch {
351
+ return false;
352
+ }
353
+ }
354
+ async function refreshMarketplaces(names) {
355
+ const unique = [...new Set(names)];
356
+ const toRefresh = [];
357
+ for (const name of unique) {
358
+ if (!await shouldSkipRefresh(name)) toRefresh.push(name);
359
+ }
360
+ if (toRefresh.length === 0) return [];
361
+ await Promise.all(
362
+ toRefresh.map((name) => run("claude", ["plugin", "marketplace", "update", name]))
363
+ );
364
+ for (const name of toRefresh) marketplaceJsonCache.delete(name);
365
+ return toRefresh;
366
+ }
301
367
 
302
368
  // src/registry/plugins/_helpers.ts
303
369
  async function ensureMarketplace(marketplaceName, marketplaceSource, ctx) {
@@ -325,6 +391,7 @@ async function updatePluginById(pluginId, ctx) {
325
391
 
326
392
  // src/registry/plugins/pua.ts
327
393
  var PLUGIN_ID = "pua@pua-skills";
394
+ var PLUGIN_NAME = "pua";
328
395
  var MARKETPLACE_NAME = "pua-skills";
329
396
  var MARKETPLACE_SOURCE = "tanweai/pua";
330
397
  var pua = {
@@ -332,7 +399,14 @@ var pua = {
332
399
  name: "pua",
333
400
  description: "tanweai/pua \u2014 Chinese Claude Code skills bundle",
334
401
  type: "plugin",
402
+ marketplaces: () => [MARKETPLACE_NAME],
335
403
  isInstalled: () => isPluginInstalled(PLUGIN_ID),
404
+ installedVersion: async () => {
405
+ const p9 = await findPlugin(PLUGIN_ID);
406
+ const v = p9?.version;
407
+ return v && v !== "unknown" ? v : null;
408
+ },
409
+ latestVersion: () => getMarketplacePluginVersion(MARKETPLACE_NAME, PLUGIN_NAME),
336
410
  install: async (ctx) => {
337
411
  await ensureMarketplace(MARKETPLACE_NAME, MARKETPLACE_SOURCE, ctx);
338
412
  await installPluginById(PLUGIN_ID, ctx);
@@ -344,6 +418,7 @@ var pua_default = pua;
344
418
 
345
419
  // src/registry/plugins/claude-mem.ts
346
420
  var PLUGIN_ID2 = "claude-mem@thedotmack";
421
+ var PLUGIN_NAME2 = "claude-mem";
347
422
  var MARKETPLACE_NAME2 = "thedotmack";
348
423
  var MARKETPLACE_SOURCE2 = "thedotmack/claude-mem";
349
424
  var claudeMem = {
@@ -351,7 +426,14 @@ var claudeMem = {
351
426
  name: "claude-mem",
352
427
  description: "thedotmack/claude-mem \u2014 persistent cross-session memory for Claude Code",
353
428
  type: "plugin",
429
+ marketplaces: () => [MARKETPLACE_NAME2],
354
430
  isInstalled: () => isPluginInstalled(PLUGIN_ID2),
431
+ installedVersion: async () => {
432
+ const p9 = await findPlugin(PLUGIN_ID2);
433
+ const v = p9?.version;
434
+ return v && v !== "unknown" ? v : null;
435
+ },
436
+ latestVersion: () => getMarketplacePluginVersion(MARKETPLACE_NAME2, PLUGIN_NAME2),
355
437
  install: async (ctx) => {
356
438
  await ensureMarketplace(MARKETPLACE_NAME2, MARKETPLACE_SOURCE2, ctx);
357
439
  await installPluginById(PLUGIN_ID2, ctx);
@@ -517,14 +599,41 @@ function findPkg(id) {
517
599
  }
518
600
 
519
601
  // 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);
602
+ async function deriveState(pkg) {
603
+ if (!await pkg.isInstalled()) return { kind: "not_installed" };
604
+ const [installed, latest] = await Promise.all([
605
+ pkg.installedVersion?.() ?? Promise.resolve(null),
606
+ pkg.latestVersion?.() ?? Promise.resolve(null)
607
+ ]);
608
+ if (installed && latest && installed !== latest) {
609
+ return { kind: "update_available", current: installed, latest };
610
+ }
611
+ return { kind: "up_to_date", version: installed };
612
+ }
613
+ function stateLabel(pkg, s) {
614
+ const head = `${pkg.name} ${pc.dim(`(${pkg.type})`)}`;
615
+ switch (s.kind) {
616
+ case "not_installed":
617
+ return `${head} ${pc.yellow(`\u2717 ${t("pkg.notInstalled")}`)}`;
618
+ case "up_to_date":
619
+ return `${head} ${pc.green(
620
+ s.version ? `\u2713 ${t("pkg.upToDateWithVersion", { version: s.version })}` : `\u2713 ${t("pkg.installed")}`
621
+ )}`;
622
+ case "update_available":
623
+ return `${head} ${pc.cyan(
624
+ `\u2191 ${t("pkg.updateAvailable", { current: s.current, latest: s.latest })}`
625
+ )}`;
626
+ }
627
+ }
628
+ async function selectInteractive(states) {
629
+ const options = PKGS.map((pkg) => {
630
+ const s = states.get(pkg.id);
631
+ return { value: pkg.id, label: stateLabel(pkg, s), hint: pkg.description };
632
+ });
633
+ const initialValues = PKGS.filter((pkg) => {
634
+ const s = states.get(pkg.id);
635
+ return s.kind === "not_installed" || s.kind === "update_available";
636
+ }).map((pkg) => pkg.id);
528
637
  const picked = await p3.multiselect({
529
638
  message: t("install.selectPrompt"),
530
639
  options,
@@ -545,12 +654,14 @@ function selectFromIds(opts) {
545
654
  }
546
655
  return found;
547
656
  }
548
- async function runOne(pkg, opts, alreadyInstalled) {
549
- let reinstall = false;
550
- if (alreadyInstalled) {
551
- if (opts.yes) {
552
- reinstall = true;
553
- } else {
657
+ async function runOne(pkg, state, opts) {
658
+ let mode;
659
+ if (state.kind === "not_installed") {
660
+ mode = "install";
661
+ } else if (state.kind === "update_available") {
662
+ mode = "update";
663
+ } else {
664
+ if (!opts.yes) {
554
665
  const ans = await p3.confirm({
555
666
  message: t("install.confirmReinstall", { name: pkg.name }),
556
667
  initialValue: false
@@ -558,8 +669,8 @@ async function runOne(pkg, opts, alreadyInstalled) {
558
669
  if (p3.isCancel(ans) || ans === false) {
559
670
  return { id: pkg.id, status: "skip", message: t("install.skippedReinstall", { name: pkg.name }) };
560
671
  }
561
- reinstall = true;
562
672
  }
673
+ mode = "reinstall";
563
674
  }
564
675
  if (pkg.prereqCheck) {
565
676
  const r = await pkg.prereqCheck(t);
@@ -569,19 +680,35 @@ async function runOne(pkg, opts, alreadyInstalled) {
569
680
  }
570
681
  }
571
682
  let config = {};
572
- if (pkg.configPrompts) {
683
+ if (pkg.configPrompts && mode !== "update") {
573
684
  const cfg = await pkg.configPrompts({ t });
574
685
  if (cfg === null) return { id: pkg.id, status: "skip", message: t("app.cancelled") };
575
686
  config = cfg;
576
687
  }
577
- const log4 = p3.taskLog({ title: t("install.starting", { name: pkg.name }) });
688
+ const titleKey = mode === "update" ? "install.updating" : "install.starting";
689
+ const titleVars = { name: pkg.name };
690
+ if (mode === "update" && state.kind === "update_available") {
691
+ titleVars["version"] = state.latest;
692
+ }
693
+ const log4 = p3.taskLog({ title: t(titleKey, titleVars) });
578
694
  try {
579
- if (reinstall) {
695
+ if (mode === "reinstall") {
580
696
  log4.message(t("reinstall.uninstalling"));
581
697
  await pkg.uninstall({ log: log4, config, t });
582
698
  log4.message(t("reinstall.installing"));
699
+ await pkg.install({ log: log4, config, t });
700
+ } else if (mode === "update") {
701
+ if (pkg.update) {
702
+ await pkg.update({ log: log4, config, t });
703
+ } else {
704
+ log4.message(t("reinstall.uninstalling"));
705
+ await pkg.uninstall({ log: log4, config, t });
706
+ log4.message(t("reinstall.installing"));
707
+ await pkg.install({ log: log4, config, t });
708
+ }
709
+ } else {
710
+ await pkg.install({ log: log4, config, t });
583
711
  }
584
- await pkg.install({ log: log4, config, t });
585
712
  log4.success(t("install.success", { name: pkg.name }));
586
713
  return { id: pkg.id, status: "ok" };
587
714
  } catch (err) {
@@ -606,27 +733,53 @@ function summarize(results) {
606
733
  ];
607
734
  p3.note(lines.join("\n"), t("install.summaryTitle"));
608
735
  }
736
+ async function maybeRefreshMarketplaces(opts) {
737
+ if (opts.noRefresh) return;
738
+ const names = /* @__PURE__ */ new Set();
739
+ for (const pkg of PKGS) {
740
+ if (pkg.marketplaces) for (const n of pkg.marketplaces()) names.add(n);
741
+ }
742
+ if (names.size === 0) return;
743
+ const sp = p3.spinner();
744
+ sp.start(t("marketplace.refreshing"));
745
+ const refreshed = await refreshMarketplaces([...names]);
746
+ sp.stop(
747
+ refreshed.length > 0 ? t("marketplace.refreshed", { count: refreshed.length }) : t("marketplace.refreshSkipped")
748
+ );
749
+ }
609
750
  async function installFlow(opts = {}) {
751
+ await maybeRefreshMarketplaces(opts);
610
752
  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) {
753
+ const candidates = explicit ? selectFromIds(opts) : [...PKGS];
754
+ if (candidates.length === 0) {
617
755
  p3.log.info(t("install.nothingSelected"));
618
756
  return;
619
757
  }
620
- const installedMap = /* @__PURE__ */ new Map();
758
+ const stateMap = /* @__PURE__ */ new Map();
621
759
  await Promise.all(
622
- targets.map(async (pkg) => {
623
- installedMap.set(pkg.id, await pkg.isInstalled());
760
+ candidates.map(async (pkg) => {
761
+ stateMap.set(pkg.id, await deriveState(pkg));
624
762
  })
625
763
  );
764
+ let targets;
765
+ if (explicit) {
766
+ targets = candidates;
767
+ } else {
768
+ const picked = await selectInteractive(stateMap);
769
+ if (picked === null) {
770
+ p3.cancel(t("app.cancelled"));
771
+ return;
772
+ }
773
+ targets = picked;
774
+ }
775
+ if (targets.length === 0) {
776
+ p3.log.info(t("install.nothingSelected"));
777
+ return;
778
+ }
626
779
  const results = [];
627
780
  for (const pkg of targets) {
628
- const installed = installedMap.get(pkg.id) ?? false;
629
- results.push(await runOne(pkg, opts, installed));
781
+ const state = stateMap.get(pkg.id) ?? { kind: "not_installed" };
782
+ results.push(await runOne(pkg, state, opts));
630
783
  }
631
784
  summarize(results);
632
785
  }
@@ -811,12 +964,23 @@ import * as p6 from "@clack/prompts";
811
964
  import pc4 from "picocolors";
812
965
  async function statusFlow(opts = {}) {
813
966
  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
- }))
967
+ PKGS.map(async (pkg) => {
968
+ const installed = await pkg.isInstalled();
969
+ const installedVersion = installed && pkg.installedVersion ? await pkg.installedVersion() : null;
970
+ const latestVersion = pkg.latestVersion ? await pkg.latestVersion() : null;
971
+ const updateAvailable = Boolean(
972
+ installed && installedVersion && latestVersion && installedVersion !== latestVersion
973
+ );
974
+ return {
975
+ id: pkg.id,
976
+ name: pkg.name,
977
+ type: pkg.type,
978
+ installed,
979
+ installedVersion,
980
+ latestVersion,
981
+ updateAvailable
982
+ };
983
+ })
820
984
  );
821
985
  if (opts.json) {
822
986
  process.stdout.write(JSON.stringify(states, null, 2) + "\n");
@@ -872,18 +1036,24 @@ var sharedArgs = {
872
1036
  lang: { type: "string", description: "Override language: zh or en" }
873
1037
  };
874
1038
  var installCmd = defineCommand({
875
- meta: { name: "install", description: "Install or reinstall plugins / MCP servers" },
1039
+ meta: { name: "install", description: "Install, reinstall, or update plugins / MCP servers" },
876
1040
  args: {
877
1041
  ...sharedArgs,
878
1042
  all: { type: "boolean", description: "Install all known items" },
879
1043
  yes: { type: "boolean", description: "Skip reinstall confirmation (assume yes)" },
1044
+ "no-refresh": { type: "boolean", description: "Skip refreshing marketplace caches" },
880
1045
  ids: { type: "positional", required: false, description: "Item ids", default: "" }
881
1046
  },
882
1047
  async run({ args }) {
883
1048
  await initLanguage(parseLang(args.lang));
884
1049
  p8.intro(t("app.intro"));
885
1050
  const ids = collectPositional(args);
886
- await installFlow({ ids, all: Boolean(args.all), yes: Boolean(args.yes) });
1051
+ await installFlow({
1052
+ ids,
1053
+ all: Boolean(args.all),
1054
+ yes: Boolean(args.yes),
1055
+ noRefresh: Boolean(args["no-refresh"])
1056
+ });
887
1057
  p8.outro(t("app.outro"));
888
1058
  }
889
1059
  });
@@ -934,7 +1104,7 @@ var SUBCOMMANDS = /* @__PURE__ */ new Set(["install", "uninstall", "update", "st
934
1104
  var root = defineCommand({
935
1105
  meta: {
936
1106
  name: "@curdx/flow",
937
- version: "3.1.0",
1107
+ version: "3.2.0",
938
1108
  description: "Interactive installer for Claude Code plugins and MCP servers"
939
1109
  },
940
1110
  args: sharedArgs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Interactive installer for Claude Code plugins and MCP servers",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",