@curdx/flow 2.0.0-beta.8 → 2.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/cli/protocols.js CHANGED
@@ -5,43 +5,41 @@
5
5
  * and reversible (uninstall removes it cleanly without touching user content).
6
6
  */
7
7
 
8
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
8
+ import {
9
+ readFileSync,
10
+ writeFileSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ renameSync,
14
+ unlinkSync,
15
+ } from "node:fs";
9
16
  import { join, dirname } from "node:path";
10
-
11
- const HOME = process.env.HOME || "";
17
+ import { homedir } from "node:os";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ // Use os.homedir() instead of process.env.HOME — HOME can be empty inside
21
+ // non-login shells (CI containers, some spawned child envs), which would
22
+ // resolve GLOBAL_CLAUDE_MD to "/.claude/CLAUDE.md" (filesystem root) and
23
+ // cause mkdir/writeFileSync to fail with EACCES. homedir() falls back to
24
+ // the effective user's passwd entry on POSIX and USERPROFILE on Windows.
25
+ const HOME = homedir();
12
26
  export const GLOBAL_CLAUDE_MD = join(HOME, ".claude", "CLAUDE.md");
13
27
 
14
28
  const SENTINEL_BEGIN =
15
29
  "<!-- BEGIN curdx-flow protocols (auto-managed; do not edit between sentinels) -->";
16
30
  const SENTINEL_END = "<!-- END curdx-flow protocols -->";
17
31
 
18
- // Protocol block is itself in English it's instructions for the model,
19
- // not user-facing prose. User chat output is still Chinese per the rule below.
20
- const PROTOCOL_BODY = `
21
- ## Global Protocols (curdx-flow)
22
-
23
- All operations MUST strictly follow these system constraints:
24
-
25
- ### Language separation
26
- - **Tool / persistence layer = English**: commit messages, code, comments, file names, function names, PR descriptions, CLI log output, error messages thrown by code, and any artifact persisted to the repository or shown in a developer terminal.
27
- - **Conversational layer = Simplified Chinese**: chat replies, explanations and reasoning shown directly to the human in a conversation interface (e.g. Claude Code chat).
28
-
29
- Rationale: English in the persistence/tool layer aligns with developer-tool industry norms (npm/git/cargo are all English) and keeps the codebase internationally collaborable. Chinese in the conversational layer matches the user's language preference. Mixing the two (e.g. Chinese commit messages, Chinese CLI log output) is a violation.
30
-
31
- ### Discovery & reasoning
32
- - **Library / framework / API questions**: query \`context7\` MCP first. Do not rely on training memory.
33
- - **Planning / design / architecture review / epic decomposition**: use \`sequential-thinking\` MCP with at least 5 thoughts.
34
- - **Cross-session memory**: query \`claude-mem\` MCP at task start when available.
35
-
36
- ### Three red lines (inherited from pua)
37
- 1. **Closed loop**: claiming "done"? Provide evidence (build output / passing tests / curl result).
38
- 2. **Fact-driven**: before saying "probably an env issue", verify it. Unverified attribution = blame-shifting.
39
- 3. **Exhaust everything**: before saying "I cannot", complete the systematic 4-stage debugging.
40
-
41
- > Source: curdx-flow CLI installer (\`curdx-flow install\`). Remove with: \`curdx-flow uninstall --purge\`.
42
- `;
32
+ // Protocol body lives in a sibling markdown file so it keeps markdown tooling
33
+ // (preview, lint, prettier) and avoids backtick-escaping noise inside a JS
34
+ // template literal. The body itself is English — it's instructions for the
35
+ // model, not user-facing prose.
36
+ const __dirname = dirname(fileURLToPath(import.meta.url));
37
+ const PROTOCOL_BODY = readFileSync(
38
+ join(__dirname, "protocols-body.md"),
39
+ "utf-8"
40
+ ).trim();
43
41
 
44
- const FULL_BLOCK = `${SENTINEL_BEGIN}\n${PROTOCOL_BODY.trim()}\n${SENTINEL_END}`;
42
+ const FULL_BLOCK = `${SENTINEL_BEGIN}\n${PROTOCOL_BODY}\n${SENTINEL_END}`;
45
43
 
46
44
  /**
47
45
  * Read existing CLAUDE.md content; return "" if missing.
@@ -53,16 +51,56 @@ function readGlobalMd() {
53
51
 
54
52
  /**
55
53
  * Locate the sentinel block in the content.
56
- * Returns { start, end } indices into content, or null if not found.
54
+ * Returns { start, end } indices into content, `null` if neither sentinel is
55
+ * present, or throws if the block is corrupted (begin without matching end).
56
+ * The throw is intentional — previously the corrupted case silently returned
57
+ * null, so the next run would append a SECOND block, producing drift.
57
58
  */
