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

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.
@@ -10,7 +10,12 @@ var ALL_PATHS_SENTINEL = "**";
10
10
  var planContextHintCommand = defineCommand({
11
11
  meta: {
12
12
  name: "plan-context-hint",
13
- description: "Emit versioned knowledge hint JSON to stdout. Used by rc.6 hooks and the fabric-import skill."
13
+ description: "Emit versioned knowledge hint JSON to stdout. Used by rc.6 hooks and the fabric-import skill.",
14
+ // rc.15 TASK-004 (C8): hidden from `fab --help` listing. The command stays
15
+ // callable so hook scripts and the fabric-import skill can still invoke
16
+ // it via `fab plan-context-hint ...`; it just no longer appears in the
17
+ // top-level usage banner alongside install/doctor/serve/uninstall/config.
18
+ hidden: true
14
19
  },
15
20
  args: {
16
21
  paths: {
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ hasActionHint,
3
4
  paint,
5
+ renderFabricError,
4
6
  symbol
5
- } from "./chunk-WWNXR34K.js";
7
+ } from "./chunk-G2CIOLD4.js";
8
+ import {
9
+ t
10
+ } from "./chunk-6ICJICVU.js";
6
11
  import {
7
12
  createDebugLogger,
8
13
  resolveDevMode
9
14
  } from "./chunk-OBQU6NHO.js";
10
- import {
11
- t
12
- } from "./chunk-6ICJICVU.js";
13
15
 
14
16
  // src/commands/serve.ts
15
17
  import { defineCommand } from "citty";
@@ -39,11 +41,6 @@ var serveCommand = defineCommand({
39
41
  type: "boolean",
40
42
  description: t("cli.serve.args.debug.description"),
41
43
  default: false
42
- },
43
- force: {
44
- type: "boolean",
45
- description: t("cli.serve.args.force.description"),
46
- default: false
47
44
  }
48
45
  },
49
46
  async run({ args }) {
@@ -55,7 +52,15 @@ var serveCommand = defineCommand({
55
52
  const authToken = readAuthTokenFromEnv();
56
53
  const host = validateHost(requestedHost, authToken);
57
54
  const projectRoot = resolution.target;
58
- acquireLock(projectRoot, { force: args.force });
55
+ try {
56
+ acquireLock(projectRoot);
57
+ } catch (err) {
58
+ if (hasActionHint(err)) {
59
+ renderFabricError(err);
60
+ process.exit(1);
61
+ }
62
+ throw err;
63
+ }
59
64
  process.on("exit", () => {
60
65
  releaseLock(projectRoot);
61
66
  });
@@ -1,33 +1,33 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- detectClientSupports,
4
- resolveClients
5
- } from "./chunk-HQLEHH4O.js";
6
2
  import {
7
3
  FABRIC_HOOK_COMMAND_PATHS,
4
+ FABRIC_SECTION_REGEX,
8
5
  HOOK_CONFIG_ARRAY_PATHS,
9
6
  HOOK_CONFIG_TARGETS,
10
7
  HOOK_SCRIPT_DESTINATIONS,
11
- IMPORT_POINTER_LINE,
12
- POINTER_LINE,
13
- POINTER_TARGETS,
14
- REVIEW_POINTER_LINE,
8
+ SECTION_TARGETS,
15
9
  SKILL_DESTINATIONS
16
- } from "./chunk-AW3G7ZH5.js";
10
+ } from "./chunk-UTF4YBDN.js";
17
11
  import {
18
- paint
19
- } from "./chunk-WWNXR34K.js";
12
+ detectClientSupports,
13
+ resolveClients
14
+ } from "./chunk-SKSYUHKK.js";
20
15
  import {
21
- createDebugLogger,
22
- resolveDevMode
23
- } from "./chunk-OBQU6NHO.js";
16
+ hasActionHint,
17
+ paint,
18
+ renderFabricError
19
+ } from "./chunk-G2CIOLD4.js";
24
20
  import {
25
21
  t
26
22
  } from "./chunk-6ICJICVU.js";
23
+ import {
24
+ createDebugLogger,
25
+ resolveDevMode
26
+ } from "./chunk-OBQU6NHO.js";
27
27
 
28
28
  // src/commands/uninstall.ts
29
29
  import { existsSync as existsSync2, statSync } from "fs";
30
- import { readdir as readdir2, rm as rm2 } from "fs/promises";
30
+ import { rm as rm2 } from "fs/promises";
31
31
  import { homedir } from "os";
32
32
  import { isAbsolute, join as join2, relative, resolve, sep } from "path";
33
33
  import { cancel, confirm, group, intro, isCancel, log, note, outro } from "@clack/prompts";
@@ -82,45 +82,42 @@ async function removeHookScripts(step, rels, projectRoot) {
82
82
  }
83
83
  return results;
84
84
  }
