@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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -5
  3. package/dist/chunk-BATF4PEJ.js +361 -0
  4. package/dist/{chunk-OBQU6NHO.js → chunk-COI5VDFU.js} +0 -18
  5. package/dist/chunk-F46ORPOA.js +903 -0
  6. package/dist/chunk-HFQVXY6P.js +86 -0
  7. package/dist/chunk-L4Q55UC4.js +52 -0
  8. package/dist/chunk-LFIKMVY7.js +27 -0
  9. package/dist/chunk-MF3OTILQ.js +544 -0
  10. package/dist/chunk-PWLW3B57.js +18 -0
  11. package/dist/chunk-RYAFBNES.js +33 -0
  12. package/dist/chunk-T5RPGCCM.js +40 -0
  13. package/dist/chunk-WU6GAPKH.js +36 -0
  14. package/dist/config-XJIPZNUP.js +13 -0
  15. package/dist/doctor-QVNPHLJK.js +920 -0
  16. package/dist/index.js +23 -8
  17. package/dist/{init-BIRSIOXO.js → install-2HDO5FTQ.js} +807 -705
  18. package/dist/metrics-ACEQFPDU.js +122 -0
  19. package/dist/onboard-coverage-MFCAEBDO.js +220 -0
  20. package/dist/{plan-context-hint-QMUPAXIB.js → plan-context-hint-FC6P3WFE.js} +34 -28
  21. package/dist/scope-explain-2F2R5URO.js +33 -0
  22. package/dist/status-GLQWLWH6.js +23 -0
  23. package/dist/store-XTSE5TY6.js +105 -0
  24. package/dist/sync-BJCWDPNC.js +245 -0
  25. package/dist/uninstall-TAXSUSKH.js +1073 -0
  26. package/dist/whoami-B6AEMSEV.js +31 -0
  27. package/package.json +30 -5
  28. package/templates/hooks/cite-policy-evict.cjs +231 -0
  29. package/templates/hooks/configs/README.md +29 -6
  30. package/templates/hooks/configs/claude-code.json +14 -3
  31. package/templates/hooks/configs/codex-hooks.json +6 -3
  32. package/templates/hooks/configs/cursor-hooks.json +8 -10
  33. package/templates/hooks/fabric-hint.cjs +873 -105
  34. package/templates/hooks/knowledge-hint-broad.cjs +549 -135
  35. package/templates/hooks/knowledge-hint-narrow.cjs +830 -26
  36. package/templates/hooks/lib/banner-i18n.cjs +309 -0
  37. package/templates/hooks/lib/bindings-snapshot-reader.cjs +81 -0
  38. package/templates/hooks/lib/cite-contract-reminder.cjs +179 -0
  39. package/templates/hooks/lib/cite-line-parser.cjs +180 -0
  40. package/templates/hooks/lib/client-adapter.cjs +106 -0
  41. package/templates/hooks/lib/config-cache.cjs +107 -0
  42. package/templates/hooks/lib/state-store.cjs +84 -0
  43. package/templates/hooks/lib/summary-fallback.cjs +210 -0
  44. package/templates/skills/fabric-archive/SKILL.md +97 -419
  45. package/templates/skills/fabric-archive/ref/dry-run-scope.md +16 -0
  46. package/templates/skills/fabric-archive/ref/e5-cron-recap.md +58 -0
  47. package/templates/skills/fabric-archive/ref/i18n-policy.md +86 -0
  48. package/templates/skills/fabric-archive/ref/phase-0-range-resolution.md +156 -0
  49. package/templates/skills/fabric-archive/ref/phase-1-5-onboard.md +218 -0
  50. package/templates/skills/fabric-archive/ref/phase-1-cross-session.md +62 -0
  51. package/templates/skills/fabric-archive/ref/phase-2-5-viability.md +68 -0
  52. package/templates/skills/fabric-archive/ref/phase-3-5-scope.md +108 -0
  53. package/templates/skills/fabric-archive/ref/phase-3-classify.md +63 -0
  54. package/templates/skills/fabric-archive/ref/phase-4-5-emit.md +78 -0
  55. package/templates/skills/fabric-archive/ref/phase-4-mcp-persist.md +89 -0
  56. package/templates/skills/fabric-archive/ref/rc-history.md +38 -0
  57. package/templates/skills/fabric-archive/ref/worked-examples.md +78 -0
  58. package/templates/skills/fabric-import/SKILL.md +77 -514
  59. package/templates/skills/fabric-import/ref/checkpoint-state.md +85 -0
  60. package/templates/skills/fabric-import/ref/i18n-policy.md +79 -0
  61. package/templates/skills/fabric-import/ref/output-contract.md +61 -0
  62. package/templates/skills/fabric-import/ref/phase-2-mining.md +213 -0
  63. package/templates/skills/fabric-import/ref/phase-3-dedup.md +75 -0
  64. package/templates/skills/fabric-import/ref/state-recovery.md +57 -0
  65. package/templates/skills/fabric-import/ref/worked-examples.md +127 -0
  66. package/templates/skills/fabric-review/SKILL.md +90 -284
  67. package/templates/skills/fabric-review/ref/askuserquestion-policy.md +66 -0
  68. package/templates/skills/fabric-review/ref/i18n-policy.md +111 -0
  69. package/templates/skills/fabric-review/ref/modify-flow.md +103 -0
  70. package/templates/skills/fabric-review/ref/output-contract.md +58 -0
  71. package/templates/skills/fabric-review/ref/per-mode-flows.md +155 -0
  72. package/templates/skills/fabric-review/ref/semantic-check.md +26 -0
  73. package/templates/skills/fabric-review/ref/worked-examples.md +95 -0
  74. package/templates/skills/fabric-sync/SKILL.md +46 -0
  75. package/templates/skills/lib/shared-policy.md +69 -0
  76. package/dist/chunk-6ICJICVU.js +0 -10
  77. package/dist/chunk-74SZWYPH.js +0 -658
  78. package/dist/chunk-EYIDD2YS.js +0 -1000
  79. package/dist/doctor-T7JWODKG.js +0 -282
  80. package/dist/hooks-Y74Y5LQS.js +0 -12
  81. package/dist/scan-LMK3UCWL.js +0 -22
  82. package/dist/serve-H554BHLG.js +0 -124
  83. package/templates/agents-md/AGENTS.md.template +0 -59
  84. package/templates/bootstrap/CLAUDE.md +0 -8
  85. package/templates/bootstrap/codex-AGENTS-header.md +0 -6
  86. 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
+ };