@fenglimg/fabric-cli 2.0.0 → 2.1.0-rc.2
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/LICENSE +21 -0
- package/README.md +6 -5
- package/dist/chunk-BATF4PEJ.js +361 -0
- package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
- package/dist/chunk-F46ORPOA.js +903 -0
- package/dist/chunk-HFQVXY6P.js +86 -0
- package/dist/chunk-L4Q55UC4.js +52 -0
- package/dist/chunk-LFIKMVY7.js +27 -0
- package/dist/chunk-MF3OTILQ.js +544 -0
- package/dist/chunk-PWLW3B57.js +18 -0
- package/dist/chunk-RYAFBNES.js +33 -0
- package/dist/chunk-T5RPGCCM.js +40 -0
- package/dist/chunk-WU6GAPKH.js +36 -0
- package/dist/config-XJIPZNUP.js +13 -0
- package/dist/doctor-QVNPHLJK.js +920 -0
- package/dist/index.js +23 -8
- package/dist/{init-BIRSIOXO.js → install-2HDO5FTQ.js} +807 -705
- package/dist/metrics-ACEQFPDU.js +122 -0
- package/dist/onboard-coverage-MFCAEBDO.js +220 -0
- package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
- package/dist/scope-explain-2F2R5URO.js +33 -0
- package/dist/status-GLQWLWH6.js +23 -0
- package/dist/store-XTSE5TY6.js +105 -0
- package/dist/sync-BJCWDPNC.js +245 -0
- package/dist/uninstall-TAXSUSKH.js +1073 -0
- package/dist/whoami-B6AEMSEV.js +31 -0
- package/package.json +30 -5
- package/templates/hooks/cite-policy-evict.cjs +231 -0
- package/templates/hooks/configs/README.md +29 -6
- package/templates/hooks/configs/claude-code.json +14 -3
- package/templates/hooks/configs/codex-hooks.json +6 -3
- package/templates/hooks/configs/cursor-hooks.json +8 -10
- package/templates/hooks/fabric-hint.cjs +873 -105
- package/templates/hooks/knowledge-hint-broad.cjs +549 -135
- package/templates/hooks/knowledge-hint-narrow.cjs +830 -26
- package/templates/hooks/lib/banner-i18n.cjs +309 -0
- package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
- package/templates/hooks/lib/cite-contract-reminder.cjs +179 -0
- package/templates/hooks/lib/cite-line-parser.cjs +180 -0
- package/templates/hooks/lib/client-adapter.cjs +106 -0
- package/templates/hooks/lib/config-cache.cjs +107 -0
- package/templates/hooks/lib/state-store.cjs +84 -0
- package/templates/hooks/lib/summary-fallback.cjs +210 -0
- package/templates/skills/fabric-archive/SKILL.md +97 -419
- package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
- package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
- package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
- package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
- package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
- package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
- package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
- package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
- package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
- package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
- package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
- package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
- package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
- package/templates/skills/fabric-import/SKILL.md +77 -514
- package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
- package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
- package/templates/skills/fabric-import/ref/output-contract.md +61 -0
- package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
- package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
- package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
- package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
- package/templates/skills/fabric-review/SKILL.md +90 -284
- package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
- package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
- package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
- package/templates/skills/fabric-review/ref/output-contract.md +58 -0
- package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
- package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
- package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
- package/templates/skills/fabric-sync/SKILL.md +46 -0
- package/templates/skills/lib/shared-policy.md +69 -0
- package/dist/chunk-6ICJICVU.js +0 -10
- package/dist/chunk-74SZWYPH.js +0 -658
- package/dist/chunk-EYIDD2YS.js +0 -1000
- package/dist/doctor-T7JWODKG.js +0 -282
- package/dist/hooks-Y74Y5LQS.js +0 -12
- package/dist/scan-LMK3UCWL.js +0 -22
- package/dist/serve-H554BHLG.js +0 -124
- package/templates/agents-md/AGENTS.md.template +0 -59
- package/templates/bootstrap/CLAUDE.md +0 -8
- package/templates/bootstrap/codex-AGENTS-header.md +0 -6
- package/templates/bootstrap/cursor-fabric-bootstrap.mdc +0 -10
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
FABRIC_HOOK_COMMAND_PATHS,
|
|
4
|
+
HOOK_CONFIG_ARRAY_PATHS,
|
|
5
|
+
HOOK_CONFIG_TARGETS,
|
|
6
|
+
HOOK_LIB_DESTINATIONS,
|
|
7
|
+
HOOK_SCRIPT_DESTINATIONS,
|
|
8
|
+
SKILL_DESTINATIONS,
|
|
9
|
+
fabricAgentsSnapshotPath
|
|
10
|
+
} from "./chunk-F46ORPOA.js";
|
|
11
|
+
import {
|
|
12
|
+
paint
|
|
13
|
+
} from "./chunk-WWNXR34K.js";
|
|
14
|
+
import {
|
|
15
|
+
createDebugLogger,
|
|
16
|
+
resolveDevMode
|
|
17
|
+
} from "./chunk-COI5VDFU.js";
|
|
18
|
+
import {
|
|
19
|
+
detectClientSupports,
|
|
20
|
+
resolveClients
|
|
21
|
+
} from "./chunk-MF3OTILQ.js";
|
|
22
|
+
import {
|
|
23
|
+
t
|
|
24
|
+
} from "./chunk-PWLW3B57.js";
|
|
25
|
+
|
|
26
|
+
// src/commands/uninstall.ts
|
|
27
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
28
|
+
import { rm as rm2 } from "fs/promises";
|
|
29
|
+
import { homedir } from "os";
|
|
30
|
+
import { isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
31
|
+
import { cancel, confirm, group, intro, isCancel, log, note, outro } from "@clack/prompts";
|
|
32
|
+
import { defineCommand } from "citty";
|
|
33
|
+
|
|
34
|
+
// src/install/uninstall-skills-and-hooks.ts
|
|
35
|
+
import { existsSync } from "fs";
|
|
36
|
+
import { readdir, readFile, rm, rmdir } from "fs/promises";
|
|
37
|
+
import { dirname, join } from "path";
|
|
38
|
+
import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
|
|
39
|
+
import { BOOTSTRAP_REGEX } from "@fenglimg/fabric-shared/templates/bootstrap-canonical";
|
|
40
|
+
async function uninstallFabricArchiveSkill(projectRoot) {
|
|
41
|
+
return removeSkill("skill", SKILL_DESTINATIONS.fabricArchive, projectRoot);
|
|
42
|
+
}
|
|
43
|
+
async function uninstallFabricReviewSkill(projectRoot) {
|
|
44
|
+
return removeSkill("skill-review", SKILL_DESTINATIONS.fabricReview, projectRoot);
|
|
45
|
+
}
|
|
46
|
+
async function uninstallFabricImportSkill(projectRoot) {
|
|
47
|
+
return removeSkill("skill-import", SKILL_DESTINATIONS.fabricImport, projectRoot);
|
|
48
|
+
}
|
|
49
|
+
async function uninstallFabricSyncSkill(projectRoot) {
|
|
50
|
+
return removeSkill("skill-sync", SKILL_DESTINATIONS.fabricSync, projectRoot);
|
|
51
|
+
}
|
|
52
|
+
async function removeSkill(step, rels, projectRoot) {
|
|
53
|
+
const results = [];
|
|
54
|
+
for (const rel of rels) {
|
|
55
|
+
const target = join(projectRoot, rel);
|
|
56
|
+
results.push(await rmIfExists(step, target));
|
|
57
|
+
results.push(await rmDirIfEmpty(`${step}-dir`, dirname(target)));
|
|
58
|
+
}
|
|
59
|
+
return results;
|
|
60
|
+
}
|
|
61
|
+
async function removeArchiveHintHook(projectRoot) {
|
|
62
|
+
return removeHookScripts("hook-script", HOOK_SCRIPT_DESTINATIONS.fabricHint, projectRoot);
|
|
63
|
+
}
|
|
64
|
+
async function removeKnowledgeHintBroadHook(projectRoot) {
|
|
65
|
+
return removeHookScripts(
|
|
66
|
+
"hook-broad-script",
|
|
67
|
+
HOOK_SCRIPT_DESTINATIONS.knowledgeHintBroad,
|
|
68
|
+
projectRoot
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
async function removeKnowledgeHintNarrowHook(projectRoot) {
|
|
72
|
+
return removeHookScripts(
|
|
73
|
+
"hook-narrow-script",
|
|
74
|
+
HOOK_SCRIPT_DESTINATIONS.knowledgeHintNarrow,
|
|
75
|
+
projectRoot
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
async function removeHookScripts(step, rels, projectRoot) {
|
|
79
|
+
const results = [];
|
|
80
|
+
for (const rel of rels) {
|
|
81
|
+
const target = join(projectRoot, rel);
|
|
82
|
+
results.push(await rmIfExists(step, target));
|
|
83
|
+
}
|
|
84
|
+
return results;
|
|
85
|
+
}
|
|
86
|
+
async function removeHookLibs(projectRoot) {
|
|
87
|
+
const results = [];
|
|
88
|
+
for (const dirRel of HOOK_LIB_DESTINATIONS) {
|
|
89
|
+
const dirAbs = join(projectRoot, dirRel);
|
|
90
|
+
if (!existsSync(dirAbs)) {
|
|
91
|
+
results.push({ step: "hook-lib", path: dirAbs, status: "skipped", message: "absent" });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = await readdir(dirAbs);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
results.push({
|
|
99
|
+
step: "hook-lib",
|
|
100
|
+
path: dirAbs,
|
|
101
|
+
status: "error",
|
|
102
|
+
message: error instanceof Error ? error.message : String(error)
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
if (!entry.endsWith(".cjs")) continue;
|
|
108
|
+
results.push(await rmIfExists("hook-lib", join(dirAbs, entry)));
|
|
109
|
+
}
|
|
110
|
+
results.push(await rmDirIfEmpty("hook-lib-dir", dirAbs));
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
async function unmergeClaudeCodeHookConfig(projectRoot) {
|
|
115
|
+
return unmergeHookConfig({
|
|
116
|
+
step: "claude-hook-config",
|
|
117
|
+
projectRoot,
|
|
118
|
+
configRel: HOOK_CONFIG_TARGETS.claudeCode,
|
|
119
|
+
arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.claudeCode],
|
|
120
|
+
fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.claudeCode),
|
|
121
|
+
extractCommands: extractClaudeCommands
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function unmergeCodexHookConfig(projectRoot) {
|
|
125
|
+
return unmergeHookConfig({
|
|
126
|
+
step: "codex-hook-config",
|
|
127
|
+
projectRoot,
|
|
128
|
+
configRel: HOOK_CONFIG_TARGETS.codex,
|
|
129
|
+
arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.codex],
|
|
130
|
+
fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.codex),
|
|
131
|
+
extractCommands: extractFlatCommands
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function unmergeCursorHookConfig(projectRoot) {
|
|
135
|
+
return unmergeHookConfig({
|
|
136
|
+
step: "cursor-hook-config",
|
|
137
|
+
projectRoot,
|
|
138
|
+
configRel: HOOK_CONFIG_TARGETS.cursor,
|
|
139
|
+
arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.cursor],
|
|
140
|
+
fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.cursor),
|
|
141
|
+
extractCommands: extractFlatCommands
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function stripFabricBootstrapBlocks(projectRoot) {
|
|
145
|
+
const results = [];
|
|
146
|
+
results.push(await stripClaudeBootstrapImports(projectRoot));
|
|
147
|
+
results.push(await stripManagedBlock(projectRoot, "AGENTS.md", { deleteWhenEmpty: false }));
|
|
148
|
+
results.push(
|
|
149
|
+
await stripManagedBlock(projectRoot, join(".cursor", "rules", "fabric-bootstrap.mdc"), {
|
|
150
|
+
deleteWhenEmpty: true
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
async function stripClaudeBootstrapImports(projectRoot) {
|
|
156
|
+
const step = "bootstrap-claude";
|
|
157
|
+
const target = join(projectRoot, "CLAUDE.md");
|
|
158
|
+
if (!existsSync(target)) {
|
|
159
|
+
return { step, path: target, status: "skipped", message: "absent" };
|
|
160
|
+
}
|
|
161
|
+
let existing;
|
|
162
|
+
try {
|
|
163
|
+
existing = await readFile(target, "utf8");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return {
|
|
166
|
+
step,
|
|
167
|
+
path: target,
|
|
168
|
+
status: "error",
|
|
169
|
+
message: error instanceof Error ? error.message : String(error)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const managedLines = /* @__PURE__ */ new Set(["@.fabric/AGENTS.md", "@.fabric/project-rules.md"]);
|
|
173
|
+
const lines = existing.split(/\r?\n/);
|
|
174
|
+
const filtered = lines.filter((l) => !managedLines.has(l.replace(/\s+$/, "")));
|
|
175
|
+
if (filtered.length === lines.length) {
|
|
176
|
+
return { step, path: target, status: "skipped", message: "no-fabric-section" };
|
|
177
|
+
}
|
|
178
|
+
while (filtered.length > 1 && filtered[filtered.length - 1] === "" && filtered[filtered.length - 2] === "") {
|
|
179
|
+
filtered.pop();
|
|
180
|
+
}
|
|
181
|
+
const next = filtered.join("\n");
|
|
182
|
+
if (next === existing) {
|
|
183
|
+
return { step, path: target, status: "skipped", message: "no-fabric-section" };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await atomicWriteText(target, next);
|
|
187
|
+
return { step, path: target, status: "removed" };
|
|
188
|
+
} catch (error) {
|
|
189
|
+
return {
|
|
190
|
+
step,
|
|
191
|
+
path: target,
|
|
192
|
+
status: "error",
|
|
193
|
+
message: error instanceof Error ? error.message : String(error)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function stripManagedBlock(projectRoot, relPath, options) {
|
|
198
|
+
const step = relPath.endsWith(".mdc") ? "bootstrap-cursor" : "bootstrap-codex";
|
|
199
|
+
const target = join(projectRoot, relPath);
|
|
200
|
+
if (!existsSync(target)) {
|
|
201
|
+
return { step, path: target, status: "skipped", message: "absent" };
|
|
202
|
+
}
|
|
203
|
+
let existing;
|
|
204
|
+
try {
|
|
205
|
+
existing = await readFile(target, "utf8");
|
|
206
|
+
} catch (error) {
|
|
207
|
+
return {
|
|
208
|
+
step,
|
|
209
|
+
path: target,
|
|
210
|
+
status: "error",
|
|
211
|
+
message: error instanceof Error ? error.message : String(error)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const match = existing.match(BOOTSTRAP_REGEX);
|
|
215
|
+
if (match === null) {
|
|
216
|
+
return { step, path: target, status: "skipped", message: "no-fabric-section" };
|
|
217
|
+
}
|
|
218
|
+
const before = existing.slice(0, match.index ?? 0);
|
|
219
|
+
const after = existing.slice((match.index ?? 0) + match[0].length);
|
|
220
|
+
const filtered = `${before}${after.replace(/^\r?\n/, "")}`;
|
|
221
|
+
if (options.deleteWhenEmpty && isFrontMatterOnly(filtered)) {
|
|
222
|
+
try {
|
|
223
|
+
await rm(target, { force: true });
|
|
224
|
+
return { step, path: target, status: "removed", message: "front-matter-only" };
|
|
225
|
+
} catch (error) {
|
|
226
|
+
return {
|
|
227
|
+
step,
|
|
228
|
+
path: target,
|
|
229
|
+
status: "error",
|
|
230
|
+
message: error instanceof Error ? error.message : String(error)
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
await atomicWriteText(target, filtered);
|
|
236
|
+
return { step, path: target, status: "removed" };
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
step,
|
|
240
|
+
path: target,
|
|
241
|
+
status: "error",
|
|
242
|
+
message: error instanceof Error ? error.message : String(error)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function isFrontMatterOnly(content) {
|
|
247
|
+
const trimmed = content.replace(/^\s+/, "");
|
|
248
|
+
const match = trimmed.match(/^---\n[\s\S]*?\n---\s*$/);
|
|
249
|
+
if (match === null) return trimmed.length === 0;
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
async function deleteFabricAgentsSnapshot(projectRoot) {
|
|
253
|
+
const target = fabricAgentsSnapshotPath(projectRoot);
|
|
254
|
+
return rmIfExists("bootstrap-snapshot", target);
|
|
255
|
+
}
|
|
256
|
+
async function uninstallBootstrapStage(projectRoot, _opts = {}) {
|
|
257
|
+
const results = [];
|
|
258
|
+
await runAndCollect(
|
|
259
|
+
results,
|
|
260
|
+
"bootstrap-blocks",
|
|
261
|
+
projectRoot,
|
|
262
|
+
() => stripFabricBootstrapBlocks(projectRoot)
|
|
263
|
+
);
|
|
264
|
+
await runAndCollectOne(
|
|
265
|
+
results,
|
|
266
|
+
"bootstrap-snapshot",
|
|
267
|
+
projectRoot,
|
|
268
|
+
() => deleteFabricAgentsSnapshot(projectRoot)
|
|
269
|
+
);
|
|
270
|
+
await runAndCollectOne(
|
|
271
|
+
results,
|
|
272
|
+
"cursor-hook-config",
|
|
273
|
+
projectRoot,
|
|
274
|
+
() => unmergeCursorHookConfig(projectRoot)
|
|
275
|
+
);
|
|
276
|
+
await runAndCollectOne(
|
|
277
|
+
results,
|
|
278
|
+
"codex-hook-config",
|
|
279
|
+
projectRoot,
|
|
280
|
+
() => unmergeCodexHookConfig(projectRoot)
|
|
281
|
+
);
|
|
282
|
+
await runAndCollectOne(
|
|
283
|
+
results,
|
|
284
|
+
"claude-hook-config",
|
|
285
|
+
projectRoot,
|
|
286
|
+
() => unmergeClaudeCodeHookConfig(projectRoot)
|
|
287
|
+
);
|
|
288
|
+
await runAndCollect(results, "hook-lib", projectRoot, () => removeHookLibs(projectRoot));
|
|
289
|
+
await runAndCollect(
|
|
290
|
+
results,
|
|
291
|
+
"hook-narrow-script",
|
|
292
|
+
projectRoot,
|
|
293
|
+
() => removeKnowledgeHintNarrowHook(projectRoot)
|
|
294
|
+
);
|
|
295
|
+
await runAndCollect(
|
|
296
|
+
results,
|
|
297
|
+
"hook-broad-script",
|
|
298
|
+
projectRoot,
|
|
299
|
+
() => removeKnowledgeHintBroadHook(projectRoot)
|
|
300
|
+
);
|
|
301
|
+
await runAndCollect(
|
|
302
|
+
results,
|
|
303
|
+
"hook-script",
|
|
304
|
+
projectRoot,
|
|
305
|
+
() => removeArchiveHintHook(projectRoot)
|
|
306
|
+
);
|
|
307
|
+
await runAndCollect(
|
|
308
|
+
results,
|
|
309
|
+
"skill-sync",
|
|
310
|
+
projectRoot,
|
|
311
|
+
() => uninstallFabricSyncSkill(projectRoot)
|
|
312
|
+
);
|
|
313
|
+
await runAndCollect(
|
|
314
|
+
results,
|
|
315
|
+
"skill-import",
|
|
316
|
+
projectRoot,
|
|
317
|
+
() => uninstallFabricImportSkill(projectRoot)
|
|
318
|
+
);
|
|
319
|
+
await runAndCollect(
|
|
320
|
+
results,
|
|
321
|
+
"skill-review",
|
|
322
|
+
projectRoot,
|
|
323
|
+
() => uninstallFabricReviewSkill(projectRoot)
|
|
324
|
+
);
|
|
325
|
+
await runAndCollect(
|
|
326
|
+
results,
|
|
327
|
+
"skill",
|
|
328
|
+
projectRoot,
|
|
329
|
+
() => uninstallFabricArchiveSkill(projectRoot)
|
|
330
|
+
);
|
|
331
|
+
return results;
|
|
332
|
+
}
|
|
333
|
+
async function runAndCollect(results, step, projectRoot, fn) {
|
|
334
|
+
try {
|
|
335
|
+
const sub = await fn();
|
|
336
|
+
results.push(...sub);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
results.push({
|
|
339
|
+
step,
|
|
340
|
+
path: projectRoot,
|
|
341
|
+
status: "error",
|
|
342
|
+
message: error instanceof Error ? error.message : String(error)
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function runAndCollectOne(results, step, projectRoot, fn) {
|
|
347
|
+
try {
|
|
348
|
+
results.push(await fn());
|
|
349
|
+
} catch (error) {
|
|
350
|
+
results.push({
|
|
351
|
+
step,
|
|
352
|
+
path: projectRoot,
|
|
353
|
+
status: "error",
|
|
354
|
+
message: error instanceof Error ? error.message : String(error)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function rmIfExists(step, target) {
|
|
359
|
+
if (!existsSync(target)) {
|
|
360
|
+
return { step, path: target, status: "skipped", message: "absent" };
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
await rm(target, { force: true });
|
|
364
|
+
return { step, path: target, status: "removed" };
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return {
|
|
367
|
+
step,
|
|
368
|
+
path: target,
|
|
369
|
+
status: "error",
|
|
370
|
+
message: error instanceof Error ? error.message : String(error)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async function rmDirIfEmpty(step, target) {
|
|
375
|
+
if (!existsSync(target)) {
|
|
376
|
+
return { step, path: target, status: "skipped", message: "absent" };
|
|
377
|
+
}
|
|
378
|
+
let entries;
|
|
379
|
+
try {
|
|
380
|
+
entries = await readdir(target);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return {
|
|
383
|
+
step,
|
|
384
|
+
path: target,
|
|
385
|
+
status: "error",
|
|
386
|
+
message: error instanceof Error ? error.message : String(error)
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (entries.length > 0) {
|
|
390
|
+
return { step, path: target, status: "skipped", message: "not-empty" };
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
await rmdir(target);
|
|
394
|
+
return { step, path: target, status: "removed" };
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return {
|
|
397
|
+
step,
|
|
398
|
+
path: target,
|
|
399
|
+
status: "error",
|
|
400
|
+
message: error instanceof Error ? error.message : String(error)
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async function unmergeHookConfig(args) {
|
|
405
|
+
const target = join(args.projectRoot, args.configRel);
|
|
406
|
+
if (!existsSync(target)) {
|
|
407
|
+
return { step: args.step, path: target, status: "skipped", message: "absent" };
|
|
408
|
+
}
|
|
409
|
+
let raw;
|
|
410
|
+
try {
|
|
411
|
+
raw = await readFile(target, "utf8");
|
|
412
|
+
} catch (error) {
|
|
413
|
+
return {
|
|
414
|
+
step: args.step,
|
|
415
|
+
path: target,
|
|
416
|
+
status: "error",
|
|
417
|
+
message: error instanceof Error ? error.message : String(error)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
if (raw.trim().length === 0) {
|
|
421
|
+
return { step: args.step, path: target, status: "skipped", message: "empty" };
|
|
422
|
+
}
|
|
423
|
+
let parsed;
|
|
424
|
+
try {
|
|
425
|
+
parsed = JSON.parse(raw);
|
|
426
|
+
} catch (error) {
|
|
427
|
+
return {
|
|
428
|
+
step: args.step,
|
|
429
|
+
path: target,
|
|
430
|
+
status: "error",
|
|
431
|
+
message: error instanceof Error ? error.message : String(error)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
435
|
+
return { step: args.step, path: target, status: "skipped", message: "not-an-object" };
|
|
436
|
+
}
|
|
437
|
+
const next = JSON.parse(JSON.stringify(parsed));
|
|
438
|
+
for (const dotted of args.arrayPaths) {
|
|
439
|
+
pruneArrayAtPath(next, dotted, args.fabricCommands, args.extractCommands);
|
|
440
|
+
}
|
|
441
|
+
if (jsonEqual(parsed, next)) {
|
|
442
|
+
return { step: args.step, path: target, status: "skipped", message: "no-fabric-entries" };
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await atomicWriteJson(target, next, { indent: 2 });
|
|
446
|
+
return { step: args.step, path: target, status: "removed" };
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return {
|
|
449
|
+
step: args.step,
|
|
450
|
+
path: target,
|
|
451
|
+
status: "error",
|
|
452
|
+
message: error instanceof Error ? error.message : String(error)
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function pruneArrayAtPath(root, path, fabricCommands, extractCommands) {
|
|
457
|
+
const keys = path.split(".");
|
|
458
|
+
const chain = [];
|
|
459
|
+
let cursor = root;
|
|
460
|
+
for (let i = 0; i < keys.length; i++) {
|
|
461
|
+
const key = keys[i];
|
|
462
|
+
if (cursor === null || typeof cursor !== "object" || Array.isArray(cursor)) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
const parent = cursor;
|
|
466
|
+
if (!(key in parent)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
chain.push({ parent, key });
|
|
470
|
+
cursor = parent[key];
|
|
471
|
+
}
|
|
472
|
+
if (!Array.isArray(cursor)) {
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
const filtered = cursor.filter((entry) => {
|
|
476
|
+
const cmds = extractCommands(entry);
|
|
477
|
+
if (cmds.length === 0) {
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
return !cmds.some((cmd) => fabricCommands.some((fabric) => cmd === fabric || cmd.endsWith(fabric)));
|
|
481
|
+
});
|
|
482
|
+
const leaf = chain[chain.length - 1];
|
|
483
|
+
leaf.parent[leaf.key] = filtered;
|
|
484
|
+
if (filtered.length > 0) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
488
|
+
const { parent, key } = chain[i];
|
|
489
|
+
const value = parent[key];
|
|
490
|
+
const isEmpty = Array.isArray(value) && value.length === 0 || value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
491
|
+
if (!isEmpty) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
delete parent[key];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function extractClaudeCommands(entry) {
|
|
498
|
+
if (entry === null || typeof entry !== "object") {
|
|
499
|
+
return [];
|
|
500
|
+
}
|
|
501
|
+
const obj = entry;
|
|
502
|
+
const inner = obj["hooks"];
|
|
503
|
+
if (!Array.isArray(inner)) {
|
|
504
|
+
return [];
|
|
505
|
+
}
|
|
506
|
+
const out = [];
|
|
507
|
+
for (const sub of inner) {
|
|
508
|
+
if (sub === null || typeof sub !== "object") {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const cmd = sub["command"];
|
|
512
|
+
if (typeof cmd === "string") {
|
|
513
|
+
out.push(cmd);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
function extractFlatCommands(entry) {
|
|
519
|
+
if (entry === null || typeof entry !== "object") {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
const cmd = entry["command"];
|
|
523
|
+
return typeof cmd === "string" ? [cmd] : [];
|
|
524
|
+
}
|
|
525
|
+
function jsonEqual(a, b) {
|
|
526
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/commands/uninstall.ts
|
|
530
|
+
var UNINSTALL_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("uninstall-wizard-group-cancelled");
|
|
531
|
+
var KNOWLEDGE_SUBDIRS = [
|
|
532
|
+
"decisions",
|
|
533
|
+
"pitfalls",
|
|
534
|
+
"guidelines",
|
|
535
|
+
"models",
|
|
536
|
+
"processes",
|
|
537
|
+
"pending"
|
|
538
|
+
];
|
|
539
|
+
var FABRIC_STATE_FILES = ["agents.meta.json", "events.jsonl", "forensic.json"];
|
|
540
|
+
var uninstallCommand = defineCommand({
|
|
541
|
+
meta: {
|
|
542
|
+
name: "uninstall",
|
|
543
|
+
description: t("cli.uninstall.description")
|
|
544
|
+
},
|
|
545
|
+
args: {
|
|
546
|
+
debug: {
|
|
547
|
+
type: "boolean",
|
|
548
|
+
description: t("cli.uninstall.args.debug.description"),
|
|
549
|
+
default: false
|
|
550
|
+
},
|
|
551
|
+
"dry-run": {
|
|
552
|
+
type: "boolean",
|
|
553
|
+
description: t("cli.uninstall.args.dry-run.description"),
|
|
554
|
+
default: false
|
|
555
|
+
},
|
|
556
|
+
target: {
|
|
557
|
+
type: "string",
|
|
558
|
+
description: t("cli.uninstall.args.target.description")
|
|
559
|
+
},
|
|
560
|
+
yes: {
|
|
561
|
+
type: "boolean",
|
|
562
|
+
description: t("cli.uninstall.args.yes.description"),
|
|
563
|
+
default: false
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
async run({ args }) {
|
|
567
|
+
await runUninstallCommand(args);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
var uninstall_default = uninstallCommand;
|
|
571
|
+
async function runUninstallCommand(args) {
|
|
572
|
+
const logger = createDebugLogger(args.debug);
|
|
573
|
+
const resolution = resolveDevMode(args.target, process.cwd());
|
|
574
|
+
const intent = resolveUninstallCliIntent(args, resolution.target);
|
|
575
|
+
logger(`uninstall target source: ${resolution.source}`);
|
|
576
|
+
for (const step of resolution.chain) {
|
|
577
|
+
logger(step);
|
|
578
|
+
}
|
|
579
|
+
const supports = detectClientSupports(intent.target);
|
|
580
|
+
const basePlan = await buildUninstallExecutionPlan(intent.target, {
|
|
581
|
+
...intent.options
|
|
582
|
+
// Carry through interactive flag for plan-summary printing.
|
|
583
|
+
});
|
|
584
|
+
const planWithSupports = {
|
|
585
|
+
...basePlan,
|
|
586
|
+
interactive: intent.interactiveSummary && !intent.wizardEnabled,
|
|
587
|
+
supports
|
|
588
|
+
};
|
|
589
|
+
const finalPlan = intent.wizardEnabled ? await resolveUninstallExecutionPlanWithWizard(planWithSupports, createDefaultUninstallWizardAdapter()) : planWithSupports;
|
|
590
|
+
if (finalPlan === null) {
|
|
591
|
+
process.exitCode = 130;
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
if (finalPlan.options.planOnly) {
|
|
595
|
+
printUninstallPlanPreview(finalPlan);
|
|
596
|
+
return {
|
|
597
|
+
plan: finalPlan,
|
|
598
|
+
stageResults: finalPlan.stages.map((stage) => ({
|
|
599
|
+
name: stage.name,
|
|
600
|
+
disposition: "skipped",
|
|
601
|
+
steps: []
|
|
602
|
+
}))
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
if (intent.interactiveSummary && !intent.wizardEnabled && args.yes !== true) {
|
|
606
|
+
const proceed = await confirmDestructive(finalPlan);
|
|
607
|
+
if (!proceed) {
|
|
608
|
+
process.exitCode = 130;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const result = await executeUninstallExecutionPlan(finalPlan);
|
|
613
|
+
printUninstallSummary(result);
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
function resolveUninstallCliIntent(args, targetInput) {
|
|
617
|
+
const target = normalizeTarget(targetInput);
|
|
618
|
+
const terminalInteractive = isInteractiveUninstall();
|
|
619
|
+
const planOnly = args["dry-run"] === true;
|
|
620
|
+
const options = {
|
|
621
|
+
planOnly
|
|
622
|
+
};
|
|
623
|
+
return {
|
|
624
|
+
target,
|
|
625
|
+
options,
|
|
626
|
+
interactiveSummary: terminalInteractive,
|
|
627
|
+
wizardEnabled: shouldUseUninstallWizard(args, terminalInteractive) && !planOnly
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
function shouldUseUninstallWizard(args, terminalInteractive = isInteractiveUninstall()) {
|
|
631
|
+
return terminalInteractive && args.yes !== true;
|
|
632
|
+
}
|
|
633
|
+
async function buildUninstallExecutionPlan(target, options = {}) {
|
|
634
|
+
const scaffold = buildUninstallFabricPlan(target, options);
|
|
635
|
+
const supports = detectClientSupports(target);
|
|
636
|
+
const stages = [
|
|
637
|
+
{ name: "scaffold", skipped: Boolean(options.skipScaffold) },
|
|
638
|
+
{ name: "bootstrap", skipped: Boolean(options.skipBootstrap) },
|
|
639
|
+
{ name: "mcp", skipped: Boolean(options.skipMcp) }
|
|
640
|
+
];
|
|
641
|
+
return {
|
|
642
|
+
target,
|
|
643
|
+
options,
|
|
644
|
+
interactive: false,
|
|
645
|
+
supports,
|
|
646
|
+
scaffold,
|
|
647
|
+
stages
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
function buildUninstallFabricPlan(target, options = {}) {
|
|
651
|
+
const absTarget = normalizeTarget(target);
|
|
652
|
+
const fabricDir = join2(absTarget, ".fabric");
|
|
653
|
+
const personalKnowledgeDir = resolve(resolvePersonalFabricRoot(), ".fabric", "knowledge");
|
|
654
|
+
const entries = [];
|
|
655
|
+
for (const name of FABRIC_STATE_FILES) {
|
|
656
|
+
const p = join2(fabricDir, name);
|
|
657
|
+
entries.push({ path: p, kind: "state-file", absent: !existsSync2(p) });
|
|
658
|
+
}
|
|
659
|
+
for (const sub of KNOWLEDGE_SUBDIRS) {
|
|
660
|
+
const gk = join2(fabricDir, "knowledge", sub, ".gitkeep");
|
|
661
|
+
entries.push({ path: gk, kind: "gitkeep", absent: !existsSync2(gk) });
|
|
662
|
+
}
|
|
663
|
+
const safeEntries = entries.filter((entry) => !isInsidePersonalRoot(entry.path, personalKnowledgeDir));
|
|
664
|
+
return {
|
|
665
|
+
target: absTarget,
|
|
666
|
+
fabricDir,
|
|
667
|
+
personalKnowledgeDir,
|
|
668
|
+
options,
|
|
669
|
+
entries: safeEntries
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async function executeUninstallFabricPlan(plan) {
|
|
673
|
+
const results = [];
|
|
674
|
+
for (const entry of plan.entries) {
|
|
675
|
+
if (entry.absent) {
|
|
676
|
+
results.push({
|
|
677
|
+
step: scaffoldStepLabel(entry.kind),
|
|
678
|
+
path: entry.path,
|
|
679
|
+
status: "skipped",
|
|
680
|
+
message: "absent"
|
|
681
|
+
});
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
await rm2(entry.path, { force: true });
|
|
686
|
+
results.push({ step: scaffoldStepLabel(entry.kind), path: entry.path, status: "removed" });
|
|
687
|
+
} catch (error) {
|
|
688
|
+
results.push({
|
|
689
|
+
step: scaffoldStepLabel(entry.kind),
|
|
690
|
+
path: entry.path,
|
|
691
|
+
status: "error",
|
|
692
|
+
message: error instanceof Error ? error.message : String(error)
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return results;
|
|
697
|
+
}
|
|
698
|
+
function scaffoldStepLabel(kind) {
|
|
699
|
+
switch (kind) {
|
|
700
|
+
case "state-file":
|
|
701
|
+
return "scaffold-state";
|
|
702
|
+
case "gitkeep":
|
|
703
|
+
return "scaffold-gitkeep";
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
async function uninstallMcpClients(target, options = {}) {
|
|
707
|
+
const workspaceRoot = resolve(target);
|
|
708
|
+
const writers = resolveClients(workspaceRoot, {});
|
|
709
|
+
const details = [];
|
|
710
|
+
const results = [];
|
|
711
|
+
for (const writer of writers) {
|
|
712
|
+
let configPath;
|
|
713
|
+
try {
|
|
714
|
+
configPath = await writer.detect(workspaceRoot);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
717
|
+
details.push({ client: writer.clientKind, status: "error", message });
|
|
718
|
+
results.push({
|
|
719
|
+
step: `mcp-${writer.clientKind}`,
|
|
720
|
+
path: "",
|
|
721
|
+
status: "error",
|
|
722
|
+
message
|
|
723
|
+
});
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (configPath === null) {
|
|
727
|
+
details.push({ client: writer.clientKind, status: "skipped", message: "no-config-path" });
|
|
728
|
+
results.push({
|
|
729
|
+
step: `mcp-${writer.clientKind}`,
|
|
730
|
+
path: "",
|
|
731
|
+
status: "skipped",
|
|
732
|
+
message: "no-config-path"
|
|
733
|
+
});
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (options.dryRun === true) {
|
|
737
|
+
details.push({ client: writer.clientKind, status: "dry-run", path: configPath });
|
|
738
|
+
results.push({
|
|
739
|
+
step: `mcp-${writer.clientKind}`,
|
|
740
|
+
path: configPath,
|
|
741
|
+
status: "skipped",
|
|
742
|
+
message: "dry-run"
|
|
743
|
+
});
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
let removeResult;
|
|
747
|
+
try {
|
|
748
|
+
removeResult = await writer.remove("fabric", workspaceRoot);
|
|
749
|
+
} catch (error) {
|
|
750
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
751
|
+
details.push({ client: writer.clientKind, status: "error", path: configPath, message });
|
|
752
|
+
results.push({
|
|
753
|
+
step: `mcp-${writer.clientKind}`,
|
|
754
|
+
path: configPath,
|
|
755
|
+
status: "error",
|
|
756
|
+
message
|
|
757
|
+
});
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
details.push({
|
|
761
|
+
client: writer.clientKind,
|
|
762
|
+
status: removeResult.status,
|
|
763
|
+
path: removeResult.path,
|
|
764
|
+
message: removeResult.message
|
|
765
|
+
});
|
|
766
|
+
results.push({
|
|
767
|
+
step: `mcp-${writer.clientKind}`,
|
|
768
|
+
path: removeResult.path ?? configPath,
|
|
769
|
+
status: removeResult.status === "removed" ? "removed" : removeResult.status === "error" ? "error" : "skipped",
|
|
770
|
+
message: removeResult.message
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return { details, results };
|
|
774
|
+
}
|
|
775
|
+
async function executeUninstallExecutionPlan(plan) {
|
|
776
|
+
const stageResults = [];
|
|
777
|
+
for (const stage of plan.stages) {
|
|
778
|
+
if (stage.skipped) {
|
|
779
|
+
stageResults.push({ name: stage.name, disposition: "skipped", steps: [] });
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
console.log(formatUninstallStageHeader(stage.name));
|
|
783
|
+
try {
|
|
784
|
+
const steps = await executeUninstallStage(plan, stage.name);
|
|
785
|
+
const disposition = steps.some((s) => s.status === "error") ? "failed" : "ran";
|
|
786
|
+
stageResults.push({ name: stage.name, disposition, steps });
|
|
787
|
+
console.log(formatUninstallStageResult(stage.name, steps));
|
|
788
|
+
} catch (error) {
|
|
789
|
+
stageResults.push({
|
|
790
|
+
name: stage.name,
|
|
791
|
+
disposition: "failed",
|
|
792
|
+
steps: [
|
|
793
|
+
{
|
|
794
|
+
step: stage.name,
|
|
795
|
+
path: plan.target,
|
|
796
|
+
status: "error",
|
|
797
|
+
message: error instanceof Error ? error.message : String(error)
|
|
798
|
+
}
|
|
799
|
+
]
|
|
800
|
+
});
|
|
801
|
+
writeStderr(formatUninstallStageFailure(stage.name, error));
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return { plan, stageResults };
|
|
805
|
+
}
|
|
806
|
+
async function executeUninstallStage(plan, stageName) {
|
|
807
|
+
switch (stageName) {
|
|
808
|
+
case "scaffold":
|
|
809
|
+
return executeUninstallFabricPlan(plan.scaffold);
|
|
810
|
+
case "bootstrap": {
|
|
811
|
+
const opts = {};
|
|
812
|
+
return uninstallBootstrapStage(plan.target, opts);
|
|
813
|
+
}
|
|
814
|
+
case "mcp": {
|
|
815
|
+
const { results } = await uninstallMcpClients(plan.target);
|
|
816
|
+
return results;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function uninstallFabric(target, options = {}) {
|
|
821
|
+
const plan = await buildUninstallExecutionPlan(target, options);
|
|
822
|
+
return executeUninstallExecutionPlan(plan);
|
|
823
|
+
}
|
|
824
|
+
async function resolveUninstallExecutionPlanWithWizard(basePlan, wizardAdapter) {
|
|
825
|
+
const selection = await wizardAdapter.run({
|
|
826
|
+
target: basePlan.target,
|
|
827
|
+
options: basePlan.options,
|
|
828
|
+
supports: basePlan.supports,
|
|
829
|
+
lockedStages: []
|
|
830
|
+
});
|
|
831
|
+
if (selection === null) {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
const nextOptions = {
|
|
835
|
+
...basePlan.options,
|
|
836
|
+
skipScaffold: !selection.scaffold,
|
|
837
|
+
skipBootstrap: !selection.bootstrap,
|
|
838
|
+
skipMcp: !selection.mcp
|
|
839
|
+
};
|
|
840
|
+
const rebuilt = await buildUninstallExecutionPlan(basePlan.target, nextOptions);
|
|
841
|
+
return {
|
|
842
|
+
...rebuilt,
|
|
843
|
+
interactive: false,
|
|
844
|
+
supports: basePlan.supports
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
function createDefaultUninstallWizardAdapter() {
|
|
848
|
+
return {
|
|
849
|
+
async run(context) {
|
|
850
|
+
intro(t("cli.uninstall.wizard.intro"));
|
|
851
|
+
note(
|
|
852
|
+
t("cli.uninstall.wizard.overview.body", {
|
|
853
|
+
target: context.target
|
|
854
|
+
}),
|
|
855
|
+
t("cli.uninstall.wizard.overview.title")
|
|
856
|
+
);
|
|
857
|
+
printUninstallPlanSummary(context.target, context.options, context.supports);
|
|
858
|
+
log.step(t("cli.uninstall.wizard.step.target"));
|
|
859
|
+
const continueWithTarget = await confirm({
|
|
860
|
+
message: t("cli.uninstall.wizard.target.confirm", { target: context.target }),
|
|
861
|
+
initialValue: true
|
|
862
|
+
});
|
|
863
|
+
if (isCancel(continueWithTarget) || !continueWithTarget) {
|
|
864
|
+
emitUninstallWizardCancellation();
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
log.step(t("cli.uninstall.wizard.step.plan"));
|
|
868
|
+
let groupedSelection;
|
|
869
|
+
try {
|
|
870
|
+
groupedSelection = await group(
|
|
871
|
+
{
|
|
872
|
+
scaffold: async () => context.lockedStages.includes("scaffold") ? false : confirmInGroup({
|
|
873
|
+
message: t("cli.uninstall.wizard.stage.scaffold", {
|
|
874
|
+
defaultValue: formatPromptDefault(!context.options.skipScaffold)
|
|
875
|
+
}),
|
|
876
|
+
initialValue: !context.options.skipScaffold
|
|
877
|
+
}),
|
|
878
|
+
bootstrap: async () => context.lockedStages.includes("bootstrap") ? false : confirmInGroup({
|
|
879
|
+
message: t("cli.uninstall.wizard.stage.bootstrap", {
|
|
880
|
+
defaultValue: formatPromptDefault(!context.options.skipBootstrap)
|
|
881
|
+
}),
|
|
882
|
+
initialValue: !context.options.skipBootstrap
|
|
883
|
+
}),
|
|
884
|
+
mcp: async () => context.lockedStages.includes("mcp") ? false : confirmInGroup({
|
|
885
|
+
message: t("cli.uninstall.wizard.stage.mcp", {
|
|
886
|
+
defaultValue: formatPromptDefault(!context.options.skipMcp)
|
|
887
|
+
}),
|
|
888
|
+
initialValue: !context.options.skipMcp
|
|
889
|
+
})
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
onCancel() {
|
|
893
|
+
throw UNINSTALL_WIZARD_GROUP_CANCELLED;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
);
|
|
897
|
+
} catch (error) {
|
|
898
|
+
if (error === UNINSTALL_WIZARD_GROUP_CANCELLED) {
|
|
899
|
+
emitUninstallWizardCancellation();
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
throw error;
|
|
903
|
+
}
|
|
904
|
+
const previewOptions = {
|
|
905
|
+
...context.options,
|
|
906
|
+
skipScaffold: !groupedSelection.scaffold,
|
|
907
|
+
skipBootstrap: !groupedSelection.bootstrap,
|
|
908
|
+
skipMcp: !groupedSelection.mcp
|
|
909
|
+
};
|
|
910
|
+
log.step(t("cli.uninstall.wizard.step.review"));
|
|
911
|
+
printUninstallPlanSummary(context.target, previewOptions, context.supports);
|
|
912
|
+
const confirmed = await confirm({
|
|
913
|
+
message: t("cli.uninstall.wizard.execute.confirm"),
|
|
914
|
+
initialValue: true
|
|
915
|
+
});
|
|
916
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
917
|
+
emitUninstallWizardCancellation();
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
outro(t("cli.uninstall.wizard.outro"));
|
|
921
|
+
return groupedSelection;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function emitUninstallWizardCancellation() {
|
|
926
|
+
cancel(t("cli.uninstall.wizard.cancelled"));
|
|
927
|
+
}
|
|
928
|
+
async function confirmInGroup(options) {
|
|
929
|
+
const result = await confirm(options);
|
|
930
|
+
if (isCancel(result)) {
|
|
931
|
+
throw UNINSTALL_WIZARD_GROUP_CANCELLED;
|
|
932
|
+
}
|
|
933
|
+
return result;
|
|
934
|
+
}
|
|
935
|
+
async function confirmDestructive(plan) {
|
|
936
|
+
printUninstallPlanSummary(plan.target, plan.options, plan.supports);
|
|
937
|
+
const answer = await confirm({
|
|
938
|
+
message: t("cli.uninstall.confirm.proceed", { target: plan.target }),
|
|
939
|
+
initialValue: false
|
|
940
|
+
});
|
|
941
|
+
if (isCancel(answer)) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
return answer === true;
|
|
945
|
+
}
|
|
946
|
+
function printUninstallPlanPreview(plan) {
|
|
947
|
+
console.log(t("cli.uninstall.plan.preview-title"));
|
|
948
|
+
printUninstallPlanSummary(plan.target, plan.options, plan.supports);
|
|
949
|
+
console.log(
|
|
950
|
+
t("cli.uninstall.plan.preview-result", {
|
|
951
|
+
scaffold: yesNoLabel(!plan.options.skipScaffold),
|
|
952
|
+
bootstrap: yesNoLabel(!plan.options.skipBootstrap),
|
|
953
|
+
mcp: yesNoLabel(!plan.options.skipMcp)
|
|
954
|
+
})
|
|
955
|
+
);
|
|
956
|
+
if (!plan.options.skipScaffold && plan.scaffold.entries.length > 0) {
|
|
957
|
+
console.log(t("cli.uninstall.plan.scaffold-entries.title"));
|
|
958
|
+
for (const entry of plan.scaffold.entries) {
|
|
959
|
+
const marker = entry.absent ? paint.muted("(absent)") : paint.success("(present)");
|
|
960
|
+
console.log(` - ${entry.path} ${marker}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
function printUninstallPlanSummary(target, options, supports) {
|
|
965
|
+
console.log(t("cli.uninstall.plan.title"));
|
|
966
|
+
console.log(t("cli.uninstall.plan.target", { target }));
|
|
967
|
+
console.log(
|
|
968
|
+
t("cli.uninstall.plan.actions", {
|
|
969
|
+
scaffold: yesNoLabel(!options.skipScaffold),
|
|
970
|
+
bootstrap: yesNoLabel(!options.skipBootstrap),
|
|
971
|
+
mcp: yesNoLabel(!options.skipMcp)
|
|
972
|
+
})
|
|
973
|
+
);
|
|
974
|
+
const detected = supports.filter((support) => support.detected);
|
|
975
|
+
console.log(
|
|
976
|
+
t("cli.uninstall.plan.detected", {
|
|
977
|
+
clients: detected.length > 0 ? detected.map((support) => support.label).join(", ") : t("cli.shared.none")
|
|
978
|
+
})
|
|
979
|
+
);
|
|
980
|
+
console.log(t("cli.uninstall.plan.preserves"));
|
|
981
|
+
console.log(` - ${target}/.fabric/knowledge/ ${paint.muted(t("cli.uninstall.plan.preserves.knowledge"))}`);
|
|
982
|
+
console.log(` - ~/.fabric/knowledge/ ${paint.muted(t("cli.uninstall.plan.preserves.personal"))}`);
|
|
983
|
+
}
|
|
984
|
+
function printUninstallSummary(result) {
|
|
985
|
+
const removed = result.stageResults.flatMap(
|
|
986
|
+
(stage) => stage.steps.filter((s) => s.status === "removed")
|
|
987
|
+
).length;
|
|
988
|
+
const skipped = result.stageResults.flatMap(
|
|
989
|
+
(stage) => stage.steps.filter((s) => s.status === "skipped")
|
|
990
|
+
).length;
|
|
991
|
+
const errors = result.stageResults.flatMap(
|
|
992
|
+
(stage) => stage.steps.filter((s) => s.status === "error")
|
|
993
|
+
).length;
|
|
994
|
+
note(
|
|
995
|
+
t("cli.uninstall.summary.body", {
|
|
996
|
+
removed: String(removed),
|
|
997
|
+
skipped: String(skipped),
|
|
998
|
+
errors: String(errors)
|
|
999
|
+
}),
|
|
1000
|
+
t("cli.uninstall.summary.title")
|
|
1001
|
+
);
|
|
1002
|
+
for (const stage of result.stageResults) {
|
|
1003
|
+
for (const step of stage.steps) {
|
|
1004
|
+
if (step.status === "error") {
|
|
1005
|
+
writeStderr(`${paint.error(t("cli.shared.error"))} ${stage.name}/${step.step} ${step.path}: ${step.message ?? "unknown error"}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
function formatUninstallStageHeader(stageName) {
|
|
1011
|
+
return `${paint.ai(t("cli.shared.next"))} ${paint.muted(t(`cli.uninstall.stages.${stageName}`))}`;
|
|
1012
|
+
}
|
|
1013
|
+
function formatUninstallStageResult(stageName, steps) {
|
|
1014
|
+
const removedCount = steps.filter((s) => s.status === "removed").length;
|
|
1015
|
+
const skippedCount = steps.filter((s) => s.status === "skipped").length;
|
|
1016
|
+
const errorCount = steps.filter((s) => s.status === "error").length;
|
|
1017
|
+
const counts = `removed=${removedCount} skipped=${skippedCount} errors=${errorCount}`;
|
|
1018
|
+
const label = errorCount > 0 ? paint.warn(t("cli.uninstall.stages.completed-with-errors")) : paint.success(t("cli.uninstall.stages.completed"));
|
|
1019
|
+
return `${label} ${stageName}: ${counts}`;
|
|
1020
|
+
}
|
|
1021
|
+
function formatUninstallStageFailure(stage, error) {
|
|
1022
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1023
|
+
return `${paint.error(t("cli.uninstall.stages.failed"))} ${stage}: ${message}`;
|
|
1024
|
+
}
|
|
1025
|
+
function resolvePersonalFabricRoot() {
|
|
1026
|
+
return process.env.FABRIC_HOME ?? homedir();
|
|
1027
|
+
}
|
|
1028
|
+
function isInsidePersonalRoot(candidate, personalKnowledgeDir) {
|
|
1029
|
+
const candidateAbs = resolve(candidate);
|
|
1030
|
+
const rootAbs = resolve(personalKnowledgeDir);
|
|
1031
|
+
if (candidateAbs === rootAbs) {
|
|
1032
|
+
return true;
|
|
1033
|
+
}
|
|
1034
|
+
const rel = relative(rootAbs, candidateAbs);
|
|
1035
|
+
return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel) && !rel.split(sep).includes("..");
|
|
1036
|
+
}
|
|
1037
|
+
function normalizeTarget(targetInput) {
|
|
1038
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
1039
|
+
}
|
|
1040
|
+
function assertExistingDirectory(target) {
|
|
1041
|
+
if (!existsSync2(target) || !statSync(target).isDirectory()) {
|
|
1042
|
+
throw new Error(t("cli.uninstall.errors.target-not-directory", { path: target }));
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
function isInteractiveUninstall() {
|
|
1046
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && Boolean(process.stderr.isTTY);
|
|
1047
|
+
}
|
|
1048
|
+
function formatPromptDefault(value) {
|
|
1049
|
+
return value ? "Y/n" : "y/N";
|
|
1050
|
+
}
|
|
1051
|
+
function yesNoLabel(value) {
|
|
1052
|
+
return value ? t("cli.shared.yes") : t("cli.shared.no");
|
|
1053
|
+
}
|
|
1054
|
+
function writeStderr(message) {
|
|
1055
|
+
process.stderr.write(`${message}
|
|
1056
|
+
`);
|
|
1057
|
+
}
|
|
1058
|
+
export {
|
|
1059
|
+
assertExistingDirectory,
|
|
1060
|
+
buildUninstallExecutionPlan,
|
|
1061
|
+
buildUninstallFabricPlan,
|
|
1062
|
+
createDefaultUninstallWizardAdapter,
|
|
1063
|
+
uninstall_default as default,
|
|
1064
|
+
executeUninstallExecutionPlan,
|
|
1065
|
+
executeUninstallFabricPlan,
|
|
1066
|
+
isInsidePersonalRoot,
|
|
1067
|
+
resolveUninstallExecutionPlanWithWizard,
|
|
1068
|
+
runUninstallCommand,
|
|
1069
|
+
shouldUseUninstallWizard,
|
|
1070
|
+
uninstallCommand,
|
|
1071
|
+
uninstallFabric,
|
|
1072
|
+
uninstallMcpClients
|
|
1073
|
+
};
|