@fenglimg/fabric-cli 1.4.0 → 1.5.0

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 CHANGED
@@ -13,14 +13,18 @@
13
13
 
14
14
  `fabric bootstrap install` refreshes the internal bootstrap guide at `.fabric/bootstrap/README.md`. It does not generate root `AGENTS.md`, `CLAUDE.md`, or `GEMINI.md`.
15
15
 
16
- ## Common Commands
17
-
18
- - `fabric init`
19
- - `fabric serve`
20
- - `fabric doctor --audit`
21
-
22
- ## Advanced Commands
23
-
24
- - `fabric bootstrap install`
25
- - `fabric config install`
26
- - `fabric hooks install`
16
+ ## Common Commands
17
+
18
+ - `fabric init`
19
+ - `fabric serve`
20
+ - `fabric doctor --audit`
21
+ - `fabric approve --interactive`
22
+ - `fabric approve --all`
23
+
24
+ ## Advanced Commands
25
+
26
+ - `fabric bootstrap install`
27
+ - `fabric config install`
28
+ - `fabric hooks install`
29
+
30
+ `fabric approve` updates drifted entries in `.fabric/human-lock.json` after review. Use `--interactive` for per-entry confirmation and `--all` only when drift has already been reviewed elsewhere.
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ padEnd
4
+ } from "./chunk-WWNXR34K.js";
5
+ import {
6
+ t
7
+ } from "./chunk-6ICJICVU.js";
8
+
9
+ // src/commands/approve.ts
10
+ import { createInterface } from "readline/promises";
11
+ import { stdin as input, stdout as output } from "process";
12
+ import { isAbsolute, resolve } from "path";
13
+ import { approveHumanLock, readHumanLock } from "@fenglimg/fabric-server";
14
+ import { defineCommand, renderUsage } from "citty";
15
+ var approveCommand = defineCommand({
16
+ meta: {
17
+ name: "approve",
18
+ description: t("cli.approve.description")
19
+ },
20
+ args: {
21
+ all: {
22
+ type: "boolean",
23
+ description: t("cli.approve.args.all.description"),
24
+ default: false
25
+ },
26
+ interactive: {
27
+ type: "boolean",
28
+ description: t("cli.approve.args.interactive.description"),
29
+ default: false
30
+ },
31
+ target: {
32
+ type: "string",
33
+ description: t("cli.approve.args.target.description"),
34
+ default: process.cwd()
35
+ }
36
+ },
37
+ async run({ args }) {
38
+ const target = normalizeTarget(args.target);
39
+ if (args.all === args.interactive) {
40
+ writeStdout(await renderUsage(approveCommand));
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ if (args.all) {
45
+ await runApproveAll(target);
46
+ return;
47
+ }
48
+ await runApproveInteractive(target);
49
+ }
50
+ });
51
+ var approve_default = approveCommand;
52
+ async function runApproveAll(projectRoot) {
53
+ const driftEntries = await readDriftEntries(projectRoot);
54
+ if (driftEntries.length === 0) {
55
+ writeStdout(t("cli.approve.no-drift"));
56
+ return;
57
+ }
58
+ let approvedCount = 0;
59
+ for (const entry of driftEntries) {
60
+ await approveEntry(projectRoot, entry);
61
+ approvedCount += 1;
62
+ }
63
+ writeStdout(t("cli.approve.summary", { approved: String(approvedCount), skipped: "0", total: String(driftEntries.length) }));
64
+ }
65
+ async function runApproveInteractive(projectRoot) {
66
+ const driftEntries = await readDriftEntries(projectRoot);
67
+ if (driftEntries.length === 0) {
68
+ writeStdout(t("cli.approve.no-drift"));
69
+ return;
70
+ }
71
+ const rl = createInterface({ input, output });
72
+ let approvedCount = 0;
73
+ let skippedCount = 0;
74
+ try {
75
+ for (const entry of driftEntries) {
76
+ writeStdout(formatEntry(entry));
77
+ const answer = (await rl.question(t("cli.approve.prompt"))).trim().toLowerCase();
78
+ if (answer === "y" || answer === "yes") {
79
+ await approveEntry(projectRoot, entry);
80
+ approvedCount += 1;
81
+ writeStdout(t("cli.approve.approved-one", { location: formatLocation(entry) }));
82
+ continue;
83
+ }
84
+ skippedCount += 1;
85
+ writeStdout(t("cli.approve.skipped-one", { location: formatLocation(entry) }));
86
+ }
87
+ } finally {
88
+ rl.close();
89
+ }
90
+ writeStdout(
91
+ t("cli.approve.summary", {
92
+ approved: String(approvedCount),
93
+ skipped: String(skippedCount),
94
+ total: String(driftEntries.length)
95
+ })
96
+ );
97
+ }
98
+ async function readDriftEntries(projectRoot) {
99
+ const entries = await readHumanLock(projectRoot);
100
+ return entries.filter((entry) => entry.drift);
101
+ }
102
+ async function approveEntry(projectRoot, entry) {
103
+ await approveHumanLock(projectRoot, {
104
+ file: entry.file,
105
+ start_line: entry.start_line,
106
+ end_line: entry.end_line,
107
+ new_hash: entry.current_hash
108
+ });
109
+ }
110
+ function normalizeTarget(targetInput) {
111
+ return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
112
+ }
113
+ function formatEntry(entry) {
114
+ return [
115
+ formatLocation(entry),
116
+ `${padEnd(t("cli.approve.table.expected"), 10)} ${shortenHash(entry.hash)}`,
117
+ `${padEnd(t("cli.approve.table.current"), 10)} ${shortenHash(entry.current_hash)}`
118
+ ].join("\n");
119
+ }
120
+ function formatLocation(entry) {
121
+ return `${entry.file}:${entry.start_line}-${entry.end_line}`;
122
+ }
123
+ function shortenHash(value) {
124
+ if (value === "missing") {
125
+ return t("cli.shared.missing");
126
+ }
127
+ return value.slice(0, 15);
128
+ }
129
+ function writeStdout(message) {
130
+ process.stdout.write(`${message}
131
+ `);
132
+ }
133
+ export {
134
+ approveCommand,
135
+ approve_default as default,
136
+ runApproveAll,
137
+ runApproveInteractive
138
+ };
@@ -3,11 +3,11 @@ import {
3
3
  bootstrapCommand,
4
4
  bootstrap_default,
5
5
  installBootstrap
6
- } from "./chunk-XQYY2U2C.js";
7
- import "./chunk-AZRKMFRY.js";
8
- import "./chunk-BEKSXO5N.js";
6
+ } from "./chunk-T2WJF5I3.js";
7
+ import "./chunk-QSAEGVKE.js";
9
8
  import "./chunk-AEOYCVBG.js";
