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

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.
@@ -3,8 +3,8 @@ import {
3
3
  configCmd,
4
4
  config_default,
5
5
  installMcpClients
6
- } from "./chunk-AIB54QRT.js";
7
- import "./chunk-SKSYUHKK.js";
6
+ } from "./chunk-KZ2YITOS.js";
7
+ import "./chunk-MF3OTILQ.js";
8
8
  import "./chunk-6ICJICVU.js";
9
9
  export {
10
10
  configCmd,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runInitScan
4
- } from "./chunk-AXIFEVAS.js";
4
+ } from "./chunk-PSVKSMRO.js";
5
5
  import {
6
6
  hasActionHint,
7
7
  paint,
@@ -13,7 +13,7 @@ import {
13
13
  } from "./chunk-6ICJICVU.js";
14
14
  import {
15
15
  resolveDevMode
16
- } from "./chunk-OBQU6NHO.js";
16
+ } from "./chunk-ZSESMG6L.js";
17
17
 
18
18
  // src/commands/doctor.ts
19
19
  import { confirm, isCancel } from "@clack/prompts";
@@ -22,6 +22,7 @@ import {
22
22
  appendEventLedgerEvent,
23
23
  checkLockOrThrow,
24
24
  runDoctorApplyLint as runDoctorFixKnowledge,
25
+ runDoctorCiteCoverage,
25
26
  runDoctorFix,
26
27
  runDoctorReport
27
28
  } from "@fenglimg/fabric-server";
@@ -75,6 +76,25 @@ var doctorCommand = defineCommand({
75
76
  type: "boolean",
76
77
  description: t("cli.doctor.args.yes.description"),
77
78
  default: false
79
+ },
80
+ // rc.20 TASK-05: cite policy adherence report (read-only). Skips standard
81
+ // inspections entirely — different output surface. Mutually exclusive
82
+ // with --fix / --fix-knowledge (enforced in run()).
83
+ "cite-coverage": {
84
+ type: "boolean",
85
+ description: t("cli.doctor.args.cite-coverage.description"),
86
+ default: false
87
+ },
88
+ since: {
89
+ type: "string",
90
+ description: t("cli.doctor.args.since.description"),
91
+ default: "7d"
92
+ },
93
+ client: {
94
+ type: "string",
95
+ description: t("cli.doctor.args.client.description"),
96
+ default: "all",
97
+ valueHint: "cc|codex|cursor|all"
78
98
  }
79
99
  },