58
59
  function findBlock(content) {
59
60
  const start = content.indexOf(SENTINEL_BEGIN);
60
- if (start === -1) return null;
61
+ if (start === -1) {
62
+ // Also check for a dangling END without BEGIN — that is also corrupted.
63
+ if (content.indexOf(SENTINEL_END) !== -1) {
64
+ throw new Error(
65
+ `Corrupted protocol block in ${GLOBAL_CLAUDE_MD}: END sentinel found without BEGIN. ` +
66
+ `Manually inspect the file and remove the dangling END line, then re-run.`
67
+ );
68
+ }
69
+ return null;
70
+ }
61
71
  const endIdx = content.indexOf(SENTINEL_END, start);
62
- if (endIdx === -1) return null;
72
+ if (endIdx === -1) {
73
+ throw new Error(
74
+ `Corrupted protocol block in ${GLOBAL_CLAUDE_MD}: BEGIN sentinel found without END. ` +
75
+ `Manually remove the orphan BEGIN line (or restore the END), then re-run.`
76
+ );
77
+ }
63
78
  return { start, end: endIdx + SENTINEL_END.length };
64
79
  }
65
80
 
81
+ /**
82
+ * Write `content` to `path` atomically: write to a sibling temp file first,
83
+ * then rename. This prevents a half-written CLAUDE.md if the process is
84
+ * interrupted mid-write, and avoids races between concurrent install /
85
+ * uninstall invocations.
86
+ */
87
+ function atomicWrite(path, content) {
88
+ const tmp = `${path}.curdx-flow.tmp.${process.pid}`;
89
+ try {
90
+ writeFileSync(tmp, content, "utf-8");
91
+ renameSync(tmp, path);
92
+ } catch (err) {
93
+ // Best-effort cleanup of the temp file; swallow errors here since we
94
+ // are already re-throwing the real failure.
95
+ try {
96
+ if (existsSync(tmp)) unlinkSync(tmp);
97
+ } catch {
98
+ // ignore
99
+ }
100
+ throw err;
101
+ }
102
+ }
103
+
66
104
  /**
67
105
  * Inject (or upgrade) the protocol block in ~/.claude/CLAUDE.md.
68
106
  * @returns {{action:"created"|"upgraded"|"unchanged", path:string}}
@@ -81,8 +119,8 @@ export function injectGlobalProtocols() {
81
119
  ? (existing.endsWith("\n") ? "\n" : "\n\n")
82
120
  : "";
83
121
  const next = existing + sep + FULL_BLOCK + "\n";
84
- writeFileSync(path, next, "utf-8");
85
- return { action: existing.length === 0 ? "created" : "created", path };
122
+ atomicWrite(path, next);
123
+ return { action: existing.length === 0 ? "created" : "appended", path };
86
124
  }
87
125
 
88
126
  // Replace existing block (handle upgrade-in-place)
@@ -92,7 +130,7 @@ export function injectGlobalProtocols() {
92
130
  }
93
131
  const next =
94
132
  existing.slice(0, block.start) + FULL_BLOCK + existing.slice(block.end);
95
- writeFileSync(path, next, "utf-8");
133
+ atomicWrite(path, next);
96
134
  return { action: "upgraded", path };
97
135
  }
98
136
 
@@ -119,6 +157,6 @@ export function removeGlobalProtocols() {
119
157
  }
120
158
 
121
159
  const next = existing.slice(0, start) + existing.slice(end);
122
- writeFileSync(path, next, "utf-8");
160
+ atomicWrite(path, next);
123
161
  return { action: "removed", path };
124
162
  }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Single source of truth for recommended companion plugins.
3
+ *
4
+ * Background: before this file existed, the list of recommended plugins lived
5
+ * in FOUR independent places (install.js, uninstall.js, upgrade.js,
6
+ * doctor.js). They drifted — chrome-devtools-mcp was added to install.js
7
+ * during the beta.8 MCP decoupling but forgotten in the other three,
8
+ * making it installable but uninstallable. This registry exists so adding
9
+ * or removing a plugin is a one-file change.
10
+ *
11
+ * Every consumer pulls what it needs via property access:
12
+ * - install.js → marketplaceSource + installSpec + hint (+ optional postInstall)
13
+ * - uninstall.js → uninstallSpec + uninstallArgs + marketplaceId
14
+ * - upgrade.js → updateSpec + marketplaceId
15
+ * - doctor.js → id + installSpec (for health checks and recovery hints)
16
+ */
17
+
18
+ export const RECOMMENDED_PLUGINS = [
19
+ {
20
+ name: "pua",
21
+ id: "pua@pua-skills",
22
+ marketplaceSource: "tanweai/pua",
23
+ marketplaceId: "pua-skills",
24
+ installSpec: "pua@pua-skills",
25
+ uninstallSpec: "pua@pua-skills",
26
+ updateSpec: "pua@pua-skills",
27
+ scope: "user",
28
+ hint: "no-give-up + three red lines",
29
+ },
30
+ {
31
+ name: "claude-mem",
32
+ id: "claude-mem@thedotmack",
33
+ marketplaceSource: "thedotmack/claude-mem",
34
+ marketplaceId: "thedotmack",
35
+ installSpec: "claude-mem@thedotmack",
36
+ uninstallSpec: "claude-mem@thedotmack",
37
+ updateSpec: "claude-mem@thedotmack",
38
+ uninstallArgs: ["--keep-data"],
39
+ scope: "user",
40
+ hint: "automatic cross-session memory",
41
+ postInstall: "claude-mem-runtimes",
42
+ },
43
+ {
44
+ name: "frontend-design",
45
+ id: "frontend-design@claude-plugins-official",
46
+ marketplaceSource: "anthropics/claude-plugins-official",
47
+ marketplaceId: "claude-plugins-official",
48
+ installSpec: "frontend-design@claude-plugins-official",
49
+ uninstallSpec: "frontend-design@claude-plugins-official",
50
+ updateSpec: "frontend-design@claude-plugins-official",
51
+ scope: "user",
52
+ hint: "Anthropic official UI skill",
53
+ },
54
+ {
55
+ name: "chrome-devtools-mcp",
56
+ id: "chrome-devtools-mcp@chrome-devtools-plugins",
57
+ marketplaceSource: "ChromeDevTools/chrome-devtools-mcp",
58
+ marketplaceId: "chrome-devtools-plugins",
59
+ installSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
60
+ uninstallSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
61
+ updateSpec: "chrome-devtools-mcp@chrome-devtools-plugins",
62
+ scope: "user",
63
+ hint: "Chrome DevTools + Puppeteer (Google official)",
64
+ },
65
+ ];
66
+
67
+ export const REQUIRED_PLUGINS = [
68
+ {
69
+ name: "context7-plugin",
70
+ id: "context7-plugin@context7-marketplace",
71
+ marketplaceSource: "upstash/context7",
72
+ marketplaceId: "context7-marketplace",
73
+ installSpec: "context7-plugin@context7-marketplace",
74
+ uninstallSpec: "context7-plugin@context7-marketplace",
75
+ updateSpec: "context7-plugin@context7-marketplace",
76
+ scope: "user",
77
+ hint: "official Context7 plugin (MCP + skill + docs-researcher agent + /context7:docs)",
78
+ requiresConfig: true,
79
+ configType: "apiKey",
80
+ },
81
+ ];
82
+
83
+ /**
84
+ * MCP servers that curdx-flow depends on for its core discipline rules and
85
+ * still registers directly. Starting beta.12 these are registered at
86
+ * USER-LEVEL via `claude mcp add` instead of plugin.json bundling, so:
87
+ *
88
+ * - Tool names stay standard (mcp__sequential-thinking__*)
89
+ * — matching every agent's and knowledge doc's hardcoded references.
90
+ *
91
+ * Context7 is installed via Upstash's official Claude Code plugin instead
92
+ * of direct `claude mcp add`: context7-plugin@context7-marketplace includes
93
+ * the MCP server, skill, docs-researcher agent, and /context7:docs command.
94
+ */
95
+ export const BUNDLED_MCPS = [
96
+ {
97
+ name: "sequential-thinking",
98
+ command: "npx",
99
+ args: ["-y", "@modelcontextprotocol/server-sequential-thinking"],
100
+ purpose: "structured reasoning for design / review (L2 Mandatory Tool)",
101
+ preserveExisting: true,
102
+ },
103
+ ];
104
+
105
+ /**
106
+ * Marketplaces to refresh during `upgrade`. Derived from RECOMMENDED_PLUGINS
107
+ * plus the curdx-flow marketplace itself.
108
+ */
109
+ export const MARKETPLACES_TO_REFRESH = [
110
+ "curdx-flow-marketplace",
111
+ ...REQUIRED_PLUGINS.map((p) => p.marketplaceId),
112
+ ...RECOMMENDED_PLUGINS.map((p) => p.marketplaceId),
113
+ ];
114
+
115
+ /**
116
+ * Plugin install specs to update during `upgrade` — includes curdx-flow
117
+ * itself plus every recommended plugin.
118
+ */
119
+ export const PLUGINS_TO_UPDATE = [
120
+ "curdx-flow@curdx-flow-marketplace",
121
+ ...REQUIRED_PLUGINS.map((p) => p.updateSpec),
122
+ ...RECOMMENDED_PLUGINS.map((p) => p.updateSpec),
123
+ ];
package/cli/uninstall.js CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
6
6
  import { join } from "node:path";