85
- async function unmergeClaudeCodeHookConfig(projectRoot, opts = {}) {
85
+ async function unmergeClaudeCodeHookConfig(projectRoot) {
86
86
  return unmergeHookConfig({
87
87
  step: "claude-hook-config",
88
88
  projectRoot,
89
89
  configRel: HOOK_CONFIG_TARGETS.claudeCode,
90
90
  arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.claudeCode],
91
91
  fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.claudeCode),
92
- extractCommands: extractClaudeCommands,
93
- cleanEmpties: opts.cleanEmpties === true
92
+ extractCommands: extractClaudeCommands
94
93
  });
95
94
  }
96
- async function unmergeCodexHookConfig(projectRoot, opts = {}) {
95
+ async function unmergeCodexHookConfig(projectRoot) {
97
96
  return unmergeHookConfig({
98
97
  step: "codex-hook-config",
99
98
  projectRoot,
100
99
  configRel: HOOK_CONFIG_TARGETS.codex,
101
100
  arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.codex],
102
101
  fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.codex),
103
- extractCommands: extractFlatCommands,
104
- cleanEmpties: opts.cleanEmpties === true
102
+ extractCommands: extractFlatCommands
105
103
  });
106
104
  }
107
- async function unmergeCursorHookConfig(projectRoot, opts = {}) {
105
+ async function unmergeCursorHookConfig(projectRoot) {
108
106
  return unmergeHookConfig({
109
107
  step: "cursor-hook-config",
110
108
  projectRoot,
111
109
  configRel: HOOK_CONFIG_TARGETS.cursor,
112
110
  arrayPaths: [...HOOK_CONFIG_ARRAY_PATHS.cursor],
113
111
  fabricCommands: Object.values(FABRIC_HOOK_COMMAND_PATHS.cursor),
114
- extractCommands: extractFlatCommands,
115
- cleanEmpties: opts.cleanEmpties === true
112
+ extractCommands: extractFlatCommands
116
113
  });
117
114
  }
118
- async function stripArchiveSkillPointers(projectRoot) {
115
+ async function stripFabricKnowledgeBaseSection(projectRoot) {
119
116
  const results = [];
120
- for (const rel of POINTER_TARGETS) {
117
+ for (const rel of SECTION_TARGETS) {
121
118
  const target = join(projectRoot, rel);
122
119
  if (!existsSync(target)) {
123
- results.push({ step: "pointer", path: target, status: "skipped", message: "absent" });
120
+ results.push({ step: "section", path: target, status: "skipped", message: "absent" });
124
121
  continue;
125
122
  }
126
123
  let existing;
@@ -128,30 +125,41 @@ async function stripArchiveSkillPointers(projectRoot) {
128
125
  existing = await readFile(target, "utf8");
129
126
  } catch (error) {
130
127
  results.push({
131
- step: "pointer",
128
+ step: "section",
132
129
  path: target,
133
130
  status: "error",
134
131
  message: error instanceof Error ? error.message : String(error)
135
132
  });
136
133
  continue;
137
134
  }
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");
135
+ const match = existing.match(FABRIC_SECTION_REGEX);
136
+ if (match === null) {
137
+ results.push({
138
+ step: "section",
139
+ path: target,
140
+ status: "skipped",
141
+ message: "no-fabric-section"
142
+ });
143
+ continue;
144
+ }
145
+ const before = existing.slice(0, match.index ?? 0);
146
+ const after = existing.slice((match.index ?? 0) + match[0].length);
147
+ const filtered = `${before}${after.replace(/^\r?\n/, "")}`;
140
148
  if (filtered === existing) {
141
149
  results.push({
142
- step: "pointer",
150
+ step: "section",
143
151
  path: target,
144
152
  status: "skipped",
145
- message: "no-fabric-pointers"
153
+ message: "no-fabric-section"
146
154
  });
147
155
  continue;
148
156
  }
149
157
  try {
150
158
  await atomicWriteText(target, filtered);
151
- results.push({ step: "pointer", path: target, status: "removed" });
159
+ results.push({ step: "section", path: target, status: "removed" });
152
160
  } catch (error) {
153
161
  results.push({
154
- step: "pointer",
162
+ step: "section",
155
163
  path: target,
156
164
  status: "error",
157
165
  message: error instanceof Error ? error.message : String(error)
@@ -160,31 +168,31 @@ async function stripArchiveSkillPointers(projectRoot) {
160
168
  }
161
169
  return results;
162
170
  }
163
- async function uninstallBootstrapStage(projectRoot, opts = {}) {
171
+ async function uninstallBootstrapStage(projectRoot, _opts = {}) {
164
172
  const results = [];
165
173
  await runAndCollect(
166
174
  results,
167
- "pointer",
175
+ "section",
168
176
  projectRoot,
169
- () => stripArchiveSkillPointers(projectRoot)
177
+ () => stripFabricKnowledgeBaseSection(projectRoot)
170
178
  );
171
179
  await runAndCollectOne(
172
180
  results,
173
181
  "cursor-hook-config",
174
182
  projectRoot,
175
- () => unmergeCursorHookConfig(projectRoot, opts)
183
+ () => unmergeCursorHookConfig(projectRoot)
176
184
  );
177
185
  await runAndCollectOne(
178
186
  results,
179
187
  "codex-hook-config",
180
188
  projectRoot,
181
- () => unmergeCodexHookConfig(projectRoot, opts)
189
+ () => unmergeCodexHookConfig(projectRoot)
182
190
  );
183
191
  await runAndCollectOne(
184
192
  results,
185
193
  "claude-hook-config",
186
194
  projectRoot,
187
- () => unmergeClaudeCodeHookConfig(projectRoot, opts)
195
+ () => unmergeClaudeCodeHookConfig(projectRoot)
188
196
  );
189
197
  await runAndCollect(
190
198
  results,
@@ -330,7 +338,7 @@ async function unmergeHookConfig(args) {
330
338
  }
331
339
  const next = JSON.parse(JSON.stringify(parsed));
332
340
  for (const dotted of args.arrayPaths) {
333
- pruneArrayAtPath(next, dotted, args.fabricCommands, args.extractCommands, args.cleanEmpties);
341
+ pruneArrayAtPath(next, dotted, args.fabricCommands, args.extractCommands);
334
342
  }
335
343
  if (jsonEqual(parsed, next)) {
336
344
  return { step: args.step, path: target, status: "skipped", message: "no-fabric-entries" };
@@ -347,7 +355,7 @@ async function unmergeHookConfig(args) {
347
355
  };
348
356
  }
349
357
  }
350
- function pruneArrayAtPath(root, path, fabricCommands, extractCommands, cleanEmpties) {
358
+ function pruneArrayAtPath(root, path, fabricCommands, extractCommands) {
351
359
  const keys = path.split(".");
352
360
  const chain = [];
353
361
  let cursor = root;
@@ -375,7 +383,7 @@ function pruneArrayAtPath(root, path, fabricCommands, extractCommands, cleanEmpt
375
383
  });
376
384
  const leaf = chain[chain.length - 1];
377
385
  leaf.parent[leaf.key] = filtered;
378
- if (!cleanEmpties || filtered.length > 0) {
386
+ if (filtered.length > 0) {
379
387
  return;
380
388
  }
381
389
  for (let i = chain.length - 1; i >= 0; i--) {
@@ -437,59 +445,24 @@ var uninstallCommand = defineCommand({
437
445
  description: t("cli.uninstall.description")
438
446
  },
439
447
  args: {
440
- target: {
441
- type: "string",
442
- description: t("cli.uninstall.args.target.description")
443
- },
444
448
  debug: {
445
449
  type: "boolean",
446
450
  description: t("cli.uninstall.args.debug.description"),
447
451
  default: false
448
452
  },
449
- force: {
453
+ "dry-run": {
450
454
  type: "boolean",
451
- description: t("cli.uninstall.args.force.description"),
455
+ description: t("cli.uninstall.args.dry-run.description"),
452
456
  default: false
453
457
  },
458
+ target: {
459
+ type: "string",
460
+ description: t("cli.uninstall.args.target.description")
461
+ },
454
462
  yes: {
455
463
  type: "boolean",
456
464
  description: t("cli.uninstall.args.yes.description"),
457
465
  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
466
  }
494
467
  },
495
468
  async run({ args }) {
@@ -505,7 +478,15 @@ async function runUninstallCommand(args) {
505
478
  for (const step of resolution.chain) {
506
479
  logger(step);
507
480
  }
508
- checkLockOrThrow(intent.target, { force: args.force });
481
+ try {
482
+ checkLockOrThrow(intent.target);
483
+ } catch (err) {
484
+ if (hasActionHint(err)) {
485
+ renderFabricError(err);
486
+ process.exit(1);
487
+ }
488
+ throw err;
489
+ }
509
490
  const supports = detectClientSupports(intent.target);
510
491
  const basePlan = await buildUninstallExecutionPlan(intent.target, {
511
492
  ...intent.options
@@ -516,7 +497,7 @@ async function runUninstallCommand(args) {
516
497
  interactive: intent.interactiveSummary && !intent.wizardEnabled,
517
498
  supports
518
499
  };
519
- const finalPlan = intent.wizardEnabled ? await resolveUninstallExecutionPlanWithWizard(planWithSupports, args, createDefaultUninstallWizardAdapter()) : planWithSupports;
500
+ const finalPlan = intent.wizardEnabled ? await resolveUninstallExecutionPlanWithWizard(planWithSupports, createDefaultUninstallWizardAdapter()) : planWithSupports;
520
501
  if (finalPlan === null) {
521
502
  process.exitCode = 130;
522
503
  return;
@@ -532,7 +513,7 @@ async function runUninstallCommand(args) {
532
513
  }))
533
514
  };
534
515
  }
535
- if (intent.interactiveSummary && !intent.wizardEnabled && args.yes !== true && args.force !== true) {
516
+ if (intent.interactiveSummary && !intent.wizardEnabled && args.yes !== true) {
536
517
  const proceed = await confirmDestructive(finalPlan);
537
518
  if (!proceed) {
538
519
  process.exitCode = 130;
@@ -546,25 +527,19 @@ async function runUninstallCommand(args) {
546
527
  function resolveUninstallCliIntent(args, targetInput) {
547
528
  const target = normalizeTarget(targetInput);
548
529
  const terminalInteractive = isInteractiveUninstall();
549
- const planOnly = args.plan === true;
530
+ const planOnly = args["dry-run"] === true;
550
531
  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
532
+ planOnly
558
533
  };
559
534
  return {
560
535
  target,
561
536
  options,
562
- interactiveSummary: args.interactive !== false && terminalInteractive,
537
+ interactiveSummary: terminalInteractive,
563
538
  wizardEnabled: shouldUseUninstallWizard(args, terminalInteractive) && !planOnly
564
539
  };
565
540
  }
566
541
  function shouldUseUninstallWizard(args, terminalInteractive = isInteractiveUninstall()) {
567
- return terminalInteractive && args.interactive !== false && args.yes !== true;
542
+ return terminalInteractive && args.yes !== true;
568
543
  }
569
544
  async function buildUninstallExecutionPlan(target, options = {}) {
570
545
  const scaffold = buildUninstallFabricPlan(target, options);
@@ -596,13 +571,6 @@ function buildUninstallFabricPlan(target, options = {}) {
596
571
  const gk = join2(fabricDir, "knowledge", sub, ".gitkeep");
597
572
  entries.push({ path: gk, kind: "gitkeep", absent: !existsSync2(gk) });
598
573
  }
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
574
  const safeEntries = entries.filter((entry) => !isInsidePersonalRoot(entry.path, personalKnowledgeDir));
607
575
  return {
608
576
  target: absTarget,
@@ -614,9 +582,7 @@ function buildUninstallFabricPlan(target, options = {}) {
614
582
  }
615
583
  async function executeUninstallFabricPlan(plan) {
616
584
  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) {
585
+ for (const entry of plan.entries) {
620
586
  if (entry.absent) {
621
587
  results.push({
622
588
  step: scaffoldStepLabel(entry.kind),
@@ -627,7 +593,7 @@ async function executeUninstallFabricPlan(plan) {
627
593
  continue;
628
594
  }
629
595
  try {
630
- await rm2(entry.path, { recursive: entry.kind === "knowledge-subdir", force: true });
596
+ await rm2(entry.path, { force: true });
631
597
  results.push({ step: scaffoldStepLabel(entry.kind), path: entry.path, status: "removed" });
632
598
  } catch (error) {
633
599
  results.push({
@@ -638,39 +604,6 @@ async function executeUninstallFabricPlan(plan) {
638
604
  });
639
605
  }
640
606
  }
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
607
  return results;
675
608
  }
676
609
  function scaffoldStepLabel(kind) {
@@ -679,10 +612,6 @@ function scaffoldStepLabel(kind) {
679
612
  return "scaffold-state";
680
613
  case "gitkeep":
681
614
  return "scaffold-gitkeep";
682
- case "knowledge-subdir":
683
- return "scaffold-knowledge";
684
- case "fabric-dir":
685
- return "fabric-dir";
686
615
  }
687
616
  }
688
617
  async function uninstallMcpClients(target, options = {}) {
@@ -790,7 +719,7 @@ async function executeUninstallStage(plan, stageName) {
790
719
  case "scaffold":
791
720
  return executeUninstallFabricPlan(plan.scaffold);
792
721
  case "bootstrap": {
793
- const opts = { cleanEmpties: plan.options.cleanEmpties === true };
722
+ const opts = {};
794
723
  return uninstallBootstrapStage(plan.target, opts);
795
724
  }
796
725
  case "mcp": {
@@ -803,12 +732,12 @@ async function uninstallFabric(target, options = {}) {
803
732
  const plan = await buildUninstallExecutionPlan(target, options);
804
733
  return executeUninstallExecutionPlan(plan);
805
734
  }
806
- async function resolveUninstallExecutionPlanWithWizard(basePlan, args, wizardAdapter) {
735
+ async function resolveUninstallExecutionPlanWithWizard(basePlan, wizardAdapter) {
807
736
  const selection = await wizardAdapter.run({
808
737
  target: basePlan.target,
809
738
  options: basePlan.options,
810
739
  supports: basePlan.supports,
811
- lockedStages: collectLockedWizardStages(args)
740
+ lockedStages: []
812
741
  });
813
742
  if (selection === null) {
814
743
  return null;
@@ -817,9 +746,7 @@ async function resolveUninstallExecutionPlanWithWizard(basePlan, args, wizardAda
817
746
  ...basePlan.options,
818
747
  skipScaffold: !selection.scaffold,
819
748
  skipBootstrap: !selection.bootstrap,
820
- skipMcp: !selection.mcp,
821
- purge: selection.purge,
822
- cleanEmpties: selection.cleanEmpties
749
+ skipMcp: !selection.mcp
823
750
  };
824
751
  const rebuilt = await buildUninstallExecutionPlan(basePlan.target, nextOptions);
825
752
  return {
@@ -870,18 +797,6 @@ function createDefaultUninstallWizardAdapter() {
870
797
  defaultValue: formatPromptDefault(!context.options.skipMcp)
871
798
  }),
872
799
  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
800
  })
886
801
  },
887
802
  {
@@ -901,9 +816,7 @@ function createDefaultUninstallWizardAdapter() {
901
816
  ...context.options,
902
817
  skipScaffold: !groupedSelection.scaffold,
903
818
  skipBootstrap: !groupedSelection.bootstrap,
904
- skipMcp: !groupedSelection.mcp,
905
- purge: groupedSelection.purge,
906
- cleanEmpties: groupedSelection.cleanEmpties
819
+ skipMcp: !groupedSelection.mcp
907
820
  };
908
821
  log.step(t("cli.uninstall.wizard.step.review"));
909
822
  printUninstallPlanSummary(context.target, previewOptions, context.supports);
@@ -930,13 +843,6 @@ async function confirmInGroup(options) {
930
843
  }
931
844
  return result;
932
845
  }
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
846
  async function confirmDestructive(plan) {
941
847
  printUninstallPlanSummary(plan.target, plan.options, plan.supports);
942
848
  const answer = await confirm({
@@ -955,9 +861,7 @@ function printUninstallPlanPreview(plan) {
955
861
  t("cli.uninstall.plan.preview-result", {
956
862
  scaffold: yesNoLabel(!plan.options.skipScaffold),
957
863
  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)
864
+ mcp: yesNoLabel(!plan.options.skipMcp)
961
865
  })
962
866
  );
963
867
  if (!plan.options.skipScaffold && plan.scaffold.entries.length > 0) {
@@ -975,9 +879,7 @@ function printUninstallPlanSummary(target, options, supports) {
975
879
  t("cli.uninstall.plan.actions", {
976
880
  scaffold: yesNoLabel(!options.skipScaffold),
977
881
  bootstrap: yesNoLabel(!options.skipBootstrap),
978
- mcp: yesNoLabel(!options.skipMcp),
979
- purge: yesNoLabel(options.purge === true),
980
- cleanEmpties: yesNoLabel(options.cleanEmpties === true)
882
+ mcp: yesNoLabel(!options.skipMcp)
981
883
  })
982
884
  );
983
885
  const detected = supports.filter((support) => support.detected);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "2.0.0-rc.11",
3
+ "version": "2.0.0-rc.15",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "fab": "dist/index.js",
@@ -20,8 +20,8 @@
20
20
  "tree-sitter-javascript": "^0.25.0",
21
21
  "tree-sitter-typescript": "^0.23.2",
22
22
  "web-tree-sitter": "^0.26.8",
23
- "@fenglimg/fabric-server": "2.0.0-rc.11",
24
- "@fenglimg/fabric-shared": "2.0.0-rc.11"
23
+ "@fenglimg/fabric-server": "2.0.0-rc.15",
24
+ "@fenglimg/fabric-shared": "2.0.0-rc.15"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.15.0",
@@ -1,6 +1,6 @@
1
1
  # Client hook config templates
2
2
 
3
- These JSON files are **fragment templates** consumed by `fabric init` and
3
+ These JSON files are **fragment templates** consumed by `fabric install` and
4
4
  `fabric hooks install`. They are not standalone client config files.
5
5
 
6
6
  The supported clients are pinned by `packages/shared/src/schemas/fabric-config.ts`
@@ -27,11 +27,15 @@ config (`~/.codex/config.toml`) is TOML.
27
27
 
28
28
  ## cursor-hooks.json
29
29
 
30
- Written to (or merged into) the user repo's `.cursor/hooks.json`. Mirrors the
31
- Codex `events.Stop[]` envelope shape — Cursor's hook event vocabulary is
32
- not stable across releases, so the canonical Stop-on-tool-finish lifecycle hook
33
- is the only entry we register today. SessionStart / PreToolUse slots are left
34
- unfilled for rc.6 to add when their semantics stabilise.
30
+ Written to (or merged into) the user repo's `.cursor/hooks.json`. Schema
31
+ authoritative source: https://cursor.com/cn/docs/hooks. Top-level requires
32
+ `version: 1` (number literal, NOT string) and a `hooks` object (NOT `events`)
33
+ keyed by camelCase event names: `stop`, `sessionStart`, `preToolUse`. Per-entry
34
+ shape stays flat (Codex-style): `{command, matcher?, type?, timeout?,
35
+ loop_limit?, failClosed?}`. rc.14 TASK-001 corrected rc.13's wrong top-level
36
+ envelope (was `{events: {Stop, SessionStart, PreToolUse}}` PascalCase, which
37
+ Cursor rejects with "Config version must be a number; Config hooks must be an
38
+ object").
35
39
 
36
40
  ## fabric-hint.cjs script paths
37
41
 
@@ -1,16 +1,13 @@
1
1
  {
2
- "events": {
3
- "Stop": [
4
- {
5
- "command": ".cursor/hooks/fabric-hint.cjs"
6
- }
2
+ "version": 1,
3
+ "hooks": {
4
+ "stop": [
5
+ { "command": ".cursor/hooks/fabric-hint.cjs" }
7
6
  ],
8
- "SessionStart": [
9
- {
10
- "command": ".cursor/hooks/knowledge-hint-broad.cjs"
11
- }
7
+ "sessionStart": [
8
+ { "command": ".cursor/hooks/knowledge-hint-broad.cjs" }
12
9
  ],
13
- "PreToolUse": [
10
+ "preToolUse": [
14
11
  {
15
12
  "matcher": "Edit|Write|MultiEdit",
16
13
  "command": ".cursor/hooks/knowledge-hint-narrow.cjs"