80
100
  async run({ args }) {
@@ -92,6 +112,34 @@ var doctorCommand = defineCommand({
92
112
  const fixKnowledge = args["fix-knowledge"] === true;
93
113
  const fix = args.fix === true;
94
114
  const rescan = args.rescan === true;
115
+ const citeCoverage = args["cite-coverage"] === true;
116
+ if (citeCoverage) {
117
+ if (fix || fixKnowledge) {
118
+ writeStderr(t("cli.doctor.errors.cite-coverage-mutex"));
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ let sinceMs;
123
+ try {
124
+ sinceMs = parseSinceDuration(args.since ?? "7d");
125
+ } catch {
126
+ writeStderr(t("cli.doctor.errors.invalid-since", { input: args.since ?? "7d" }));
127
+ process.exitCode = 1;
128
+ return;
129
+ }
130
+ const clientFilter = args.client ?? "all";
131
+ if (!isValidClientFilter(clientFilter)) {
132
+ writeStderr(t("cli.doctor.errors.invalid-client", { input: clientFilter }));
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ const report2 = await runDoctorCiteCoverage(resolution.target, {
137
+ since: sinceMs,
138
+ client: clientFilter
139
+ });
140
+ renderCiteCoverageReport(report2, args.json === true);
141
+ return;
142
+ }
95
143
  if (fixKnowledge && fix) {
96
144
  writeStderr(t("cli.doctor.errors.fix-knowledge-fix-mutually-exclusive"));
97
145
  process.exitCode = 1;
@@ -293,7 +341,85 @@ async function resolveFixKnowledgeConsent(options) {
293
341
  }
294
342
  return "proceed";
295
343
  }
344
+ var CITE_COVERAGE_CLIENT_FILTERS = /* @__PURE__ */ new Set([
345
+ "cc",
346
+ "codex",
347
+ "cursor",
348
+ "all"
349
+ ]);
350
+ function isValidClientFilter(input) {
351
+ return CITE_COVERAGE_CLIENT_FILTERS.has(input);
352
+ }
353
+ function renderCiteCoverageReport(report, jsonMode) {
354
+ if (jsonMode) {
355
+ writeStdout(JSON.stringify(report, null, 2));
356
+ return;
357
+ }
358
+ if (report.status === "skipped") {
359
+ writeStdout(t("doctor.cite.status.skipped"));
360
+ return;
361
+ }
362
+ const lines = [];
363
+ lines.push(t("doctor.section.cite-coverage"));
364
+ lines.push(
365
+ t("doctor.cite.header", {
366
+ since: new Date(report.since_ts).toISOString(),
367
+ marker: new Date(report.marker_ts).toISOString()
368
+ })
369
+ );
370
+ if (report.marker_emitted_now) {
371
+ lines.push(t("doctor.cite.warning.justActivated"));
372
+ }
373
+ lines.push("");
374
+ lines.push(` ${t("doctor.cite.metric.editsTouched")}: ${report.metrics.edits_touched}`);
375
+ lines.push(` ${t("doctor.cite.metric.qualifyingCites")}: ${report.metrics.qualifying_cites}`);
376
+ lines.push(` ${t("doctor.cite.metric.recalledUnverified")}: ${report.metrics.recalled_unverified}`);
377
+ lines.push(` ${t("doctor.cite.metric.expectedButMissed")}: ${report.metrics.expected_but_missed}`);
378
+ lines.push(` ${t("doctor.cite.metric.totalTurns")}: ${report.metrics.total_turns}`);
379
+ if (report.per_client !== void 0 && Object.keys(report.per_client).length > 1) {
380
+ lines.push("");
381
+ lines.push(`### ${t("doctor.cite.section.perClient")}`);
382
+ for (const [client, metrics] of Object.entries(report.per_client)) {
383
+ const summary = Object.entries(metrics).map(([k, v]) => `${k}=${v}`).join(" / ");
384
+ lines.push(` ${client}: ${summary}`);
385
+ }
386
+ }
387
+ if (report.dismissed_reason_histogram !== void 0 && Object.keys(report.dismissed_reason_histogram).length > 0) {
388
+ lines.push("");
389
+ lines.push(`### ${t("doctor.cite.section.dismissedReasons")}`);
390
+ for (const [reason, count] of Object.entries(report.dismissed_reason_histogram)) {
391
+ const label = t(`doctor.cite.dismissed.${reason}`);
392
+ lines.push(` ${label}: ${count}`);
393
+ }
394
+ }
395
+ writeStdout(lines.join("\n"));
396
+ }
397
+ function parseSinceDuration(input) {
398
+ const trimmed = input.trim();
399
+ if (trimmed.length === 0) {
400
+ throw new Error(`invalid --since value: ${input}`);
401
+ }
402
+ const durationMatch = /^(\d+)([dhm])$/.exec(trimmed);
403
+ if (durationMatch !== null) {
404
+ const value = Number.parseInt(durationMatch[1], 10);
405
+ const unit = durationMatch[2];
406
+ if (!Number.isFinite(value) || value <= 0) {
407
+ throw new Error(`invalid --since value: ${input}`);
408
+ }
409
+ const unitMs = unit === "d" ? 864e5 : unit === "h" ? 36e5 : 6e4;
410
+ return Date.now() - value * unitMs;
411
+ }
412
+ if (/^\d+$/.test(trimmed)) {
413
+ const value = Number.parseInt(trimmed, 10);
414
+ if (!Number.isFinite(value) || value < 0) {
415
+ throw new Error(`invalid --since value: ${input}`);
416
+ }
417
+ return value;
418
+ }
419
+ throw new Error(`invalid --since value: ${input}`);
420
+ }
296
421
  export {
297
422
  doctor_default as default,
298
- doctorCommand
423
+ doctorCommand,
424
+ parseSinceDuration
299
425
  };
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ t
4
+ } from "./chunk-6ICJICVU.js";
2
5
 
3
6
  // src/index.ts
4
7
  import { realpathSync } from "fs";
@@ -8,20 +11,20 @@ import { defineCommand, runMain } from "citty";
8
11
 
9
12
  // src/commands/index.ts
10
13
  var allCommands = {
11
- install: () => import("./install-JLDCHAXV.js").then((module) => module.default),
12
- doctor: () => import("./doctor-6XHLQJXB.js").then((module) => module.default),
13
- serve: () => import("./serve-L3X5UHG2.js").then((module) => module.default),
14
- uninstall: () => import("./uninstall-DD6FIFCI.js").then((module) => module.default),
15
- config: () => import("./config-7YD365I3.js").then((module) => module.default),
16
- "plan-context-hint": () => import("./plan-context-hint-73U4FGKO.js").then((module) => module.default)
14
+ install: () => import("./install-WJZQZM7D.js").then((module) => module.default),
15
+ doctor: () => import("./doctor-HIX2FFEP.js").then((module) => module.default),
16
+ serve: () => import("./serve-6PPQX7AW.js").then((module) => module.default),
17
+ uninstall: () => import("./uninstall-L2HEEOU3.js").then((module) => module.default),
18
+ config: () => import("./config-AYP5F72E.js").then((module) => module.default),
19
+ "plan-context-hint": () => import("./plan-context-hint-RYVSMULL.js").then((module) => module.default)
17
20
  };
18
21
 
19
22
  // src/index.ts
20
23
  var main = defineCommand({
21
24
  meta: {
22
25
  name: "fabric",
23
- version: "2.0.0-rc.15",
24
- description: 'Initialize and manage Fabric projects. Use "fabric install" for one-shot setup.'
26
+ version: "2.0.0-rc.22",
27
+ description: t("cli.main.description")
25
28
  },
26
29
  subCommands: allCommands
27
30
  });
@@ -1,27 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  installMcpClients
4
- } from "./chunk-AIB54QRT.js";
4
+ } from "./chunk-KZ2YITOS.js";
5
5
  import {
6
6
  detectExistingLanguage,
7
7
  runInitScan
8
- } from "./chunk-AXIFEVAS.js";
8
+ } from "./chunk-PSVKSMRO.js";
9
9
  import {
10
- addFabricKnowledgeBaseSection,
11
10
  installArchiveHintHook,
12
11
  installFabricArchiveSkill,
13
12
  installFabricImportSkill,
14
13
  installFabricReviewSkill,
14
+ installHookLibs,
15
15
  installKnowledgeHintBroadHook,
16
16
  installKnowledgeHintNarrowHook,
17
17
  mergeClaudeCodeHookConfig,
18
18
  mergeCodexHookConfig,
19
19
  mergeCursorHookConfig,
20
- readFabricLanguagePreference
21
- } from "./chunk-UTF4YBDN.js";
20
+ readFabricLanguagePreference,
21
+ writeClaudeBootstrapThinShell,
22
+ writeCodexBootstrapManagedBlock,
23
+ writeCursorBootstrapManagedBlock,
24
+ writeFabricAgentsSnapshot
25
+ } from "./chunk-4HC5ZK7H.js";
22
26
  import {
23
27
  detectClientSupports
24
- } from "./chunk-SKSYUHKK.js";
28
+ } from "./chunk-MF3OTILQ.js";
25
29
  import {
26
30
  displayWidth,
27
31
  hasActionHint,
@@ -35,7 +39,7 @@ import {
35
39
  import {
36
40
  createDebugLogger,
37
41
  resolveDevMode
38
- } from "./chunk-OBQU6NHO.js";
42
+ } from "./chunk-ZSESMG6L.js";
39
43
 
40
44
  // src/commands/install.ts
41
45
  import { randomUUID } from "crypto";
@@ -45,7 +49,7 @@ import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync as r
45
49
  import { dirname, isAbsolute as isAbsolute3, join as join3, resolve as resolve3 } from "path";
46
50
  import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
47
51
  import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
48
- import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
52
+ import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
49
53
  import { defineCommand } from "citty";
50
54
  import { checkLockOrThrow } from "@fenglimg/fabric-server";
51
55
 
@@ -62,11 +66,14 @@ async function installHooks(target, _options = {}) {
62
66
  results.push(...await runStep(() => installArchiveHintHook(normalizedTarget)));
63
67
  results.push(...await runStep(() => installKnowledgeHintBroadHook(normalizedTarget)));
64
68
  results.push(...await runStep(() => installKnowledgeHintNarrowHook(normalizedTarget)));
69
+ results.push(...await runStep(() => installHookLibs(normalizedTarget)));
65
70
  results.push(await runSingleStep("claude-hook-config", () => mergeClaudeCodeHookConfig(normalizedTarget)));
66
71
  results.push(await runSingleStep("codex-hook-config", () => mergeCodexHookConfig(normalizedTarget)));
67
72
  results.push(await runSingleStep("cursor-hook-config", () => mergeCursorHookConfig(normalizedTarget)));
68
- const fabricLanguage = readFabricLanguagePreference(normalizedTarget);
69
- results.push(...await runStep(() => addFabricKnowledgeBaseSection(normalizedTarget, fabricLanguage)));
73
+ results.push(await runSingleStep("bootstrap-snapshot", () => writeFabricAgentsSnapshot(normalizedTarget)));
74
+ results.push(await runSingleStep("bootstrap-claude", () => writeClaudeBootstrapThinShell(normalizedTarget)));
75
+ results.push(await runSingleStep("bootstrap-codex", () => writeCodexBootstrapManagedBlock(normalizedTarget)));
76
+ results.push(await runSingleStep("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(normalizedTarget)));
70
77
  results.push(...validateHookPaths(normalizedTarget));
71
78
  return summarizeResults(results);
72
79
  }
@@ -1292,7 +1299,7 @@ function readProjectName(target) {
1292
1299
  return basename(target);
1293
1300
  }
1294
1301
  function getCliVersion() {
1295
- return true ? "2.0.0-rc.15" : "unknown";
1302
+ return true ? "2.0.0-rc.22" : "unknown";
1296
1303
  }
1297
1304
  function sortRecord(record) {
1298
1305
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -1302,15 +1309,6 @@ function toPosixPath(path) {
1302
1309
  }
1303
1310
 
1304
1311
  // src/commands/install.ts
1305
- var AGENTS_MD_DEFAULT_CONTENT = `# Project Knowledge
1306
-
1307
- This project uses [Fabric](https://github.com/fenglimg/fabric) for cross-client AI knowledge management.
1308
-
1309
- Knowledge entries live in \`.fabric/knowledge/\` (team) and \`~/.fabric/knowledge/\` (personal).
1310
- Run \`fabric doctor\` to verify state.
1311
-
1312
- See \`.fabric/knowledge/\` for project decisions, pitfalls, guidelines, models, and processes.
1313
- `;
1314
1312
  var LOCAL_FABRIC_SERVER_PATH = join3("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1315
1313
  var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
1316
1314
  var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
@@ -1626,9 +1624,6 @@ async function executeInitFabricPlan(plan) {
1626
1624
  }
1627
1625
  mkdirSync(plan.fabricDir, { recursive: true });
1628
1626
  writeDefaultFabricConfig(plan.fabricDir, plan.target);
1629
- if (plan.agentsMdAction === "created" && !existsSync3(plan.agentsMdPath)) {
1630
- await atomicWriteText(plan.agentsMdPath, AGENTS_MD_DEFAULT_CONTENT);
1631
- }
1632
1627
  mkdirSync(plan.knowledgeDir, { recursive: true });
1633
1628
  for (const sub of KNOWLEDGE_SUBDIRS) {
1634
1629
  const teamSubDir = join3(plan.knowledgeDir, sub);
@@ -1820,11 +1815,14 @@ async function executeInitStagePlan(plan, stageName) {
1820
1815
  installResults.push(...await runBestEffort("hook-script", () => installArchiveHintHook(plan.target)));
1821
1816
  installResults.push(...await runBestEffort("hook-broad-script", () => installKnowledgeHintBroadHook(plan.target)));
1822
1817
  installResults.push(...await runBestEffort("hook-narrow-script", () => installKnowledgeHintNarrowHook(plan.target)));
1818
+ installResults.push(...await runBestEffort("hook-lib", () => installHookLibs(plan.target)));
1823
1819
  installResults.push(await runBestEffortSingle("claude-hook-config", () => mergeClaudeCodeHookConfig(plan.target)));
1824
1820
  installResults.push(await runBestEffortSingle("codex-hook-config", () => mergeCodexHookConfig(plan.target)));
1825
1821
  installResults.push(await runBestEffortSingle("cursor-hook-config", () => mergeCursorHookConfig(plan.target)));
1826
- const fabricLanguage = readFabricLanguagePreference(plan.target);
1827
- installResults.push(...await runBestEffort("section", () => addFabricKnowledgeBaseSection(plan.target, fabricLanguage)));
1822
+ installResults.push(await runBestEffortSingle("bootstrap-snapshot", () => writeFabricAgentsSnapshot(plan.target)));
1823
+ installResults.push(await runBestEffortSingle("bootstrap-claude", () => writeClaudeBootstrapThinShell(plan.target)));
1824
+ installResults.push(await runBestEffortSingle("bootstrap-codex", () => writeCodexBootstrapManagedBlock(plan.target)));
1825
+ installResults.push(await runBestEffortSingle("bootstrap-cursor", () => writeCursorBootstrapManagedBlock(plan.target)));
1828
1826
  const installedCount = installResults.filter((r) => r.status === "written").length;
1829
1827
  const skippedCount = installResults.filter((r) => r.status === "skipped").length;
1830
1828
  const errorCount = installResults.filter((r) => r.status === "error").length;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveDevMode
4
- } from "./chunk-OBQU6NHO.js";
4
+ } from "./chunk-ZSESMG6L.js";
5
5
 
6
6
  // src/commands/plan-context-hint.ts
7
7
  import { defineCommand } from "citty";
@@ -66,19 +66,26 @@ async function runPlanContextHint(opts) {
66
66
  // scope_glob matches the requested path.
67
67
  dedupeByStableId(result.entries.flatMap((entry) => entry.description_index))
68
68
  );
69
- const narrow = narrowSource.map((item) => ({
69
+ const entries = narrowSource.map((item) => ({
70
70
  id: item.stable_id,
71
71
  type: item.type ?? item.description.knowledge_type ?? "",
72
72
  maturity: item.maturity ?? item.description.maturity ?? "",
73
73
  summary: item.description.summary
74
74
  }));
75
- return {
76
- version: 1,
75
+ const output = {
76
+ version: 2,
77
77
  revision_hash: result.revision_hash,
78
78
  target_paths: targetPaths,
79
- narrow,
79
+ entries,
80
80
  broad_count: sharedIndex.length
81
81
  };
82
+ if (result.auto_healed === true) {
83
+ output.auto_healed = true;
84
+ if (typeof result.previous_revision_hash === "string") {
85
+ output.previous_revision_hash = result.previous_revision_hash;
86
+ }
87
+ }
88
+ return output;
82
89
  }
83
90
  function parsePathsArg(raw) {
84
91
  if (raw === void 0 || raw.length === 0) {
@@ -11,7 +11,7 @@ import {
11
11
  import {
12
12
  createDebugLogger,
13
13
  resolveDevMode
14
- } from "./chunk-OBQU6NHO.js";
14
+ } from "./chunk-ZSESMG6L.js";
15
15
 
16
16
  // src/commands/serve.ts
17
17
  import { defineCommand } from "citty";
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FABRIC_HOOK_COMMAND_PATHS,
4
- FABRIC_SECTION_REGEX,
5
4
  HOOK_CONFIG_ARRAY_PATHS,
6
5
  HOOK_CONFIG_TARGETS,
6
+ HOOK_LIB_DESTINATIONS,
7
7
  HOOK_SCRIPT_DESTINATIONS,
8
- SECTION_TARGETS,
9
- SKILL_DESTINATIONS
10
- } from "./chunk-UTF4YBDN.js";
8
+ SKILL_DESTINATIONS,
9
+ fabricAgentsSnapshotPath
10
+ } from "./chunk-4HC5ZK7H.js";
11
11
  import {
12
12
  detectClientSupports,
13
13
  resolveClients
14
- } from "./chunk-SKSYUHKK.js";
14
+ } from "./chunk-MF3OTILQ.js";
15
15
  import {
16
16
  hasActionHint,
17
17
  paint,
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  createDebugLogger,
25
25
  resolveDevMode
26
- } from "./chunk-OBQU6NHO.js";
26
+ } from "./chunk-ZSESMG6L.js";
27
27
 
28
28
  // src/commands/uninstall.ts
29
29
  import { existsSync as existsSync2, statSync } from "fs";
@@ -39,6 +39,7 @@ import { existsSync } from "fs";
39
39
  import { readdir, readFile, rm, rmdir } from "fs/promises";
40
40
  import { dirname, join } from "path";
41
41
  import { atomicWriteJson, atomicWriteText } from "@fenglimg/fabric-shared/node/atomic-write";
42
+ import { BOOTSTRAP_REGEX } from "@fenglimg/fabric-shared/templates/bootstrap-canonical";
42
43
  async function uninstallFabricArchiveSkill(projectRoot) {
43
44
  return removeSkill("skill", SKILL_DESTINATIONS.fabricArchive, projectRoot);
44
45
  }
@@ -82,6 +83,34 @@ async function removeHookScripts(step, rels, projectRoot) {
82
83
  }
83
84
  return results;
84
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
+ }
85
114
  async function unmergeClaudeCodeHookConfig(projectRoot) {
86
115
  return unmergeHookConfig({
87
116
  step: "claude-hook-config",
@@ -112,69 +141,131 @@ async function unmergeCursorHookConfig(projectRoot) {
112
141
  extractCommands: extractFlatCommands
113
142
  });
114
143
  }
115
- async function stripFabricKnowledgeBaseSection(projectRoot) {
144
+ async function stripFabricBootstrapBlocks(projectRoot) {
116
145
  const results = [];
117
- for (const rel of SECTION_TARGETS) {
118
- const target = join(projectRoot, rel);
119
- if (!existsSync(target)) {
120
- results.push({ step: "section", path: target, status: "skipped", message: "absent" });
121
- continue;
122
- }
123
- let existing;
124
- try {
125
- existing = await readFile(target, "utf8");
126
- } catch (error) {
127
- results.push({
128
- step: "section",
129
- path: target,
130
- status: "error",
131
- message: error instanceof Error ? error.message : String(error)
132
- });
133
- continue;
134
- }
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/, "")}`;
148
- if (filtered === existing) {
149
- results.push({
150
- step: "section",
151
- path: target,
152
- status: "skipped",
153
- message: "no-fabric-section"
154
- });
155
- continue;
156
- }
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)) {
157
222
  try {
158
- await atomicWriteText(target, filtered);
159
- results.push({ step: "section", path: target, status: "removed" });
223
+ await rm(target, { force: true });
224
+ return { step, path: target, status: "removed", message: "front-matter-only" };
160
225
  } catch (error) {
161
- results.push({
162
- step: "section",
226
+ return {
227
+ step,
163
228
  path: target,
164
229
  status: "error",
165
230
  message: error instanceof Error ? error.message : String(error)
166
- });
231
+ };
167
232
  }
168
233
  }
169
- return results;
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);
170
255
  }
171
256
  async function uninstallBootstrapStage(projectRoot, _opts = {}) {
172
257
  const results = [];
173
258
  await runAndCollect(
174
259
  results,
175
- "section",
260
+ "bootstrap-blocks",
261
+ projectRoot,
262
+ () => stripFabricBootstrapBlocks(projectRoot)
263
+ );
264
+ await runAndCollectOne(
265
+ results,
266
+ "bootstrap-snapshot",
176
267
  projectRoot,
177
- () => stripFabricKnowledgeBaseSection(projectRoot)
268
+ () => deleteFabricAgentsSnapshot(projectRoot)
178
269
  );
179
270
  await runAndCollectOne(
180
271
  results,
@@ -194,6 +285,7 @@ async function uninstallBootstrapStage(projectRoot, _opts = {}) {
194
285
  projectRoot,
195
286
  () => unmergeClaudeCodeHookConfig(projectRoot)
196
287
  );
288
+ await runAndCollect(results, "hook-lib", projectRoot, () => removeHookLibs(projectRoot));
197
289
  await runAndCollect(
198
290
  results,
199
291
  "hook-narrow-script",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "2.0.0-rc.15",
3
+ "version": "2.0.0-rc.22",
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.15",
24
- "@fenglimg/fabric-shared": "2.0.0-rc.15"
23
+ "@fenglimg/fabric-server": "2.0.0-rc.22",
24
+ "@fenglimg/fabric-shared": "2.0.0-rc.22"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.15.0",