@fenglimg/fabric-cli 2.0.0-rc.1 → 2.0.0-rc.11

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