@curdx/flow 2.2.0 → 2.2.4
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +19 -2
- package/README.md +15 -8
- package/README.zh.md +5 -3
- package/agent-preamble/preamble.md +33 -0
- package/agents/flow-adversary.md +1 -1
- package/agents/flow-architect.md +2 -1
- package/agents/flow-brownfield-analyst.md +153 -0
- package/agents/flow-debugger.md +6 -11
- package/agents/flow-edge-hunter.md +1 -1
- package/agents/flow-executor.md +30 -8
- package/agents/flow-planner.md +38 -5
- package/agents/flow-product-designer.md +2 -1
- package/agents/flow-qa-engineer.md +9 -5
- package/agents/flow-researcher.md +2 -1
- package/agents/flow-reviewer.md +23 -5
- package/agents/flow-security-auditor.md +5 -3
- package/agents/flow-triage-analyst.md +5 -24
- package/agents/flow-ui-researcher.md +4 -3
- package/agents/flow-ux-designer.md +12 -39
- package/agents/flow-verifier.md +35 -3
- package/cli/README.md +3 -1
- package/cli/doctor-workflow.js +165 -2
- package/cli/doctor.js +8 -0
- package/cli/help.js +2 -0
- package/cli/lib/doctor-claude-settings.js +736 -0
- package/cli/lib/doctor-report.js +256 -1
- package/cli/lib/doctor-runtime-environment.js +196 -0
- package/cli/lib/frontmatter.js +44 -0
- package/cli/lib/json-schema.js +57 -0
- package/cli/lib/runtime.js +20 -2
- package/cli/lib/semver.js +14 -0
- package/cli/uninstall-actions.js +323 -0
- package/cli/uninstall.js +9 -253
- package/cli/utils.js +6 -1
- package/gates/adversarial-review-gate.md +1 -1
- package/gates/security-gate.md +2 -2
- package/gates/test-quality-gate.md +59 -0
- package/hooks/hooks.json +16 -2
- package/hooks/scripts/common.sh +4 -0
- package/hooks/scripts/session-start.sh +17 -2
- package/hooks/scripts/stop-watcher.sh +69 -18
- package/hooks/scripts/subagent-artifact-guard.sh +159 -0
- package/hooks/scripts/subagent-statusline.sh +105 -0
- package/knowledge/atomic-commits.md +1 -1
- package/knowledge/claude-code-runtime-contracts.md +203 -0
- package/knowledge/epic-decomposition.md +1 -1
- package/knowledge/execution-strategies.md +23 -1
- package/knowledge/planning-reviews.md +2 -2
- package/knowledge/poc-first-workflow.md +8 -8
- package/knowledge/review-feedback-intake.md +57 -0
- package/knowledge/two-stage-review.md +19 -6
- package/knowledge/wave-execution.md +16 -1
- package/output-styles/curdx-evidence-first.md +34 -0
- package/package.json +7 -1
- package/schemas/agent-frontmatter.schema.json +0 -7
- package/schemas/config.schema.json +14 -0
- package/schemas/hooks.schema.json +34 -2
- package/schemas/output-style-frontmatter.schema.json +22 -0
- package/schemas/plugin-manifest.schema.json +387 -17
- package/schemas/plugin-settings.schema.json +29 -0
- package/schemas/skill-frontmatter.schema.json +109 -4
- package/schemas/spec-state.schema.json +29 -4
- package/settings.json +6 -0
- package/skills/brownfield-index/SKILL.md +31 -35
- package/skills/browser-qa/SKILL.md +11 -3
- package/skills/cancel/SKILL.md +82 -0
- package/skills/debug/SKILL.md +6 -2
- package/skills/epic/SKILL.md +5 -3
- package/skills/fast/SKILL.md +1 -0
- package/skills/help/SKILL.md +17 -7
- package/skills/implement/SKILL.md +38 -7
- package/skills/init/SKILL.md +2 -1
- package/skills/review/SKILL.md +4 -1
- package/skills/security-audit/SKILL.md +17 -3
- package/skills/spec/SKILL.md +2 -1
- package/skills/start/SKILL.md +18 -18
- package/skills/status/SKILL.md +85 -0
- package/skills/ui-sketch/SKILL.md +11 -3
- package/skills/verify/SKILL.md +13 -1
- package/templates/config.json.tmpl +4 -1
- package/templates/progress.md.tmpl +19 -0
- package/templates/tasks.md.tmpl +26 -3
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { existsSync, lstatSync, unlinkSync, rmSync, readlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
|
|
6
|
+
import {
|
|
7
|
+
removeMcp,
|
|
8
|
+
removePluginMarketplace,
|
|
9
|
+
uninstallPlugin,
|
|
10
|
+
} from "./lib/claude-ops.js";
|
|
11
|
+
import {
|
|
12
|
+
confirm,
|
|
13
|
+
color,
|
|
14
|
+
listPlugins,
|
|
15
|
+
log,
|
|
16
|
+
resultLastLine,
|
|
17
|
+
resultOutput,
|
|
18
|
+
} from "./utils.js";
|
|
19
|
+
import {
|
|
20
|
+
UNINSTALL_STEP_COUNT,
|
|
21
|
+
getInstalledTargets,
|
|
22
|
+
getManagedMarketplaceIds,
|
|
23
|
+
selectRecommendedPluginsToRemove,
|
|
24
|
+
shouldKeepBundledMcps,
|
|
25
|
+
shouldKeepRequiredPlugins,
|
|
26
|
+
} from "./uninstall-workflow.js";
|
|
27
|
+
|
|
28
|
+
const HOME = homedir();
|
|
29
|
+
|
|
30
|
+
const RECOMMENDED = RECOMMENDED_PLUGINS.map(toUninstallTarget);
|
|
31
|
+
const REQUIRED = REQUIRED_PLUGINS.map(toUninstallTarget);
|
|
32
|
+
|
|
33
|
+
// Symlinks created by install.js (only cleaned with --purge)
|
|
34
|
+
const MANAGED_SYMLINKS = [
|
|
35
|
+
join(HOME, ".local", "bin", "bun"),
|
|
36
|
+
join(HOME, ".local", "bin", "uv"),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export async function uninstallCurdxFlowPlugin(
|
|
40
|
+
{
|
|
41
|
+
listPluginsImpl = listPlugins,
|
|
42
|
+
uninstallPluginImpl = uninstallPlugin,
|
|
43
|
+
logImpl = log,
|
|
44
|
+
resultOutputImpl = resultOutput,
|
|
45
|
+
} = {}
|
|
46
|
+
) {
|
|
47
|
+
logImpl.blank();
|
|
48
|
+
logImpl.step(1, UNINSTALL_STEP_COUNT, "Uninstalling curdx-flow plugin...");
|
|
49
|
+
const curdx = listPluginsImpl().find((plugin) => plugin.name === "curdx-flow");
|
|
50
|
+
if (!curdx) {
|
|
51
|
+
logImpl.info("curdx-flow not installed, skipping");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = await uninstallPluginImpl({
|
|
56
|
+
scope: "user",
|
|
57
|
+
uninstallSpec: "curdx-flow@curdx-flow-marketplace",
|
|
58
|
+
});
|
|
59
|
+
if (result.code === 0) {
|
|
60
|
+
logImpl.ok("curdx-flow uninstalled");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logImpl.err(`Uninstall failed: ${resultOutputImpl(result)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function maybeUninstallRecommendedPlugins(
|
|
68
|
+
{ yes, keepRecommended },
|
|
69
|
+
{
|
|
70
|
+
getInstalledTargetsImpl = getInstalledTargets,
|
|
71
|
+
selectRecommendedPluginsToRemoveImpl = selectRecommendedPluginsToRemove,
|
|
72
|
+
uninstallNamedPluginImpl = uninstallNamedPlugin,
|
|
73
|
+
logImpl = log,
|
|
74
|
+
} = {}
|
|
75
|
+
) {
|
|
76
|
+
logImpl.blank();
|
|
77
|
+
logImpl.step(2, UNINSTALL_STEP_COUNT, "Recommended plugins");
|
|
78
|
+
if (keepRecommended) {
|
|
79
|
+
logImpl.info("Keeping recommended plugins (--keep-recommended)");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const present = getInstalledTargetsImpl(RECOMMENDED);
|
|
84
|
+
if (present.length === 0) {
|
|
85
|
+
logImpl.info("No installed recommended plugins");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const selected = await selectRecommendedPluginsToRemoveImpl({ yes, present });
|
|
90
|
+
for (const name of selected) {
|
|
91
|
+
const entry = present.find((plugin) => plugin.name === name);
|
|
92
|
+
if (!entry) continue;
|
|
93
|
+
await uninstallNamedPluginImpl(entry);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function uninstallNamedPlugin(
|
|
98
|
+
entry,
|
|
99
|
+
{
|
|
100
|
+
uninstallPluginImpl = uninstallPlugin,
|
|
101
|
+
resultLastLineImpl = resultLastLine,
|
|
102
|
+
} = {}
|
|
103
|
+
) {
|
|
104
|
+
log.blank();
|
|
105
|
+
console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
|
|
106
|
+
const result = await uninstallPluginImpl(entry);
|
|
107
|
+
if (result.code === 0) {
|
|
108
|
+
console.log(` ${color.green("✓")} ${entry.name} uninstalled`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(
|
|
113
|
+
` ${color.red("✗")} ${entry.name} uninstall failed: ${resultLastLineImpl(result)}`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function maybeRemoveBundledMcps(
|
|
118
|
+
{ yes, keepRecommended },
|
|
119
|
+
{
|
|
120
|
+
shouldKeepBundledMcpsImpl = shouldKeepBundledMcps,
|
|
121
|
+
confirmImpl = confirm,
|
|
122
|
+
removeMcpImpl = removeMcp,
|
|
123
|
+
logImpl = log,
|
|
124
|
+
} = {}
|
|
125
|
+
) {
|
|
126
|
+
logImpl.blank();
|
|
127
|
+
logImpl.info("Required MCP servers (context7, sequential-thinking)");
|
|
128
|
+
if (shouldKeepBundledMcpsImpl({ yes, keepRecommended })) {
|
|
129
|
+
logImpl.info(
|
|
130
|
+
color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const removeMcps = await confirmImpl(
|
|
136
|
+
`Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((mcp) => mcp.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
|
|
137
|
+
false
|
|
138
|
+
);
|
|
139
|
+
if (!removeMcps) {
|
|
140
|
+
logImpl.info("Keeping user-level MCPs");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const mcp of BUNDLED_MCPS) {
|
|
145
|
+
const result = await removeMcpImpl({ name: mcp.name });
|
|
146
|
+
if (result.code === 0) {
|
|
147
|
+
logImpl.ok(` ${mcp.name.padEnd(22)} removed`);
|
|
148
|
+
} else {
|
|
149
|
+
logImpl.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function maybeUninstallRequiredPlugins(
|
|
155
|
+
{ yes },
|
|
156
|
+
{
|
|
157
|
+
shouldKeepRequiredPluginsImpl = shouldKeepRequiredPlugins,
|
|
158
|
+
confirmImpl = confirm,
|
|
159
|
+
uninstallPluginImpl = uninstallPlugin,
|
|
160
|
+
logImpl = log,
|
|
161
|
+
} = {}
|
|
162
|
+
) {
|
|
163
|
+
logImpl.blank();
|
|
164
|
+
logImpl.info("Required companion plugins");
|
|
165
|
+
if (shouldKeepRequiredPluginsImpl({ yes })) {
|
|
166
|
+
logImpl.info(
|
|
167
|
+
color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const removeRequired = await confirmImpl(
|
|
173
|
+
`Remove required companion plugins (${REQUIRED.map((plugin) => plugin.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
|
|
174
|
+
false
|
|
175
|
+
);
|
|
176
|
+
if (!removeRequired) {
|
|
177
|
+
logImpl.info("Keeping required companion plugins");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const plugin of REQUIRED) {
|
|
182
|
+
const result = await uninstallPluginImpl(plugin);
|
|
183
|
+
if (result.code === 0) {
|
|
184
|
+
logImpl.ok(` ${plugin.name.padEnd(22)} uninstalled`);
|
|
185
|
+
} else {
|
|
186
|
+
logImpl.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function maybePurgeRuntimeArtifacts(
|
|
192
|
+
{ purge },
|
|
193
|
+
{
|
|
194
|
+
purgeManagedMarketplacesImpl = purgeManagedMarketplaces,
|
|
195
|
+
removeManagedSymlinksImpl = removeManagedSymlinks,
|
|
196
|
+
logImpl = log,
|
|
197
|
+
} = {}
|
|
198
|
+
) {
|
|
199
|
+
logImpl.blank();
|
|
200
|
+
logImpl.step(3, UNINSTALL_STEP_COUNT, "Runtime symlinks and marketplaces");
|
|
201
|
+
if (!purge) {
|
|
202
|
+
logImpl.info(
|
|
203
|
+
color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
|
|
204
|
+
);
|
|
205
|
+
logImpl.info(
|
|
206
|
+
color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await purgeManagedMarketplacesImpl();
|
|
212
|
+
removeManagedSymlinksImpl();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function maybeRemoveProjectState(
|
|
216
|
+
{ yes },
|
|
217
|
+
{
|
|
218
|
+
cwd = process.cwd(),
|
|
219
|
+
confirmImpl = confirm,
|
|
220
|
+
existsSyncImpl = existsSync,
|
|
221
|
+
rmSyncImpl = rmSync,
|
|
222
|
+
logImpl = log,
|
|
223
|
+
} = {}
|
|
224
|
+
) {
|
|
225
|
+
logImpl.blank();
|
|
226
|
+
logImpl.step(4, UNINSTALL_STEP_COUNT, "Project state directory");
|
|
227
|
+
const flowDir = join(cwd, ".flow");
|
|
228
|
+
if (!existsSyncImpl(flowDir)) {
|
|
229
|
+
logImpl.info(".flow/ does not exist, skipping");
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (yes) {
|
|
234
|
+
logImpl.info(
|
|
235
|
+
color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ok = await confirmImpl(
|
|
241
|
+
`${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
|
|
242
|
+
false
|
|
243
|
+
);
|
|
244
|
+
if (!ok) {
|
|
245
|
+
logImpl.info("Keeping .flow/");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
rmSyncImpl(flowDir, { recursive: true, force: true });
|
|
251
|
+
logImpl.ok(`Removed ${flowDir}`);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logImpl.err(`Removal failed: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function purgeManagedMarketplaces(
|
|
258
|
+
{
|
|
259
|
+
getManagedMarketplaceIdsImpl = getManagedMarketplaceIds,
|
|
260
|
+
removePluginMarketplaceImpl = removePluginMarketplace,
|
|
261
|
+
logImpl = log,
|
|
262
|
+
} = {}
|
|
263
|
+
) {
|
|
264
|
+
const marketplaceIds = getManagedMarketplaceIdsImpl(RECOMMENDED.concat(REQUIRED));
|
|
265
|
+
|
|
266
|
+
for (const marketplaceId of marketplaceIds) {
|
|
267
|
+
const result = await removePluginMarketplaceImpl(marketplaceId);
|
|
268
|
+
if (result.code === 0) {
|
|
269
|
+
logImpl.ok(`Removed marketplace ${marketplaceId}`);
|
|
270
|
+
} else if (!result.stderr.includes("not found")) {
|
|
271
|
+
logImpl.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(result)}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function removeManagedSymlinks(
|
|
277
|
+
{
|
|
278
|
+
existsSyncImpl = existsSync,
|
|
279
|
+
isBrokenSymlinkImpl = isBrokenSymlink,
|
|
280
|
+
lstatSyncImpl = lstatSync,
|
|
281
|
+
readlinkSyncImpl = readlinkSync,
|
|
282
|
+
unlinkSyncImpl = unlinkSync,
|
|
283
|
+
logImpl = log,
|
|
284
|
+
} = {}
|
|
285
|
+
) {
|
|
286
|
+
for (const link of MANAGED_SYMLINKS) {
|
|
287
|
+
if (!existsSyncImpl(link) && !isBrokenSymlinkImpl(link)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const stat = lstatSyncImpl(link);
|
|
292
|
+
if (!stat.isSymbolicLink()) {
|
|
293
|
+
logImpl.warn(
|
|
294
|
+
`${link} is not a symlink (likely a real file placed by the user), skipping`
|
|
295
|
+
);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const target = readlinkSyncImpl(link);
|
|
299
|
+
unlinkSyncImpl(link);
|
|
300
|
+
logImpl.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
logImpl.warn(`Failed to remove ${link}: ${err.message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function toUninstallTarget(entry) {
|
|
308
|
+
return {
|
|
309
|
+
name: entry.name,
|
|
310
|
+
uninstallSpec: entry.uninstallSpec,
|
|
311
|
+
uninstallArgs: entry.uninstallArgs || [],
|
|
312
|
+
marketplaceId: entry.marketplaceId,
|
|
313
|
+
scope: entry.scope,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isBrokenSymlink(pathname) {
|
|
318
|
+
try {
|
|
319
|
+
return lstatSync(pathname).isSymbolicLink();
|
|
320
|
+
} catch {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
package/cli/uninstall.js
CHANGED
|
@@ -2,48 +2,22 @@
|
|
|
2
2
|
* uninstall command — remove curdx-flow plugin (and optionally recommended plugins / artifacts).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
color,
|
|
11
|
-
log,
|
|
12
|
-
resultLastLine,
|
|
13
|
-
resultOutput,
|
|
14
|
-
confirm,
|
|
15
|
-
listPlugins,
|
|
16
|
-
} from "./utils.js";
|
|
17
|
-
import { REQUIRED_PLUGINS, RECOMMENDED_PLUGINS, BUNDLED_MCPS } from "./registry.js";
|
|
18
|
-
import {
|
|
19
|
-
removeMcp,
|
|
20
|
-
removePluginMarketplace,
|
|
21
|
-
uninstallPlugin,
|
|
22
|
-
} from "./lib/claude-ops.js";
|
|
5
|
+
import { log } from "./utils.js";
|
|
23
6
|
import {
|
|
24
7
|
createUninstallContext,
|
|
25
8
|
ensureClaudeCliAvailableForUninstall,
|
|
26
|
-
getInstalledTargets,
|
|
27
|
-
getManagedMarketplaceIds,
|
|
28
9
|
printUninstallSummary,
|
|
29
10
|
removeProtocolsStep,
|
|
30
|
-
selectRecommendedPluginsToRemove,
|
|
31
|
-
shouldKeepBundledMcps,
|
|
32
|
-
shouldKeepRequiredPlugins,
|
|
33
|
-
UNINSTALL_STEP_COUNT,
|
|
34
11
|
confirmUninstallStep,
|
|
35
12
|
} from "./uninstall-workflow.js";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
join(HOME, ".local", "bin", "bun"),
|
|
45
|
-
join(HOME, ".local", "bin", "uv"),
|
|
46
|
-
];
|
|
13
|
+
import {
|
|
14
|
+
maybePurgeRuntimeArtifacts,
|
|
15
|
+
maybeRemoveBundledMcps,
|
|
16
|
+
maybeRemoveProjectState,
|
|
17
|
+
maybeUninstallRecommendedPlugins,
|
|
18
|
+
maybeUninstallRequiredPlugins,
|
|
19
|
+
uninstallCurdxFlowPlugin,
|
|
20
|
+
} from "./uninstall-actions.js";
|
|
47
21
|
|
|
48
22
|
export async function uninstall(args = []) {
|
|
49
23
|
const context = createUninstallContext(args);
|
|
@@ -66,221 +40,3 @@ export async function uninstall(args = []) {
|
|
|
66
40
|
|
|
67
41
|
printUninstallSummary(context);
|
|
68
42
|
}
|
|
69
|
-
|
|
70
|
-
async function uninstallCurdxFlowPlugin() {
|
|
71
|
-
log.blank();
|
|
72
|
-
log.step(1, UNINSTALL_STEP_COUNT, "Uninstalling curdx-flow plugin...");
|
|
73
|
-
const curdx = listPlugins().find((plugin) => plugin.name === "curdx-flow");
|
|
74
|
-
if (!curdx) {
|
|
75
|
-
log.info("curdx-flow not installed, skipping");
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const result = await uninstallPlugin({
|
|
80
|
-
scope: "user",
|
|
81
|
-
uninstallSpec: "curdx-flow@curdx-flow-marketplace",
|
|
82
|
-
});
|
|
83
|
-
if (result.code === 0) {
|
|
84
|
-
log.ok("curdx-flow uninstalled");
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
log.err(`Uninstall failed: ${resultOutput(result)}`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async function maybeUninstallRecommendedPlugins({ yes, keepRecommended }) {
|
|
92
|
-
log.blank();
|
|
93
|
-
log.step(2, UNINSTALL_STEP_COUNT, "Recommended plugins");
|
|
94
|
-
if (keepRecommended) {
|
|
95
|
-
log.info("Keeping recommended plugins (--keep-recommended)");
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const present = getInstalledTargets(RECOMMENDED);
|
|
100
|
-
if (present.length === 0) {
|
|
101
|
-
log.info("No installed recommended plugins");
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const selected = await selectRecommendedPluginsToRemove({ yes, present });
|
|
106
|
-
for (const name of selected) {
|
|
107
|
-
const entry = present.find((plugin) => plugin.name === name);
|
|
108
|
-
if (!entry) continue;
|
|
109
|
-
await uninstallNamedPlugin(entry);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async function uninstallNamedPlugin(entry) {
|
|
114
|
-
log.blank();
|
|
115
|
-
console.log(` ${color.cyan("▸")} Uninstalling ${color.bold(entry.name)}...`);
|
|
116
|
-
const result = await uninstallPlugin(entry);
|
|
117
|
-
if (result.code === 0) {
|
|
118
|
-
console.log(` ${color.green("✓")} ${entry.name} uninstalled`);
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
console.log(
|
|
123
|
-
` ${color.red("✗")} ${entry.name} uninstall failed: ${resultLastLine(result)}`
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function maybeRemoveBundledMcps({ yes, keepRecommended }) {
|
|
128
|
-
log.blank();
|
|
129
|
-
log.info("Required MCP servers (context7, sequential-thinking)");
|
|
130
|
-
if (shouldKeepBundledMcps({ yes, keepRecommended })) {
|
|
131
|
-
log.info(
|
|
132
|
-
color.dim("--yes or --keep-recommended: keeping user-level MCPs (remove manually with `claude mcp remove <name>`)")
|
|
133
|
-
);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const removeMcps = await confirm(
|
|
138
|
-
`Remove user-level MCPs registered by install (${BUNDLED_MCPS.map((mcp) => mcp.name).join(", ")})? ${color.dim("(keeps them if other tools depend on them)")}`,
|
|
139
|
-
false
|
|
140
|
-
);
|
|
141
|
-
if (!removeMcps) {
|
|
142
|
-
log.info("Keeping user-level MCPs");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
for (const mcp of BUNDLED_MCPS) {
|
|
147
|
-
const result = await removeMcp({ name: mcp.name });
|
|
148
|
-
if (result.code === 0) {
|
|
149
|
-
log.ok(` ${mcp.name.padEnd(22)} removed`);
|
|
150
|
-
} else {
|
|
151
|
-
log.info(` ${mcp.name.padEnd(22)} ${color.dim("not present or already removed")}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function maybeUninstallRequiredPlugins({ yes }) {
|
|
157
|
-
log.blank();
|
|
158
|
-
log.info("Required companion plugins");
|
|
159
|
-
if (shouldKeepRequiredPlugins({ yes })) {
|
|
160
|
-
log.info(
|
|
161
|
-
color.dim("--yes mode: keeping required companion plugins (use --purge to remove them)")
|
|
162
|
-
);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const removeRequired = await confirm(
|
|
167
|
-
`Remove required companion plugins (${REQUIRED.map((plugin) => plugin.name).join(", ")})? ${color.dim("(keeps shared tools available if other workflows depend on them)")}`,
|
|
168
|
-
false
|
|
169
|
-
);
|
|
170
|
-
if (!removeRequired) {
|
|
171
|
-
log.info("Keeping required companion plugins");
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
for (const plugin of REQUIRED) {
|
|
176
|
-
const result = await uninstallPlugin(plugin);
|
|
177
|
-
if (result.code === 0) {
|
|
178
|
-
log.ok(` ${plugin.name.padEnd(22)} uninstalled`);
|
|
179
|
-
} else {
|
|
180
|
-
log.info(` ${plugin.name.padEnd(22)} ${color.dim("not present or already removed")}`);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function maybePurgeRuntimeArtifacts({ purge }) {
|
|
186
|
-
log.blank();
|
|
187
|
-
log.step(3, UNINSTALL_STEP_COUNT, "Runtime symlinks and marketplaces");
|
|
188
|
-
if (!purge) {
|
|
189
|
-
log.info(
|
|
190
|
-
color.dim("Keeping ~/.local/bin/bun, ~/.local/bin/uv (use --purge to remove)")
|
|
191
|
-
);
|
|
192
|
-
log.info(
|
|
193
|
-
color.dim("Reason: these bun/uv binaries may be used by other tools — confirm before deleting")
|
|
194
|
-
);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
await purgeManagedMarketplaces();
|
|
199
|
-
removeManagedSymlinks();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async function purgeManagedMarketplaces() {
|
|
203
|
-
const marketplaceIds = getManagedMarketplaceIds(RECOMMENDED.concat(REQUIRED));
|
|
204
|
-
|
|
205
|
-
for (const marketplaceId of marketplaceIds) {
|
|
206
|
-
const result = await removePluginMarketplace(marketplaceId);
|
|
207
|
-
if (result.code === 0) {
|
|
208
|
-
log.ok(`Removed marketplace ${marketplaceId}`);
|
|
209
|
-
} else if (!result.stderr.includes("not found")) {
|
|
210
|
-
log.warn(`Failed to remove marketplace ${marketplaceId}: ${resultLastLine(result)}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function removeManagedSymlinks() {
|
|
216
|
-
for (const link of MANAGED_SYMLINKS) {
|
|
217
|
-
if (!existsSync(link) && !isBrokenSymlink(link)) {
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
try {
|
|
221
|
-
const stat = lstatSync(link);
|
|
222
|
-
if (!stat.isSymbolicLink()) {
|
|
223
|
-
log.warn(
|
|
224
|
-
`${link} is not a symlink (likely a real file placed by the user), skipping`
|
|
225
|
-
);
|
|
226
|
-
continue;
|
|
227
|
-
}
|
|
228
|
-
const target = readlinkSync(link);
|
|
229
|
-
unlinkSync(link);
|
|
230
|
-
log.ok(`Removed symlink ${link} ${color.dim(`(was → ${target})`)}`);
|
|
231
|
-
} catch (err) {
|
|
232
|
-
log.warn(`Failed to remove ${link}: ${err.message}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
async function maybeRemoveProjectState({ yes }) {
|
|
238
|
-
log.blank();
|
|
239
|
-
log.step(4, UNINSTALL_STEP_COUNT, "Project state directory");
|
|
240
|
-
const flowDir = join(process.cwd(), ".flow");
|
|
241
|
-
if (!existsSync(flowDir)) {
|
|
242
|
-
log.info(".flow/ does not exist, skipping");
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (yes) {
|
|
247
|
-
log.info(
|
|
248
|
-
color.dim("--yes mode: keeping .flow/ (contains specs & decisions — confirm by hand before deleting)")
|
|
249
|
-
);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const ok = await confirm(
|
|
254
|
-
`${color.red("DANGER:")} delete the ${color.bold(".flow/")} directory of the current project? ${color.dim("(includes all specs / decisions, not recoverable)")}`,
|
|
255
|
-
false
|
|
256
|
-
);
|
|
257
|
-
if (!ok) {
|
|
258
|
-
log.info("Keeping .flow/");
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
rmSync(flowDir, { recursive: true, force: true });
|
|
264
|
-
log.ok(`Removed ${flowDir}`);
|
|
265
|
-
} catch (err) {
|
|
266
|
-
log.err(`Removal failed: ${err.message}`);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function toUninstallTarget(entry) {
|
|
271
|
-
return {
|
|
272
|
-
name: entry.name,
|
|
273
|
-
uninstallSpec: entry.uninstallSpec,
|
|
274
|
-
uninstallArgs: entry.uninstallArgs || [],
|
|
275
|
-
marketplaceId: entry.marketplaceId,
|
|
276
|
-
scope: entry.scope,
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function isBrokenSymlink(pathname) {
|
|
281
|
-
try {
|
|
282
|
-
return lstatSync(pathname).isSymbolicLink();
|
|
283
|
-
} catch {
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
|
-
}
|
package/cli/utils.js
CHANGED
|
@@ -32,4 +32,9 @@ export {
|
|
|
32
32
|
parsePluginListJson,
|
|
33
33
|
readUserMcpConfig,
|
|
34
34
|
} from "./lib/claude.js";
|
|
35
|
-
export {
|
|
35
|
+
export {
|
|
36
|
+
ensureClaudeMemRuntimes,
|
|
37
|
+
ensureRuntimeInPath,
|
|
38
|
+
inspectClaudeMemRuntimes,
|
|
39
|
+
inspectRuntimeInPath,
|
|
40
|
+
} from "./lib/runtime.js";
|
|
@@ -17,7 +17,7 @@ depends_on: []
|
|
|
17
17
|
|
|
18
18
|
- /curdx-flow:review command
|
|
19
19
|
- Before Phase transitions (requirements → design, design → tasks)
|
|
20
|
-
- Before code merge
|
|
20
|
+
- Before code merge or human PR/release handoff
|
|
21
21
|
- Enabled by default in Enterprise mode
|
|
22
22
|
|
|
23
23
|
---
|
package/gates/security-gate.md
CHANGED
|
@@ -14,7 +14,7 @@ depends_on: []
|
|
|
14
14
|
## Trigger Timing
|
|
15
15
|
|
|
16
16
|
- When the `security-audit` skill runs
|
|
17
|
-
- Before `/curdx-flow:
|
|
17
|
+
- Before human PR/release handoff, after `/curdx-flow:verify` and `/curdx-flow:review`
|
|
18
18
|
- When committing specs involving auth / payments / PII
|
|
19
19
|
|
|
20
20
|
---
|
|
@@ -154,7 +154,7 @@ pnpm audit
|
|
|
154
154
|
|
|
155
155
|
### Blocking Items
|
|
156
156
|
|
|
157
|
-
- If SR-01 ~ SR-05 are found → block immediately
|
|
157
|
+
- If SR-01 ~ SR-05 are found → block immediately; do not hand off for PR/release
|
|
158
158
|
- Must fix or explicitly exempt (record in STATE.md as tech debt + commitment to fix before release)
|
|
159
159
|
|
|
160
160
|
### Warning Items
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
gate: test-quality-gate
|
|
3
|
+
category: standard-mode
|
|
4
|
+
severity: blocking
|
|
5
|
+
depends_on: []
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Test Quality Gate
|
|
9
|
+
|
|
10
|
+
A green test suite is not enough. Tests must exercise real behavior and fail for the right reason.
|
|
11
|
+
|
|
12
|
+
## Blocking Findings
|
|
13
|
+
|
|
14
|
+
Flag as blocking when a test is the only evidence for an FR/AC and any of these hold:
|
|
15
|
+
|
|
16
|
+
1. **Mock-only behavior**
|
|
17
|
+
- Assertions only check mock calls (`toHaveBeenCalled`, `calledWith`, spy counts).
|
|
18
|
+
- The real module/function under test is never invoked.
|
|
19
|
+
- The test would still pass if the production implementation were empty.
|
|
20
|
+
|
|
21
|
+
2. **Mock setup dominates evidence**
|
|
22
|
+
- Mock/stub/spy setup lines are more than 3x real behavioral assertions.
|
|
23
|
+
- The test mostly restates fixture wiring instead of asserting output, state, persistence, or user-visible behavior.
|
|
24
|
+
|
|
25
|
+
3. **Skipped or inert tests**
|
|
26
|
+
- `it.skip`, `describe.skip`, `test.skip`, `xit`, `pending`, or equivalent on covered behavior.
|
|
27
|
+
- Test has no assertions and no meaningful side-effect check.
|
|
28
|
+
|
|
29
|
+
4. **Implementation-biased regression**
|
|
30
|
+
- Test was added after implementation without evidence of RED failure when the task claims TDD.
|
|
31
|
+
- Test asserts internal private structure instead of externally observable behavior.
|
|
32
|
+
|
|
33
|
+
5. **Missing cleanup for stateful mocks**
|
|
34
|
+
- Stateful mocks/spies are used across tests without `afterEach` cleanup (`restoreAllMocks`, `clearAllMocks`, sandbox restore, etc.).
|
|
35
|
+
- Shared mock state can leak between tests.
|
|
36
|
+
|
|
37
|
+
## Acceptable Mock Usage
|
|
38
|
+
|
|
39
|
+
Mocks are acceptable when they isolate a boundary and the assertion still verifies real behavior:
|
|
40
|
+
|
|
41
|
+
- Network/payment/email provider mocked, but service logic and error handling are real.
|
|
42
|
+
- Clock/randomness mocked to make deterministic assertions.
|
|
43
|
+
- Database mocked only when a separate integration test covers persistence behavior.
|
|
44
|
+
|
|
45
|
+
## Evidence Checklist
|
|
46
|
+
|
|
47
|
+
For each FR/AC test evidence, record:
|
|
48
|
+
|
|
49
|
+
- Test file and test name.
|
|
50
|
+
- What real code path is invoked.
|
|
51
|
+
- What behavioral assertion proves the requirement.
|
|
52
|
+
- Whether the test was observed RED before GREEN when TDD is claimed.
|
|
53
|
+
- Whether mocks are boundary-only or behavior-replacing.
|
|
54
|
+
|
|
55
|
+
## Verdicts
|
|
56
|
+
|
|
57
|
+
- `PASS`: Tests exercise real behavior with meaningful assertions.
|
|
58
|
+
- `WARN`: Mock-heavy but supported by separate integration/e2e coverage.
|
|
59
|
+
- `FAIL`: Mock-only/skipped/no-assertion test is used as primary evidence.
|