@curdx/flow 4.0.0 → 5.0.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,18 @@
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
+ ## 4.0.1 — 2026-04-27
6
+
7
+ ### Fixed
8
+
9
+ - **Migration cleanup is now exhaustive.** v4.0.0's auto-migration for the `ralph-specum` → `curdx-flow` rename only invoked `claude plugin uninstall`, which leaves substantial residue when the marketplace's plugin id has been renamed (the CLI can't resolve the legacy id and bails). The installer now manually purges every leftover artifact for legacy slugs (`ralph-specum@curdx-flow`, `ralph-specum@smart-ralph`):
10
+ - `~/.claude/settings.json` → removes `enabledPlugins[<legacyId>]`
11
+ - `~/.claude/plugins/installed_plugins.json` → removes `plugins[<legacyId>]`
12
+ - `~/.claude/plugins/cache/<marketplace>/<name>/` → recursive remove
13
+ - `~/.claude/plugins/data/<name>-<marketplace>/` → recursive remove
14
+ - Marketplace registrations (`known_marketplaces.json`, `extraKnownMarketplaces`) are deliberately left alone — those are user-managed.
15
+ - Implementation lives in `src/runner/legacy-cleanup.ts::purgeLegacyPluginArtifacts`. Idempotent and safe: every step swallows ENOENT silently and reports JSON / IO errors via the install task log without failing the flow.
16
+
5
17
  ## 4.0.0 — 2026-04-27
6
18
 
7
19
  ### Breaking
package/dist/index.mjs CHANGED
@@ -520,6 +520,78 @@ var frontendDesign = {
520
520
  };
521
521
  var frontend_design_default = frontendDesign;
522
522
 
523
+ // src/runner/legacy-cleanup.ts
524
+ import { promises as fs2 } from "fs";
525
+ import path2 from "path";
526
+ import os2 from "os";
527
+ async function purgeLegacyPluginArtifacts(legacyId, ctx) {
528
+ const at = legacyId.indexOf("@");
529
+ if (at <= 0 || at === legacyId.length - 1) return;
530
+ const name = legacyId.slice(0, at);
531
+ const marketplace = legacyId.slice(at + 1);
532
+ const home = os2.homedir();
533
+ const settingsPath = path2.join(home, ".claude", "settings.json");
534
+ const installedPath = path2.join(home, ".claude", "plugins", "installed_plugins.json");
535
+ const cacheDir = path2.join(home, ".claude", "plugins", "cache", marketplace, name);
536
+ const dataDir = path2.join(home, ".claude", "plugins", "data", `${name}-${marketplace}`);
537
+ let removedAny = false;
538
+ removedAny = await deleteJsonKey(settingsPath, ["enabledPlugins", legacyId], ctx) || removedAny;
539
+ removedAny = await deleteJsonKey(installedPath, ["plugins", legacyId], ctx) || removedAny;
540
+ removedAny = await rmDir(cacheDir, ctx) || removedAny;
541
+ removedAny = await rmDir(dataDir, ctx) || removedAny;
542
+ if (removedAny) {
543
+ ctx.log.message(`Purged legacy artifacts for ${legacyId}.`);
544
+ clearStateCache();
545
+ }
546
+ }
547
+ async function deleteJsonKey(filePath, keyPath, ctx) {
548
+ let raw;
549
+ try {
550
+ raw = await fs2.readFile(filePath, "utf8");
551
+ } catch (err) {
552
+ if (err.code === "ENOENT") return false;
553
+ ctx.log.message(`Skip purge of ${filePath}: ${err.message}`);
554
+ return false;
555
+ }
556
+ let json;
557
+ try {
558
+ json = JSON.parse(raw);
559
+ } catch (err) {
560
+ ctx.log.message(`Skip purge of ${filePath}: invalid JSON (${err.message})`);
561
+ return false;
562
+ }
563
+ let cursor = json;
564
+ for (let i = 0; i < keyPath.length - 1; i++) {
565
+ const next = cursor?.[keyPath[i]];
566
+ if (!next || typeof next !== "object") return false;
567
+ cursor = next;
568
+ }
569
+ const finalKey = keyPath[keyPath.length - 1];
570
+ if (!cursor || !(finalKey in cursor)) return false;
571
+ delete cursor[finalKey];
572
+ try {
573
+ await fs2.writeFile(filePath, JSON.stringify(json, null, 2) + "\n", "utf8");
574
+ return true;
575
+ } catch (err) {
576
+ ctx.log.message(`Failed to rewrite ${filePath}: ${err.message}`);
577
+ return false;
578
+ }
579
+ }
580
+ async function rmDir(dirPath, ctx) {
581
+ try {
582
+ await fs2.access(dirPath);
583
+ } catch {
584
+ return false;
585
+ }
586
+ try {
587
+ await fs2.rm(dirPath, { recursive: true, force: true });
588
+ return true;
589
+ } catch (err) {
590
+ ctx.log.message(`Failed to remove ${dirPath}: ${err.message}`);
591
+ return false;
592
+ }
593
+ }
594
+
523
595
  // src/registry/plugins/curdx-flow.ts
524
596
  var PLUGIN_ID5 = "curdx-flow@curdx";
525
597
  var PLUGIN_NAME3 = "curdx-flow";
@@ -528,10 +600,12 @@ var MARKETPLACE_SOURCE4 = "curdx/curdx-flow";
528
600
  var LEGACY_PLUGIN_IDS = ["ralph-specum@curdx-flow", "ralph-specum@smart-ralph"];
529
601
  async function uninstallLegacyIfPresent(ctx) {
530
602
  for (const legacyId of LEGACY_PLUGIN_IDS) {
531
- if (await isPluginInstalled(legacyId)) {
603
+ const installed = await isPluginInstalled(legacyId);
604
+ if (installed) {
532
605
  ctx.log.message(`Removing legacy plugin ${legacyId} (renamed to ${PLUGIN_ID5})\u2026`);
533
606
  await uninstallPluginById(legacyId, ctx);
534
607
  }
608
+ await purgeLegacyPluginArtifacts(legacyId, ctx);
535
609
  }
536
610
  }
537
611
  var curdxFlow = {
@@ -669,15 +743,15 @@ function findPkg(id) {
669
743
  }
670
744
 
671
745
  // src/runner/claudeMd.ts
672
- import { promises as fs2 } from "fs";
673
- import path2 from "path";
674
- import os2 from "os";
746
+ import { promises as fs3 } from "fs";
747
+ import path3 from "path";
748
+ import os3 from "os";
675
749
  import * as p3 from "@clack/prompts";
676
750
  var BEGIN_MARKER = "<!-- BEGIN @curdx/flow v1 -->";
677
751
  var END_MARKER = "<!-- END @curdx/flow v1 -->";
678
752
  var BLOCK_RE = /<!-- BEGIN @curdx\/flow v\d+[^>]*-->[\s\S]*?<!-- END @curdx\/flow v\d+ -->/;
679
753
  function claudeMdPath() {
680
- return path2.join(os2.homedir(), ".claude", "CLAUDE.md");
754
+ return path3.join(os3.homedir(), ".claude", "CLAUDE.md");
681
755
  }
682
756
  function renderItemLine(item) {
683
757
  let line = `- ${item.name}`;
@@ -786,7 +860,7 @@ async function syncClaudeMd(opts) {
786
860
  let existing = "";
787
861
  let existed = true;
788
862
  try {
789
- existing = await fs2.readFile(file, "utf8");
863
+ existing = await fs3.readFile(file, "utf8");
790
864
  } catch (err) {
791
865
  if (err.code === "ENOENT") {
792
866
  existed = false;
@@ -808,10 +882,10 @@ async function syncClaudeMd(opts) {
808
882
  if (next === existing) {
809
883
  return { status: "unchanged", path: file };
810
884
  }
811
- await fs2.mkdir(path2.dirname(file), { recursive: true });
885
+ await fs3.mkdir(path3.dirname(file), { recursive: true });
812
886
  const tmp = `${file}.tmp.${process.pid}`;
813
- await fs2.writeFile(tmp, next, "utf8");
814
- await fs2.rename(tmp, file);
887
+ await fs3.writeFile(tmp, next, "utf8");
888
+ await fs3.rename(tmp, file);
815
889
  if (!existed) return { status: "created", path: file };
816
890
  if (hadBlock && items.length === 0) return { status: "removed", path: file };
817
891
  return { status: "updated", path: file };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@curdx/flow",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Interactive installer for Claude Code plugins and MCP servers",
5
5
  "type": "module",
6
6
  "bin": "./dist/index.mjs",