@fenglimg/fabric-cli 1.4.0 → 1.5.1
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 +25 -21
- package/dist/approve-YT4DEABS.js +138 -0
- package/dist/{bootstrap-B6RCVJZJ.js → bootstrap-VGL3AR26.js} +3 -3
- package/dist/{chunk-AZRKMFRY.js → chunk-QSAEGVKE.js} +2 -2
- package/dist/{chunk-XQYY2U2C.js → chunk-T2WJF5I3.js} +9 -9
- package/dist/index.js +6 -5
- package/dist/{init-QC2MLFHR.js → init-QPKI2QOW.js} +328 -38
- package/dist/{scan-43R3IBLR.js → scan-QH76LC7Z.js} +1 -1
- package/dist/scanner/tree-sitter-probe.d.ts +24 -0
- package/dist/scanner/tree-sitter-probe.js +107 -0
- package/package.json +6 -3
- package/templates/codex-hooks/fabric-session-start.cjs +1 -1
- package/templates/codex-hooks/fabric-stop-reminder.cjs +1 -1
- package/templates/codex-skills/fabric-init/SKILL.md +15 -15
- package/dist/{update-FY2WKWPB.js → update-M5M5PYKE.js} +6 -6
package/README.md
CHANGED
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
# @fenglimg/fabric-cli
|
|
2
2
|
|
|
3
|
-
`fabric`
|
|
3
|
+
`fabric` 是 Fabric 的主命令,`fab` 是永久别名,两者等价。
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
1. Install dependencies from the monorepo root with `pnpm install`.
|
|
8
|
-
2. Build the CLI with `pnpm --filter @fenglimg/fabric-cli build`.
|
|
9
|
-
3. Run `fabric init` in the target project for the one-shot setup flow.
|
|
10
|
-
4. Start `fabric serve` and verify `fab_get_rules` in your client.
|
|
11
|
-
|
|
12
|
-
`fabric init` auto-runs `bootstrap install`, `config install`, and `hooks install`. Use them standalone only for targeted re-runs.
|
|
5
|
+
## 快速开始
|
|
13
6
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## Advanced Commands
|
|
7
|
+
1. 在 monorepo 根目录运行 `pnpm install`。
|
|
8
|
+
2. 用 `pnpm --filter @fenglimg/fabric-cli build` 构建 CLI。
|
|
9
|
+
3. 在目标项目运行 `fabric init`,完成一站式初始化。
|
|
10
|
+
4. 启动 `fabric serve`,再去客户端里验证 `fab_get_rules`。
|
|
11
|
+
|
|
12
|
+
`fabric init` 会自动执行 `bootstrap install`、`config install` 和 `hooks install`。只有在需要单独重跑某个阶段时,才需要单独调用它们。
|
|
13
|
+
|
|
14
|
+
`fabric bootstrap install` 只会刷新 `.fabric/bootstrap/README.md` 里的内部初始化说明,不会再生成根级 `AGENTS.md`、`CLAUDE.md` 或 `GEMINI.md`。
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- `fabric
|
|
16
|
+
## 常用命令
|
|
17
|
+
|
|
18
|
+
- `fabric init`
|
|
19
|
+
- `fabric serve`
|
|
20
|
+
- `fabric doctor --audit`
|
|
21
|
+
- `fabric approve --interactive`
|
|
22
|
+
- `fabric approve --all`
|
|
23
|
+
|
|
24
|
+
## 进阶命令
|
|
25
|
+
|
|
26
|
+
- `fabric bootstrap install`
|
|
27
|
+
- `fabric config install`
|
|
28
|
+
- `fabric hooks install`
|
|
29
|
+
|
|
30
|
+
`fabric approve` 会在审查完成后更新 `.fabric/human-lock.json` 中已经发生漂移的条目。需要逐项确认时使用 `--interactive`,只有在别处已经完成审查时才使用 `--all`。
|
|
@@ -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-
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
approve: () => import("./approve-YT4DEABS.js").then((module) => module.default),
|
|
12
|
+
init: () => import("./init-QPKI2QOW.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-
|
|
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.
|
|
36
|
+
version: "1.5.1",
|
|
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-
|
|
5
|
+
} from "./chunk-T2WJF5I3.js";
|
|
6
6
|
import {
|
|
7
7
|
detectFramework
|
|
8
|
-
} from "./chunk-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
1150
|
+
return true ? "1.5.1" : "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;
|
|
@@ -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.
|
|
3
|
+
"version": "1.5.1",
|
|
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
|
-
"
|
|
23
|
-
"
|
|
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.1",
|
|
26
|
+
"@fenglimg/fabric-shared": "1.5.1"
|
|
24
27
|
},
|
|
25
28
|
"devDependencies": {
|
|
26
29
|
"@types/iarna__toml": "^2.0.5",
|
|
@@ -13,7 +13,7 @@ process.stdout.write(
|
|
|
13
13
|
JSON.stringify({
|
|
14
14
|
hookSpecificOutput: {
|
|
15
15
|
additionalContext:
|
|
16
|
-
"Fabric
|
|
16
|
+
"这个仓库的 Fabric 初始化还没完成。继续操作前,请先查看 .fabric/forensic.json 和 .fabric/bootstrap/README.md,并使用仓库内的 .agents/skills/fabric-init/SKILL.md。若 Codex hooks 没有触发,请确认配置里已启用 features.codex_hooks = true。",
|
|
17
17
|
},
|
|
18
18
|
}),
|
|
19
19
|
);
|
|
@@ -13,6 +13,6 @@ process.stdout.write(
|
|
|
13
13
|
JSON.stringify({
|
|
14
14
|
decision: "block",
|
|
15
15
|
reason:
|
|
16
|
-
"fab init
|
|
16
|
+
"fab init 已经收集完当前仓库的初始化依据,但后续初始化还没完成。请先确认 Codex 已启用 features.codex_hooks = true,然后查看 .fabric/forensic.json 和 .fabric/bootstrap/README.md,并使用仓库内的 .agents/skills/fabric-init/SKILL.md 继续初始化。",
|
|
17
17
|
}),
|
|
18
18
|
);
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: fabric-init
|
|
3
|
-
description: Use this skill when `.fabric/forensic.json` exists and
|
|
3
|
+
description: Use this skill when `.fabric/forensic.json` exists and this repository still needs the remaining Fabric initialization steps.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
## Hard Rules (
|
|
6
|
+
## Hard Rules (不要翻译受保护 token)
|
|
7
7
|
|
|
8
|
-
MUST:
|
|
9
|
-
MUST:
|
|
10
|
-
MUST:
|
|
11
|
-
MUST:
|
|
8
|
+
MUST: 先读取 `.fabric/forensic.json`,再做其他动作。
|
|
9
|
+
MUST: 把 `.fabric/bootstrap/README.md` 视为当前仓库的初始化说明。
|
|
10
|
+
MUST: 如果 `.fabric/init-context.json` 已存在,立即停止并报告当前仓库看起来已经完成后续初始化。
|
|
11
|
+
MUST: 使用 `.fabric/forensic.json` 和仓库结构中的依据,判断接下来该做什么。
|
|
12
12
|
MUST: Preserve protected tokens exactly: `AGENTS.md`, `FABRIC.md`, `.fabric/agents.meta.json`, `.fabric/human-lock.json`, `.fabric/init-context.json`, `.fabric/forensic.json`, `MUST`, `NEVER`.
|
|
13
|
-
NEVER:
|
|
14
|
-
NEVER:
|
|
15
|
-
NEVER:
|
|
13
|
+
NEVER: 在没有检查 `.fabric/init-context.json` 的情况下声称初始化已经完成。
|
|
14
|
+
NEVER: 改写或翻译受保护 token。
|
|
15
|
+
NEVER: 在判断下一步初始化动作时忽略 `.fabric/bootstrap/README.md`。
|
|
16
16
|
|
|
17
17
|
## Purpose
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
当你在 Codex 中处理这个仓库,并且 `fab init` 已经生成 `.fabric/forensic.json` 时,使用这个 skill 继续完成仓库专属的 Fabric 初始化。目标是基于当前仓库的初始化依据和内部说明,明确下一步该做什么,而不是重新解释一遍通用流程。
|
|
20
20
|
|
|
21
21
|
## Workflow
|
|
22
22
|
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
26
|
-
4.
|
|
27
|
-
5.
|
|
23
|
+
1. 读取 `.fabric/forensic.json`。
|
|
24
|
+
2. 读取 `.fabric/bootstrap/README.md`。
|
|
25
|
+
3. 检查 `.fabric/init-context.json` 是否已经存在。
|
|
26
|
+
4. 如果初始化仍未完成,明确总结当前仓库接下来要做的初始化动作。
|
|
27
|
+
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";
|