@fenglimg/fabric-cli 2.0.0-rc.21 → 2.0.0-rc.23

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.
@@ -1,11 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  installMcpClients
4
- } from "./chunk-KZ2YITOS.js";
5
- import {
6
- detectExistingLanguage,
7
- runInitScan
8
- } from "./chunk-FNO7CQDG.js";
4
+ } from "./chunk-STLR2GHP.js";
9
5
  import {
10
6
  installArchiveHintHook,
11
7
  installFabricArchiveSkill,
@@ -22,7 +18,7 @@ import {
22
18
  writeCodexBootstrapManagedBlock,
23
19
  writeCursorBootstrapManagedBlock,
24
20
  writeFabricAgentsSnapshot
25
- } from "./chunk-4HC5ZK7H.js";
21
+ } from "./chunk-AXKII55Y.js";
26
22
  import {
27
23
  detectClientSupports
28
24
  } from "./chunk-MF3OTILQ.js";
@@ -39,14 +35,14 @@ import {
39
35
  import {
40
36
  createDebugLogger,
41
37
  resolveDevMode
42
- } from "./chunk-ZSESMG6L.js";
38
+ } from "./chunk-COI5VDFU.js";
43
39
 
44
40
  // src/commands/install.ts
45
41
  import { randomUUID } from "crypto";
46
42
  import { homedir } from "os";
47
43
  import * as childProcess from "child_process";
48
- import { appendFileSync, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, statSync as statSync3, writeFileSync } from "fs";
49
- import { dirname, isAbsolute as isAbsolute3, join as join3, resolve as resolve3 } from "path";
44
+ import { appendFileSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, rmSync, statSync as statSync4, writeFileSync } from "fs";
45
+ import { dirname, isAbsolute as isAbsolute3, join as join4, resolve as resolve3 } from "path";
50
46
  import { cancel, confirm, group, intro, isCancel, log, note, outro, select } from "@clack/prompts";
51
47
  import { defaultAgentsMetaCounters } from "@fenglimg/fabric-shared";
52
48
  import { atomicWriteJson } from "@fenglimg/fabric-shared/node/atomic-write";
@@ -184,11 +180,64 @@ function assertExistingDirectory(target) {
184
180
  }
185
181
  }
186
182
 
183
+ // src/lib/detect-language.ts
184
+ import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
185
+ import { join as join2 } from "path";
186
+ function detectExistingLanguage(target) {
187
+ const ZH_CN_RATIO_THRESHOLD = 0.3;
188
+ const samples = [];
189
+ const readmePath = join2(target, "README.md");
190
+ if (existsSync2(readmePath)) {
191
+ try {
192
+ samples.push(readFileSync(readmePath, "utf8"));
193
+ } catch {
194
+ }
195
+ }
196
+ const docsDir = join2(target, "docs");
197
+ if (existsSync2(docsDir)) {
198
+ try {
199
+ const stat = statSync2(docsDir);
200
+ if (stat.isDirectory()) {
201
+ for (const entry of readdirSync(docsDir, { withFileTypes: true })) {
202
+ if (!entry.isFile()) continue;
203
+ if (!/\.(md|mdx|txt)$/iu.test(entry.name)) continue;
204
+ try {
205
+ samples.push(readFileSync(join2(docsDir, entry.name), "utf8"));
206
+ } catch {
207
+ }
208
+ }
209
+ }
210
+ } catch {
211
+ }
212
+ }
213
+ if (samples.length === 0) {
214
+ return "en";
215
+ }
216
+ let cjkCount = 0;
217
+ let asciiLetterCount = 0;
218
+ for (const sample of samples) {
219
+ for (const ch of sample) {
220
+ const code = ch.codePointAt(0) ?? 0;
221
+ if (code >= 19968 && code <= 40959) {
222
+ cjkCount += 1;
223
+ } else if (code >= 65 && code <= 90 || code >= 97 && code <= 122) {
224
+ asciiLetterCount += 1;
225
+ }
226
+ }
227
+ }
228
+ const denominator = cjkCount + asciiLetterCount;
229
+ if (denominator === 0) {
230
+ return "en";
231
+ }
232
+ const ratio = cjkCount / denominator;
233
+ return ratio > ZH_CN_RATIO_THRESHOLD ? "zh-CN-hybrid" : "en";
234
+ }
235
+
187
236
  // src/scanner/forensic.ts
