@curdx/flow 1.1.1

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.
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Global protocol injection — manage a curdx-flow-owned block in ~/.claude/CLAUDE.md.
3
+ *
4
+ * Uses sentinel markers so the block is idempotent (re-install upgrades it)
5
+ * and reversible (uninstall removes it cleanly without touching user content).
6
+ */
7
+
8
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
9
+ import { join, dirname } from "node:path";
10
+
11
+ const HOME = process.env.HOME || "";
12
+ export const GLOBAL_CLAUDE_MD = join(HOME, ".claude", "CLAUDE.md");
13
+
14
+ const SENTINEL_BEGIN =
15
+ "<!-- BEGIN curdx-flow protocols (auto-managed; do not edit between sentinels) -->";
16
+ const SENTINEL_END = "<!-- END curdx-flow protocols -->";
17
+
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
+ `;
43
+
44
+ const FULL_BLOCK = `${SENTINEL_BEGIN}\n${PROTOCOL_BODY.trim()}\n${SENTINEL_END}`;
45
+
46
+ /**
47
+ * Read existing CLAUDE.md content; return "" if missing.
48
+ */
49
+ function readGlobalMd() {
50
+ if (!existsSync(GLOBAL_CLAUDE_MD)) return "";
51
+ return readFileSync(GLOBAL_CLAUDE_MD, "utf-8");
52
+ }
53
+
54
+ /**
55
+ * Locate the sentinel block in the content.
56
+ * Returns { start, end } indices into content, or null if not found.
57
+ */
58
+ function findBlock(content) {
59
+ const start = content.indexOf(SENTINEL_BEGIN);
60
+ if (start === -1) return null;
61
+ const endIdx = content.indexOf(SENTINEL_END, start);
62
+ if (endIdx === -1) return null;
63
+ return { start, end: endIdx + SENTINEL_END.length };
64
+ }
65
+
66
+ /**
67
+ * Inject (or upgrade) the protocol block in ~/.claude/CLAUDE.md.
68
+ * @returns {{action:"created"|"upgraded"|"unchanged", path:string}}
69
+ */
70
+ export function injectGlobalProtocols() {
71
+ const path = GLOBAL_CLAUDE_MD;
72
+ const dir = dirname(path);
73
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
74
+
75
+ const existing = readGlobalMd();
76
+ const block = findBlock(existing);
77
+
78
+ if (!block) {
79
+ // Append — preserve all user content, add a separator if file had content
80
+ const sep = existing.length > 0 && !existing.endsWith("\n\n")
81
+ ? (existing.endsWith("\n") ? "\n" : "\n\n")
82
+ : "";
83
+ const next = existing + sep + FULL_BLOCK + "\n";
84
+ writeFileSync(path, next, "utf-8");
85
+ return { action: existing.length === 0 ? "created" : "created", path };
86
+ }
87
+
88
+ // Replace existing block (handle upgrade-in-place)
89
+ const currentBlock = existing.slice(block.start, block.end);
90
+ if (currentBlock === FULL_BLOCK) {
91
+ return { action: "unchanged", path };
92
+ }
93
+ const next =
94
+ existing.slice(0, block.start) + FULL_BLOCK + existing.slice(block.end);
95
+ writeFileSync(path, next, "utf-8");
96
+ return { action: "upgraded", path };
97
+ }
98
+
99
+ /**
100
+ * Remove the protocol block from ~/.claude/CLAUDE.md.
101
+ * @returns {{action:"removed"|"not-present"|"no-file", path:string}}
102
+ */
103
+ export function removeGlobalProtocols() {
104
+ const path = GLOBAL_CLAUDE_MD;
105
+ if (!existsSync(path)) return { action: "no-file", path };
106
+
107
+ const existing = readGlobalMd();
108
+ const block = findBlock(existing);
109
+ if (!block) return { action: "not-present", path };
110
+
111
+ // Trim leading/trailing whitespace adjacent to the block
112
+ let start = block.start;
113
+ let end = block.end;
114
+ // Eat one trailing newline if present
115
+ if (existing[end] === "\n") end++;
116
+ // Eat one leading newline if previous char is also newline (collapse blank lines)
117
+ if (start > 0 && existing[start - 1] === "\n" && existing[start - 2] === "\n") {
118
+ start--;
119
+ }
120
+
121
+ const next = existing.slice(0, start) + existing.slice(end);
122
+ writeFileSync(path, next, "utf-8");
123
+ return { action: "removed", path };
124
+ }
@@ -0,0 +1,238 @@
1
+ /**
2
+ * uninstall command — remove curdx-flow plugin (and optionally recommended plugins / artifacts).
3
+ */
4
+
5
+ import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
6
+ import { join } from "node:path";
7
+
8
+ import {
9
+ color,
10
+ log,
11
+ run,
12
+ confirm,
13
+ multiSelect,
14
+ claudeVersion,
15
+ listPlugins,
16
+ } from "./utils.js";
17
+ import { removeGlobalProtocols, GLOBAL_CLAUDE_MD } from "./protocols.js";
18
+
19
+ const HOME = process.env.HOME || "";
20
+
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
+ ];
30
+
31
+ // Symlinks created by install.js (only cleaned with --purge)
32
+ const MANAGED_SYMLINKS = [
33
+ join(HOME, ".local", "bin", "bun"),
34
+ join(HOME, ".local", "bin", "uv"),
35
+ ];
36
+
37
+ export async function uninstall(args = []) {
38
+ const yes = args.includes("--yes") || args.includes("-y");
39
+ const purge = args.includes("--purge");
40
+ const keepRecommended = args.includes("--keep-recommended");
41
+
42
+ log.title("🗑️ CurDX-Flow Uninstaller");
43
+
44
+ // ---------- Step 1: claude CLI present? ----------
45
+ const cv = claudeVersion();
46
+ if (!cv) {
47
+ log.err("claude CLI not found, cannot uninstall plugin.");
48
+ process.exit(1);
49
+ }
50
+
51
+ // ---------- Step 2: confirmation ----------
52
+ if (!yes) {
53
+ const ok = await confirm(
54
+ `This will uninstall the ${color.bold("curdx-flow")} plugin. Continue?`,
55
+ false
56
+ );
57
+ if (!ok) {
58
+ log.info("Cancelled");
59
+ return;
60
+ }
61
+ }
62
+
63
+ // ---------- Step 3: uninstall curdx-flow plugin ----------
64
+ log.blank();
65
+ log.step(1, 4, "Uninstalling curdx-flow plugin...");
66
+ const installed = listPlugins();
67
+ const curdx = installed.find((p) => p.name === "curdx-flow");
68
+ if (!curdx) {
69
+ log.info("curdx-flow not installed, skipping");
70
+ } else {
71
+ const r = await run(
72
+ "claude",
73
+ ["plugin", "uninstall", "curdx-flow@curdx-flow-marketplace"],
74
+ { silent: true }
75
+ );
76
+ if (r.code === 0) {
77
+ log.ok("curdx-flow uninstalled");
78
+ } else {
79
+ log.err(`Uninstall failed: ${r.stderr.trim() || r.stdout.trim()}`);
80
+ }
81
+ }
82
+
83
+ // ---------- Step 4: optionally uninstall recommended ----------
84
+ log.blank();
85
+ log.step(2, 4, "Recommended plugins");
86
+ if (keepRecommended) {
87
+ log.info("Keeping recommended plugins (--keep-recommended)");
88
+ } else {
89
+ const currentlyInstalled = listPlugins();
90
+ const presentRecs = RECOMMENDED.filter((r) =>
91
+ currentlyInstalled.find((p) => p.name === r.name)
92
+ );
93
+
94
+ if (presentRecs.length === 0) {
95
+ log.info("No installed recommended plugins");
96
+ } else {
97
+ let toRemove;
98
+ if (yes) {
99
+ toRemove = []; // Default to none — user must opt in explicitly
100
+ log.info(
101
+ color.dim("--yes mode: keeping recommended plugins (use --purge to remove them)")
102
+ );
103
+ } else {
104
+ const choices = presentRecs.map((r) => ({
105
+ label: color.bold(r.name),
106
+ value: r.name,
107
+ hint: "",
108
+ }));
109
+ toRemove = await multiSelect(
110
+ "Which recommended plugins to also uninstall? (default: none)",
111
+ choices,
112
+ [] // default: nothing checked
113
+ );
114
+ }
115
+
116
+ for (const name of toRemove) {
117
+ const rec = presentRecs.find((r) => r.name === name);
118
+ log.blank();
119
+ console.log(` ${color.cyan("⏳")} Uninstalling ${color.bold(rec.name)}...`);
120
+ const r = await run(
121
+ "claude",
122
+ ["plugin", "uninstall", rec.uninstallSpec],
123
+ { silent: true }
124
+ );
125
+ if (r.code === 0) {
126
+ console.log(` ${color.green("✓")} ${rec.name} uninstalled`);
127
+ } else {
128
+ console.log(
129
+ ` ${color.red("✗")} ${rec.name} uninstall failed: ${r.stderr.trim().split("\n").pop()}`
130
+ );
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ // ---------- Step 5: cleanup symlinks (only with --purge) ----------
137
+ log.blank();
138
+ log.step(3, 4, "Runtime symlinks");
139
+ if (!purge) {
140
+ log.info(
141
+ color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
142
+ );
143
+ log.info(
144
+ color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
145
+ );
146
+ } else {
147
+ for (const link of MANAGED_SYMLINKS) {
148
+ if (!existsSync(link) && !isBrokenSymlink(link)) {
149
+ continue;
150
+ }
151
+ try {
152
+ const stat = lstatSync(link);
153
+ if (!stat.isSymbolicLink()) {
154
+ log.warn(
155
+ `${link} is not a symlink (likely a real file placed by the user), skipping`
156
+ );
157
+ continue;
158
+ }
159
+ const target = readlinkSync(link);
160
+ unlinkSync(link);
161
+ log.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
162
+ } catch (err) {
163
+ log.warn(`Failed to remove ${link}: ${err.message}`);
164
+ }
165
+ }
166
+ }
167
+
168
+ // ---------- Step 5.5: remove global protocols block ----------
169
+ log.blank();
170
+ console.log(color.dim("Removing global protocols from ~/.claude/CLAUDE.md..."));
171
+ try {
172
+ const r = removeGlobalProtocols();
173
+ if (r.action === "removed") {
174
+ log.ok(`Global protocols removed ${color.dim(`(${GLOBAL_CLAUDE_MD})`)}`);
175
+ } else if (r.action === "not-present") {
176
+ log.info(`Global protocols not present, skipping`);
177
+ } else {
178
+ log.info(`~/.claude/CLAUDE.md does not exist, skipping`);
179
+ }
180
+ } catch (err) {
181
+ log.warn(`Protocol removal failed: ${err.message}`);
182
+ }
183
+
184
+ // ---------- Step 6: project .flow/ ----------
185
+ log.blank();
186
+ log.step(4, 4, "Project state directory");
187
+ const flowDir = join(process.cwd(), ".flow");
188
+ if (!existsSync(flowDir)) {
189
+ log.info(".flow/ does not exist, skipping");
190
+ } else if (yes) {
191
+ log.info(
192
+ color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
193
+ );
194
+ } else {
195
+ const ok = await confirm(
196
+ `${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
197
+ false
198
+ );
199
+ if (ok) {
200
+ try {
201
+ rmSync(flowDir, { recursive: true, force: true });
202
+ log.ok(`Removed ${flowDir}`);
203
+ } catch (err) {
204
+ log.err(`Removal failed: ${err.message}`);
205
+ }
206
+ } else {
207
+ log.info("Keeping .flow/");
208
+ }
209
+ }
210
+
211
+ // ---------- Summary ----------
212
+ log.blank();
213
+ console.log(color.bold("✅ Uninstall complete"));
214
+ if (!purge) {
215
+ console.log(
216
+ color.dim(
217
+ `\nArtifacts kept:\n` +
218
+ ` - ~/.local/bin/bun, ~/.local/bin/uv (symlinks; use --purge to remove)\n` +
219
+ ` - bun/uv binaries themselves (~/.bun/bin/bun, ~/.local/bin/uv real installs)\n` +
220
+ ` - claude-mem data (~/.claude-mem/)\n` +
221
+ ` - claude marketplace cache`
222
+ )
223
+ );
224
+ console.log(
225
+ color.dim(
226
+ `\nFully purge: ${color.cyan("curdx-flow uninstall --purge")}`
227
+ )
228
+ );
229
+ }
230
+ }
231
+
232
+ function isBrokenSymlink(p) {
233
+ try {
234
+ return lstatSync(p).isSymbolicLink();
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
package/cli/upgrade.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * upgrade command — update curdx-flow + recommended plugins to latest.
3
+ */
4
+
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
+ ];
13
+
14
+ export async function upgrade(args = []) {
15
+ log.title("⬆️ CurDX-Flow upgrade");
16
+
17
+ if (!claudeVersion()) {
18
+ log.err("claude CLI not found");
19
+ process.exit(1);
20
+ }
21
+
22
+ // Refresh marketplaces first
23
+ log.step(1, 2, "Refreshing marketplaces...");
24
+ const marketplaces = ["curdx-flow-marketplace", "pua", "thedotmack"];
25
+ for (const mp of marketplaces) {
26
+ const r = await run(
27
+ "claude",
28
+ ["plugin", "marketplace", "update", mp],
29
+ { silent: true }
30
+ );
31
+ if (r.code === 0) {
32
+ log.ok(` ${mp}`);
33
+ } else {
34
+ // Not a fatal — might not be added
35
+ if (!r.stderr.includes("not found")) {
36
+ log.warn(` ${mp}: ${r.stderr.trim().split("\n").pop()}`);
37
+ }
38
+ }
39
+ }
40
+
41
+ // Update each plugin
42
+ log.blank();
43
+ log.step(2, 2, "Updating installed plugins...");
44
+ const installed = listPlugins();
45
+ const installedNames = new Set(installed.map((p) => p.name));
46
+
47
+ for (const spec of PLUGINS_TO_UPDATE) {
48
+ const pluginName = spec.split("@")[0];
49
+ if (!installedNames.has(pluginName)) {
50
+ log.info(` ${pluginName.padEnd(22)} not installed, skipping`);
51
+ continue;
52
+ }
53
+
54
+ const r = await run("claude", ["plugin", "update", spec], { silent: true });
55
+ if (r.code === 0) {
56
+ const updated = r.stdout.includes("updated from");
57
+ if (updated) {
58
+ const m = r.stdout.match(/updated from (\S+) to (\S+)/);
59
+ if (m) {
60
+ log.ok(` ${pluginName.padEnd(22)} ${color.dim(`${m[1]} → ${m[2]}`)}`);
61
+ } else {
62
+ log.ok(` ${pluginName.padEnd(22)} updated`);
63
+ }
64
+ } else {
65
+ log.info(` ${pluginName.padEnd(22)} already up to date`);
66
+ }
67
+ } else {
68
+ log.warn(` ${pluginName.padEnd(22)} ${r.stderr.trim().split("\n").pop()}`);
69
+ }
70
+ }
71
+
72
+ log.blank();
73
+ log.ok("Upgrade complete");
74
+ console.log(color.dim(" Some changes require a Claude Code restart to take effect"));
75
+ }