@fenglimg/fabric-cli 2.0.0-rc.22 → 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.
- package/README.md +1 -2
- package/dist/{chunk-4HC5ZK7H.js → chunk-AXKII55Y.js} +6 -6
- package/dist/{chunk-ZSESMG6L.js → chunk-COI5VDFU.js} +0 -12
- package/dist/{chunk-KZ2YITOS.js → chunk-STLR2GHP.js} +137 -1
- package/dist/{config-AYP5F72E.js → config-XGUUAYX6.js} +1 -1
- package/dist/{doctor-HIX2FFEP.js → doctor-R2E2XO6A.js} +72 -13
- package/dist/index.js +10 -7
- package/dist/{install-WJZQZM7D.js → install-TDZYZV54.js} +106 -67
- package/dist/onboard-coverage-JJ5NGU7I.js +213 -0
- package/dist/{plan-context-hint-RYVSMULL.js → plan-context-hint-KPGOW3QC.js} +1 -1
- package/dist/{serve-6PPQX7AW.js → serve-NPCI342P.js} +1 -1
- package/dist/{uninstall-L2HEEOU3.js → uninstall-MQM6NUFM.js} +2 -2
- package/package.json +3 -3
- package/templates/hooks/configs/claude-code.json +3 -3
- package/templates/hooks/configs/codex-hooks.json +3 -3
- package/templates/hooks/fabric-hint.cjs +7 -2
- package/templates/skills/fabric-archive/SKILL.md +161 -3
- package/templates/skills/fabric-import/SKILL.md +26 -4
- package/templates/skills/fabric-review/SKILL.md +1 -1
- package/dist/chunk-PSVKSMRO.js +0 -896
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
installMcpClients
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import {
|
|
6
|
-
detectExistingLanguage,
|
|
7
|
-
runInitScan
|
|
8
|
-
} from "./chunk-PSVKSMRO.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-
|
|
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-
|
|
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
|
|
49
|
-
import { dirname, isAbsolute as isAbsolute3, join as
|
|
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
|
|
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
|
|
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
|
|
335
|
-
const absolutePath =
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
461
|
-
if (!
|
|
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(
|
|
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 =
|
|
802
|
-
const hasContributing =
|
|
803
|
-
if (!
|
|
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 =
|
|
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 =
|
|
1289
|
-
if (
|
|
1337
|
+
const packageJsonPath = join3(target, "package.json");
|
|
1338
|
+
if (existsSync3(packageJsonPath)) {
|
|
1290
1339
|
try {
|
|
1291
|
-
const packageJson = JSON.parse(
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1388
|
-
if (
|
|
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 (
|
|
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 =
|
|
1581
|
-
const agentsMdPath =
|
|
1582
|
-
const agentsMdAction =
|
|
1583
|
-
const knowledgeDir =
|
|
1584
|
-
const personalKnowledgeDir =
|
|
1585
|
-
const forensicPath =
|
|
1586
|
-
const eventsPath =
|
|
1587
|
-
const metaPath =
|
|
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 =
|
|
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 =
|
|
1678
|
+
const teamSubDir = join4(plan.knowledgeDir, sub);
|
|
1630
1679
|
mkdirSync(teamSubDir, { recursive: true });
|
|
1631
|
-
const teamGitkeep =
|
|
1632
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
1912
|
+
if (!existsSync4(path)) {
|
|
1874
1913
|
return false;
|
|
1875
1914
|
}
|
|
1876
|
-
if (
|
|
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 (!
|
|
1921
|
+
if (!existsSync4(path)) {
|
|
1883
1922
|
return { path, state: "missing" };
|
|
1884
1923
|
}
|
|
1885
1924
|
let stat;
|
|
1886
1925
|
try {
|
|
1887
|
-
stat =
|
|
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 =
|
|
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" &&
|
|
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 (!
|
|
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 (
|
|
2130
|
+
if (existsSync4(join4(workspaceRoot, "pnpm-lock.yaml"))) {
|
|
2092
2131
|
return "pnpm";
|
|
2093
2132
|
}
|
|
2094
|
-
if (
|
|
2133
|
+
if (existsSync4(join4(workspaceRoot, "yarn.lock"))) {
|
|
2095
2134
|
return "yarn";
|
|
2096
2135
|
}
|
|
2097
|
-
if (
|
|
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
|
+
};
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
HOOK_SCRIPT_DESTINATIONS,
|
|
8
8
|
SKILL_DESTINATIONS,
|
|
9
9
|
fabricAgentsSnapshotPath
|
|
10
|
-
} from "./chunk-
|
|
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-
|
|
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.
|
|
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.
|
|
24
|
-
"@fenglimg/fabric-shared": "2.0.0-rc.
|
|
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",
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
9
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/fabric-hint.cjs"
|
|
10
10
|
}
|
|
11
11
|
]
|
|
12
12
|
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"hooks": [
|
|
18
18
|
{
|
|
19
19
|
"type": "command",
|
|
20
|
-
"command": "
|
|
20
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-broad.cjs"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"hooks": [
|
|
29
29
|
{
|
|
30
30
|
"type": "command",
|
|
31
|
-
"command": "
|
|
31
|
+
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/knowledge-hint-narrow.cjs"
|
|
32
32
|
}
|
|
33
33
|
]
|
|
34
34
|
}
|