188
237
  import { execFileSync } from "child_process";
189
- import { existsSync as existsSync2, readdirSync, readFileSync, statSync as statSync2 } from "fs";
238
+ import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
190
239
  import { createRequire } from "module";
191
- import { basename, extname, isAbsolute as isAbsolute2, join as join2, posix, relative, resolve as resolve2, sep } from "path";
240
+ import { basename, extname, isAbsolute as isAbsolute2, join as join3, posix, relative, resolve as resolve2, sep } from "path";
192
241
  import {
193
242
  forensicReportSchema
194
243
  } from "@fenglimg/fabric-shared";
@@ -331,8 +380,8 @@ function buildTopology(root) {
331
380
  if (current === void 0) {
332
381
  continue;
333
382
  }
334
- for (const entry of readdirSync(current, { withFileTypes: true })) {
335
- const absolutePath = join2(current, entry.name);
383
+ for (const entry of readdirSync2(current, { withFileTypes: true })) {
384
+ const absolutePath = join3(current, entry.name);
336
385
  const relativePath = toPosixPath(relative(root, absolutePath));
337
386
  if (relativePath.length === 0) {
338
387
  continue;
@@ -352,7 +401,7 @@ function buildTopology(root) {
352
401
  if (!entry.isFile()) {
353
402
  continue;
354
403
  }
355
- const stats = statSync2(absolutePath);
404
+ const stats = statSync3(absolutePath);
356
405
  const extension = extname(entry.name) || "[none]";
357
406
  byExt[extension] = (byExt[extension] ?? 0) + 1;
358
407
  totalFiles += 1;
@@ -371,7 +420,7 @@ function buildTopology(root) {
371
420
  };
372
421
  }
373
422
  function assertExistingDirectory2(target) {
374
- if (!existsSync2(target) || !statSync2(target).isDirectory()) {
423
+ if (!existsSync3(target) || !statSync3(target).isDirectory()) {
375
424
  throw new Error(`Target must be an existing directory: ${target}`);
376
425
  }
377
426
  }
@@ -420,7 +469,7 @@ function getEntryPointReason(relativePath) {
420
469
  async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
421
470
  const samples = [];
422
471
  for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
423
- const absolutePath = join2(target, ...entryPoint.path.split("/"));
472
+ const absolutePath = join3(target, ...entryPoint.path.split("/"));
424
473
  const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
425
474
  const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
426
475
  frameworkKind,
@@ -440,7 +489,7 @@ async function buildCodeSamples(target, entryPoints, frameworkKind, topology, pa
440
489
  }
441
490
  function readFirstLines(path, lineLimit) {
442
491
  try {
443
- const lines = readFileSync(path, "utf8").split(/\r?\n/);
492
+ const lines = readFileSync2(path, "utf8").split(/\r?\n/);
444
493
  if (lines.at(-1) === "") {
445
494
  lines.pop();
446
495
  }
@@ -457,12 +506,12 @@ function readFirstLines(path, lineLimit) {
457
506
  }
458
507
  }
459
508
  function readPackageDependencies(target) {
460
- const packageJsonPath = join2(target, "package.json");
461
- if (!existsSync2(packageJsonPath)) {
509
+ const packageJsonPath = join3(target, "package.json");
510
+ if (!existsSync3(packageJsonPath)) {
462
511
  return /* @__PURE__ */ new Map();
463
512
  }
464
513
  try {
465
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
514
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
466
515
  return new Map([
467
516
  ...Object.entries(packageJson.dependencies ?? {}),
468
517
  ...Object.entries(packageJson.devDependencies ?? {}),
@@ -798,16 +847,16 @@ function scoreFrameworkConfidence(input) {
798
847
  return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
799
848
  }
800
849
  function readReadmeInfo(target) {
801
- const readmePath = join2(target, "README.md");
802
- const hasContributing = existsSync2(join2(target, "CONTRIBUTING.md"));
803
- if (!existsSync2(readmePath)) {
850
+ const readmePath = join3(target, "README.md");
851
+ const hasContributing = existsSync3(join3(target, "CONTRIBUTING.md"));
852
+ if (!existsSync3(readmePath)) {
804
853
  return {
805
854
  quality: "missing",
806
855
  line_count: 0,
807
856
  has_contributing: hasContributing
808
857
  };
809
858
  }
810
- const readme = readFileSync(readmePath, "utf8");
859
+ const readme = readFileSync2(readmePath, "utf8");
811
860
  const wordCount = readme.trim().split(/\s+/).filter(Boolean).length;
812
861
  return {
813
862
  quality: wordCount >= 200 ? "ok" : "stub",
@@ -1285,10 +1334,10 @@ function buildSkillRecommendations(frameworkKind, topology, readme) {
1285
1334
  return recommendations;
1286
1335
  }
1287
1336
  function readProjectName(target) {
1288
- const packageJsonPath = join2(target, "package.json");
1289
- if (existsSync2(packageJsonPath)) {
1337
+ const packageJsonPath = join3(target, "package.json");
1338
+ if (existsSync3(packageJsonPath)) {
1290
1339
  try {
1291
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1340
+ const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
1292
1341
  if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
1293
1342
  return packageJson.name;
1294
1343
  }
@@ -1299,7 +1348,7 @@ function readProjectName(target) {
1299
1348
  return basename(target);
1300
1349
  }
1301
1350
  function getCliVersion() {
1302
- return true ? "2.0.0-rc.21" : "unknown";
1351
+ return true ? "2.0.0-rc.23" : "unknown";
1303
1352
  }
1304
1353
  function sortRecord(record) {
1305
1354
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -1309,7 +1358,7 @@ function toPosixPath(path) {
1309
1358
  }
1310
1359
 
1311
1360
  // src/commands/install.ts
1312
- var LOCAL_FABRIC_SERVER_PATH = join3("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1361
+ var LOCAL_FABRIC_SERVER_PATH = join4("node_modules", "@fenglimg", "fabric-server", "dist", "index.js");
1313
1362
  var FABRIC_SERVER_PACKAGE = "@fenglimg/fabric-server";
1314
1363
  var INIT_WIZARD_GROUP_CANCELLED = /* @__PURE__ */ Symbol("init-wizard-group-cancelled");
1315
1364
  var installCommand = defineCommand({
@@ -1347,7 +1396,7 @@ async function runInitCommand(args) {
1347
1396
  const logger = createDebugLogger(args.debug);
1348
1397
  const resolution = resolveDevMode(args.target, process.cwd());
1349
1398
  const intent = resolveInitCliIntent(args, resolution.target);
1350
- const fabricInitialized = existsSync3(join3(intent.target, ".fabric", "events.jsonl"));
1399
+ const fabricInitialized = existsSync4(join4(intent.target, ".fabric", "events.jsonl"));
1351
1400
  if (fabricInitialized) {
1352
1401
  try {
1353
1402
  checkLockOrThrow(intent.target);
@@ -1384,8 +1433,8 @@ async function runInitCommand(args) {
1384
1433
  return result;
1385
1434
  }
1386
1435
  function writeDefaultFabricConfig(fabricDir, targetRoot) {
1387
- const target = join3(fabricDir, "fabric-config.json");
1388
- if (existsSync3(target)) return;
1436
+ const target = join4(fabricDir, "fabric-config.json");
1437
+ if (existsSync4(target)) return;
1389
1438
  const detectedLanguage = detectExistingLanguage(targetRoot);
1390
1439
  const FABRIC_CONFIG_DEFAULTS = {
1391
1440
  // Scan/import language policy. Fixated at init time by probing
@@ -1524,7 +1573,7 @@ async function executeInitExecutionPlan(plan) {
1524
1573
  finalSupports: plan.supports
1525
1574
  };
1526
1575
  }
1527
- if (existsSync3(plan.scaffold.fabricDir) && !statSync3(plan.scaffold.fabricDir).isDirectory()) {
1576
+ if (existsSync4(plan.scaffold.fabricDir) && !statSync4(plan.scaffold.fabricDir).isDirectory()) {
1528
1577
  throw new Error(
1529
1578
  t("cli.install.diff.drift-abort", { path: plan.scaffold.fabricDir })
1530
1579
  );
@@ -1577,16 +1626,16 @@ function resolvePersonalFabricRoot() {
1577
1626
  }
1578
1627
  async function buildInitFabricPlan(target, options) {
1579
1628
  assertExistingDirectory3(target);
1580
- const fabricDir = join3(target, ".fabric");
1581
- const agentsMdPath = join3(target, "AGENTS.md");
1582
- const agentsMdAction = existsSync3(agentsMdPath) ? "preserved" : "created";
1583
- const knowledgeDir = join3(fabricDir, "knowledge");
1584
- const personalKnowledgeDir = join3(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1585
- const forensicPath = join3(fabricDir, "forensic.json");
1586
- const eventsPath = join3(fabricDir, "events.jsonl");
1587
- const metaPath = join3(fabricDir, "agents.meta.json");
1629
+ const fabricDir = join4(target, ".fabric");
1630
+ const agentsMdPath = join4(target, "AGENTS.md");
1631
+ const agentsMdAction = existsSync4(agentsMdPath) ? "preserved" : "created";
1632
+ const knowledgeDir = join4(fabricDir, "knowledge");
1633
+ const personalKnowledgeDir = join4(resolvePersonalFabricRoot(), ".fabric", "knowledge");
1634
+ const forensicPath = join4(fabricDir, "forensic.json");
1635
+ const eventsPath = join4(fabricDir, "events.jsonl");
1636
+ const metaPath = join4(fabricDir, "agents.meta.json");
1588
1637
  const replaceFabricDir = shouldReplaceWritableDirectory(fabricDir, options);
1589
- const knowledgeDirAction = existsSync3(knowledgeDir) ? "overwritten" : "created";
1638
+ const knowledgeDirAction = existsSync4(knowledgeDir) ? "overwritten" : "created";
1590
1639
  const metaClassification = classifyFreshPath(metaPath, "structural");
1591
1640
  const eventsClassification = classifyFreshPath(eventsPath, "presence");
1592
1641
  const forensicClassification = classifyFreshPath(forensicPath, "always-rewrite");
@@ -1626,17 +1675,17 @@ async function executeInitFabricPlan(plan) {
1626
1675
  writeDefaultFabricConfig(plan.fabricDir, plan.target);
1627
1676
  mkdirSync(plan.knowledgeDir, { recursive: true });
1628
1677
  for (const sub of KNOWLEDGE_SUBDIRS) {
1629
- const teamSubDir = join3(plan.knowledgeDir, sub);
1678
+ const teamSubDir = join4(plan.knowledgeDir, sub);
1630
1679
  mkdirSync(teamSubDir, { recursive: true });
1631
- const teamGitkeep = join3(teamSubDir, ".gitkeep");
1632
- if (!existsSync3(teamGitkeep)) {
1680
+ const teamGitkeep = join4(teamSubDir, ".gitkeep");
1681
+ if (!existsSync4(teamGitkeep)) {
1633
1682
  writeFileSync(teamGitkeep, "", "utf8");
1634
1683
  }
1635
1684
  }
1636
1685
  try {
1637
1686
  mkdirSync(plan.personalKnowledgeDir, { recursive: true });
1638
1687
  for (const sub of KNOWLEDGE_SUBDIRS) {
1639
- mkdirSync(join3(plan.personalKnowledgeDir, sub), { recursive: true });
1688
+ mkdirSync(join4(plan.personalKnowledgeDir, sub), { recursive: true });
1640
1689
  }
1641
1690
  } catch {
1642
1691
  }
@@ -1651,17 +1700,7 @@ async function executeInitFabricPlan(plan) {
1651
1700
  }
1652
1701
  preparePlannedPath(plan.forensicPath, plan.forensicAction);
1653
1702
  await atomicWriteJson(plan.forensicPath, plan.forensicReport);
1654
- const wasCanonicalReRun = plan.metaState === "present-canonical" && plan.eventsState === "present-canonical";
1655
- if (!wasCanonicalReRun) {
1656
- try {
1657
- await runInitScan(plan.target, { source: "init" });
1658
- } catch (error) {
1659
- writeStderr(
1660
- `[warn] init-scan failed: ${error instanceof Error ? error.message : String(error)} \u2014 re-run \`fab scan\` to populate baseline knowledge entries.`
1661
- );
1662
- }
1663
- }
1664
- if (existsSync3(plan.eventsPath)) {
1703
+ if (existsSync4(plan.eventsPath)) {
1665
1704
  const applied = [];
1666
1705
  const canonical = [];
1667
1706
  const drifted = [];
@@ -1870,21 +1909,21 @@ async function executeInitStagePlan(plan, stageName) {
1870
1909
  }
1871
1910
  }
1872
1911
  function shouldReplaceWritableDirectory(path, _options) {
1873
- if (!existsSync3(path)) {
1912
+ if (!existsSync4(path)) {
1874
1913
  return false;
1875
1914
  }
1876
- if (statSync3(path).isDirectory()) {
1915
+ if (statSync4(path).isDirectory()) {
1877
1916
  return false;
1878
1917
  }
1879
1918
  return false;
1880
1919
  }
1881
1920
  function classifyFreshPath(path, strategy) {
1882
- if (!existsSync3(path)) {
1921
+ if (!existsSync4(path)) {
1883
1922
  return { path, state: "missing" };
1884
1923
  }
1885
1924
  let stat;
1886
1925
  try {
1887
- stat = statSync3(path);
1926
+ stat = statSync4(path);
1888
1927
  } catch (error) {
1889
1928
  return {
1890
1929
  path,
@@ -1899,7 +1938,7 @@ function classifyFreshPath(path, strategy) {
1899
1938
  return { path, state: "present-canonical" };
1900
1939
  }
1901
1940
  try {
1902
- const raw = readFileSync2(path, "utf8");
1941
+ const raw = readFileSync3(path, "utf8");
1903
1942
  const parsed = JSON.parse(raw);
1904
1943
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1905
1944
  return { path, state: "user-modified", reason: "not a JSON object" };
@@ -1928,7 +1967,7 @@ function formatDiffFileState(state) {
1928
1967
  }
1929
1968
  function preparePlannedPath(path, action) {
1930
1969
  mkdirSync(dirname(path), { recursive: true });
1931
- if (action === "overwritten" && existsSync3(path)) {
1970
+ if (action === "overwritten" && existsSync4(path)) {
1932
1971
  rmSync(path, { recursive: true, force: true });
1933
1972
  }
1934
1973
  }
@@ -2082,19 +2121,19 @@ function normalizeTarget3(targetInput) {
2082
2121
  return isAbsolute3(targetInput) ? targetInput : resolve3(process.cwd(), targetInput);
2083
2122
  }
2084
2123
  function assertExistingDirectory3(target) {
2085
- if (!existsSync3(target) || !statSync3(target).isDirectory()) {
2124
+ if (!existsSync4(target) || !statSync4(target).isDirectory()) {
2086
2125
  throw new Error(`Target must be an existing directory: ${target}`);
2087
2126
  }
2088
2127
  }
2089
2128
  function detectPackageManager(cwd) {
2090
2129
  const workspaceRoot = resolve3(cwd);
2091
- if (existsSync3(join3(workspaceRoot, "pnpm-lock.yaml"))) {
2130
+ if (existsSync4(join4(workspaceRoot, "pnpm-lock.yaml"))) {
2092
2131
  return "pnpm";
2093
2132
  }
2094
- if (existsSync3(join3(workspaceRoot, "yarn.lock"))) {
2133
+ if (existsSync4(join4(workspaceRoot, "yarn.lock"))) {
2095
2134
  return "yarn";
2096
2135
  }
2097
- if (existsSync3(join3(workspaceRoot, "package-lock.json"))) {
2136
+ if (existsSync4(join4(workspaceRoot, "package-lock.json"))) {
2098
2137
  return "npm";
2099
2138
  }
2100
2139
  return "npm";
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/onboard-coverage.ts
4
+ import { existsSync, readdirSync, readFileSync } from "fs";
5
+ import { join, resolve } from "path";
6
+ import { defineCommand } from "citty";
7
+ import {
8
+ ONBOARD_SLOT_NAMES,
9
+ ONBOARD_SLOT_TOTAL
10
+ } from "@fenglimg/fabric-shared";
11
+ var KNOWLEDGE_TYPE_DIRS = [
12
+ "decisions",
13
+ "pitfalls",
14
+ "guidelines",
15
+ "models",
16
+ "processes"
17
+ ];
18
+ var FABRIC_CONFIG_PATH = [".fabric", "fabric-config.json"];
19
+ function emptyFilled() {
20
+ const filled = {};
21
+ for (const slot of ONBOARD_SLOT_NAMES) {
22
+ filled[slot] = [];
23
+ }
24
+ return filled;
25
+ }
26
+ function readOnboardSlotFrontmatter(filePath) {
27
+ let content;
28
+ try {
29
+ content = readFileSync(filePath, "utf8");
30
+ } catch {
31
+ return void 0;
32
+ }
33
+ const match = /^---\n([\s\S]*?)\n---/u.exec(content);
34
+ if (match === null) return void 0;
35
+ const block = match[1];
36
+ if (block === void 0) return void 0;
37
+ for (const rawLine of block.split(/\r?\n/u)) {
38
+ const line = rawLine.trim();
39
+ const sep = line.indexOf(":");
40
+ if (sep === -1) continue;
41
+ const key = line.slice(0, sep).trim();
42
+ if (key !== "onboard_slot") continue;
43
+ let value = line.slice(sep + 1).trim();
44
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
45
+ value = value.slice(1, -1);
46
+ }
47
+ return value;
48
+ }
49
+ return void 0;
50
+ }
51
+ function readStableIdFrontmatter(filePath, fallbackName) {
52
+ let content;
53
+ try {
54
+ content = readFileSync(filePath, "utf8");
55
+ } catch {
56
+ return fallbackName.replace(/\.md$/u, "");
57
+ }
58
+ const match = /^---\n([\s\S]*?)\n---/u.exec(content);
59
+ if (match === null) return fallbackName.replace(/\.md$/u, "");
60
+ const block = match[1];
61
+ if (block === void 0) return fallbackName.replace(/\.md$/u, "");
62
+ for (const rawLine of block.split(/\r?\n/u)) {
63
+ const line = rawLine.trim();
64
+ const sep = line.indexOf(":");
65
+ if (sep === -1) continue;
66
+ if (line.slice(0, sep).trim() !== "id") continue;
67
+ return line.slice(sep + 1).trim();
68
+ }
69
+ return fallbackName.replace(/\.md$/u, "");
70
+ }
71
+ function readOptedOutSlots(projectRoot) {
72
+ const path = join(projectRoot, ...FABRIC_CONFIG_PATH);
73
+ if (!existsSync(path)) return [];
74
+ let raw;
75
+ try {
76
+ raw = readFileSync(path, "utf8");
77
+ } catch {
78
+ return [];
79
+ }
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(raw);
83
+ } catch (err) {
84
+ process.stderr.write(
85
+ `onboard-coverage: ignoring malformed fabric-config.json (${err instanceof Error ? err.message : String(err)})
86
+ `
87
+ );
88
+ return [];
89
+ }
90
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
91
+ return [];
92
+ }
93
+ const slots = parsed.onboard_slots_opted_out;
94
+ if (!Array.isArray(slots)) return [];
95
+ return slots.filter((v) => typeof v === "string");
96
+ }
97
+ function runOnboardCoverage(projectRoot) {
98
+ const filled = emptyFilled();
99
+ const knowledgeRoot = join(projectRoot, ".fabric", "knowledge");
100
+ if (existsSync(knowledgeRoot)) {
101
+ for (const typeDir of KNOWLEDGE_TYPE_DIRS) {
102
+ const dir = join(knowledgeRoot, typeDir);
103
+ if (!existsSync(dir)) continue;
104
+ let entries;
105
+ try {
106
+ entries = readdirSync(dir, { withFileTypes: true });
107
+ } catch {
108
+ continue;
109
+ }
110
+ for (const entry of entries) {
111
+ if (!entry.isFile()) continue;
112
+ if (!entry.name.endsWith(".md")) continue;
113
+ const filePath = join(dir, entry.name);
114
+ const slot = readOnboardSlotFrontmatter(filePath);
115
+ if (slot === void 0) continue;
116
+ if (!ONBOARD_SLOT_NAMES.includes(slot)) continue;
117
+ const stableId = readStableIdFrontmatter(filePath, entry.name);
118
+ filled[slot].push(stableId);
119
+ }
120
+ }
121
+ }
122
+ const optedOut = readOptedOutSlots(projectRoot);
123
+ const missing = ONBOARD_SLOT_NAMES.filter((slot) => {
124
+ if (filled[slot].length > 0) return false;
125
+ if (optedOut.includes(slot)) return false;
126
+ return true;
127
+ });
128
+ for (const slot of ONBOARD_SLOT_NAMES) {
129
+ filled[slot].sort();
130
+ }
131
+ return {
132
+ filled,
133
+ missing,
134
+ opted_out: optedOut,
135
+ total: ONBOARD_SLOT_TOTAL
136
+ };
137
+ }
138
+ function renderHumanReadable(report) {
139
+ const filledCount = ONBOARD_SLOT_NAMES.filter(
140
+ (slot) => report.filled[slot].length > 0
141
+ ).length;
142
+ const optedOutCount = report.opted_out.length;
143
+ const missingCount = report.missing.length;
144
+ const lines = [];
145
+ lines.push(
146
+ `Onboard coverage: ${filledCount}/${report.total} filled, ${optedOutCount} opted-out, ${missingCount} missing`
147
+ );
148
+ lines.push("");
149
+ lines.push(" slot status entries");
150
+ lines.push(" -------------------------- ----------- -------------------------");
151
+ for (const slot of ONBOARD_SLOT_NAMES) {
152
+ const entries = report.filled[slot];
153
+ let status;
154
+ let detail;
155
+ if (entries.length > 0) {
156
+ status = "filled";
157
+ detail = entries.join(", ");
158
+ } else if (report.opted_out.includes(slot)) {
159
+ status = "opted-out";
160
+ detail = "(user-dismissed; run `fab config onboard-reset` to re-open)";
161
+ } else {
162
+ status = "missing";
163
+ detail = "(run /fabric-archive to onboard)";
164
+ }
165
+ lines.push(` ${slot.padEnd(26)} ${status.padEnd(11)} ${detail}`);
166
+ }
167
+ return lines.join("\n");
168
+ }
169
+ var onboardCoverageCommand = defineCommand({
170
+ meta: {
171
+ name: "onboard-coverage",
172
+ description: "Report S5 onboard-slot coverage for the workspace. Used by the fabric-archive Skill's first-run phase to detect unclaimed project-tone slots.",
173
+ // Mirrors `plan-context-hint`: hidden from `fab --help` so the top-level
174
+ // banner stays focused on install/doctor/serve/config. The command stays
175
+ // callable directly from Skills via `fab onboard-coverage --json`.
176
+ hidden: true
177
+ },
178
+ args: {
179
+ json: {
180
+ type: "boolean",
181
+ description: "Emit machine-readable JSON to stdout instead of the human table.",
182
+ default: false
183
+ },
184
+ target: {
185
+ type: "string",
186
+ description: "Override the project root (defaults to cwd)."
187
+ }
188
+ },
189
+ async run({ args }) {
190
+ try {
191
+ const projectRoot = resolve(args.target ?? process.cwd());
192
+ const report = runOnboardCoverage(projectRoot);
193
+ if (args.json === true) {
194
+ process.stdout.write(`${JSON.stringify(report)}
195
+ `);
196
+ } else {
197
+ process.stdout.write(`${renderHumanReadable(report)}
198
+ `);
199
+ }
200
+ } catch (error) {
201
+ const message = error instanceof Error ? error.message : String(error);
202
+ process.stderr.write(`onboard-coverage failed: ${message}
203
+ `);
204
+ process.exitCode = 1;
205
+ }
206
+ }
207
+ });
208
+ var onboard_coverage_default = onboardCoverageCommand;
209
+ export {
210
+ onboard_coverage_default as default,
211
+ onboardCoverageCommand,
212
+ runOnboardCoverage
213
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  resolveDevMode
4
- } from "./chunk-ZSESMG6L.js";
4
+ } from "./chunk-COI5VDFU.js";
5
5
 
6
6
  // src/commands/plan-context-hint.ts
7
7
  import { defineCommand } from "citty";
@@ -72,13 +72,20 @@ async function runPlanContextHint(opts) {
72
72
  maturity: item.maturity ?? item.description.maturity ?? "",
73
73
  summary: item.description.summary
74
74
  }));
75
- return {
75
+ const output = {
76
76
  version: 2,
77
77
  revision_hash: result.revision_hash,
78
78
  target_paths: targetPaths,
79
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-ZSESMG6L.js";
14
+ } from "./chunk-COI5VDFU.js";
15
15
 
16
16
  // src/commands/serve.ts
17
17
  import { defineCommand } from "citty";
@@ -7,7 +7,7 @@ import {
7
7
  HOOK_SCRIPT_DESTINATIONS,
8
8
  SKILL_DESTINATIONS,
9
9
  fabricAgentsSnapshotPath
10
- } from "./chunk-4HC5ZK7H.js";
10
+ } from "./chunk-AXKII55Y.js";
11
11
  import {
12
12
  detectClientSupports,
13
13
  resolveClients
@@ -23,7 +23,7 @@ import {
23
23
  import {
24
24
  createDebugLogger,
25
25
  resolveDevMode
26
- } from "./chunk-ZSESMG6L.js";
26
+ } from "./chunk-COI5VDFU.js";
27
27
 
28
28
  // src/commands/uninstall.ts
29
29
  import { existsSync as existsSync2, statSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "2.0.0-rc.21",
3
+ "version": "2.0.0-rc.23",
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.21",
24
- "@fenglimg/fabric-shared": "2.0.0-rc.21"
23
+ "@fenglimg/fabric-server": "2.0.0-rc.23",
24
+ "@fenglimg/fabric-shared": "2.0.0-rc.23"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^22.15.0",