@fenglimg/fabric-cli 1.0.0 → 1.1.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/dist/{chunk-5BSTO745.js → chunk-6UUPKSDE.js} +70 -31
- package/dist/{chunk-DKQ3HOTK.js → chunk-JWUO6TIS.js} +18 -4
- package/dist/doctor-QTSG2RWF.js +125 -0
- package/dist/index.js +6 -5
- package/dist/init-R73E5YTG.js +1164 -0
- package/dist/{pre-commit-IEIXHKOD.js → pre-commit-BLSUMT3P.js} +4 -5
- package/dist/{scan-6CURGC3D.js → scan-JBGFRB7P.js} +1 -2
- package/dist/{sync-meta-L6M4AEUT.js → sync-meta-THZSEM7Y.js} +5 -2
- package/package.json +3 -3
- package/templates/agents-md/AGENTS.md.template +20 -35
- package/templates/agents-md/variants/cocos.md +20 -37
- package/templates/agents-md/variants/next.md +20 -37
- package/templates/agents-md/variants/vite.md +20 -37
- package/dist/chunk-P4KVFB2T.js +0 -22
- package/dist/init-G6Q3OOMC.js +0 -601
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createScanReport,
|
|
4
|
+
detectFramework
|
|
5
|
+
} from "./chunk-JWUO6TIS.js";
|
|
6
|
+
import {
|
|
7
|
+
createDebugLogger,
|
|
8
|
+
resolveDevMode
|
|
9
|
+
} from "./chunk-AEOYCVBG.js";
|
|
10
|
+
import {
|
|
11
|
+
paint
|
|
12
|
+
} from "./chunk-WWNXR34K.js";
|
|
13
|
+
import {
|
|
14
|
+
t
|
|
15
|
+
} from "./chunk-6ICJICVU.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/init.ts
|
|
18
|
+
import { createHash } from "crypto";
|
|
19
|
+
import { chmodSync, copyFileSync, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, renameSync, statSync as statSync2, writeFileSync } from "fs";
|
|
20
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, parse, resolve as resolve2 } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { defineCommand } from "citty";
|
|
23
|
+
|
|
24
|
+
// src/scanner/forensic.ts
|
|
25
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
26
|
+
import { basename, extname, isAbsolute, join, posix, relative, resolve, sep } from "path";
|
|
27
|
+
import {
|
|
28
|
+
forensicReportSchema
|
|
29
|
+
} from "@fenglimg/fabric-shared";
|
|
30
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
31
|
+
".fabric",
|
|
32
|
+
".git",
|
|
33
|
+
".next",
|
|
34
|
+
".turbo",
|
|
35
|
+
"Library",
|
|
36
|
+
"Temp",
|
|
37
|
+
"build",
|
|
38
|
+
"coverage",
|
|
39
|
+
"dist",
|
|
40
|
+
"node_modules"
|
|
41
|
+
]);
|
|
42
|
+
var KEY_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
|
|
43
|
+
"app",
|
|
44
|
+
"components",
|
|
45
|
+
"pages",
|
|
46
|
+
"prefabs",
|
|
47
|
+
"scenes",
|
|
48
|
+
"scripts",
|
|
49
|
+
"src"
|
|
50
|
+
]);
|
|
51
|
+
var SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
52
|
+
var DOMAIN_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".json", ".md"]);
|
|
53
|
+
var EXPECTED_CONFIG_FILES_BY_FRAMEWORK = {
|
|
54
|
+
"cocos-creator": ["package.json", "project.config.json", "tsconfig.json"],
|
|
55
|
+
next: ["package.json", "tsconfig.json"],
|
|
56
|
+
vite: ["package.json", "tsconfig.json"]
|
|
57
|
+
};
|
|
58
|
+
var SAMPLE_LIMIT = 5;
|
|
59
|
+
var SAMPLE_LINE_LIMIT = 30;
|
|
60
|
+
var ENTRY_FAMILY_LIMIT = 1;
|
|
61
|
+
var FAMILY_LIMIT = 3;
|
|
62
|
+
var CANDIDATE_FILE_LIMIT = 12;
|
|
63
|
+
var DEFAULT_SAMPLING_BUDGET = {
|
|
64
|
+
max_files: 15,
|
|
65
|
+
max_lines_per_file: 100
|
|
66
|
+
};
|
|
67
|
+
function buildForensicReport(targetInput) {
|
|
68
|
+
const target = normalizeTarget(targetInput);
|
|
69
|
+
const framework = detectFramework(target);
|
|
70
|
+
const topology = buildTopology(target);
|
|
71
|
+
const entryPoints = collectEntryPoints(topology.files);
|
|
72
|
+
const codeSamples = buildCodeSamples(target, entryPoints);
|
|
73
|
+
const assertions = buildAssertions(framework.kind, topology, codeSamples);
|
|
74
|
+
const candidateFiles = buildCandidateFiles(topology, codeSamples, entryPoints);
|
|
75
|
+
const readme = readReadmeInfo(target);
|
|
76
|
+
const report = {
|
|
77
|
+
version: "1.0",
|
|
78
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
79
|
+
generated_by: `fab-cli@${getCliVersion()}`,
|
|
80
|
+
target,
|
|
81
|
+
project_name: readProjectName(target),
|
|
82
|
+
framework,
|
|
83
|
+
topology: {
|
|
84
|
+
total_files: topology.total_files,
|
|
85
|
+
by_ext: topology.by_ext,
|
|
86
|
+
key_dirs: topology.key_dirs,
|
|
87
|
+
max_depth: topology.max_depth
|
|
88
|
+
},
|
|
89
|
+
entry_points: entryPoints,
|
|
90
|
+
code_samples: codeSamples.map(({ pattern_analysis: _patternAnalysis, evidence: _evidence, ...sample }) => sample),
|
|
91
|
+
assertions,
|
|
92
|
+
candidate_files: candidateFiles,
|
|
93
|
+
sampling_budget: DEFAULT_SAMPLING_BUDGET,
|
|
94
|
+
readme,
|
|
95
|
+
recommendations_for_skill: buildSkillRecommendations(framework.kind, topology, readme)
|
|
96
|
+
};
|
|
97
|
+
const validation = forensicReportSchema.safeParse(report);
|
|
98
|
+
if (!validation.success) {
|
|
99
|
+
throw new Error(`ForensicReport schema validation failed: ${validation.error.message}`);
|
|
100
|
+
}
|
|
101
|
+
return validation.data;
|
|
102
|
+
}
|
|
103
|
+
function normalizeTarget(targetInput) {
|
|
104
|
+
return isAbsolute(targetInput) ? targetInput : resolve(process.cwd(), targetInput);
|
|
105
|
+
}
|
|
106
|
+
function buildTopology(root) {
|
|
107
|
+
assertExistingDirectory(root);
|
|
108
|
+
const byExt = {};
|
|
109
|
+
const keyDirs = /* @__PURE__ */ new Set();
|
|
110
|
+
const files = [];
|
|
111
|
+
let totalFiles = 0;
|
|
112
|
+
let maxDepth = 0;
|
|
113
|
+
const stack = [root];
|
|
114
|
+
while (stack.length > 0) {
|
|
115
|
+
const current = stack.pop();
|
|
116
|
+
if (current === void 0) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
120
|
+
const absolutePath = join(current, entry.name);
|
|
121
|
+
const relativePath = toPosixPath(relative(root, absolutePath));
|
|
122
|
+
if (relativePath.length === 0) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const depth = relativePath.split("/").length;
|
|
126
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
127
|
+
if (entry.isDirectory()) {
|
|
128
|
+
if (IGNORED_DIRECTORIES.has(entry.name)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (isKeyDirectory(relativePath)) {
|
|
132
|
+
keyDirs.add(relativePath);
|
|
133
|
+
}
|
|
134
|
+
stack.push(absolutePath);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (!entry.isFile()) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const stats = statSync(absolutePath);
|
|
141
|
+
const extension = extname(entry.name) || "[none]";
|
|
142
|
+
byExt[extension] = (byExt[extension] ?? 0) + 1;
|
|
143
|
+
totalFiles += 1;
|
|
144
|
+
files.push({
|
|
145
|
+
relativePath,
|
|
146
|
+
sizeBytes: stats.size
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
total_files: totalFiles,
|
|
152
|
+
by_ext: sortRecord(byExt),
|
|
153
|
+
key_dirs: [...keyDirs].sort(),
|
|
154
|
+
max_depth: maxDepth,
|
|
155
|
+
files: files.sort((left, right) => left.relativePath.localeCompare(right.relativePath))
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function assertExistingDirectory(target) {
|
|
159
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) {
|
|
160
|
+
throw new Error(`Target must be an existing directory: ${target}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isKeyDirectory(relativePath) {
|
|
164
|
+
const name = basename(relativePath);
|
|
165
|
+
return KEY_DIRECTORY_NAMES.has(name);
|
|
166
|
+
}
|
|
167
|
+
function collectEntryPoints(files) {
|
|
168
|
+
const entryPoints = [];
|
|
169
|
+
for (const file of files) {
|
|
170
|
+
const reason = getEntryPointReason(file.relativePath);
|
|
171
|
+
if (reason === null) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
entryPoints.push({
|
|
175
|
+
path: file.relativePath,
|
|
176
|
+
reason,
|
|
177
|
+
size_bytes: file.sizeBytes
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return entryPoints;
|
|
181
|
+
}
|
|
182
|
+
function getEntryPointReason(relativePath) {
|
|
183
|
+
if (!SCRIPT_EXTENSIONS.has(extname(relativePath))) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
const directory = posix.dirname(relativePath);
|
|
187
|
+
const fileName = basename(relativePath);
|
|
188
|
+
const fileBase = basename(relativePath, extname(relativePath));
|
|
189
|
+
if (directory === "assets/scripts" || directory === "scripts") {
|
|
190
|
+
return "top-level script";
|
|
191
|
+
}
|
|
192
|
+
if (directory === "src" && /^(App|app|index|main)$/.test(fileBase)) {
|
|
193
|
+
return "application entry";
|
|
194
|
+
}
|
|
195
|
+
if ((directory === "app" || directory.startsWith("app/")) && /^(layout|page|route)$/.test(fileBase)) {
|
|
196
|
+
return "next app route";
|
|
197
|
+
}
|
|
198
|
+
if ((directory === "pages" || directory.startsWith("pages/")) && fileName !== "_app.d.ts") {
|
|
199
|
+
return "next page route";
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function buildCodeSamples(target, entryPoints) {
|
|
204
|
+
return entryPoints.slice(0, SAMPLE_LIMIT).map((entryPoint) => {
|
|
205
|
+
const absolutePath = join(target, ...entryPoint.path.split("/"));
|
|
206
|
+
const sample = readFirstLines(absolutePath, SAMPLE_LINE_LIMIT);
|
|
207
|
+
const patternAnalysis = inferPatternHint(entryPoint.path, sample.snippet);
|
|
208
|
+
return {
|
|
209
|
+
path: entryPoint.path,
|
|
210
|
+
lines: `1-${sample.lineCount}`,
|
|
211
|
+
snippet: sample.snippet,
|
|
212
|
+
pattern_hint: patternAnalysis.pattern,
|
|
213
|
+
pattern_analysis: patternAnalysis,
|
|
214
|
+
evidence: buildEvidenceAnchors(entryPoint.path, sample.snippet, patternAnalysis.evidence_lines)
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function readFirstLines(path, lineLimit) {
|
|
219
|
+
try {
|
|
220
|
+
const lines = readFileSync(path, "utf8").split(/\r?\n/);
|
|
221
|
+
if (lines.at(-1) === "") {
|
|
222
|
+
lines.pop();
|
|
223
|
+
}
|
|
224
|
+
const sampledLines = lines.slice(0, lineLimit);
|
|
225
|
+
return {
|
|
226
|
+
snippet: sampledLines.join("\n"),
|
|
227
|
+
lineCount: sampledLines.length
|
|
228
|
+
};
|
|
229
|
+
} catch {
|
|
230
|
+
return {
|
|
231
|
+
snippet: "",
|
|
232
|
+
lineCount: 0
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function inferPatternHint(relativePath, snippet) {
|
|
237
|
+
const cocosCoOccurring = compactPatternNames([
|
|
238
|
+
snippet.includes('from "cc"') || snippet.includes("from 'cc'") ? "cc-import" : null,
|
|
239
|
+
snippet.includes("@ccclass(") || snippet.includes("ccclass(") ? "ccclass-decorator" : null,
|
|
240
|
+
snippet.includes("extends Component") ? "component-base" : null,
|
|
241
|
+
snippet.includes("const { ccclass } = _decorator") ? "decorator-destructure" : null
|
|
242
|
+
]);
|
|
243
|
+
if (cocosCoOccurring.length > 0) {
|
|
244
|
+
const astLevel = snippet.includes("@ccclass(");
|
|
245
|
+
return {
|
|
246
|
+
pattern: "cocos-component-class",
|
|
247
|
+
type: "pattern",
|
|
248
|
+
confidence: determineConfidence(1, cocosCoOccurring, astLevel),
|
|
249
|
+
evidence_lines: compactPatternNames([
|
|
250
|
+
snippet.includes("_decorator") ? "_decorator" : null,
|
|
251
|
+
snippet.includes("@ccclass(") ? "@ccclass(" : null,
|
|
252
|
+
snippet.includes("extends Component") ? "extends Component" : null
|
|
253
|
+
]),
|
|
254
|
+
co_occurring: cocosCoOccurring,
|
|
255
|
+
family: "component",
|
|
256
|
+
ast_level: astLevel,
|
|
257
|
+
statement: "Sampled entry files use Cocos Creator component classes.",
|
|
258
|
+
proposed_rule: "Treat assets/scripts/*.ts and adjacent .meta files as framework-owned structure unless the user says otherwise.",
|
|
259
|
+
alternatives: ["Generic TypeScript utility module"],
|
|
260
|
+
rationale: "Cocos-specific decorators and Component inheritance co-occur in sampled entry files."
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const reactCoOccurring = compactPatternNames([
|
|
264
|
+
snippet.includes("createRoot(") ? "create-root" : null,
|
|
265
|
+
snippet.includes("ReactDOM.render(") ? "react-dom-render" : null,
|
|
266
|
+
snippet.includes('from "react-dom"') || snippet.includes("from 'react-dom'") ? "react-dom-import" : null
|
|
267
|
+
]);
|
|
268
|
+
if (reactCoOccurring.length > 0) {
|
|
269
|
+
return {
|
|
270
|
+
pattern: "react-root",
|
|
271
|
+
type: "pattern",
|
|
272
|
+
confidence: determineConfidence(1, reactCoOccurring, false),
|
|
273
|
+
evidence_lines: compactPatternNames([
|
|
274
|
+
snippet.includes("createRoot(") ? "createRoot(" : null,
|
|
275
|
+
snippet.includes("ReactDOM.render(") ? "ReactDOM.render(" : null
|
|
276
|
+
]),
|
|
277
|
+
co_occurring: reactCoOccurring,
|
|
278
|
+
family: "entry",
|
|
279
|
+
ast_level: false,
|
|
280
|
+
statement: "Sampled entry files bootstrap a React DOM root.",
|
|
281
|
+
proposed_rule: "Keep root rendering logic in the main application entry file.",
|
|
282
|
+
alternatives: ["Server-rendered route module"],
|
|
283
|
+
rationale: "React DOM root markers identify a frontend entrypoint."
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
if (relativePath.startsWith("app/") || relativePath.startsWith("pages/")) {
|
|
287
|
+
const coOccurring = compactPatternNames([
|
|
288
|
+
relativePath.startsWith("app/") ? "app-router" : null,
|
|
289
|
+
relativePath.startsWith("pages/") ? "pages-router" : null,
|
|
290
|
+
snippet.includes("export default") ? "default-export-route" : null
|
|
291
|
+
]);
|
|
292
|
+
return {
|
|
293
|
+
pattern: "next-route-component",
|
|
294
|
+
type: "pattern",
|
|
295
|
+
confidence: determineConfidence(1, coOccurring, false),
|
|
296
|
+
evidence_lines: compactPatternNames([
|
|
297
|
+
relativePath.startsWith("app/") ? "app/" : null,
|
|
298
|
+
relativePath.startsWith("pages/") ? "pages/" : null
|
|
299
|
+
]),
|
|
300
|
+
co_occurring: coOccurring,
|
|
301
|
+
family: "entry",
|
|
302
|
+
ast_level: false,
|
|
303
|
+
statement: "Sampled entry files align with Next.js route modules.",
|
|
304
|
+
proposed_rule: "Preserve route-segment boundaries when editing app/ or pages/ files.",
|
|
305
|
+
alternatives: ["Generic source module"],
|
|
306
|
+
rationale: "Route directory placement anchors these files to the Next.js request surface."
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (relativePath === "src/main.ts" || relativePath === "src/main.js") {
|
|
310
|
+
const coOccurring = compactPatternNames([
|
|
311
|
+
"main-entry",
|
|
312
|
+
snippet.includes("import.meta") ? "import-meta" : null,
|
|
313
|
+
snippet.includes("createRoot(") ? "react-root" : null
|
|
314
|
+
]);
|
|
315
|
+
return {
|
|
316
|
+
pattern: "vite-main-entry",
|
|
317
|
+
type: "pattern",
|
|
318
|
+
confidence: determineConfidence(1, coOccurring, false),
|
|
319
|
+
evidence_lines: ["src/main"],
|
|
320
|
+
co_occurring: coOccurring,
|
|
321
|
+
family: "entry",
|
|
322
|
+
ast_level: false,
|
|
323
|
+
statement: "Sampled entry files use the conventional Vite main entrypoint.",
|
|
324
|
+
proposed_rule: "Keep primary bootstrapping logic inside src/main.*.",
|
|
325
|
+
alternatives: ["Alternative bundler entrypoint"],
|
|
326
|
+
rationale: "src/main.* is the expected Vite bootstrap path."
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
pattern: "source-entry",
|
|
331
|
+
type: "pattern",
|
|
332
|
+
confidence: "LOW",
|
|
333
|
+
evidence_lines: [basename(relativePath)],
|
|
334
|
+
co_occurring: [],
|
|
335
|
+
family: "domain",
|
|
336
|
+
ast_level: false,
|
|
337
|
+
statement: "Sampled entry file appears to be a generic source entry.",
|
|
338
|
+
alternatives: ["Framework-specific entrypoint"],
|
|
339
|
+
rationale: "No strong framework markers were detected in the sampled snippet."
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function readReadmeInfo(target) {
|
|
343
|
+
const readmePath = join(target, "README.md");
|
|
344
|
+
const hasContributing = existsSync(join(target, "CONTRIBUTING.md"));
|
|
345
|
+
if (!existsSync(readmePath)) {
|
|
346
|
+
return {
|
|
347
|
+
quality: "missing",
|
|
348
|
+
line_count: 0,
|
|
349
|
+
has_contributing: hasContributing
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const readme = readFileSync(readmePath, "utf8");
|
|
353
|
+
const wordCount = readme.trim().split(/\s+/).filter(Boolean).length;
|
|
354
|
+
return {
|
|
355
|
+
quality: wordCount >= 200 ? "ok" : "stub",
|
|
356
|
+
line_count: readme.length === 0 ? 0 : readme.split(/\r?\n/).length,
|
|
357
|
+
has_contributing: hasContributing
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
function buildAssertions(frameworkKind, topology, codeSamples) {
|
|
361
|
+
const assertions = [
|
|
362
|
+
buildFrameworkAssertion(frameworkKind, topology, codeSamples),
|
|
363
|
+
buildDominantPatternAssertion(codeSamples),
|
|
364
|
+
buildEntryDirectoryAssertion(frameworkKind, codeSamples),
|
|
365
|
+
buildMetaSidecarAssertion(frameworkKind, topology),
|
|
366
|
+
buildConfigAssertion(frameworkKind, topology),
|
|
367
|
+
buildDomainAssertion(codeSamples)
|
|
368
|
+
];
|
|
369
|
+
return assertions.filter((assertion) => assertion !== null);
|
|
370
|
+
}
|
|
371
|
+
function buildCandidateFiles(topology, codeSamples, entryPoints) {
|
|
372
|
+
const selected = /* @__PURE__ */ new Map();
|
|
373
|
+
const codeSamplesByPath = new Map(codeSamples.map((sample) => [sample.path, sample]));
|
|
374
|
+
const configFiles = topology.files.filter((file) => isConfigFile(file.relativePath));
|
|
375
|
+
const testFiles = topology.files.filter((file) => isTestFile(file.relativePath));
|
|
376
|
+
const domainFiles = topology.files.filter((file) => isDomainFile(file.relativePath));
|
|
377
|
+
const componentSamples = codeSamples.filter((sample) => sample.pattern_analysis.family === "component").sort((left, right) => compareCandidateScore(buildComponentCandidateScore(right), buildComponentCandidateScore(left)));
|
|
378
|
+
addCandidateFamily(
|
|
379
|
+
selected,
|
|
380
|
+
entryPoints.map((entryPoint) => ({
|
|
381
|
+
path: entryPoint.path,
|
|
382
|
+
family: "entry",
|
|
383
|
+
rationale: `Representative ${entryPoint.reason} used as an application entry surface.`,
|
|
384
|
+
score: buildEntryCandidateScore(entryPoint)
|
|
385
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
386
|
+
ENTRY_FAMILY_LIMIT
|
|
387
|
+
);
|
|
388
|
+
addCandidateFamily(
|
|
389
|
+
selected,
|
|
390
|
+
componentSamples.map((sample) => ({
|
|
391
|
+
path: sample.path,
|
|
392
|
+
family: "component",
|
|
393
|
+
rationale: sample.pattern_analysis.rationale,
|
|
394
|
+
score: buildComponentCandidateScore(sample)
|
|
395
|
+
})),
|
|
396
|
+
FAMILY_LIMIT
|
|
397
|
+
);
|
|
398
|
+
addCandidateFamily(
|
|
399
|
+
selected,
|
|
400
|
+
configFiles.map((file) => ({
|
|
401
|
+
path: file.relativePath,
|
|
402
|
+
family: "config",
|
|
403
|
+
rationale: "Bootstrap or compiler configuration file used to infer framework and project boundaries.",
|
|
404
|
+
score: buildConfigCandidateScore(file.relativePath)
|
|
405
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
406
|
+
FAMILY_LIMIT
|
|
407
|
+
);
|
|
408
|
+
addCandidateFamily(
|
|
409
|
+
selected,
|
|
410
|
+
testFiles.map((file) => ({
|
|
411
|
+
path: file.relativePath,
|
|
412
|
+
family: "test",
|
|
413
|
+
rationale: "Existing test coverage surface that captures behavior expectations.",
|
|
414
|
+
score: file.relativePath.includes("__tests__") ? 2 : 1
|
|
415
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
416
|
+
FAMILY_LIMIT
|
|
417
|
+
);
|
|
418
|
+
addCandidateFamily(
|
|
419
|
+
selected,
|
|
420
|
+
domainFiles.filter((file) => !codeSamplesByPath.has(file.relativePath)).map((file) => ({
|
|
421
|
+
path: file.relativePath,
|
|
422
|
+
family: "domain",
|
|
423
|
+
rationale: "Representative domain file outside entry/config/test hotspots.",
|
|
424
|
+
score: buildDomainCandidateScore(file.relativePath)
|
|
425
|
+
})).sort((left, right) => compareCandidateScore(right.score, left.score)),
|
|
426
|
+
FAMILY_LIMIT
|
|
427
|
+
);
|
|
428
|
+
return [...selected.values()].slice(0, CANDIDATE_FILE_LIMIT);
|
|
429
|
+
}
|
|
430
|
+
function buildFrameworkAssertion(frameworkKind, topology, codeSamples) {
|
|
431
|
+
if (frameworkKind === "unknown") {
|
|
432
|
+
return createAssertion({
|
|
433
|
+
type: "framework",
|
|
434
|
+
statement: "Framework could not be determined from the sampled topology.",
|
|
435
|
+
evidence: codeSamples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
436
|
+
matched: 0,
|
|
437
|
+
total: codeSamples.length,
|
|
438
|
+
coOccurring: [],
|
|
439
|
+
alternatives: ["Ask the user to confirm the primary framework"]
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const matchedSamples = codeSamples.filter((sample) => matchesFrameworkPattern(frameworkKind, sample.pattern_analysis.pattern));
|
|
443
|
+
const coOccurring = compactPatternNames([
|
|
444
|
+
...matchedSamples.flatMap((sample) => sample.pattern_analysis.co_occurring),
|
|
445
|
+
hasFile(topology.files, "project.config.json") ? "project-config-json" : null,
|
|
446
|
+
(topology.by_ext[".meta"] ?? 0) > 0 ? "meta-sidecars" : null,
|
|
447
|
+
hasFile(topology.files, "package.json") ? "package-json" : null
|
|
448
|
+
]);
|
|
449
|
+
const evidence = [
|
|
450
|
+
...matchedSamples.flatMap((sample) => sample.evidence),
|
|
451
|
+
...buildTopologyEvidence(topology, getExpectedConfigFiles(frameworkKind))
|
|
452
|
+
].slice(0, 3);
|
|
453
|
+
return createAssertion({
|
|
454
|
+
type: "framework",
|
|
455
|
+
statement: buildFrameworkStatement(frameworkKind),
|
|
456
|
+
evidence,
|
|
457
|
+
matched: matchedSamples.length,
|
|
458
|
+
total: codeSamples.length,
|
|
459
|
+
coOccurring,
|
|
460
|
+
astLevel: matchedSamples.some((sample) => sample.pattern_analysis.ast_level),
|
|
461
|
+
proposedRule: buildFrameworkRule(frameworkKind),
|
|
462
|
+
alternatives: frameworkKind === "cocos-creator" ? ["Generic TypeScript utility modules"] : ["Alternative framework entry layout"]
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
function buildDominantPatternAssertion(codeSamples) {
|
|
466
|
+
if (codeSamples.length === 0) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
const counts = /* @__PURE__ */ new Map();
|
|
470
|
+
for (const sample of codeSamples) {
|
|
471
|
+
const existing = counts.get(sample.pattern_analysis.pattern) ?? [];
|
|
472
|
+
existing.push(sample);
|
|
473
|
+
counts.set(sample.pattern_analysis.pattern, existing);
|
|
474
|
+
}
|
|
475
|
+
const dominant = [...counts.entries()].sort((left, right) => right[1].length - left[1].length)[0];
|
|
476
|
+
if (dominant === void 0) {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
const [, samples] = dominant;
|
|
480
|
+
const first = samples[0];
|
|
481
|
+
return createAssertion({
|
|
482
|
+
type: first.pattern_analysis.type,
|
|
483
|
+
statement: first.pattern_analysis.statement,
|
|
484
|
+
evidence: samples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
485
|
+
matched: samples.length,
|
|
486
|
+
total: codeSamples.length,
|
|
487
|
+
coOccurring: compactPatternNames(samples.flatMap((sample) => sample.pattern_analysis.co_occurring)),
|
|
488
|
+
astLevel: samples.some((sample) => sample.pattern_analysis.ast_level),
|
|
489
|
+
proposedRule: first.pattern_analysis.proposed_rule,
|
|
490
|
+
alternatives: first.pattern_analysis.alternatives
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function buildEntryDirectoryAssertion(frameworkKind, codeSamples) {
|
|
494
|
+
if (codeSamples.length === 0) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
const directoryGroups = /* @__PURE__ */ new Map();
|
|
498
|
+
for (const sample of codeSamples) {
|
|
499
|
+
const directory2 = posix.dirname(sample.path);
|
|
500
|
+
const existing = directoryGroups.get(directory2) ?? [];
|
|
501
|
+
existing.push(sample);
|
|
502
|
+
directoryGroups.set(directory2, existing);
|
|
503
|
+
}
|
|
504
|
+
const primaryDirectory = [...directoryGroups.entries()].sort((left, right) => right[1].length - left[1].length)[0];
|
|
505
|
+
if (primaryDirectory === void 0) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
const [directory, samples] = primaryDirectory;
|
|
509
|
+
return createAssertion({
|
|
510
|
+
type: "pattern",
|
|
511
|
+
statement: `Entry samples are concentrated in ${directory}, indicating a stable primary source boundary.`,
|
|
512
|
+
evidence: samples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
513
|
+
matched: samples.length,
|
|
514
|
+
total: codeSamples.length,
|
|
515
|
+
coOccurring: compactPatternNames([
|
|
516
|
+
directory === "." ? "root-entry" : directory,
|
|
517
|
+
frameworkKind !== "unknown" ? frameworkKind : null,
|
|
518
|
+
...samples.flatMap((sample) => sample.pattern_analysis.co_occurring.slice(0, 1))
|
|
519
|
+
]),
|
|
520
|
+
proposedRule: directory === "." ? "Keep primary entry files at the repository root only if the framework expects it." : `Treat ${directory} as the main execution boundary during initialization.`
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
function buildMetaSidecarAssertion(frameworkKind, topology) {
|
|
524
|
+
const relevantScripts = topology.files.filter((file) => SCRIPT_EXTENSIONS.has(extname(file.relativePath)));
|
|
525
|
+
if (relevantScripts.length === 0) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
const matchedScripts = relevantScripts.filter((file) => hasFile(topology.files, `${file.relativePath}.meta`));
|
|
529
|
+
if (matchedScripts.length === 0 && frameworkKind !== "cocos-creator") {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return createAssertion({
|
|
533
|
+
type: "invariant",
|
|
534
|
+
statement: matchedScripts.length > 0 ? "Script files have adjacent .meta sidecars, which should be treated as coupled assets." : "No .meta sidecars were detected for sampled scripts.",
|
|
535
|
+
evidence: matchedScripts.length > 0 ? matchedScripts.slice(0, 3).map((file) => makeSyntheticEvidence(`${file.relativePath}.meta`, `${file.relativePath}.meta sidecar present`)) : buildTopologyEvidence(topology, relevantScripts.slice(0, 1).map((file) => file.relativePath)),
|
|
536
|
+
matched: matchedScripts.length,
|
|
537
|
+
total: relevantScripts.length,
|
|
538
|
+
coOccurring: compactPatternNames([
|
|
539
|
+
matchedScripts.length > 0 ? "meta-sidecar" : null,
|
|
540
|
+
frameworkKind === "cocos-creator" ? "cocos-creator" : null,
|
|
541
|
+
relevantScripts.some((file) => file.relativePath.startsWith("assets/scripts/")) ? "assets-scripts" : null
|
|
542
|
+
]),
|
|
543
|
+
proposedRule: matchedScripts.length > 0 ? "Do not edit or delete .meta sidecars without explicit user confirmation." : void 0
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function buildConfigAssertion(frameworkKind, topology) {
|
|
547
|
+
const expectedFiles = getExpectedConfigFiles(frameworkKind);
|
|
548
|
+
if (expectedFiles.length === 0) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const matchedFiles = expectedFiles.filter((file) => hasFile(topology.files, file));
|
|
552
|
+
return createAssertion({
|
|
553
|
+
type: "invariant",
|
|
554
|
+
statement: `Project configuration is anchored by ${expectedFiles.join(", ")}.`,
|
|
555
|
+
evidence: buildTopologyEvidence(topology, matchedFiles),
|
|
556
|
+
matched: matchedFiles.length,
|
|
557
|
+
total: expectedFiles.length,
|
|
558
|
+
coOccurring: compactPatternNames(matchedFiles.map(normalizeConfigPattern)),
|
|
559
|
+
proposedRule: "Read bootstrap and compiler config before generating new rules or project structure."
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function buildDomainAssertion(codeSamples) {
|
|
563
|
+
if (codeSamples.length === 0) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const namedSamples = codeSamples.filter((sample) => {
|
|
567
|
+
const fileBase = basename(sample.path, extname(sample.path));
|
|
568
|
+
return sample.snippet.includes(`class ${fileBase}`) || sample.snippet.includes(`class ${sanitizeIdentifier(fileBase)}`);
|
|
569
|
+
});
|
|
570
|
+
if (namedSamples.length === 0) {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
const namedModules = compactPatternNames(namedSamples.map((sample) => basename(sample.path, extname(sample.path))));
|
|
574
|
+
return createAssertion({
|
|
575
|
+
type: "domain",
|
|
576
|
+
statement: `Sampled modules are named as concrete domain concepts (${namedModules.join(", ")}).`,
|
|
577
|
+
evidence: namedSamples.flatMap((sample) => sample.evidence).slice(0, 3),
|
|
578
|
+
matched: namedSamples.length,
|
|
579
|
+
total: codeSamples.length,
|
|
580
|
+
coOccurring: compactPatternNames([
|
|
581
|
+
namedSamples.every((sample) => /^[A-Z]/.test(basename(sample.path))) ? "pascal-case-modules" : null,
|
|
582
|
+
namedModules.length >= 2 ? "domain-named-components" : null,
|
|
583
|
+
namedSamples.some((sample) => sample.snippet.includes("start():")) ? "lifecycle-hook" : null
|
|
584
|
+
]),
|
|
585
|
+
proposedRule: "Preserve domain-specific module names when mirroring structure into AGENTS.md or .fabric/agents/."
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function createAssertion(input) {
|
|
589
|
+
const coverage = {
|
|
590
|
+
ratio: input.total === 0 ? 0 : roundCoverageRatio(input.matched / input.total),
|
|
591
|
+
total: input.total,
|
|
592
|
+
matched: input.matched,
|
|
593
|
+
co_occurring_patterns: compactPatternNames(input.coOccurring)
|
|
594
|
+
};
|
|
595
|
+
return {
|
|
596
|
+
type: input.type,
|
|
597
|
+
statement: input.statement,
|
|
598
|
+
confidence: determineConfidence(coverage.ratio, coverage.co_occurring_patterns, input.astLevel ?? false),
|
|
599
|
+
evidence: dedupeEvidence(input.evidence),
|
|
600
|
+
coverage,
|
|
601
|
+
proposed_rule: input.proposedRule,
|
|
602
|
+
alternatives: input.alternatives
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function buildEvidenceAnchors(relativePath, snippet, evidenceLines) {
|
|
606
|
+
const lines = snippet.split("\n");
|
|
607
|
+
const anchors = [];
|
|
608
|
+
const seen = /* @__PURE__ */ new Set();
|
|
609
|
+
for (const pattern of evidenceLines) {
|
|
610
|
+
const lineIndex = lines.findIndex((line) => line.includes(pattern));
|
|
611
|
+
if (lineIndex === -1) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
const key = `${relativePath}:${lineIndex + 1}`;
|
|
615
|
+
if (seen.has(key)) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
seen.add(key);
|
|
619
|
+
anchors.push({
|
|
620
|
+
file: relativePath,
|
|
621
|
+
line: String(lineIndex + 1),
|
|
622
|
+
snippet: lines[lineIndex]?.trim() ?? ""
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (anchors.length > 0) {
|
|
626
|
+
return anchors;
|
|
627
|
+
}
|
|
628
|
+
const fallbackIndex = lines.findIndex((line) => line.trim().length > 0);
|
|
629
|
+
return [
|
|
630
|
+
{
|
|
631
|
+
file: relativePath,
|
|
632
|
+
line: String(fallbackIndex === -1 ? 1 : fallbackIndex + 1),
|
|
633
|
+
snippet: fallbackIndex === -1 ? "" : lines[fallbackIndex]?.trim() ?? ""
|
|
634
|
+
}
|
|
635
|
+
];
|
|
636
|
+
}
|
|
637
|
+
function addCandidateFamily(selected, candidates, familyLimit) {
|
|
638
|
+
let added = 0;
|
|
639
|
+
for (const candidate of candidates) {
|
|
640
|
+
if (selected.size >= CANDIDATE_FILE_LIMIT || added >= familyLimit || selected.has(candidate.path)) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
selected.set(candidate.path, {
|
|
644
|
+
path: candidate.path,
|
|
645
|
+
family: candidate.family,
|
|
646
|
+
rationale: candidate.rationale
|
|
647
|
+
});
|
|
648
|
+
added += 1;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function buildTopologyEvidence(topology, preferredPaths) {
|
|
652
|
+
return preferredPaths.filter((path) => hasFile(topology.files, path)).slice(0, 3).map((path) => makeSyntheticEvidence(path, `${path} present in project topology`));
|
|
653
|
+
}
|
|
654
|
+
function makeSyntheticEvidence(file, snippet) {
|
|
655
|
+
return {
|
|
656
|
+
file,
|
|
657
|
+
line: "1",
|
|
658
|
+
snippet
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function dedupeEvidence(evidence) {
|
|
662
|
+
const seen = /* @__PURE__ */ new Set();
|
|
663
|
+
const deduped = [];
|
|
664
|
+
for (const entry of evidence) {
|
|
665
|
+
const key = `${entry.file}:${entry.line}`;
|
|
666
|
+
if (seen.has(key)) {
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
seen.add(key);
|
|
670
|
+
deduped.push(entry);
|
|
671
|
+
}
|
|
672
|
+
return deduped.slice(0, 3);
|
|
673
|
+
}
|
|
674
|
+
function matchesFrameworkPattern(frameworkKind, pattern) {
|
|
675
|
+
if (frameworkKind === "cocos-creator") {
|
|
676
|
+
return pattern === "cocos-component-class";
|
|
677
|
+
}
|
|
678
|
+
if (frameworkKind === "next") {
|
|
679
|
+
return pattern === "next-route-component";
|
|
680
|
+
}
|
|
681
|
+
if (frameworkKind === "vite") {
|
|
682
|
+
return pattern === "vite-main-entry" || pattern === "react-root";
|
|
683
|
+
}
|
|
684
|
+
return pattern !== "source-entry";
|
|
685
|
+
}
|
|
686
|
+
function buildFrameworkStatement(frameworkKind) {
|
|
687
|
+
if (frameworkKind === "cocos-creator") {
|
|
688
|
+
return "Project strongly matches a Cocos Creator TypeScript component layout.";
|
|
689
|
+
}
|
|
690
|
+
if (frameworkKind === "next") {
|
|
691
|
+
return "Project topology and entry samples align with a Next.js route-driven application.";
|
|
692
|
+
}
|
|
693
|
+
if (frameworkKind === "vite") {
|
|
694
|
+
return "Project topology aligns with a Vite-style application bootstrap.";
|
|
695
|
+
}
|
|
696
|
+
return `Project surfaces align with ${frameworkKind}.`;
|
|
697
|
+
}
|
|
698
|
+
function buildFrameworkRule(frameworkKind) {
|
|
699
|
+
if (frameworkKind === "cocos-creator") {
|
|
700
|
+
return "Preserve Cocos component decorators, lifecycle methods, and paired .meta files during initialization.";
|
|
701
|
+
}
|
|
702
|
+
if (frameworkKind === "next") {
|
|
703
|
+
return "Respect app/pages route boundaries when generating instructions or edits.";
|
|
704
|
+
}
|
|
705
|
+
if (frameworkKind === "vite") {
|
|
706
|
+
return "Keep bootstrap logic centered on src/main.* and surrounding config files.";
|
|
707
|
+
}
|
|
708
|
+
return void 0;
|
|
709
|
+
}
|
|
710
|
+
function determineConfidence(ratio, coOccurringPatterns, astLevel, hasConflict = false) {
|
|
711
|
+
if (hasConflict) {
|
|
712
|
+
return "LOW";
|
|
713
|
+
}
|
|
714
|
+
if (astLevel) {
|
|
715
|
+
return "HIGH";
|
|
716
|
+
}
|
|
717
|
+
if (ratio < 0.5) {
|
|
718
|
+
return "LOW";
|
|
719
|
+
}
|
|
720
|
+
if (ratio >= 0.8 && coOccurringPatterns.length >= 2) {
|
|
721
|
+
return "HIGH";
|
|
722
|
+
}
|
|
723
|
+
return "MEDIUM";
|
|
724
|
+
}
|
|
725
|
+
function compactPatternNames(patterns) {
|
|
726
|
+
return [...new Set(patterns.filter((pattern) => pattern !== null && pattern !== void 0 && pattern.length > 0))];
|
|
727
|
+
}
|
|
728
|
+
function roundCoverageRatio(value) {
|
|
729
|
+
return Math.round(value * 1e3) / 1e3;
|
|
730
|
+
}
|
|
731
|
+
function getExpectedConfigFiles(frameworkKind) {
|
|
732
|
+
return EXPECTED_CONFIG_FILES_BY_FRAMEWORK[frameworkKind] ?? ["package.json"];
|
|
733
|
+
}
|
|
734
|
+
function hasFile(files, relativePath) {
|
|
735
|
+
return files.some((file) => file.relativePath === relativePath);
|
|
736
|
+
}
|
|
737
|
+
function normalizeConfigPattern(relativePath) {
|
|
738
|
+
return relativePath.replace(/\./g, "-");
|
|
739
|
+
}
|
|
740
|
+
function sanitizeIdentifier(value) {
|
|
741
|
+
return value.replace(/[^A-Za-z0-9_$]/g, "");
|
|
742
|
+
}
|
|
743
|
+
function compareCandidateScore(left, right) {
|
|
744
|
+
return left - right;
|
|
745
|
+
}
|
|
746
|
+
function buildEntryCandidateScore(entryPoint) {
|
|
747
|
+
let score = 0;
|
|
748
|
+
if (entryPoint.reason === "application entry") {
|
|
749
|
+
score += 3;
|
|
750
|
+
}
|
|
751
|
+
if (entryPoint.reason.includes("route")) {
|
|
752
|
+
score += 2;
|
|
753
|
+
}
|
|
754
|
+
if ((entryPoint.size_bytes ?? 0) > 0) {
|
|
755
|
+
score += 1;
|
|
756
|
+
}
|
|
757
|
+
return score;
|
|
758
|
+
}
|
|
759
|
+
function buildComponentCandidateScore(sample) {
|
|
760
|
+
let score = sample.pattern_analysis.co_occurring.length;
|
|
761
|
+
if (sample.pattern_analysis.ast_level) {
|
|
762
|
+
score += 3;
|
|
763
|
+
}
|
|
764
|
+
if (sample.pattern_analysis.confidence === "HIGH") {
|
|
765
|
+
score += 2;
|
|
766
|
+
}
|
|
767
|
+
return score;
|
|
768
|
+
}
|
|
769
|
+
function buildConfigCandidateScore(relativePath) {
|
|
770
|
+
if (relativePath === "project.config.json") {
|
|
771
|
+
return 4;
|
|
772
|
+
}
|
|
773
|
+
if (relativePath === "package.json") {
|
|
774
|
+
return 3;
|
|
775
|
+
}
|
|
776
|
+
if (relativePath === "tsconfig.json") {
|
|
777
|
+
return 2;
|
|
778
|
+
}
|
|
779
|
+
return 1;
|
|
780
|
+
}
|
|
781
|
+
function buildDomainCandidateScore(relativePath) {
|
|
782
|
+
let score = 0;
|
|
783
|
+
if (relativePath.startsWith("src/") || relativePath.startsWith("assets/")) {
|
|
784
|
+
score += 2;
|
|
785
|
+
}
|
|
786
|
+
if (SCRIPT_EXTENSIONS.has(extname(relativePath))) {
|
|
787
|
+
score += 1;
|
|
788
|
+
}
|
|
789
|
+
if (relativePath.includes("/domain/") || relativePath.includes("/models/")) {
|
|
790
|
+
score += 1;
|
|
791
|
+
}
|
|
792
|
+
return score;
|
|
793
|
+
}
|
|
794
|
+
function isConfigFile(relativePath) {
|
|
795
|
+
return /(^|\/)(package\.json|project\.config\.json|tsconfig\.json|vite\.config\.[^.]+|next\.config\.[^.]+)$/.test(relativePath);
|
|
796
|
+
}
|
|
797
|
+
function isTestFile(relativePath) {
|
|
798
|
+
return /(^|\/)(__tests__|tests)(\/|$)/.test(relativePath) || /\.(test|spec)\.[^.]+$/.test(relativePath);
|
|
799
|
+
}
|
|
800
|
+
function isDomainFile(relativePath) {
|
|
801
|
+
const extension = extname(relativePath);
|
|
802
|
+
if (!DOMAIN_FILE_EXTENSIONS.has(extension)) {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
return !isConfigFile(relativePath) && !isTestFile(relativePath);
|
|
806
|
+
}
|
|
807
|
+
function buildSkillRecommendations(frameworkKind, topology, readme) {
|
|
808
|
+
const recommendations = [];
|
|
809
|
+
if (frameworkKind === "cocos-creator") {
|
|
810
|
+
recommendations.push("\u5EFA\u8BAE\u5411\u7528\u6237\u786E\u8BA4 Cocos Creator Component \u751F\u547D\u5468\u671F(onLoad/onEnable/start)\u987A\u5E8F\u3002");
|
|
811
|
+
recommendations.push("\u5EFA\u8BAE\u8BE2\u95EE assets/prefabs \u548C assets/scenes \u662F\u5426\u5C5E\u4E8E @HUMAN \u4FDD\u62A4\u533A\u57DF\u3002");
|
|
812
|
+
if ((topology.by_ext[".meta"] ?? 0) > 0) {
|
|
813
|
+
recommendations.push("\u68C0\u6D4B\u5230 .meta \u6587\u4EF6,\u5EFA\u8BAE\u5728 @HUMAN \u9501\u5B9A .meta \u4E0D\u88AB AI \u6539\u52A8\u3002");
|
|
814
|
+
}
|
|
815
|
+
} else if (frameworkKind === "next") {
|
|
816
|
+
recommendations.push("\u5EFA\u8BAE\u786E\u8BA4 app/pages \u8DEF\u7531\u8FB9\u754C\u548C\u670D\u52A1\u7AEF\u7EC4\u4EF6\u7EA6\u675F\u3002");
|
|
817
|
+
} else if (frameworkKind === "vite") {
|
|
818
|
+
recommendations.push("\u5EFA\u8BAE\u786E\u8BA4 src/main \u5165\u53E3\u3001\u7EC4\u4EF6\u76EE\u5F55\u548C\u6784\u5EFA\u811A\u672C\u7684\u7EF4\u62A4\u8FB9\u754C\u3002");
|
|
819
|
+
} else if (frameworkKind === "unknown") {
|
|
820
|
+
recommendations.push("\u672A\u68C0\u6D4B\u5230\u660E\u786E\u6846\u67B6,\u5EFA\u8BAE\u5148\u8BA9\u7528\u6237\u786E\u8BA4\u6280\u672F\u6808\u548C\u4E3B\u8981\u5165\u53E3\u3002");
|
|
821
|
+
} else {
|
|
822
|
+
recommendations.push(`\u5EFA\u8BAE\u56F4\u7ED5 ${frameworkKind} \u7684\u4E3B\u8981\u5165\u53E3\u548C\u751F\u6210\u76EE\u5F55\u786E\u8BA4 AGENTS.md \u5206\u5C42\u8FB9\u754C\u3002`);
|
|
823
|
+
}
|
|
824
|
+
if (readme.quality !== "ok") {
|
|
825
|
+
recommendations.push("README \u4FE1\u606F\u4E0D\u8DB3,\u5EFA\u8BAE\u5728\u521D\u59CB\u5316\u8BBF\u8C08\u4E2D\u8865\u9F50\u9879\u76EE\u76EE\u6807\u3001\u8FD0\u884C\u65B9\u5F0F\u548C\u7981\u6539\u533A\u57DF\u3002");
|
|
826
|
+
}
|
|
827
|
+
return recommendations;
|
|
828
|
+
}
|
|
829
|
+
function readProjectName(target) {
|
|
830
|
+
const packageJsonPath = join(target, "package.json");
|
|
831
|
+
if (existsSync(packageJsonPath)) {
|
|
832
|
+
try {
|
|
833
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
834
|
+
if (packageJson.name !== void 0 && packageJson.name.trim().length > 0) {
|
|
835
|
+
return packageJson.name;
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
return basename(target);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return basename(target);
|
|
842
|
+
}
|
|
843
|
+
function getCliVersion() {
|
|
844
|
+
return true ? "1.1.0" : "unknown";
|
|
845
|
+
}
|
|
846
|
+
function sortRecord(record) {
|
|
847
|
+
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left.localeCompare(right)));
|
|
848
|
+
}
|
|
849
|
+
function toPosixPath(path) {
|
|
850
|
+
return path.split(sep).join("/");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/commands/init.ts
|
|
854
|
+
var AGENTS_TEMPLATE_BY_FRAMEWORK = {
|
|
855
|
+
"cocos-creator": "templates/agents-md/variants/cocos.md",
|
|
856
|
+
vite: "templates/agents-md/variants/vite.md",
|
|
857
|
+
next: "templates/agents-md/variants/next.md"
|
|
858
|
+
};
|
|
859
|
+
var CLAUDE_INIT_SKILL_TEMPLATE = "templates/claude-skills/agents-md-init/SKILL.md";
|
|
860
|
+
var CLAUDE_INIT_REMINDER_HOOK_TEMPLATE = "templates/claude-hooks/agents-md-init-reminder.cjs";
|
|
861
|
+
var CLAUDE_INIT_REMINDER_COMMAND = ".claude/hooks/agents-md-init-reminder.cjs";
|
|
862
|
+
var initCommand = defineCommand({
|
|
863
|
+
meta: {
|
|
864
|
+
name: "init",
|
|
865
|
+
description: t("cli.init.description")
|
|
866
|
+
},
|
|
867
|
+
args: {
|
|
868
|
+
target: {
|
|
869
|
+
type: "string",
|
|
870
|
+
description: t("cli.init.args.target.description")
|
|
871
|
+
},
|
|
872
|
+
debug: {
|
|
873
|
+
type: "boolean",
|
|
874
|
+
description: t("cli.init.args.debug.description"),
|
|
875
|
+
default: false
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
async run({ args }) {
|
|
879
|
+
const logger = createDebugLogger(args.debug);
|
|
880
|
+
const resolution = resolveDevMode(args.target, process.cwd());
|
|
881
|
+
const target = normalizeTarget2(resolution.target);
|
|
882
|
+
logger(`init target source: ${resolution.source}`);
|
|
883
|
+
for (const step of resolution.chain) {
|
|
884
|
+
logger(step);
|
|
885
|
+
}
|
|
886
|
+
const created = initFabric(target);
|
|
887
|
+
console.log(t("cli.init.created-path", { label: createdLabel(), path: created.agentsPath }));
|
|
888
|
+
console.log(t("cli.init.created-path", { label: createdLabel(), path: created.metaPath }));
|
|
889
|
+
console.log(t("cli.init.created-path", { label: createdLabel(), path: created.humanLockPath }));
|
|
890
|
+
console.log(t("cli.init.created-path", { label: createdLabel(), path: created.forensicPath }));
|
|
891
|
+
writeStderr(
|
|
892
|
+
created.claudeSkillAction === "created" ? t("cli.init.created-path", { label: createdLabel(), path: created.claudeSkillPath }) : t("cli.init.skipped-existing-path", { label: skippedLabel(), path: created.claudeSkillPath })
|
|
893
|
+
);
|
|
894
|
+
writeStderr(
|
|
895
|
+
created.claudeHookAction === "created" ? t("cli.init.created-path", { label: createdLabel(), path: created.claudeHookPath }) : t("cli.init.skipped-existing-path", { label: skippedLabel(), path: created.claudeHookPath })
|
|
896
|
+
);
|
|
897
|
+
writeStderr(formatClaudeSettingsAction(created.claudeSettingsPath, created.claudeSettingsAction));
|
|
898
|
+
console.log(
|
|
899
|
+
t("cli.init.next-step", {
|
|
900
|
+
label: nextLabel(),
|
|
901
|
+
message: paint.muted(t("cli.init.next-step.message"))
|
|
902
|
+
})
|
|
903
|
+
);
|
|
904
|
+
console.log(
|
|
905
|
+
t("cli.init.reason-message", {
|
|
906
|
+
label: reasonLabel(),
|
|
907
|
+
message: paint.muted(t("cli.init.reason-message.body"))
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
var init_default = initCommand;
|
|
913
|
+
function initFabric(target) {
|
|
914
|
+
assertExistingDirectory2(target);
|
|
915
|
+
const agentsPath = join2(target, "AGENTS.md");
|
|
916
|
+
const fabricDir = join2(target, ".fabric");
|
|
917
|
+
const forensicPath = join2(fabricDir, "forensic.json");
|
|
918
|
+
const claudeSkillPath = join2(target, ".claude", "skills", "agents-md-init", "SKILL.md");
|
|
919
|
+
const claudeHookPath = join2(target, ".claude", "hooks", "agents-md-init-reminder.cjs");
|
|
920
|
+
const claudeSettingsPath = join2(target, ".claude", "settings.json");
|
|
921
|
+
if (existsSync2(forensicPath)) {
|
|
922
|
+
throw new Error(`ABORT: ${forensicPath} already exists. fab init is non-destructive.`);
|
|
923
|
+
}
|
|
924
|
+
if (existsSync2(agentsPath)) {
|
|
925
|
+
throw new Error(`ABORT: ${agentsPath} already exists. fab init is non-destructive.`);
|
|
926
|
+
}
|
|
927
|
+
if (existsSync2(fabricDir)) {
|
|
928
|
+
throw new Error(`ABORT: ${fabricDir} already exists. fab init is non-destructive.`);
|
|
929
|
+
}
|
|
930
|
+
const scanReport = createScanReport(target);
|
|
931
|
+
const forensicReport = buildForensicReport(target);
|
|
932
|
+
const template = readFileSync2(findAgentsTemplatePath(scanReport.framework.kind), "utf8");
|
|
933
|
+
const humanLockTemplate = readFileSync2(findTemplatePath("templates/fabric/human-lock.json"), "utf8");
|
|
934
|
+
const packageName = readPackageName(target) ?? parse(target).base;
|
|
935
|
+
const agentsContent = template.replaceAll("{ projectName }", packageName).replaceAll("{ frameworkKind }", scanReport.framework.kind);
|
|
936
|
+
const agentsHash = sha256(agentsContent);
|
|
937
|
+
const meta = createInitialMeta(agentsHash);
|
|
938
|
+
const metaPath = join2(fabricDir, "agents.meta.json");
|
|
939
|
+
const humanLockPath = join2(fabricDir, "human-lock.json");
|
|
940
|
+
mkdirSync(fabricDir, { recursive: false });
|
|
941
|
+
writeNewFile(agentsPath, agentsContent);
|
|
942
|
+
writeNewFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
943
|
+
`);
|
|
944
|
+
writeNewFile(humanLockPath, humanLockTemplate.endsWith("\n") ? humanLockTemplate : `${humanLockTemplate}
|
|
945
|
+
`);
|
|
946
|
+
writeNewFile(forensicPath, `${JSON.stringify(forensicReport, null, 2)}
|
|
947
|
+
`);
|
|
948
|
+
const claudeSkillAction = copyTemplateIfMissing(findTemplatePath(CLAUDE_INIT_SKILL_TEMPLATE), claudeSkillPath);
|
|
949
|
+
const claudeHookAction = copyExecutableTemplateIfMissing(
|
|
950
|
+
findTemplatePath(CLAUDE_INIT_REMINDER_HOOK_TEMPLATE),
|
|
951
|
+
claudeHookPath
|
|
952
|
+
);
|
|
953
|
+
const claudeSettingsAction = mergeClaudeStopHook(claudeSettingsPath);
|
|
954
|
+
return {
|
|
955
|
+
agentsPath,
|
|
956
|
+
metaPath,
|
|
957
|
+
humanLockPath,
|
|
958
|
+
forensicPath,
|
|
959
|
+
claudeSkillPath,
|
|
960
|
+
claudeSkillAction,
|
|
961
|
+
claudeHookPath,
|
|
962
|
+
claudeHookAction,
|
|
963
|
+
claudeSettingsPath,
|
|
964
|
+
claudeSettingsAction
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function findAgentsTemplatePath(frameworkKind) {
|
|
968
|
+
const relativePath = AGENTS_TEMPLATE_BY_FRAMEWORK[frameworkKind] ?? "templates/agents-md/AGENTS.md.template";
|
|
969
|
+
return findTemplatePath(relativePath);
|
|
970
|
+
}
|
|
971
|
+
function normalizeTarget2(targetInput) {
|
|
972
|
+
return isAbsolute2(targetInput) ? targetInput : resolve2(process.cwd(), targetInput);
|
|
973
|
+
}
|
|
974
|
+
function assertExistingDirectory2(target) {
|
|
975
|
+
if (!existsSync2(target) || !statSync2(target).isDirectory()) {
|
|
976
|
+
throw new Error(`Target must be an existing directory: ${target}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
function createInitialMeta(agentsHash) {
|
|
980
|
+
return {
|
|
981
|
+
revision: sha256(agentsHash),
|
|
982
|
+
nodes: {
|
|
983
|
+
L0: {
|
|
984
|
+
file: "AGENTS.md",
|
|
985
|
+
scope_glob: "**",
|
|
986
|
+
deps: [],
|
|
987
|
+
priority: "high",
|
|
988
|
+
layer: "L0",
|
|
989
|
+
topology_type: "mirror",
|
|
990
|
+
hash: agentsHash
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
function readPackageName(target) {
|
|
996
|
+
const packageJsonPath = join2(target, "package.json");
|
|
997
|
+
if (!existsSync2(packageJsonPath)) {
|
|
998
|
+
return void 0;
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf8"));
|
|
1002
|
+
return packageJson.name;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return void 0;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function findTemplatePath(relativePath) {
|
|
1008
|
+
const currentModuleDir = dirname(fileURLToPath(import.meta.url));
|
|
1009
|
+
const candidates = [
|
|
1010
|
+
...templateCandidatesFrom(process.cwd(), relativePath),
|
|
1011
|
+
...templateCandidatesFrom(currentModuleDir, relativePath)
|
|
1012
|
+
];
|
|
1013
|
+
for (const candidate of candidates) {
|
|
1014
|
+
if (existsSync2(candidate)) {
|
|
1015
|
+
return candidate;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
throw new Error(t("cli.shared.template-not-found", { path: relativePath }));
|
|
1019
|
+
}
|
|
1020
|
+
function templateCandidatesFrom(start, relativePath) {
|
|
1021
|
+
const candidates = [];
|
|
1022
|
+
let current = resolve2(start);
|
|
1023
|
+
while (true) {
|
|
1024
|
+
candidates.push(join2(current, ...relativePath.split("/")));
|
|
1025
|
+
const parent = dirname(current);
|
|
1026
|
+
if (parent === current || parse(current).root === current) {
|
|
1027
|
+
break;
|
|
1028
|
+
}
|
|
1029
|
+
current = parent;
|
|
1030
|
+
}
|
|
1031
|
+
return candidates.reverse();
|
|
1032
|
+
}
|
|
1033
|
+
function writeNewFile(path, content) {
|
|
1034
|
+
if (existsSync2(path)) {
|
|
1035
|
+
throw new Error(`ABORT: ${path} already exists. fab init is non-destructive.`);
|
|
1036
|
+
}
|
|
1037
|
+
writeFileSync(path, content, "utf8");
|
|
1038
|
+
}
|
|
1039
|
+
function copyTemplateIfMissing(templatePath, targetPath) {
|
|
1040
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
1041
|
+
if (existsSync2(targetPath)) {
|
|
1042
|
+
return "skipped";
|
|
1043
|
+
}
|
|
1044
|
+
copyFileSync(templatePath, targetPath);
|
|
1045
|
+
return "created";
|
|
1046
|
+
}
|
|
1047
|
+
function copyExecutableTemplateIfMissing(templatePath, targetPath) {
|
|
1048
|
+
const action = copyTemplateIfMissing(templatePath, targetPath);
|
|
1049
|
+
if (action === "created") {
|
|
1050
|
+
chmodSync(targetPath, 493);
|
|
1051
|
+
}
|
|
1052
|
+
return action;
|
|
1053
|
+
}
|
|
1054
|
+
function mergeClaudeStopHook(settingsPath) {
|
|
1055
|
+
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
1056
|
+
let settings;
|
|
1057
|
+
let action = "updated";
|
|
1058
|
+
if (!existsSync2(settingsPath)) {
|
|
1059
|
+
settings = {};
|
|
1060
|
+
action = "created";
|
|
1061
|
+
} else {
|
|
1062
|
+
try {
|
|
1063
|
+
const parsed = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
1064
|
+
if (!isRecord(parsed)) {
|
|
1065
|
+
writeStderr(t("cli.init.claude-settings.invalid-object", { label: skippedLabel(), path: settingsPath }));
|
|
1066
|
+
return "skipped-invalid";
|
|
1067
|
+
}
|
|
1068
|
+
settings = parsed;
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
const reason = error instanceof Error ? error.message : "unknown parse error";
|
|
1071
|
+
writeStderr(t("cli.init.claude-settings.invalid-json", { label: skippedLabel(), path: settingsPath, reason }));
|
|
1072
|
+
return "skipped-invalid";
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
if (settings.hooks !== void 0 && !isRecord(settings.hooks)) {
|
|
1076
|
+
writeStderr(t("cli.init.claude-settings.invalid-hooks", { label: skippedLabel(), path: settingsPath }));
|
|
1077
|
+
return "skipped-invalid";
|
|
1078
|
+
}
|
|
1079
|
+
const hooks = settings.hooks ?? {};
|
|
1080
|
+
const stopHooksValue = hooks.Stop;
|
|
1081
|
+
if (stopHooksValue !== void 0 && !Array.isArray(stopHooksValue)) {
|
|
1082
|
+
writeStderr(t("cli.init.claude-settings.invalid-stop-array", { label: skippedLabel(), path: settingsPath }));
|
|
1083
|
+
return "skipped-invalid";
|
|
1084
|
+
}
|
|
1085
|
+
const stopHooks = Array.isArray(stopHooksValue) ? stopHooksValue : [];
|
|
1086
|
+
if (hasClaudeInitReminderHook(stopHooks)) {
|
|
1087
|
+
return "skipped";
|
|
1088
|
+
}
|
|
1089
|
+
stopHooks.push({
|
|
1090
|
+
matcher: "*",
|
|
1091
|
+
hooks: [
|
|
1092
|
+
{
|
|
1093
|
+
type: "command",
|
|
1094
|
+
command: CLAUDE_INIT_REMINDER_COMMAND
|
|
1095
|
+
}
|
|
1096
|
+
]
|
|
1097
|
+
});
|
|
1098
|
+
settings.hooks = {
|
|
1099
|
+
...hooks,
|
|
1100
|
+
Stop: stopHooks
|
|
1101
|
+
};
|
|
1102
|
+
writeJsonAtomically(settingsPath, settings);
|
|
1103
|
+
return action;
|
|
1104
|
+
}
|
|
1105
|
+
function hasClaudeInitReminderHook(stopHooks) {
|
|
1106
|
+
return stopHooks.some((entry) => {
|
|
1107
|
+
if (!isRecord(entry) || !Array.isArray(entry.hooks)) {
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
return entry.hooks.some(
|
|
1111
|
+
(hook) => isRecord(hook) && hook.type === "command" && typeof hook.command === "string" && hook.command.includes("agents-md-init-reminder.cjs")
|
|
1112
|
+
);
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
function writeJsonAtomically(path, value) {
|
|
1116
|
+
const tempPath = `${path}.${process.pid}.tmp`;
|
|
1117
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}
|
|
1118
|
+
`, "utf8");
|
|
1119
|
+
renameSync(tempPath, path);
|
|
1120
|
+
}
|
|
1121
|
+
function isRecord(value) {
|
|
1122
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1123
|
+
}
|
|
1124
|
+
function formatClaudeSettingsAction(settingsPath, action) {
|
|
1125
|
+
switch (action) {
|
|
1126
|
+
case "created":
|
|
1127
|
+
return t("cli.init.claude-settings.created", { label: createdLabel(), path: settingsPath });
|
|
1128
|
+
case "updated":
|
|
1129
|
+
return t("cli.init.claude-settings.updated", { label: updatedLabel(), path: settingsPath });
|
|
1130
|
+
case "skipped":
|
|
1131
|
+
return t("cli.init.claude-settings.skipped", { label: skippedLabel(), path: settingsPath });
|
|
1132
|
+
case "skipped-invalid":
|
|
1133
|
+
return t("cli.init.claude-settings.skipped-invalid", { label: skippedLabel(), path: settingsPath });
|
|
1134
|
+
default:
|
|
1135
|
+
return t("cli.init.claude-settings.updated", { label: updatedLabel(), path: settingsPath });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function createdLabel() {
|
|
1139
|
+
return paint.success(t("cli.shared.created"));
|
|
1140
|
+
}
|
|
1141
|
+
function skippedLabel() {
|
|
1142
|
+
return paint.muted(t("cli.shared.skipped"));
|
|
1143
|
+
}
|
|
1144
|
+
function nextLabel() {
|
|
1145
|
+
return paint.ai(t("cli.shared.next"));
|
|
1146
|
+
}
|
|
1147
|
+
function reasonLabel() {
|
|
1148
|
+
return paint.human(t("cli.shared.reason"));
|
|
1149
|
+
}
|
|
1150
|
+
function updatedLabel() {
|
|
1151
|
+
return paint.success(t("cli.shared.updated"));
|
|
1152
|
+
}
|
|
1153
|
+
function writeStderr(message) {
|
|
1154
|
+
process.stderr.write(`${message}
|
|
1155
|
+
`);
|
|
1156
|
+
}
|
|
1157
|
+
function sha256(content) {
|
|
1158
|
+
return `sha256:${createHash("sha256").update(content).digest("hex")}`;
|
|
1159
|
+
}
|
|
1160
|
+
export {
|
|
1161
|
+
init_default as default,
|
|
1162
|
+
initCommand,
|
|
1163
|
+
initFabric
|
|
1164
|
+
};
|