7
+ import { homedir } from "node:os";
7
8
 
8
9
  import {
9
10
  color,
@@ -15,18 +16,25 @@ import {
15
16
  listPlugins,
16
17
  } from "./utils.js";
17
18
  import { removeGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
19
+ import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
18
20
 
19
- const HOME = process.env.HOME || "";
21
+ const HOME = homedir();
20
22
 
21
- // Keep aligned with install.js
22
- const RECOMMENDED = [
23
- { name: "pua", uninstallSpec: "pua@pua-skills" },
24
- { name: "claude-mem", uninstallSpec: "claude-mem@thedotmack" },
25
- {
26
- name: "frontend-design",
27
- uninstallSpec: "frontend-design@claude-plugins-official",
28
- },
29
- ];
23
+ // Pull uninstall-relevant subset from the single registry. See registry.js.
24
+ const RECOMMENDED = RECOMMENDED_PLUGINS.map(({ name, uninstallSpec, uninstallArgs, marketplaceId, scope }) => ({
25
+ name,
26
+ uninstallSpec,
27
+ uninstallArgs: uninstallArgs || [],
28
+ marketplaceId,
29
+ scope,
30
+ }));
31
+ const REQUIRED = REQUIRED_PLUGINS.map(({ name, uninstallSpec, uninstallArgs, marketplaceId, scope }) => ({
32
+ name,
33
+ uninstallSpec,
34
+ uninstallArgs: uninstallArgs || [],
35
+ marketplaceId,
36
+ scope,
37
+ }));
30
38
 
31
39
  // Symlinks created by install.js (only cleaned with --purge)
32
40
  const MANAGED_SYMLINKS = [
@@ -70,7 +78,7 @@ export async function uninstall(args = []) {
70
78
  } else {
71
79
  const r = await run(
72
80
  "claude",
73
- ["plugin", "uninstall", "curdx-flow@curdx-flow-marketplace"],
81
+ ["plugin", "uninstall", "--scope", "user", "curdx-flow@curdx-flow-marketplace"],
74
82
  { silent: true }
75
83
  );
76
84
  if (r.code === 0) {
@@ -116,10 +124,10 @@ export async function uninstall(args = []) {
116
124
  for (const name of toRemove) {
117
125
  const rec = presentRecs.find((r) => r.name === name);
118
126
  log.blank();
119
- console.log(` ${color.cyan("")} Uninstalling ${color.bold(rec.name)}...`);
127
+ console.log(` ${color.cyan("")} Uninstalling ${color.bold(rec.name)}...`);
120
128
  const r = await run(
121
129
  "claude",
122
- ["plugin", "uninstall", rec.uninstallSpec],
130
+ ["plugin", "uninstall", "--scope", rec.scope, ...rec.uninstallArgs, rec.uninstallSpec],
123
131
  { silent: true }
124
132
  );
125
133
  if (r.code === 0) {
@@ -133,9 +141,71 @@ export async function uninstall(args = []) {
133
141
  }
134
142
  }
135
143
 
144
+ // ---------- Step 4.5: optionally remove user-level MCPs ----------
145
+ // Starting beta.12, the install command registers context7 +
146
+ // sequential-thinking at user-level (not plugin-bundled). Ask before
147
+ // removing because the user may have customised args (e.g. --api-key)
148
+ // or still be using these MCPs outside curdx-flow.
149
+ log.blank();
150
+ log.info("Required MCP servers (context7, sequential-thinking)");
151
+ if (keepRecommended || yes) {
152
+ log.info(
153
+ color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
154
+ );
155
+ } else {
156
+ const removeMcps = await confirm(
157
+ `Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((m) => m.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
158
+ false
159
+ );
160
+ if (removeMcps) {
161
+ for (const mcp of BUNDLED_MCPS) {
162
+ const r = await run("claude", ["mcp", "remove", mcp.name], {
163
+ silent: true,
164
+ });
165
+ if (r.code === 0) {
166
+ log.ok(` ${mcp.name.padEnd(22)} removed`);
167
+ } else {
168
+ log.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
169
+ }
170
+ }
171
+ } else {
172
+ log.info("Keeping user-level MCPs");
173
+ }
174
+ }
175
+
176
+ // ---------- Step 4.75: uninstall required companion plugins ----------
177
+ log.blank();
178
+ log.info("Required companion plugins");
179
+ if (yes) {
180
+ log.info(
181
+ color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
182
+ );
183
+ } else {
184
+ const removeRequired = await confirm(
185
+ `Remove required companion plugins (${REQUIRED.map((p) => p.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
186
+ false
187
+ );
188
+ if (removeRequired) {
189
+ for (const plugin of REQUIRED) {
190
+ const r = await run(
191
+ "claude",
192
+ ["plugin", "uninstall", "--scope", plugin.scope, ...plugin.uninstallArgs, plugin.uninstallSpec],
193
+ { silent: true }
194
+ );
195
+ if (r.code === 0) {
196
+ log.ok(` ${plugin.name.padEnd(22)} uninstalled`);
197
+ } else {
198
+ log.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
199
+ }
200
+ }
201
+ } else {
202
+ log.info("Keeping required companion plugins");
203
+ }
204
+ }
205
+
136
206
  // ---------- Step 5: cleanup symlinks (only with --purge) ----------
137
207
  log.blank();
138
- log.step(3, 4, "Runtime symlinks");
208
+ log.step(3, 4, "Runtime symlinks and marketplaces");
139
209
  if (!purge) {
140
210
  log.info(
141
211
  color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
@@ -144,6 +214,27 @@ export async function uninstall(args = []) {
144
214
  color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
145
215
  );
146
216
  } else {
217
+ const marketplaceIds = [
218
+ ...new Set(
219
+ RECOMMENDED
220
+ .concat(REQUIRED)
221
+ .map((r) => r.marketplaceId)
222
+ .filter((id) => id && id !== "claude-plugins-official")
223
+ ),
224
+ ];
225
+ for (const marketplaceId of marketplaceIds) {
226
+ const r = await run(
227
+ "claude",
228
+ ["plugin", "marketplace", "remove", marketplaceId],
229
+ { silent: true }
230
+ );
231
+ if (r.code === 0) {
232
+ log.ok(`Removed marketplace ${marketplaceId}`);
233
+ } else if (!r.stderr.includes("not found")) {
234
+ log.warn(`Failed to remove marketplace ${marketplaceId}: ${r.stderr.trim().split("\n").pop()}`);
235
+ }
236
+ }
237
+
147
238
  for (const link of MANAGED_SYMLINKS) {
148
239
  if (!existsSync(link) && !isBrokenSymlink(link)) {
149
240
  continue;
package/cli/upgrade.js CHANGED
@@ -3,13 +3,10 @@
3
3
  */
4
4
 
5
5
  import { color, log, run, listPlugins, claudeVersion } from "./utils.js";
6
-
7
- const PLUGINS_TO_UPDATE = [
8
- "curdx-flow@curdx-flow-marketplace",
9
- "pua@pua-skills",
10
- "claude-mem@thedotmack",
11
- "frontend-design@claude-plugins-official",
12
- ];
6
+ import {
7
+ PLUGINS_TO_UPDATE,
8
+ MARKETPLACES_TO_REFRESH,
9
+ } from "./registry.js";
13
10
 
14
11
  export async function upgrade(args = []) {
15
12
  log.title("⬆️ CurDX-Flow upgrade");
@@ -19,10 +16,9 @@ export async function upgrade(args = []) {
19
16
  process.exit(1);
20
17
  }
21
18
 
22
- // Refresh marketplaces first
19
+ // Refresh marketplaces first (derived from cli/registry.js)
23
20
  log.step(1, 2, "Refreshing marketplaces...");
24
- const marketplaces = ["curdx-flow-marketplace", "pua", "thedotmack"];
25
- for (const mp of marketplaces) {
21
+ for (const mp of MARKETPLACES_TO_REFRESH) {
26
22
  const r = await run(
27
23
  "claude",
28
24
  ["plugin", "marketplace", "update", mp],
@@ -51,7 +47,7 @@ export async function upgrade(args = []) {
51
47
  continue;
52
48
  }
53
49
 
54
- const r = await run("claude", ["plugin", "update", spec], { silent: true });
50
+ const r = await run("claude", ["plugin", "update", "--scope", "user", spec], { silent: true });
55
51
  if (r.code === 0) {
56
52
  const updated = r.stdout.includes("updated from");
57
53
  if (updated) {