agent-rules-init 0.2.1 → 0.4.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/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/cli.d.ts +38 -1
- package/dist/cli.js +203 -40
- package/dist/core/canonical-commands.d.ts +4 -0
- package/dist/core/canonical-commands.js +169 -0
- package/dist/core/config.d.ts +25 -0
- package/dist/core/config.js +125 -0
- package/dist/core/i18n.d.ts +47 -0
- package/dist/core/i18n.js +207 -0
- package/dist/core/llm-bridge.d.ts +5 -1
- package/dist/core/llm-bridge.js +56 -4
- package/dist/core/project-unit-output.d.ts +13 -0
- package/dist/core/project-unit-output.js +25 -0
- package/dist/core/project-units.d.ts +16 -0
- package/dist/core/project-units.js +95 -0
- package/dist/core/prompt-engine.d.ts +3 -1
- package/dist/core/prompt-engine.js +19 -14
- package/dist/core/repo-facts.d.ts +30 -0
- package/dist/core/repo-facts.js +420 -0
- package/dist/core/scanner.js +153 -27
- package/dist/core/templates.d.ts +6 -4
- package/dist/core/templates.js +127 -29
- package/dist/core/types.d.ts +84 -2
- package/dist/core/writer.d.ts +1 -1
- package/dist/core/writer.js +6 -4
- package/dist/packs/cpp.js +33 -27
- package/dist/packs/csharp.js +32 -27
- package/dist/packs/dart.js +30 -31
- package/dist/packs/elixir.js +31 -27
- package/dist/packs/go.js +31 -27
- package/dist/packs/java.js +82 -30
- package/dist/packs/js-ts.js +150 -37
- package/dist/packs/kotlin.js +56 -31
- package/dist/packs/php.js +30 -27
- package/dist/packs/python.js +107 -34
- package/dist/packs/r.js +30 -27
- package/dist/packs/ruby.js +36 -27
- package/dist/packs/rust.js +33 -27
- package/dist/packs/scala.js +32 -27
- package/dist/packs/swift.js +31 -27
- package/package.json +7 -4
package/dist/core/scanner.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
const IGNORED_DIRS = new Set([
|
|
3
|
+
const IGNORED_DIRS = new Set([
|
|
4
|
+
"node_modules", ".git", ".hg", ".svn",
|
|
5
|
+
"dist", "build", "out", "target", "coverage", "vendor",
|
|
6
|
+
".venv", "venv", ".tox", "__pycache__",
|
|
7
|
+
".next", ".nuxt", ".svelte-kit", ".turbo", ".cache",
|
|
8
|
+
".gradle", ".dart_tool",
|
|
9
|
+
]);
|
|
4
10
|
const MAX_DEPTH = 4;
|
|
5
11
|
function walk(rootPath) {
|
|
6
12
|
const results = [];
|
|
@@ -14,6 +20,9 @@ function walk(rootPath) {
|
|
|
14
20
|
catch {
|
|
15
21
|
return;
|
|
16
22
|
}
|
|
23
|
+
// Filesystem enumeration order is not guaranteed. Keep discovery stable so
|
|
24
|
+
// precedence-sensitive facts (for example, the first CI command) are reproducible.
|
|
25
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
17
26
|
for (const entry of entries) {
|
|
18
27
|
if (entry.isDirectory()) {
|
|
19
28
|
if (IGNORED_DIRS.has(entry.name))
|
|
@@ -21,6 +30,8 @@ function walk(rootPath) {
|
|
|
21
30
|
recurse(path.join(dir, entry.name), depth + 1);
|
|
22
31
|
}
|
|
23
32
|
else {
|
|
33
|
+
if (entry.name.includes(".generated."))
|
|
34
|
+
continue;
|
|
24
35
|
results.push(path.relative(rootPath, path.join(dir, entry.name)));
|
|
25
36
|
}
|
|
26
37
|
}
|
|
@@ -28,21 +39,76 @@ function walk(rootPath) {
|
|
|
28
39
|
recurse(rootPath, 0);
|
|
29
40
|
return results;
|
|
30
41
|
}
|
|
42
|
+
// Windows editors (Notepad, PowerShell's Set-Content, some IDE defaults) save UTF-8
|
|
43
|
+
// with a leading BOM, which JSON.parse rejects and line-anchored regexes trip over.
|
|
44
|
+
function stripBom(content) {
|
|
45
|
+
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
46
|
+
}
|
|
31
47
|
function readJsonIfExists(filePath) {
|
|
32
48
|
if (!fs.existsSync(filePath))
|
|
33
49
|
return undefined;
|
|
34
50
|
try {
|
|
35
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
51
|
+
return JSON.parse(stripBom(fs.readFileSync(filePath, "utf-8")));
|
|
36
52
|
}
|
|
37
53
|
catch {
|
|
38
54
|
return undefined;
|
|
39
55
|
}
|
|
40
56
|
}
|
|
57
|
+
function toPackageJsonManifest(raw, relativePath) {
|
|
58
|
+
return {
|
|
59
|
+
path: relativePath.split(path.sep).join("/"),
|
|
60
|
+
name: raw.name,
|
|
61
|
+
main: typeof raw.main === "string" ? raw.main : undefined,
|
|
62
|
+
dependencies: raw.dependencies ?? {},
|
|
63
|
+
devDependencies: raw.devDependencies ?? {},
|
|
64
|
+
scripts: raw.scripts ?? {},
|
|
65
|
+
moduleType: raw.type === "module" ? "module" : "commonjs",
|
|
66
|
+
packageManager: parsePackageManager(raw.packageManager),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parsePackageManager(value) {
|
|
70
|
+
if (typeof value !== "string")
|
|
71
|
+
return undefined;
|
|
72
|
+
const name = value.trim().split("@")[0].toLowerCase();
|
|
73
|
+
return name === "npm" || name === "pnpm" || name === "yarn" || name === "bun" ? name : undefined;
|
|
74
|
+
}
|
|
75
|
+
const LOCK_MANAGERS = {
|
|
76
|
+
"package-lock.json": "npm",
|
|
77
|
+
"npm-shrinkwrap.json": "npm",
|
|
78
|
+
"pnpm-lock.yaml": "pnpm",
|
|
79
|
+
"yarn.lock": "yarn",
|
|
80
|
+
"bun.lock": "bun",
|
|
81
|
+
"bun.lockb": "bun",
|
|
82
|
+
};
|
|
83
|
+
function managerFromClosestLock(manifestPath, normalizedFiles) {
|
|
84
|
+
let dir = path.posix.dirname(manifestPath.split(path.sep).join("/"));
|
|
85
|
+
while (true) {
|
|
86
|
+
for (const [lockName, manager] of Object.entries(LOCK_MANAGERS)) {
|
|
87
|
+
const candidate = dir === "." ? lockName : `${dir}/${lockName}`;
|
|
88
|
+
if (normalizedFiles.has(candidate))
|
|
89
|
+
return manager;
|
|
90
|
+
}
|
|
91
|
+
if (dir === ".")
|
|
92
|
+
return undefined;
|
|
93
|
+
dir = path.posix.dirname(dir);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function managerFromClosestDeclaration(manifestPath, declarations) {
|
|
97
|
+
let dir = path.posix.dirname(manifestPath);
|
|
98
|
+
while (true) {
|
|
99
|
+
const manager = declarations.get(dir);
|
|
100
|
+
if (manager)
|
|
101
|
+
return manager;
|
|
102
|
+
if (dir === ".")
|
|
103
|
+
return undefined;
|
|
104
|
+
dir = path.posix.dirname(dir);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
41
107
|
function readTextIfExists(filePath) {
|
|
42
108
|
if (!fs.existsSync(filePath))
|
|
43
109
|
return undefined;
|
|
44
110
|
try {
|
|
45
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
111
|
+
return stripBom(fs.readFileSync(filePath, "utf-8"));
|
|
46
112
|
}
|
|
47
113
|
catch {
|
|
48
114
|
return undefined;
|
|
@@ -71,6 +137,8 @@ const NON_PROJECT_DIRS = new Set([
|
|
|
71
137
|
"example",
|
|
72
138
|
"benchmark",
|
|
73
139
|
"benchmarks",
|
|
140
|
+
"fixture",
|
|
141
|
+
"fixtures",
|
|
74
142
|
]);
|
|
75
143
|
function isUnderNonProjectDir(relativePath) {
|
|
76
144
|
const segments = relativePath.split(path.sep).slice(0, -1);
|
|
@@ -100,17 +168,45 @@ function pickShallowest(paths) {
|
|
|
100
168
|
export function scanRepo(rootPath) {
|
|
101
169
|
const files = walk(rootPath);
|
|
102
170
|
const fileSet = new Set(files.map((f) => f.split(path.sep).join("/")));
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
171
|
+
// Keep every project package manifest, not only the root workspace manifest. Root
|
|
172
|
+
// package.json files are often workspace glue and contain none of the dependencies
|
|
173
|
+
// that reveal React/Vitest/etc. Manifests under docs/examples/tooling are excluded to
|
|
174
|
+
// avoid treating auxiliary demos, fixtures and documentation builds as the project.
|
|
175
|
+
const packageJsonPaths = files.filter((f) => path.basename(f) === "package.json" && !isUnderNonProjectDir(f));
|
|
176
|
+
const packageJsons = packageJsonPaths
|
|
177
|
+
.map((relativePath) => {
|
|
178
|
+
const raw = readJsonIfExists(path.join(rootPath, relativePath));
|
|
179
|
+
return raw ? toPackageJsonManifest(raw, relativePath) : undefined;
|
|
180
|
+
})
|
|
181
|
+
.filter((manifest) => manifest !== undefined)
|
|
182
|
+
.sort((a, b) => {
|
|
183
|
+
const depth = a.path.split("/").length - b.path.split("/").length;
|
|
184
|
+
return depth || a.path.localeCompare(b.path);
|
|
185
|
+
});
|
|
186
|
+
const declaredManagers = new Map(packageJsons
|
|
187
|
+
.filter((manifest) => manifest.packageManager !== undefined)
|
|
188
|
+
.map((manifest) => [path.posix.dirname(manifest.path), manifest.packageManager]));
|
|
189
|
+
// A nested lock takes precedence over an ancestor workspace declaration because it
|
|
190
|
+
// denotes an independently installed package. Both lookups are O(path depth).
|
|
191
|
+
for (const manifest of packageJsons) {
|
|
192
|
+
if (manifest.packageManager)
|
|
193
|
+
continue;
|
|
194
|
+
manifest.packageManager =
|
|
195
|
+
managerFromClosestLock(manifest.path, fileSet) ??
|
|
196
|
+
managerFromClosestDeclaration(manifest.path, declaredManagers);
|
|
197
|
+
}
|
|
198
|
+
const primaryPackageJson = packageJsons[0];
|
|
199
|
+
const packageJson = primaryPackageJson
|
|
108
200
|
? {
|
|
109
|
-
name:
|
|
110
|
-
dependencies:
|
|
111
|
-
devDependencies:
|
|
112
|
-
scripts
|
|
113
|
-
|
|
201
|
+
name: primaryPackageJson.name,
|
|
202
|
+
dependencies: Object.assign({}, ...packageJsons.map((p) => p.dependencies)),
|
|
203
|
+
devDependencies: Object.assign({}, ...packageJsons.map((p) => p.devDependencies)),
|
|
204
|
+
// Root scripts remain the default command surface. Repo facts reads packageJsons
|
|
205
|
+
// directly and adds executable --prefix commands for nested packages.
|
|
206
|
+
scripts: primaryPackageJson.scripts,
|
|
207
|
+
moduleType: primaryPackageJson.moduleType,
|
|
208
|
+
packageManager: primaryPackageJson.packageManager,
|
|
209
|
+
main: primaryPackageJson.main,
|
|
114
210
|
}
|
|
115
211
|
: undefined;
|
|
116
212
|
const composerJsonPath = findFirst(files, "composer.json");
|
|
@@ -121,6 +217,7 @@ export function scanRepo(rootPath) {
|
|
|
121
217
|
? {
|
|
122
218
|
require: rawComposerJson.require ?? {},
|
|
123
219
|
requireDev: rawComposerJson["require-dev"] ?? {},
|
|
220
|
+
scripts: rawComposerJson.scripts ?? {},
|
|
124
221
|
}
|
|
125
222
|
: undefined;
|
|
126
223
|
const pyprojectCandidate = findFirstPreferringRealProjectDirs(files, "pyproject.toml");
|
|
@@ -141,27 +238,45 @@ export function scanRepo(rootPath) {
|
|
|
141
238
|
// Multi-module Maven/Gradle projects (parent + child modules) each have their own
|
|
142
239
|
// pom.xml/build.gradle, and the module that actually declares the framework/Kotlin
|
|
143
240
|
// plugin/test runner isn't necessarily the shallowest one. Aggregate all of them.
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
241
|
+
const projectFiles = files.filter((file) => !isUnderNonProjectDir(file));
|
|
242
|
+
const pomPaths = findAllByNames(projectFiles, ["pom.xml"]);
|
|
243
|
+
const buildGradlePaths = findAllByNames(projectFiles, ["build.gradle", "build.gradle.kts"]);
|
|
244
|
+
const gemfilePath = findFirst(projectFiles, "Gemfile");
|
|
245
|
+
const goModPath = findFirst(projectFiles, "go.mod");
|
|
246
|
+
const cargoTomlPath = findFirst(projectFiles, "Cargo.toml");
|
|
149
247
|
// .NET solutions routinely split into several .csproj files (domain/infra/web/tests);
|
|
150
248
|
// picking just the "shallowest" one is close to arbitrary and often lands on a plain
|
|
151
249
|
// class library that has neither the web framework nor the test runner reference.
|
|
152
250
|
// Concatenate all of them so detection can find those references wherever they live.
|
|
153
|
-
const csprojPaths = findAllByExtension(
|
|
154
|
-
const packageSwiftPath = findFirst(
|
|
251
|
+
const csprojPaths = findAllByExtension(projectFiles, ".csproj");
|
|
252
|
+
const packageSwiftPath = findFirst(projectFiles, "Package.swift");
|
|
155
253
|
// Melos/pub workspaces have a root pubspec.yaml that's just workspace glue (no
|
|
156
254
|
// `flutter`/`flutter_test` dependency) plus one real pubspec.yaml per package under
|
|
157
255
|
// packages/*. Aggregate all of them so the actual framework/test runner is found.
|
|
158
|
-
const pubspecYamlPaths = findAllByNames(
|
|
159
|
-
const cmakeListsPath = findFirst(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
256
|
+
const pubspecYamlPaths = findAllByNames(projectFiles, ["pubspec.yaml"]);
|
|
257
|
+
const cmakeListsPath = findFirst(projectFiles, "CMakeLists.txt");
|
|
258
|
+
// Un Makefile que solo existe bajo docs/, tools/, etc. es tooling auxiliar (p. ej. el
|
|
259
|
+
// Makefile de Sphinx en docs/ de Flask): sus targets no se pueden ejecutar desde la
|
|
260
|
+
// raíz y no describen el proyecto. Mismo criterio que los manifiestos Python.
|
|
261
|
+
const makefilePath = findFirstPreferringRealProjectDirs(files, "Makefile") ??
|
|
262
|
+
findFirstPreferringRealProjectDirs(files, "makefile");
|
|
263
|
+
const mixExsPath = findFirst(projectFiles, "mix.exs");
|
|
264
|
+
const buildSbtPath = findFirst(projectFiles, "build.sbt");
|
|
265
|
+
const rDescriptionPath = findFirst(projectFiles, "DESCRIPTION");
|
|
266
|
+
const renvLockPath = findFirst(projectFiles, "renv.lock");
|
|
267
|
+
const toxIniPath = findFirst(projectFiles, "tox.ini");
|
|
268
|
+
const workflowPaths = files.filter((f) => {
|
|
269
|
+
const normalized = f.split(path.sep).join("/");
|
|
270
|
+
return (normalized.startsWith(".github/workflows/") &&
|
|
271
|
+
(normalized.endsWith(".yml") || normalized.endsWith(".yaml")));
|
|
272
|
+
});
|
|
273
|
+
const guidanceNames = new Set([
|
|
274
|
+
"CONTRIBUTING.md", ".editorconfig", "tsconfig.json", "pyproject.toml",
|
|
275
|
+
]);
|
|
276
|
+
const guidancePaths = files.filter((file) => {
|
|
277
|
+
const normalized = file.split(path.sep).join("/");
|
|
278
|
+
return guidanceNames.has(path.posix.basename(normalized));
|
|
279
|
+
});
|
|
165
280
|
return {
|
|
166
281
|
rootPath,
|
|
167
282
|
files,
|
|
@@ -169,6 +284,7 @@ export function scanRepo(rootPath) {
|
|
|
169
284
|
hasDir: (relativeDir) => fs.existsSync(path.join(rootPath, relativeDir)) &&
|
|
170
285
|
fs.statSync(path.join(rootPath, relativeDir)).isDirectory(),
|
|
171
286
|
packageJson,
|
|
287
|
+
packageJsons,
|
|
172
288
|
pyprojectToml: pyprojectPath ? readTextIfExists(path.join(rootPath, pyprojectPath)) : undefined,
|
|
173
289
|
requirementsTxt: requirementsPath
|
|
174
290
|
? readTextIfExists(path.join(rootPath, requirementsPath))
|
|
@@ -191,5 +307,15 @@ export function scanRepo(rootPath) {
|
|
|
191
307
|
buildSbt: buildSbtPath ? readTextIfExists(path.join(rootPath, buildSbtPath)) : undefined,
|
|
192
308
|
rDescription: rDescriptionPath ? readTextIfExists(path.join(rootPath, rDescriptionPath)) : undefined,
|
|
193
309
|
renvLock: renvLockPath ? readTextIfExists(path.join(rootPath, renvLockPath)) : undefined,
|
|
310
|
+
toxIni: toxIniPath ? readTextIfExists(path.join(rootPath, toxIniPath)) : undefined,
|
|
311
|
+
githubWorkflows: workflowPaths
|
|
312
|
+
.map((p) => ({
|
|
313
|
+
path: p.split(path.sep).join("/"),
|
|
314
|
+
content: readTextIfExists(path.join(rootPath, p)),
|
|
315
|
+
}))
|
|
316
|
+
.filter((w) => w.content !== undefined),
|
|
317
|
+
guidanceFiles: guidancePaths
|
|
318
|
+
.map((p) => ({ path: p.split(path.sep).join("/"), content: readTextIfExists(path.join(rootPath, p)) }))
|
|
319
|
+
.filter((f) => f.content !== undefined),
|
|
194
320
|
};
|
|
195
321
|
}
|
package/dist/core/templates.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type Lang } from "./i18n.js";
|
|
2
|
+
import type { DetectionResult, PromptTemplate, RepoFacts, RuleSet } from "./types.js";
|
|
2
3
|
export interface RenderEntry {
|
|
3
4
|
detection: DetectionResult;
|
|
4
5
|
ruleSet: RuleSet;
|
|
5
6
|
}
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
7
|
+
export declare function renderRepoFacts(facts: RepoFacts, lang: Lang): string;
|
|
8
|
+
export declare function renderClaudeMd(entries: RenderEntry[], facts: RepoFacts | undefined, lang: Lang): string;
|
|
9
|
+
export declare function renderAgentsMd(entries: RenderEntry[], facts: RepoFacts | undefined, lang: Lang): string;
|
|
10
|
+
export declare function renderCopilotInstructions(entries: RenderEntry[], facts: RepoFacts | undefined, lang: Lang): string;
|
|
9
11
|
export declare function renderPromptFiles(packId: string, templates: PromptTemplate[]): {
|
|
10
12
|
path: string;
|
|
11
13
|
content: string;
|
package/dist/core/templates.js
CHANGED
|
@@ -1,49 +1,147 @@
|
|
|
1
|
-
|
|
1
|
+
import { UI } from "./i18n.js";
|
|
2
|
+
import { SOURCE_FILES } from "./canonical-commands.js";
|
|
3
|
+
function renderSection(entries, lang, options = {}) {
|
|
4
|
+
const ui = UI[lang];
|
|
5
|
+
const { summaries = true } = options;
|
|
2
6
|
return entries
|
|
3
7
|
.map(({ detection, ruleSet }) => {
|
|
4
|
-
const
|
|
8
|
+
const conventionItems = options.operationalConventions === false
|
|
9
|
+
? ruleSet.conventions.filter((item) => !/^(?:Run the tests|Run the repository|Ejecuta los tests|Ejecuta la suite)/i.test(item))
|
|
10
|
+
: ruleSet.conventions;
|
|
11
|
+
const conventions = conventionItems.map((c) => `- ${c}`).join("\n");
|
|
5
12
|
const architecture = ruleSet.architectureNotes.map((a) => `- ${a}`).join("\n");
|
|
6
|
-
|
|
13
|
+
const parts = [
|
|
7
14
|
`## ${detection.language} (${detection.packId})`,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"",
|
|
11
|
-
|
|
12
|
-
conventions
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
].join("\n");
|
|
15
|
+
];
|
|
16
|
+
if (summaries)
|
|
17
|
+
parts.push("", ruleSet.summary);
|
|
18
|
+
if (options.conventions !== false && conventions)
|
|
19
|
+
parts.push("", `### ${ui.sections.conventions}`, conventions);
|
|
20
|
+
if (options.architecture !== false && architecture)
|
|
21
|
+
parts.push("", `### ${ui.sections.architecture}`, architecture);
|
|
22
|
+
return parts.join("\n");
|
|
17
23
|
})
|
|
18
24
|
.join("\n\n");
|
|
19
25
|
}
|
|
20
|
-
|
|
21
|
-
return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
function evidenceLabel(lang) {
|
|
27
|
+
return lang === "es" ? "evidencia" : "evidence";
|
|
28
|
+
}
|
|
29
|
+
function renderEvidenceSection(title, facts, lang) {
|
|
30
|
+
const high = facts.filter((fact) => fact.confidence === "high");
|
|
31
|
+
if (high.length === 0)
|
|
32
|
+
return "";
|
|
33
|
+
const lines = high.map((fact) => `- ${fact.statement} (${evidenceLabel(lang)}: ${fact.evidence.map((item) => `\`${item}\``).join(", ")})`);
|
|
34
|
+
return [`## ${title}`, "", ...lines].join("\n");
|
|
35
|
+
}
|
|
36
|
+
function architectureFactsTitle(lang) {
|
|
37
|
+
return lang === "es" ? "Arquitectura observada" : "Observed architecture";
|
|
28
38
|
}
|
|
29
|
-
|
|
39
|
+
function localConventionsTitle(lang) {
|
|
40
|
+
return lang === "es" ? "Convenciones locales verificadas" : "Verified local conventions";
|
|
41
|
+
}
|
|
42
|
+
function renderCanonical(facts, lang) {
|
|
43
|
+
const commands = facts.canonical.filter((command) => command.confidence === "high");
|
|
44
|
+
if (commands.length === 0)
|
|
45
|
+
return "";
|
|
30
46
|
return [
|
|
31
|
-
|
|
32
|
-
"",
|
|
33
|
-
"Generado por agent-rules-init a partir de lo detectado en este repo.",
|
|
47
|
+
`## ${UI[lang].sections.canonical}`,
|
|
34
48
|
"",
|
|
35
|
-
|
|
49
|
+
...commands.map((command) => `- ${command.kind}: \`${command.command}\` (${command.source})`),
|
|
36
50
|
].join("\n");
|
|
37
51
|
}
|
|
38
|
-
export function
|
|
52
|
+
export function renderRepoFacts(facts, lang) {
|
|
53
|
+
const ui = UI[lang];
|
|
54
|
+
const sections = [];
|
|
55
|
+
const canonical = facts.canonical.filter((c) => c.confidence === "high");
|
|
56
|
+
if (canonical.length > 0) {
|
|
57
|
+
const lines = canonical.map((c) => `- ${c.kind}: \`${c.command}\` (${c.source})`);
|
|
58
|
+
sections.push([`## ${ui.sections.canonical}`, "", ...lines].join("\n"));
|
|
59
|
+
}
|
|
60
|
+
if (facts.commands.length > 0) {
|
|
61
|
+
const lines = facts.commands.map((c) => {
|
|
62
|
+
const sourceFile = c.manifestPath ?? SOURCE_FILES[c.source];
|
|
63
|
+
return c.detail && c.detail !== c.invocation
|
|
64
|
+
? `- \`${c.invocation}\` → \`${c.detail}\` (${sourceFile})`
|
|
65
|
+
: `- \`${c.invocation}\` (${sourceFile})`;
|
|
66
|
+
});
|
|
67
|
+
for (const o of facts.omittedCommands)
|
|
68
|
+
lines.push(`- ${ui.andMore(o.count, SOURCE_FILES[o.source])}`);
|
|
69
|
+
sections.push([`## ${ui.sections.commands}`, "", ...lines].join("\n"));
|
|
70
|
+
}
|
|
71
|
+
if (facts.structure.length > 0 || facts.testDirs.length > 0 || facts.entrypoints.length > 0) {
|
|
72
|
+
const lines = facts.structure.map((d) => (d.note ? `- \`${d.dir}\` — ${d.note}` : `- \`${d.dir}\``));
|
|
73
|
+
for (const dir of facts.testDirs) {
|
|
74
|
+
if (!facts.structure.some((d) => d.dir === dir))
|
|
75
|
+
lines.push(`- \`${dir}\` — ${ui.testDirNote}`);
|
|
76
|
+
}
|
|
77
|
+
for (const entry of facts.entrypoints) {
|
|
78
|
+
lines.push(`- ${ui.entrypointNote}: \`${entry.target}\` (${entry.source} "${entry.label}")`);
|
|
79
|
+
}
|
|
80
|
+
sections.push([`## ${ui.sections.structure}`, "", ...lines].join("\n"));
|
|
81
|
+
}
|
|
82
|
+
if (facts.ciCommands.length > 0) {
|
|
83
|
+
const lines = facts.ciCommands.map((c) => `- \`${c.command}\` (${c.workflow})`);
|
|
84
|
+
if (facts.omittedCiCount > 0)
|
|
85
|
+
lines.push(`- ${ui.andMore(facts.omittedCiCount)}`);
|
|
86
|
+
sections.push([`## ${ui.sections.ci}`, "", ...lines].join("\n"));
|
|
87
|
+
}
|
|
88
|
+
const architecture = renderEvidenceSection(architectureFactsTitle(lang), facts.architectureFacts ?? [], lang);
|
|
89
|
+
if (architecture)
|
|
90
|
+
sections.push(architecture);
|
|
91
|
+
const conventions = renderEvidenceSection(localConventionsTitle(lang), facts.conventionFacts ?? [], lang);
|
|
92
|
+
if (conventions)
|
|
93
|
+
sections.push(conventions);
|
|
94
|
+
return sections.join("\n\n");
|
|
95
|
+
}
|
|
96
|
+
function renderDocument(title, entries, facts, lang) {
|
|
97
|
+
const factsBlock = facts ? renderRepoFacts(facts, lang) : "";
|
|
39
98
|
return [
|
|
40
|
-
|
|
99
|
+
title,
|
|
41
100
|
"",
|
|
42
|
-
|
|
101
|
+
UI[lang].generatedHeader,
|
|
43
102
|
"",
|
|
44
|
-
renderSection(entries),
|
|
103
|
+
renderSection(entries, lang, { architecture: !(facts?.architectureFacts?.length) }),
|
|
104
|
+
...(factsBlock ? ["", factsBlock] : []),
|
|
45
105
|
].join("\n");
|
|
46
106
|
}
|
|
107
|
+
export function renderClaudeMd(entries, facts, lang) {
|
|
108
|
+
return renderDocument("# CLAUDE.md", entries, facts, lang);
|
|
109
|
+
}
|
|
110
|
+
export function renderAgentsMd(entries, facts, lang) {
|
|
111
|
+
const intro = lang === "es"
|
|
112
|
+
? "Reglas operativas para modificar este repositorio. Respeta el alcance y valida los cambios con los comandos indicados."
|
|
113
|
+
: "Operational rules for modifying this repository. Respect scope and validate changes with the commands below.";
|
|
114
|
+
const blocks = [
|
|
115
|
+
"# AGENTS.md", "", UI[lang].generatedHeader, "", intro, "",
|
|
116
|
+
renderSection(entries, lang, { architecture: !(facts?.architectureFacts?.length) }),
|
|
117
|
+
];
|
|
118
|
+
if (facts) {
|
|
119
|
+
const canonical = renderCanonical(facts, lang);
|
|
120
|
+
const architecture = renderEvidenceSection(architectureFactsTitle(lang), facts.architectureFacts ?? [], lang);
|
|
121
|
+
const conventions = renderEvidenceSection(localConventionsTitle(lang), facts.conventionFacts ?? [], lang);
|
|
122
|
+
for (const block of [canonical, architecture, conventions])
|
|
123
|
+
if (block)
|
|
124
|
+
blocks.push("", block);
|
|
125
|
+
}
|
|
126
|
+
return blocks.join("\n");
|
|
127
|
+
}
|
|
128
|
+
export function renderCopilotInstructions(entries, facts, lang) {
|
|
129
|
+
const intro = lang === "es"
|
|
130
|
+
? "Aplica estas convenciones al completar y modificar código. Omite tareas operativas de terminal salvo que sean necesarias para el cambio."
|
|
131
|
+
: "Apply these conventions when completing and modifying code. Omit terminal operations unless the change requires them.";
|
|
132
|
+
const blocks = [
|
|
133
|
+
"# Copilot Instructions", "", UI[lang].generatedHeader, "", intro, "",
|
|
134
|
+
renderSection(entries, lang, { architecture: false, operationalConventions: false }),
|
|
135
|
+
];
|
|
136
|
+
if (facts) {
|
|
137
|
+
const conventions = renderEvidenceSection(localConventionsTitle(lang), facts.conventionFacts ?? [], lang);
|
|
138
|
+
const architecture = renderEvidenceSection(architectureFactsTitle(lang), facts.architectureFacts ?? [], lang);
|
|
139
|
+
for (const block of [conventions, architecture])
|
|
140
|
+
if (block)
|
|
141
|
+
blocks.push("", block);
|
|
142
|
+
}
|
|
143
|
+
return blocks.join("\n");
|
|
144
|
+
}
|
|
47
145
|
export function renderPromptFiles(packId, templates) {
|
|
48
146
|
return templates.flatMap((template) => [
|
|
49
147
|
{
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
import type { Lang } from "./i18n.js";
|
|
2
|
+
export type JsPackageManager = "npm" | "pnpm" | "yarn" | "bun";
|
|
1
3
|
export interface PackageJsonManifest {
|
|
2
4
|
name?: string;
|
|
5
|
+
main?: string;
|
|
3
6
|
dependencies: Record<string, string>;
|
|
4
7
|
devDependencies: Record<string, string>;
|
|
5
8
|
scripts: Record<string, string>;
|
|
6
9
|
moduleType: "module" | "commonjs";
|
|
10
|
+
/** Gestor aplicable al manifiesto, declarado en packageManager o inferido por el lock más cercano. */
|
|
11
|
+
packageManager?: JsPackageManager;
|
|
12
|
+
}
|
|
13
|
+
export interface LocatedPackageJsonManifest extends PackageJsonManifest {
|
|
14
|
+
path: string;
|
|
7
15
|
}
|
|
8
16
|
export interface ComposerJsonManifest {
|
|
9
17
|
require: Record<string, string>;
|
|
10
18
|
requireDev: Record<string, string>;
|
|
19
|
+
scripts?: Record<string, unknown>;
|
|
11
20
|
}
|
|
12
21
|
export interface RepoSignals {
|
|
13
22
|
rootPath: string;
|
|
@@ -15,6 +24,7 @@ export interface RepoSignals {
|
|
|
15
24
|
hasFile: (relativePath: string) => boolean;
|
|
16
25
|
hasDir: (relativeDir: string) => boolean;
|
|
17
26
|
packageJson?: PackageJsonManifest;
|
|
27
|
+
packageJsons?: LocatedPackageJsonManifest[];
|
|
18
28
|
pyprojectToml?: string;
|
|
19
29
|
requirementsTxt?: string;
|
|
20
30
|
environmentYml?: string;
|
|
@@ -33,6 +43,16 @@ export interface RepoSignals {
|
|
|
33
43
|
buildSbt?: string;
|
|
34
44
|
rDescription?: string;
|
|
35
45
|
renvLock?: string;
|
|
46
|
+
toxIni?: string;
|
|
47
|
+
githubWorkflows?: {
|
|
48
|
+
path: string;
|
|
49
|
+
content: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** Archivos locales de guía/configuración seleccionados de forma conservadora. */
|
|
52
|
+
guidanceFiles?: {
|
|
53
|
+
path: string;
|
|
54
|
+
content: string;
|
|
55
|
+
}[];
|
|
36
56
|
}
|
|
37
57
|
export type Confidence = "high" | "low";
|
|
38
58
|
export interface DetectionField<T> {
|
|
@@ -46,6 +66,8 @@ export interface DetectionResult {
|
|
|
46
66
|
packageManager?: DetectionField<string>;
|
|
47
67
|
testRunner?: DetectionField<string>;
|
|
48
68
|
linter?: DetectionField<string>;
|
|
69
|
+
/** Framework implementado por el propio repositorio; no implica una dependencia. */
|
|
70
|
+
frameworkSource?: string;
|
|
49
71
|
usesTypeScript?: boolean;
|
|
50
72
|
moduleFormat?: "module" | "commonjs";
|
|
51
73
|
}
|
|
@@ -59,9 +81,69 @@ export interface PromptTemplate {
|
|
|
59
81
|
title: string;
|
|
60
82
|
body: string;
|
|
61
83
|
}
|
|
84
|
+
export type CommandSource = JsPackageManager | "composer" | "make" | "mix" | "tox";
|
|
85
|
+
export interface CommandEntry {
|
|
86
|
+
source: CommandSource;
|
|
87
|
+
invocation: string;
|
|
88
|
+
detail?: string;
|
|
89
|
+
manifestPath?: string;
|
|
90
|
+
}
|
|
91
|
+
export interface DirEntry {
|
|
92
|
+
dir: string;
|
|
93
|
+
note?: string;
|
|
94
|
+
}
|
|
95
|
+
export interface CiCommand {
|
|
96
|
+
command: string;
|
|
97
|
+
workflow: string;
|
|
98
|
+
}
|
|
99
|
+
export interface CanonicalCommand {
|
|
100
|
+
kind: "test" | "lint" | "build" | "format" | "typecheck";
|
|
101
|
+
command: string;
|
|
102
|
+
/** Archivo o señal del que procede el comando, p. ej. `package.json`, `ci: ci.yml`, `mvnw`. */
|
|
103
|
+
source: string;
|
|
104
|
+
confidence: Confidence;
|
|
105
|
+
/** "." para la raíz del repo; ruta del workspace para unidades anidadas. */
|
|
106
|
+
scope: string;
|
|
107
|
+
}
|
|
108
|
+
export interface EvidenceFact {
|
|
109
|
+
statement: string;
|
|
110
|
+
evidence: string[];
|
|
111
|
+
scope: string;
|
|
112
|
+
confidence: Confidence;
|
|
113
|
+
}
|
|
114
|
+
export interface ArchitectureFact extends EvidenceFact {
|
|
115
|
+
kind: "entrypoint" | "tests" | "layers" | "workspace" | "source-layout";
|
|
116
|
+
}
|
|
117
|
+
export interface ConventionFact extends EvidenceFact {
|
|
118
|
+
kind: "formatting" | "typescript" | "python-style" | "contributing";
|
|
119
|
+
}
|
|
120
|
+
export interface RepoFacts {
|
|
121
|
+
commands: CommandEntry[];
|
|
122
|
+
omittedCommands: {
|
|
123
|
+
source: CommandSource;
|
|
124
|
+
count: number;
|
|
125
|
+
}[];
|
|
126
|
+
structure: DirEntry[];
|
|
127
|
+
ciCommands: CiCommand[];
|
|
128
|
+
omittedCiCount: number;
|
|
129
|
+
canonical: CanonicalCommand[];
|
|
130
|
+
testDirs: string[];
|
|
131
|
+
entrypoints: {
|
|
132
|
+
label: string;
|
|
133
|
+
target: string;
|
|
134
|
+
source: string;
|
|
135
|
+
}[];
|
|
136
|
+
architectureFacts: ArchitectureFact[];
|
|
137
|
+
conventionFacts: ConventionFact[];
|
|
138
|
+
}
|
|
139
|
+
export interface PackContext {
|
|
140
|
+
facts: RepoFacts;
|
|
141
|
+
/** Señales del mismo alcance que facts; permite evitar canónicos de otro stack. */
|
|
142
|
+
signals?: RepoSignals;
|
|
143
|
+
}
|
|
62
144
|
export interface Pack {
|
|
63
145
|
id: string;
|
|
64
146
|
detect(signals: RepoSignals): DetectionResult | null;
|
|
65
|
-
rules(detection: DetectionResult): RuleSet;
|
|
66
|
-
promptTemplates(detection: DetectionResult): PromptTemplate[];
|
|
147
|
+
rules(detection: DetectionResult, lang: Lang, ctx?: PackContext): RuleSet;
|
|
148
|
+
promptTemplates(detection: DetectionResult, lang: Lang, ctx?: PackContext): PromptTemplate[];
|
|
67
149
|
}
|
package/dist/core/writer.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface GeneratedFile {
|
|
|
4
4
|
}
|
|
5
5
|
export interface WriteResult {
|
|
6
6
|
path: string;
|
|
7
|
-
status: "written" | "error";
|
|
7
|
+
status: "written" | "skipped" | "error";
|
|
8
8
|
error?: string;
|
|
9
9
|
}
|
|
10
10
|
export declare function writeGeneratedFiles(rootPath: string, files: GeneratedFile[]): WriteResult[];
|
package/dist/core/writer.js
CHANGED
|
@@ -4,14 +4,16 @@ export function writeGeneratedFiles(rootPath, files) {
|
|
|
4
4
|
return files.map(({ path: relativePath, content }) => {
|
|
5
5
|
const absolutePath = path.join(rootPath, relativePath);
|
|
6
6
|
try {
|
|
7
|
-
if (fs.existsSync(absolutePath)) {
|
|
8
|
-
return { path: relativePath, status: "error", error: "file already exists" };
|
|
9
|
-
}
|
|
10
7
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
11
|
-
|
|
8
|
+
// `wx` makes the no-overwrite guarantee atomic: even if another process creates
|
|
9
|
+
// the file between directory creation and this call, Node returns EEXIST.
|
|
10
|
+
fs.writeFileSync(absolutePath, content, { flag: "wx" });
|
|
12
11
|
return { path: relativePath, status: "written" };
|
|
13
12
|
}
|
|
14
13
|
catch (err) {
|
|
14
|
+
if (err.code === "EEXIST") {
|
|
15
|
+
return { path: relativePath, status: "skipped" };
|
|
16
|
+
}
|
|
15
17
|
return { path: relativePath, status: "error", error: err.message };
|
|
16
18
|
}
|
|
17
19
|
});
|