10
9
  import "./chunk-WWNXR34K.js";
10
+ import "./chunk-BEKSXO5N.js";
11
11
  import "./chunk-6ICJICVU.js";
12
12
  export {
13
13
  bootstrapCommand,
@@ -40,7 +40,7 @@ function resolveIgnores(fabricConfig) {
40
40
  }
41
41
 
42
42
  // src/commands/scan.ts
43
- function createScanReport(targetInput = process.cwd(), fabricConfig) {
43
+ async function createScanReport(targetInput = process.cwd(), fabricConfig) {
44
44
  const target = normalizeTarget(targetInput);
45
45
  const framework = detectFramework(target);
46
46
  const readmeQuality = getReadmeQuality(target);
@@ -93,7 +93,7 @@ var scanCommand = defineCommand({
93
93
  for (const step of resolution.chain) {
94
94
  logger(step);
95
95
  }
96
- const report = createScanReport(resolution.target, fabricConfig);
96
+ const report = await createScanReport(resolution.target, fabricConfig);
97
97
  if (args.json) {
98
98
  console.log(JSON.stringify(report, null, 2));
99
99
  return;
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createScanReport
4
- } from "./chunk-AZRKMFRY.js";
5
- import {
6
- resolveClients
7
- } from "./chunk-BEKSXO5N.js";
4
+ } from "./chunk-QSAEGVKE.js";
8
5
  import {
9
6
  readFabricConfig
10
7
  } from "./chunk-AEOYCVBG.js";
8
+ import {
9
+ resolveClients
10
+ } from "./chunk-BEKSXO5N.js";
11
11
  import {
12
12
  t
13
13
  } from "./chunk-6ICJICVU.js";
@@ -26,22 +26,22 @@ var AGENTS_TEMPLATE_BY_FRAMEWORK = {
26
26
  next: "templates/agents-md/variants/next.md"
27
27
  };
28
28
  var FABRIC_GUIDE_PATH = ".fabric/bootstrap/README.md";
29
- function buildFabricBootstrapGuide(target) {
29
+ async function buildFabricBootstrapGuide(target) {
30
30
  const workspaceRoot = normalizeTarget(target);
31
- const scanReport = createScanReport(workspaceRoot);
31
+ const scanReport = await createScanReport(workspaceRoot);
32
32
  const template = readFileSync(findBootstrapTemplatePath(scanReport.framework.kind), "utf8");
33
33
  const packageName = readPackageName(workspaceRoot) ?? parse(workspaceRoot).base;
34
34
  return ensureTrailingNewline(
35
35
  template.replaceAll("{ projectName }", packageName).replaceAll("{ frameworkKind }", scanReport.framework.kind)
36
36
  );
37
37
  }
38
- function ensureFabricBootstrapGuide(workspaceRoot, force) {
38
+ async function ensureFabricBootstrapGuide(workspaceRoot, force) {
39
39
  const guidePath = resolve(workspaceRoot, FABRIC_GUIDE_PATH);
40
40
  if (existsSync(guidePath) && !force) {
41
41
  return;
42
42
  }
43
43
  mkdirSync(dirname(guidePath), { recursive: true });
44
- writeFileSync(guidePath, buildFabricBootstrapGuide(workspaceRoot), "utf8");
44
+ writeFileSync(guidePath, await buildFabricBootstrapGuide(workspaceRoot), "utf8");
45
45
  }
46
46
  function findBootstrapTemplatePath(frameworkKind) {
47
47
  const relativePath = AGENTS_TEMPLATE_BY_FRAMEWORK[frameworkKind] ?? "templates/agents-md/AGENTS.md.template";
@@ -170,7 +170,7 @@ async function installBootstrap(target, options = {}) {
170
170
  const installed = [];
171
171
  const skipped = [];
172
172
  const details = [];
173
- ensureFabricBootstrapGuide(workspaceRoot, options.force);
173
+ await ensureFabricBootstrapGuide(workspaceRoot, options.force);
174
174
  for (const bootstrapTarget of targets) {
175
175
  details.push({
176
176
  client: bootstrapTarget.client,
package/dist/index.js CHANGED
@@ -8,16 +8,17 @@ import { defineCommand, runMain } from "citty";
8
8
 
9
9
  // src/commands/index.ts
10
10
  var allCommands = {
11
- init: () => import("./init-QC2MLFHR.js").then((module) => module.default),
12
- update: () => import("./update-FY2WKWPB.js").then((module) => module.default),
13
- scan: () => import("./scan-43R3IBLR.js").then((module) => module.default),
11
+ approve: () => import("./approve-YT4DEABS.js").then((module) => module.default),
12
+ init: () => import("./init-YR7EVBYQ.js").then((module) => module.default),
13
+ update: () => import("./update-M5M5PYKE.js").then((module) => module.default),
14
+ scan: () => import("./scan-QH76LC7Z.js").then((module) => module.default),
14
15
  serve: () => import("./serve-4J2CQY25.js").then((module) => module.default),
15
16
  doctor: () => import("./doctor-QTSG2RWF.js").then((module) => module.default),
16
17
  "sync-meta": () => import("./sync-meta-LKVSO6TS.js").then((module) => module.default),
17
18
  "human-lint": () => import("./human-lint-YSFOZHZ7.js").then((module) => module.default),
18
19
  "ledger-append": () => import("./ledger-append-DULKJ6Q2.js").then((module) => module.default),
19
20
  "pre-commit": () => import("./pre-commit-IK6SJOPT.js").then((module) => module.default),
20
- bootstrap: () => import("./bootstrap-B6RCVJZJ.js").then((module) => module.default),
21
+ bootstrap: () => import("./bootstrap-VGL3AR26.js").then((module) => module.default),
21
22
  config: () => import("./config-EC5L2QNI.js").then((module) => module.configCmd),
22
23
  hooks: () => import("./hooks-ZSWVH2JD.js").then((module) => ({
23
24
  ...module.default,
@@ -32,7 +33,7 @@ var allCommands = {
32
33
  var main = defineCommand({
33
34
  meta: {
34
35
  name: "fabric",
35
- version: "1.4.0",
36
+ version: "1.5.0",
36
37
  description: 'Initialize and manage Fabric projects. Use "fabric init" for one-shot setup.'
37
38
  },
38
39
  subCommands: allCommands
@@ -2,19 +2,10 @@
2
2
  import {
3
3
  buildFabricBootstrapGuide,
4
4
  installBootstrap
5
- } from "./chunk-XQYY2U2C.js";
5
+ } from "./chunk-T2WJF5I3.js";
6
6
  import {
7
7
  detectFramework
8
- } from "./chunk-AZRKMFRY.js";
9
- import {
10
- installMcpClients
11
- } from "./chunk-BVTMVW5M.js";
12
- import {
13
- detectClientSupports
14
- } from "./chunk-BEKSXO5N.js";
15
- import {
16
- installHooks
17
- } from "./chunk-YDZJRLHL.js";
8
+ } from "./chunk-QSAEGVKE.js";
18
9
  import {
19
10
  createDebugLogger,
20
11
  resolveDevMode
@@ -24,6 +15,15 @@ import {
24
15
  padEnd,
25
16
  paint
26
17
  } from "./chunk-WWNXR34K.js";
18
+ import {
19
+ installMcpClients
20
+ } from "./chunk-BVTMVW5M.js";
21
+ import {
22
+ detectClientSupports
23
+ } from "./chunk-BEKSXO5N.js";
24
+ import {
25
+ installHooks
26
+ } from "./chunk-YDZJRLHL.js";
27
27
  import {
28
28
  t
29
29
  } from "./chunk-6ICJICVU.js";
@@ -38,11 +38,14 @@ import { cancel, confirm, group, intro, isCancel, log, note, outro, select } fro
38
38
  import { defineCommand } from "citty";
39
39
 
40
40
  // src/scanner/forensic.ts
41
+ import { execFileSync } from "child_process";
41
42
  import { existsSync, readdirSync, readFileSync, statSync } from "fs";
43
+ import { createRequire } from "module";
42
44
  import { basename, extname, isAbsolute, join, posix, relative, resolve, sep } from "path";
43
45
  import {
44
46
  forensicReportSchema
45
47
  } from "@fenglimg/fabric-shared";
48
+ var require2 = createRequire(import.meta.url);
46
49
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
47
50
  ".fabric",
48
51
  ".git",
@@ -68,9 +71,48 @@ var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
68
71
  var DOMAIN_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".json", ".md"]);
69
72
  var EXPECTED_CONFIG_FILES_BY_FRAMEWORK = {
70
73
  "cocos-creator": ["package.json", "project.config.json", "tsconfig.json"],
74
+ react: ["package.json", "tsconfig.json"],
71
75
  next: ["package.json", "tsconfig.json"],
72
76
  vite: ["package.json", "tsconfig.json"]
73
77
  };
78
+ var FRAMEWORK_IMPORT_PROFILES = {
79
+ "cocos-creator": {
80
+ pattern: "cocos-component-class",
81
+ family: "component",
82
+ statement: "Sampled entry files use Cocos Creator component classes.",
83
+ proposedRule: "Treat assets/scripts/*.ts and adjacent .meta files as framework-owned structure unless the user says otherwise.",
84
+ alternatives: ["Generic TypeScript utility module"],
85
+ rationale: "Cocos framework imports and component markers co-occur in sampled entry files.",
86
+ packages: ["cc"]
87
+ },
88
+ react: {
89
+ pattern: "react-root",
90
+ family: "entry",
91
+ statement: "Sampled entry files import React framework packages.",
92
+ proposedRule: "Keep root rendering and component composition aligned with React entry conventions.",
93
+ alternatives: ["Server-rendered route module"],
94
+ rationale: "AST import declarations reference React packages rather than comments or strings.",
95
+ packages: ["react", "react-dom", "react/jsx-runtime", "react-dom/client"]
96
+ },
97
+ vite: {
98
+ pattern: "vite-main-entry",
99
+ family: "entry",
100
+ statement: "Sampled entry files use the conventional Vite main entrypoint.",
101
+ proposedRule: "Keep primary bootstrapping logic inside src/main.*.",
102
+ alternatives: ["Alternative bundler entrypoint"],
103
+ rationale: "Entry path and framework imports align with a Vite bootstrap surface.",
104
+ packages: ["@vitejs/plugin-react", "@vitejs/plugin-vue", "vite", "react", "vue"]
105
+ },
106
+ next: {
107
+ pattern: "next-route-component",
108
+ family: "entry",
109
+ statement: "Sampled entry files align with Next.js route modules.",
110
+ proposedRule: "Preserve route-segment boundaries when editing app/ or pages/ files.",
111
+ alternatives: ["Generic source module"],
112
+ rationale: "Route placement and Next/React imports anchor these files to the request surface.",
113
+ packages: ["next", "next/link", "next/navigation", "react"]
114
+ }
115
+ };
74
116
  var SAMPLE_LIMIT = 5;
75
117
  var SAMPLE_LINE_LIMIT = 30;
76
118
  var ENTRY_FAMILY_LIMIT = 1;
@@ -80,12 +122,17 @@ var DEFAULT_SAMPLING_BUDGET = {
80
122
  max_files: 15,
81
123
  max_lines_per_file: 100
82
124
  };
83
- function buildForensicReport(targetInput) {
125
+ var treeSitterModulePromise = null;
126
+ var parserInitPromise = null;
127
+ var languagePromiseByKind = {};
128
+ var parserBundlePromiseByKind = {};
129
+ async function buildForensicReport(targetInput) {
84
130
  const target = normalizeTarget(targetInput);
85
131
  const framework = detectFramework(target);
86
132
  const topology = buildTopology(target);
87
- const entryPoints = collectEntryPoints(topology.files);
88
- const codeSamples = buildCodeSamples(target, entryPoints);
133
+ const entryPoints = collectEntryPoints(target, topology.files);
134
+ const packageDependencies = readPackageDependencies(target);
135
+ const codeSamples = await buildCodeSamples(target, entryPoints, framework.kind, topology, packageDependencies);
89
136
  const assertions = buildAssertions(framework.kind, topology, codeSamples);
90
137
  const candidateFiles = buildCandidateFiles(topology, codeSamples, entryPoints);
91
138
  const readme = readReadmeInfo(target);
@@ -180,7 +227,7 @@ function isKeyDirectory(relativePath) {
180
227
  const name = basename(relativePath);
181
228
  return KEY_DIRECTORY_NAMES.has(name);
182
229
  }
183
- function collectEntryPoints(files) {
230
+ function collectEntryPoints(target, files) {
184
231
  const entryPoints = [];
185
232
  for (const file of files) {
186
233
  const reason = getEntryPointReason(file.relativePath);
@@ -193,7 +240,9 @@ function collectEntryPoints(files) {
193
240
  size_bytes: file.sizeBytes
194
241
  });
195
242
  }
196
- return entryPoints;
243
+ return entryPoints.sort(
244
+ (left, right) => compareCandidateScore(readGitChurnWeight(target, right.path), readGitChurnWeight(target, left.path))
245
+ );
197
246
  }
198
247
  function getEntryPointReason(relativePath) {
199
248
  if (!SCRIPT_EXTENSIONS.has(extname(relativePath))) {
@@ -216,20 +265,26 @@ function getEntryPointReason(relativePath) {
216
265
  }
217
266
  return null;
218
267
  }
219
- function buildCodeSamples(target, entryPoints) {
220
- return entryPoints.slice(0, SAMPLE_LIMIT).map((entryPoint) => {
268
+ async function buildCodeSamples(target, entryPoints, frameworkKind, topology, packageDependencies) {
269
+ const samples = [];
270
+ for (const entryPoint of entryPoints.slice(0, SAMPLE_LIMIT)) {
221
271
  const absolutePath = join(target, ...entryPoint.path.split("/"));
222
272
  const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
223
- const patternAnalysis = inferPatternHint(entryPoint.path, sample.snippet);
224
- return {
273
+ const patternAnalysis = await inferPatternHint(entryPoint.path, sample.snippet, {
274
+ frameworkKind,
275
+ topology,
276
+ packageDependencies
277
+ });
278
+ samples.push({
225
279
  path: entryPoint.path,
226
280
  lines: `1-${sample.lineCount}`,
227
281
  snippet: sample.snippet,
228
282
  pattern_hint: patternAnalysis.pattern,
229
283
  pattern_analysis: patternAnalysis,
230
284
  evidence: buildEvidenceAnchors(entryPoint.path, sample.snippet, patternAnalysis.evidence_lines)
231
- };
232
- });
285
+ });
286
+ }
287
+ return samples;
233
288
  }
234
289
  function readFirstLines(path, lineLimit) {
235
290
  try {
@@ -249,7 +304,100 @@ function readFirstLines(path, lineLimit) {
249
304
  };
250
305
  }
251
306
  }
252
- function inferPatternHint(relativePath, snippet) {
307
+ function readPackageDependencies(target) {
308
+ const packageJsonPath = join(target, "package.json");
309
+ if (!existsSync(packageJsonPath)) {
310
+ return /* @__PURE__ */ new Map();
311
+ }
312
+ try {
313
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
314
+ return new Map([
315
+ ...Object.entries(packageJson.dependencies ?? {}),
316
+ ...Object.entries(packageJson.devDependencies ?? {}),
317
+ ...Object.entries(packageJson.peerDependencies ?? {}),
318
+ ...Object.entries(packageJson.optionalDependencies ?? {})
319
+ ]);
320
+ } catch {
321
+ return /* @__PURE__ */ new Map();
322
+ }
323
+ }
324
+ function readGitChurnWeight(target, relativePath) {
325
+ try {
326
+ const output = execFileSync("git", ["log", "--follow", "--oneline", "-20", "--", relativePath], {
327
+ cwd: target,
328
+ encoding: "utf8",
329
+ stdio: ["ignore", "pipe", "ignore"],
330
+ timeout: 1e3
331
+ });
332
+ return output.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
333
+ } catch {
334
+ return 0;
335
+ }
336
+ }
337
+ async function inferPatternHint(relativePath, snippet, options = {}) {
338
+ const input = {
339
+ relativePath,
340
+ snippet,
341
+ frameworkKind: options.frameworkKind ?? "unknown",
342
+ topology: options.topology ?? createEmptyTopology(),
343
+ packageDependencies: options.packageDependencies ?? /* @__PURE__ */ new Map()
344
+ };
345
+ const importAnalysis = await analyzeImports(input.relativePath, input.snippet);
346
+ if (importAnalysis.astLevel) {
347
+ const astResult = buildAstPatternHint(input, importAnalysis.imports);
348
+ if (astResult !== null) {
349
+ return astResult;
350
+ }
351
+ }
352
+ return inferTextPatternHint(input.relativePath, input.snippet);
353
+ }
354
+ function createEmptyTopology() {
355
+ return {
356
+ total_files: 0,
357
+ by_ext: {},
358
+ key_dirs: [],
359
+ max_depth: 0,
360
+ files: []
361
+ };
362
+ }
363
+ function buildAstPatternHint(input, imports) {
364
+ const profile = resolveFrameworkImportProfile(input.frameworkKind, input.relativePath, imports);
365
+ if (profile === null) {
366
+ return null;
367
+ }
368
+ const matchingImports = imports.filter((source) => matchesAnyFrameworkPackage(source, profile.packages));
369
+ const configFiles = getExpectedConfigFiles(input.frameworkKind).filter((file) => hasFile(input.topology.files, file));
370
+ const packageMatches = profile.packages.filter((packageName) => input.packageDependencies.has(packageName));
371
+ const coOccurring = compactPatternNames([
372
+ ...matchingImports.map((source) => `import:${source}`),
373
+ ...configFiles.map(normalizeConfigPattern),
374
+ ...packageMatches.map((packageName) => `package:${packageName}`),
375
+ input.relativePath.startsWith("app/") ? "app-router" : null,
376
+ input.relativePath.startsWith("pages/") ? "pages-router" : null,
377
+ input.relativePath === "src/main.ts" || input.relativePath === "src/main.js" ? "main-entry" : null,
378
+ input.snippet.includes("@ccclass(") ? "ccclass-decorator" : null,
379
+ input.snippet.includes("extends Component") ? "component-base" : null
380
+ ]);
381
+ return {
382
+ pattern: profile.pattern,
383
+ type: "pattern",
384
+ confidence: scoreFrameworkConfidence({
385
+ importCount: matchingImports.length,
386
+ configCount: configFiles.length,
387
+ packageCount: packageMatches.length,
388
+ astLevel: true
389
+ }),
390
+ evidence_lines: matchingImports.length > 0 ? matchingImports : imports.slice(0, 3),
391
+ co_occurring: coOccurring,
392
+ family: profile.family,
393
+ ast_level: true,
394
+ statement: profile.statement,
395
+ proposed_rule: profile.proposedRule,
396
+ alternatives: profile.alternatives,
397
+ rationale: profile.rationale
398
+ };
399
+ }
400
+ function inferTextPatternHint(relativePath, snippet) {
253
401
  const cocosCoOccurring = compactPatternNames([
254
402
  snippet.includes('from "cc"') || snippet.includes("from 'cc'") ? "cc-import" : null,
255
403
  snippet.includes("@ccclass(") || snippet.includes("ccclass(") ? "ccclass-decorator" : null,
@@ -257,11 +405,16 @@ function inferPatternHint(relativePath, snippet) {
257
405
  snippet.includes("const { ccclass } = _decorator") ? "decorator-destructure" : null
258
406
  ]);
259
407
  if (cocosCoOccurring.length > 0) {
260
- const astLevel = snippet.includes("@ccclass(");
261
408
  return {
262
409
  pattern: "cocos-component-class",
263
410
  type: "pattern",
264
- confidence: determineConfidence(1, cocosCoOccurring, astLevel),
411
+ confidence: scoreFrameworkConfidence({
412
+ importCount: 0,
413
+ configCount: 0,
414
+ packageCount: 0,
415
+ astLevel: false,
416
+ keywordCount: cocosCoOccurring.length
417
+ }),
265
418
  evidence_lines: compactPatternNames([
266
419
  snippet.includes("_decorator") ? "_decorator" : null,
267
420
  snippet.includes("@ccclass(") ? "@ccclass(" : null,
@@ -269,7 +422,7 @@ function inferPatternHint(relativePath, snippet) {
269
422
  ]),
270
423
  co_occurring: cocosCoOccurring,
271
424
  family: "component",
272
- ast_level: astLevel,
425
+ ast_level: false,
273
426
  statement: "Sampled entry files use Cocos Creator component classes.",
274
427
  proposed_rule: "Treat assets/scripts/*.ts and adjacent .meta files as framework-owned structure unless the user says otherwise.",
275
428
  alternatives: ["Generic TypeScript utility module"],
@@ -285,7 +438,13 @@ function inferPatternHint(relativePath, snippet) {
285
438
  return {
286
439
  pattern: "react-root",
287
440
  type: "pattern",
288
- confidence: determineConfidence(1, reactCoOccurring, false),
441
+ confidence: scoreFrameworkConfidence({
442
+ importCount: 0,
443
+ configCount: 0,
444
+ packageCount: 0,
445
+ astLevel: false,
446
+ keywordCount: reactCoOccurring.length
447
+ }),
289
448
  evidence_lines: compactPatternNames([
290
449
  snippet.includes("createRoot(") ? "createRoot(" : null,
291
450
  snippet.includes("ReactDOM.render(") ? "ReactDOM.render(" : null
@@ -308,7 +467,13 @@ function inferPatternHint(relativePath, snippet) {
308
467
  return {
309
468
  pattern: "next-route-component",
310
469
  type: "pattern",
311
- confidence: determineConfidence(1, coOccurring, false),
470
+ confidence: scoreFrameworkConfidence({
471
+ importCount: 0,
472
+ configCount: 0,
473
+ packageCount: 0,
474
+ astLevel: false,
475
+ keywordCount: coOccurring.length
476
+ }),
312
477
  evidence_lines: compactPatternNames([
313
478
  relativePath.startsWith("app/") ? "app/" : null,
314
479
  relativePath.startsWith("pages/") ? "pages/" : null
@@ -331,7 +496,13 @@ function inferPatternHint(relativePath, snippet) {
331
496
  return {
332
497
  pattern: "vite-main-entry",
333
498
  type: "pattern",
334
- confidence: determineConfidence(1, coOccurring, false),
499
+ confidence: scoreFrameworkConfidence({
500
+ importCount: 0,
501
+ configCount: 0,
502
+ packageCount: 0,
503
+ astLevel: false,
504
+ keywordCount: coOccurring.length
505
+ }),
335
506
  evidence_lines: ["src/main"],
336
507
  co_occurring: coOccurring,
337
508
  family: "entry",
@@ -355,6 +526,125 @@ function inferPatternHint(relativePath, snippet) {
355
526
  rationale: "No strong framework markers were detected in the sampled snippet."
356
527
  };
357
528
  }
529
+ async function analyzeImports(relativePath, snippet) {
530
+ if (snippet.trim().length === 0) {
531
+ return { imports: [], astLevel: false };
532
+ }
533
+ try {
534
+ const imports = await extractImports(snippet, getLanguageKindForPath(relativePath));
535
+ return { imports, astLevel: true };
536
+ } catch {
537
+ return { imports: [], astLevel: false };
538
+ }
539
+ }
540
+ async function extractImports(source, languageKind) {
541
+ const { parser } = await loadTreeSitter(languageKind);
542
+ let tree = null;
543
+ try {
544
+ tree = parser.parse(source);
545
+ if (tree === null || tree.rootNode.hasError) {
546
+ throw new Error("tree-sitter parse failed");
547
+ }
548
+ const imports = [];
549
+ collectImportSources(tree.rootNode, imports);
550
+ return compactPatternNames(imports);
551
+ } finally {
552
+ tree?.delete();
553
+ }
554
+ }
555
+ async function loadTreeSitter(languageKind) {
556
+ parserBundlePromiseByKind[languageKind] ??= createTreeSitterParserBundle(languageKind);
557
+ return parserBundlePromiseByKind[languageKind];
558
+ }
559
+ async function createTreeSitterParserBundle(languageKind) {
560
+ const treeSitter = await loadTreeSitterModule();
561
+ await initTreeSitterParser(treeSitter);
562
+ const language = await loadTreeSitterLanguage(treeSitter, languageKind);
563
+ const parser = new treeSitter.Parser();
564
+ parser.setLanguage(language);
565
+ return { parser, language };
566
+ }
567
+ function loadTreeSitterModule() {
568
+ treeSitterModulePromise ??= import("web-tree-sitter");
569
+ return treeSitterModulePromise;
570
+ }
571
+ function initTreeSitterParser(treeSitter) {
572
+ parserInitPromise ??= treeSitter.Parser.init({
573
+ locateFile: (scriptName) => scriptName.endsWith(".wasm") ? require2.resolve("web-tree-sitter/web-tree-sitter.wasm") : scriptName
574
+ });
575
+ return parserInitPromise;
576
+ }
577
+ function loadTreeSitterLanguage(treeSitter, languageKind) {
578
+ languagePromiseByKind[languageKind] ??= treeSitter.Language.load(resolveTreeSitterGrammarPath(languageKind));
579
+ return languagePromiseByKind[languageKind];
580
+ }
581
+ function resolveTreeSitterGrammarPath(languageKind) {
582
+ switch (languageKind) {
583
+ case "typescript":
584
+ return require2.resolve("tree-sitter-typescript/tree-sitter-typescript.wasm");
585
+ case "tsx":
586
+ return require2.resolve("tree-sitter-typescript/tree-sitter-tsx.wasm");
587
+ case "javascript":
588
+ return require2.resolve("tree-sitter-javascript/tree-sitter-javascript.wasm");
589
+ }
590
+ }
591
+ function getLanguageKindForPath(relativePath) {
592
+ const extension = extname(relativePath);
593
+ if (extension === ".tsx") {
594
+ return "tsx";
595
+ }
596
+ if (extension === ".ts") {
597
+ return "typescript";
598
+ }
599
+ return "javascript";
600
+ }
601
+ function collectImportSources(node, imports) {
602
+ if (node.type === "import_statement" || node.type === "import_declaration") {
603
+ const sourceNode = node.childForFieldName("source");
604
+ if (sourceNode !== null) {
605
+ const source = stripStringLiteral(sourceNode.text);
606
+ if (source.length > 0) {
607
+ imports.push(source);
608
+ }
609
+ }
610
+ }
611
+ for (let index = 0; index < node.namedChildCount; index += 1) {
612
+ const child = node.namedChild(index);
613
+ if (child !== null) {
614
+ collectImportSources(child, imports);
615
+ }
616
+ }
617
+ }
618
+ function stripStringLiteral(value) {
619
+ return value.replace(/^['"]|['"]$/g, "");
620
+ }
621
+ function resolveFrameworkImportProfile(frameworkKind, relativePath, imports) {
622
+ const primaryProfile = FRAMEWORK_IMPORT_PROFILES[frameworkKind];
623
+ if (primaryProfile !== void 0 && imports.some((source) => matchesAnyFrameworkPackage(source, primaryProfile.packages))) {
624
+ return primaryProfile;
625
+ }
626
+ if ((relativePath.startsWith("app/") || relativePath.startsWith("pages/")) && FRAMEWORK_IMPORT_PROFILES.next !== void 0) {
627
+ return FRAMEWORK_IMPORT_PROFILES.next;
628
+ }
629
+ return Object.values(FRAMEWORK_IMPORT_PROFILES).find(
630
+ (profile) => imports.some((source) => matchesAnyFrameworkPackage(source, profile.packages))
631
+ ) ?? null;
632
+ }
633
+ function matchesAnyFrameworkPackage(source, packageNames) {
634
+ return packageNames.some((packageName) => source === packageName || source.startsWith(`${packageName}/`));
635
+ }
636
+ function scoreFrameworkConfidence(input) {
637
+ if (!input.astLevel) {
638
+ return (input.keywordCount ?? 0) > 0 ? "MEDIUM" : "LOW";
639
+ }
640
+ if (input.importCount > 3) {
641
+ return "HIGH";
642
+ }
643
+ if (input.importCount >= 1 && input.importCount <= 3) {
644
+ return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "MEDIUM";
645
+ }
646
+ return input.configCount > 0 || input.packageCount > 0 ? "MEDIUM" : "LOW";
647
+ }
358
648
  function readReadmeInfo(target) {
359
649
  const readmePath = join(target, "README.md");
360
650
  const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
@@ -857,7 +1147,7 @@ function readProjectName(target) {
857
1147
  return basename(target);
858
1148
  }
859
1149
  function getCliVersion() {
860
- return true ? "1.4.0" : "unknown";
1150
+ return true ? "1.5.0" : "unknown";
861
1151
  }
862
1152
  function sortRecord(record) {
863
1153
  return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
@@ -962,7 +1252,7 @@ async function runInitCommand(args) {
962
1252
  writeStderr(t("cli.init.compat.legacy-stage-flags"));
963
1253
  }
964
1254
  const supports = detectClientSupports(intent.target);
965
- const basePlan = buildInitExecutionPlan({
1255
+ const basePlan = await buildInitExecutionPlan({
966
1256
  target: intent.target,
967
1257
  options: intent.options,
968
1258
  mcpInstallMode: intent.mcpInstallMode,
@@ -998,9 +1288,9 @@ function resolveInitCliIntent(args, targetInput) {
998
1288
  wizardEnabled: shouldUseInitWizard(args, terminalInteractive) && !planOnly
999
1289
  };
1000
1290
  }
1001
- function buildInitExecutionPlan(input) {
1291
+ async function buildInitExecutionPlan(input) {
1002
1292
  const options = input.options ?? {};
1003
- const scaffold = buildInitFabricPlan(input.target, options);
1293
+ const scaffold = await buildInitFabricPlan(input.target, options);
1004
1294
  const supports = input.supports ?? detectClientSupports(input.target);
1005
1295
  const mcpInstallMode = input.mcpInstallMode ?? "global";
1006
1296
  const stages = [
@@ -1080,7 +1370,7 @@ async function executeInitExecutionPlan(plan) {
1080
1370
  finalSupports
1081
1371
  };
1082
1372
  }
1083
- function buildInitFabricPlan(target, options) {
1373
+ async function buildInitFabricPlan(target, options) {
1084
1374
  assertExistingDirectory2(target);
1085
1375
  const fabricDir = join2(target, ".fabric");
1086
1376
  const bootstrapPath = join2(fabricDir, "bootstrap", "README.md");
@@ -1099,9 +1389,9 @@ function buildInitFabricPlan(target, options) {
1099
1389
  const metaAction = planFreshPath(metaPath, options);
1100
1390
  const humanLockAction = planFreshPath(humanLockPath, options);
1101
1391
  const forensicAction = planFreshPath(forensicPath, options);
1102
- const forensicReport = buildForensicReport(target);
1392
+ const forensicReport = await buildForensicReport(target);
1103
1393
  const humanLockTemplate = readFileSync2(findTemplatePath("templates/fabric/human-lock.json"), "utf8");
1104
- const bootstrapContent = buildFabricBootstrapGuide(target);
1394
+ const bootstrapContent = await buildFabricBootstrapGuide(target);
1105
1395
  const bootstrapHash = sha256(bootstrapContent);
1106
1396
  const meta = createInitialMeta(bootstrapHash);
1107
1397
  return {
@@ -1194,8 +1484,8 @@ function executeInitFabricPlan(plan) {
1194
1484
  claudeSettingsAction: plan.claudeSettings.action
1195
1485
  };
1196
1486
  }
1197
- function initFabric(target, options) {
1198
- return executeInitFabricPlan(buildInitFabricPlan(target, options));
1487
+ async function initFabric(target, options) {
1488
+ return executeInitFabricPlan(await buildInitFabricPlan(target, options));
1199
1489
  }
1200
1490
  function shouldUseInitWizard(args, terminalInteractive = isInteractiveInit()) {
1201
1491
  return terminalInteractive && args.interactive !== false && args.yes !== true;
@@ -3,7 +3,7 @@ import {
3
3
  createScanReport,
4
4
  scanCommand,
5
5
  scan_default
6
- } from "./chunk-AZRKMFRY.js";
6
+ } from "./chunk-QSAEGVKE.js";
7
7
  import "./chunk-AEOYCVBG.js";
8
8
  import "./chunk-WWNXR34K.js";
9
9
  import "./chunk-6ICJICVU.js";
@@ -0,0 +1,24 @@
1
+ type TreeSitterProbeResult = {
2
+ ok: boolean;
3
+ node_version: string;
4
+ package_engines: "not-declared";
5
+ root_node_type: string;
6
+ has_error: boolean;
7
+ elapsed_ms: number;
8
+ wasm: {
9
+ runtime_path: string;
10
+ runtime_bytes: number;
11
+ javascript_grammar_path: string;
12
+ javascript_grammar_bytes: number;
13
+ };
14
+ decision: {
15
+ status: "feasible";
16
+ loading_strategy: "lazy";
17
+ bundle_size_impact: string;
18
+ grammar_strategy: string;
19
+ integration_note: string;
20
+ };
21
+ };
22
+ declare function runTreeSitterProbe(source?: string): Promise<TreeSitterProbeResult>;
23
+
24
+ export { type TreeSitterProbeResult, runTreeSitterProbe };
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/scanner/tree-sitter-probe.ts
4
+ import { realpathSync, statSync } from "fs";
5
+ import { createRequire } from "module";
6
+ import { resolve } from "path";
7
+ import { performance } from "perf_hooks";
8
+ import { fileURLToPath } from "url";
9
+ var require2 = createRequire(import.meta.url);
10
+ var PROBE_SOURCE = `import { strict as assert } from "node:assert";
11
+
12
+ const users = [{ id: "1", name: "Ada" }];
13
+
14
+ export function findUser(id) {
15
+ return users.find((user) => user.id === id);
16
+ }
17
+
18
+ assert.equal(findUser("1")?.name, "Ada");
19
+ console.log("parsed");`;
20
+ var treeSitterModulePromise = null;
21
+ var parserInitPromise = null;
22
+ var javascriptLanguagePromise = null;
23
+ async function runTreeSitterProbe(source = PROBE_SOURCE) {
24
+ const startedAt = performance.now();
25
+ const assets = resolveTreeSitterAssets();
26
+ const treeSitter = await loadTreeSitterModule();
27
+ await initParser(treeSitter, assets.runtimeWasmPath);
28
+ const language = await loadJavaScriptLanguage(treeSitter, assets.javascriptGrammarPath);
29
+ const parser = new treeSitter.Parser();
30
+ const runtimeBytes = statSync(assets.runtimeWasmPath).size;
31
+ const javascriptGrammarBytes = statSync(assets.javascriptGrammarPath).size;
32
+ let tree = null;
33
+ try {
34
+ parser.setLanguage(language);
35
+ tree = parser.parse(source);
36
+ if (tree === null) {
37
+ throw new Error("web-tree-sitter probe failed: parser returned null syntax tree");
38
+ }
39
+ const rootNode = tree.rootNode;
40
+ return {
41
+ ok: !rootNode.hasError,
42
+ node_version: process.version,
43
+ package_engines: "not-declared",
44
+ root_node_type: rootNode.type,
45
+ has_error: rootNode.hasError,
46
+ elapsed_ms: Math.round(performance.now() - startedAt),
47
+ wasm: {
48
+ runtime_path: assets.runtimeWasmPath,
49
+ runtime_bytes: runtimeBytes,
50
+ javascript_grammar_path: assets.javascriptGrammarPath,
51
+ javascript_grammar_bytes: javascriptGrammarBytes
52
+ },
53
+ decision: {
54
+ status: "feasible",
55
+ loading_strategy: "lazy",
56
+ bundle_size_impact: formatBundleImpact(runtimeBytes, javascriptGrammarBytes),
57
+ grammar_strategy: "Use tree-sitter-javascript WASM for JavaScript and TS-compatible syntax; evaluate tree-sitter-typescript before parsing TypeScript-only syntax.",
58
+ integration_note: "Keep web-tree-sitter behind a dynamic import at the forensic inferPatternHint() call site to avoid CLI startup cost."
59
+ }
60
+ };
61
+ } finally {
62
+ tree?.delete();
63
+ parser.delete();
64
+ }
65
+ }
66
+ function resolveTreeSitterAssets() {
67
+ return {
68
+ runtimeWasmPath: require2.resolve("web-tree-sitter/web-tree-sitter.wasm"),
69
+ javascriptGrammarPath: require2.resolve("tree-sitter-javascript/tree-sitter-javascript.wasm")
70
+ };
71
+ }
72
+ function loadTreeSitterModule() {
73
+ treeSitterModulePromise ??= import("web-tree-sitter");
74
+ return treeSitterModulePromise;
75
+ }
76
+ function initParser(treeSitter, runtimeWasmPath) {
77
+ parserInitPromise ??= treeSitter.Parser.init({
78
+ locateFile: (scriptName) => scriptName.endsWith(".wasm") ? runtimeWasmPath : scriptName
79
+ });
80
+ return parserInitPromise;
81
+ }
82
+ function loadJavaScriptLanguage(treeSitter, javascriptGrammarPath) {
83
+ javascriptLanguagePromise ??= treeSitter.Language.load(javascriptGrammarPath);
84
+ return javascriptLanguagePromise;
85
+ }
86
+ function formatBundleImpact(runtimeBytes, javascriptGrammarBytes) {
87
+ const combinedBytes = runtimeBytes + javascriptGrammarBytes;
88
+ return `${formatBytes(combinedBytes)} combined WASM payload (${formatBytes(runtimeBytes)} runtime + ${formatBytes(javascriptGrammarBytes)} JavaScript grammar); package unpacked sizes are larger and acceptable only with lazy loading.`;
89
+ }
90
+ function formatBytes(bytes) {
91
+ return `${(bytes / 1024).toFixed(1)}KB`;
92
+ }
93
+ var entrypoint = process.argv[1];
94
+ var currentFilePath = fileURLToPath(import.meta.url);
95
+ var isMainModule = entrypoint !== void 0 && realpathSync(resolve(entrypoint)) === currentFilePath;
96
+ if (isMainModule) {
97
+ runTreeSitterProbe().then((result) => {
98
+ console.log(JSON.stringify(result, null, 2));
99
+ }).catch((error) => {
100
+ const message = error instanceof Error ? error.message : String(error);
101
+ console.error(`web-tree-sitter probe failed: ${message}`);
102
+ process.exitCode = 1;
103
+ });
104
+ }
105
+ export {
106
+ runTreeSitterProbe
107
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenglimg/fabric-cli",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "fab": "dist/index.js",
@@ -19,8 +19,11 @@
19
19
  "minimatch": "^10.0.1",
20
20
  "picocolors": "^1.1.1",
21
21
  "string-width": "^7.2.0",
22
- "@fenglimg/fabric-server": "1.4.0",
23
- "@fenglimg/fabric-shared": "1.4.0"
22
+ "tree-sitter-javascript": "^0.25.0",
23
+ "tree-sitter-typescript": "^0.23.2",
24
+ "web-tree-sitter": "^0.26.8",
25
+ "@fenglimg/fabric-server": "1.5.0",
26
+ "@fenglimg/fabric-shared": "1.5.0"
24
27
  },
25
28
  "devDependencies": {
26
29
  "@types/iarna__toml": "^2.0.5",
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ resolveDevModeTarget
4
+ } from "./chunk-AEOYCVBG.js";
5
+ import {
6
+ paint
7
+ } from "./chunk-WWNXR34K.js";
2
8
  import {
3
9
  installMcpClients
4
10
  } from "./chunk-BVTMVW5M.js";
@@ -6,12 +12,6 @@ import "./chunk-BEKSXO5N.js";
6
12
  import {
7
13
  installHooks
8
14
  } from "./chunk-YDZJRLHL.js";
9
- import {
10
- resolveDevModeTarget
11
- } from "./chunk-AEOYCVBG.js";
12
- import {
13
- paint
14
- } from "./chunk-WWNXR34K.js";
15
15
  import {
16
16
  t
17
17
  } from "./chunk-6ICJICVU.js";