agent-rules-init 0.2.1 → 0.3.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/cli.d.ts +18 -0
- package/dist/cli.js +77 -23
- package/dist/core/i18n.d.ts +44 -0
- package/dist/core/i18n.js +176 -0
- package/dist/core/llm-bridge.d.ts +2 -1
- package/dist/core/llm-bridge.js +4 -4
- package/dist/core/prompt-engine.d.ts +3 -1
- package/dist/core/prompt-engine.js +19 -14
- package/dist/core/repo-facts.d.ts +20 -0
- package/dist/core/repo-facts.js +198 -0
- package/dist/core/scanner.js +26 -3
- package/dist/core/templates.d.ts +6 -4
- package/dist/core/templates.js +48 -23
- package/dist/core/types.d.ts +33 -2
- package/dist/core/writer.d.ts +1 -1
- package/dist/core/writer.js +3 -1
- 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 +33 -27
- package/dist/packs/js-ts.js +42 -32
- package/dist/packs/kotlin.js +32 -27
- package/dist/packs/php.js +30 -27
- package/dist/packs/python.js +35 -27
- 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 +3 -2
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { UI } from "./i18n.js";
|
|
4
|
+
const NPM_DIRECT_LIFECYCLE = new Set(["test", "start", "stop", "restart"]);
|
|
5
|
+
export function extractNpmCommands(signals) {
|
|
6
|
+
const scripts = signals.packageJson?.scripts ?? {};
|
|
7
|
+
const entries = [];
|
|
8
|
+
for (const [name, body] of Object.entries(scripts)) {
|
|
9
|
+
if (typeof body !== "string" || body.trim() === "")
|
|
10
|
+
continue;
|
|
11
|
+
entries.push({
|
|
12
|
+
source: "npm",
|
|
13
|
+
invocation: NPM_DIRECT_LIFECYCLE.has(name) ? `npm ${name}` : `npm run ${name}`,
|
|
14
|
+
detail: body.trim(),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return entries;
|
|
18
|
+
}
|
|
19
|
+
export function extractMakeTargets(signals) {
|
|
20
|
+
const makefile = signals.makefile;
|
|
21
|
+
if (!makefile)
|
|
22
|
+
return [];
|
|
23
|
+
const targets = new Set();
|
|
24
|
+
for (const line of makefile.split(/\r?\n/)) {
|
|
25
|
+
// Un target va a inicio de línea (las recetas van indentadas con tab) y su nombre
|
|
26
|
+
// no lleva %, ni empieza por "." (targets especiales tipo .PHONY). El (?!=) evita
|
|
27
|
+
// asignaciones ":=". La clase de caracteres ya excluye espacios, con lo que
|
|
28
|
+
// "CFLAGS :=", los comentarios "#" y las URLs en comentarios tampoco matchean.
|
|
29
|
+
const match = /^([A-Za-z0-9_/-][A-Za-z0-9_./-]*)\s*:(?!=)/.exec(line);
|
|
30
|
+
if (!match)
|
|
31
|
+
continue;
|
|
32
|
+
targets.add(match[1]);
|
|
33
|
+
}
|
|
34
|
+
return [...targets].map((target) => ({ source: "make", invocation: `make ${target}` }));
|
|
35
|
+
}
|
|
36
|
+
export function extractMixAliases(signals) {
|
|
37
|
+
const mixExs = signals.mixExs;
|
|
38
|
+
if (!mixExs)
|
|
39
|
+
return [];
|
|
40
|
+
// Cuerpo de la función aliases (defp aliases do ... end). Si no existe con esa
|
|
41
|
+
// forma, no se emite nada — omitir antes que inventar.
|
|
42
|
+
const fnMatch = mixExs.match(/defp?\s+aliases\s*(?:\(\))?\s*do([\s\S]*?)\n\s*end/);
|
|
43
|
+
if (!fnMatch)
|
|
44
|
+
return [];
|
|
45
|
+
const body = fnMatch[1];
|
|
46
|
+
const names = new Set();
|
|
47
|
+
// Claves del keyword list cuyo valor empieza por lista o string: `setup: [...]`,
|
|
48
|
+
// `"ecto.setup": [...]`. Un alias con valor función (&fun/1) se omite.
|
|
49
|
+
for (const match of body.matchAll(/(?:"([^"]+)"|([a-z_][a-zA-Z0-9_.]*)):\s*(?=[["'])/g)) {
|
|
50
|
+
names.add(match[1] ?? match[2]);
|
|
51
|
+
}
|
|
52
|
+
return [...names].map((alias) => ({ source: "mix", invocation: `mix ${alias}` }));
|
|
53
|
+
}
|
|
54
|
+
export function extractToxEnvs(signals) {
|
|
55
|
+
const toxIni = signals.toxIni;
|
|
56
|
+
if (!toxIni)
|
|
57
|
+
return [];
|
|
58
|
+
const match = toxIni.match(/^[ \t]*env_?list\s*=\s*(.+(?:\n[ \t]+\S.*)*)/m);
|
|
59
|
+
if (!match)
|
|
60
|
+
return [];
|
|
61
|
+
const envs = match[1]
|
|
62
|
+
.split(/[,\s]+/)
|
|
63
|
+
.map((env) => env.trim())
|
|
64
|
+
// Los envs con factores generadores (py3{10,11}) se omiten en vez de expandirlos.
|
|
65
|
+
// El split por comas los parte en trozos ("py3{10", "11}"), así que basta con
|
|
66
|
+
// descartar cualquier trozo que contenga una llave.
|
|
67
|
+
.filter((env) => env !== "" && !env.startsWith("#") && !/[{}]/.test(env));
|
|
68
|
+
return [...new Set(envs)].map((env) => ({ source: "tox", invocation: `tox -e ${env}` }));
|
|
69
|
+
}
|
|
70
|
+
export function extractComposerCommands(signals) {
|
|
71
|
+
const scripts = signals.composerJson?.scripts ?? {};
|
|
72
|
+
const entries = [];
|
|
73
|
+
for (const [name, raw] of Object.entries(scripts)) {
|
|
74
|
+
const parts = Array.isArray(raw)
|
|
75
|
+
? raw.filter((p) => typeof p === "string")
|
|
76
|
+
: typeof raw === "string"
|
|
77
|
+
? [raw]
|
|
78
|
+
: [];
|
|
79
|
+
if (parts.length === 0)
|
|
80
|
+
continue;
|
|
81
|
+
entries.push({ source: "composer", invocation: `composer ${name}`, detail: parts.join(" && ") });
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
const WELL_KNOWN_NAMES = new Set([
|
|
86
|
+
"test", "tests", "build", "lint", "fmt", "format", "check", "dev", "start",
|
|
87
|
+
"typecheck", "ci", "coverage", "e2e", "unit", "docs", "clean", "install",
|
|
88
|
+
"setup", "release", "watch", "fix", "all",
|
|
89
|
+
]);
|
|
90
|
+
const MAX_COMMANDS_PER_SOURCE = 15;
|
|
91
|
+
function invocationName(entry) {
|
|
92
|
+
const parts = entry.invocation.split(" ");
|
|
93
|
+
return parts[parts.length - 1];
|
|
94
|
+
}
|
|
95
|
+
export function filterCommands(entries) {
|
|
96
|
+
const kept = [];
|
|
97
|
+
const omitted = [];
|
|
98
|
+
const sources = [...new Set(entries.map((e) => e.source))];
|
|
99
|
+
for (const source of sources) {
|
|
100
|
+
const group = entries.filter((e) => e.source === source);
|
|
101
|
+
const wellKnown = group.filter((e) => WELL_KNOWN_NAMES.has(invocationName(e)));
|
|
102
|
+
const rest = group.filter((e) => !WELL_KNOWN_NAMES.has(invocationName(e)));
|
|
103
|
+
const keptGroup = new Set([...wellKnown, ...rest].slice(0, MAX_COMMANDS_PER_SOURCE));
|
|
104
|
+
// Se emite en el orden original del manifiesto, no con los conocidos primero.
|
|
105
|
+
kept.push(...group.filter((e) => keptGroup.has(e)));
|
|
106
|
+
const omittedCount = group.length - keptGroup.size;
|
|
107
|
+
if (omittedCount > 0)
|
|
108
|
+
omitted.push({ source, count: omittedCount });
|
|
109
|
+
}
|
|
110
|
+
return { kept, omitted };
|
|
111
|
+
}
|
|
112
|
+
const MAX_CI_COMMANDS = 30;
|
|
113
|
+
// Líneas de un script multilínea que son control de flujo del shell, no comandos:
|
|
114
|
+
// `if [[ ... ]]; then`, `else`, `fi`, `for x in ...; do`, `done`, `case`/`esac`...
|
|
115
|
+
const SHELL_CONTROL_FLOW = /^(?:if|elif|else|fi|then|do|done|for|while|until|case|esac)\b|^(?:fi|then|else|done|esac)$/;
|
|
116
|
+
function isShellControlFlow(line) {
|
|
117
|
+
return SHELL_CONTROL_FLOW.test(line);
|
|
118
|
+
}
|
|
119
|
+
export function extractCiCommands(signals) {
|
|
120
|
+
const seen = new Map(); // comando -> workflow de origen
|
|
121
|
+
for (const workflow of signals.githubWorkflows ?? []) {
|
|
122
|
+
let doc;
|
|
123
|
+
try {
|
|
124
|
+
doc = parse(workflow.content);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
continue; // YAML inválido: omitir antes que inventar
|
|
128
|
+
}
|
|
129
|
+
if (!doc || typeof doc !== "object")
|
|
130
|
+
continue;
|
|
131
|
+
const jobs = doc.jobs;
|
|
132
|
+
if (!jobs || typeof jobs !== "object")
|
|
133
|
+
continue;
|
|
134
|
+
const workflowName = workflow.path.split("/").pop() ?? workflow.path;
|
|
135
|
+
for (const job of Object.values(jobs)) {
|
|
136
|
+
if (!job || typeof job !== "object")
|
|
137
|
+
continue;
|
|
138
|
+
const steps = job.steps;
|
|
139
|
+
if (!Array.isArray(steps))
|
|
140
|
+
continue;
|
|
141
|
+
for (const step of steps) {
|
|
142
|
+
if (!step || typeof step !== "object")
|
|
143
|
+
continue;
|
|
144
|
+
const run = step.run;
|
|
145
|
+
if (typeof run !== "string")
|
|
146
|
+
continue;
|
|
147
|
+
for (const rawLine of run.split(/\r?\n/)) {
|
|
148
|
+
const line = rawLine.trim();
|
|
149
|
+
if (line === "" || line.startsWith("#") || isShellControlFlow(line))
|
|
150
|
+
continue;
|
|
151
|
+
if (!seen.has(line))
|
|
152
|
+
seen.set(line, workflowName);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const all = [...seen.entries()].map(([command, workflow]) => ({ command, workflow }));
|
|
158
|
+
return {
|
|
159
|
+
commands: all.slice(0, MAX_CI_COMMANDS),
|
|
160
|
+
omittedCount: Math.max(0, all.length - MAX_CI_COMMANDS),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
const MAX_DIRS = 20;
|
|
164
|
+
export function extractStructure(signals, lang) {
|
|
165
|
+
const dirNotes = UI[lang].dirNotes;
|
|
166
|
+
const dirs = new Set();
|
|
167
|
+
for (const file of signals.files) {
|
|
168
|
+
const normalized = file.split(path.sep).join("/");
|
|
169
|
+
const slash = normalized.indexOf("/");
|
|
170
|
+
if (slash > 0)
|
|
171
|
+
dirs.add(normalized.slice(0, slash));
|
|
172
|
+
}
|
|
173
|
+
return [...dirs]
|
|
174
|
+
.sort()
|
|
175
|
+
.slice(0, MAX_DIRS)
|
|
176
|
+
.map((dir) => {
|
|
177
|
+
const note = dirNotes[dir.toLowerCase()];
|
|
178
|
+
return note ? { dir: `${dir}/`, note } : { dir: `${dir}/` };
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
export function buildRepoFacts(signals, lang) {
|
|
182
|
+
const allCommands = [
|
|
183
|
+
...extractNpmCommands(signals),
|
|
184
|
+
...extractComposerCommands(signals),
|
|
185
|
+
...extractMakeTargets(signals),
|
|
186
|
+
...extractMixAliases(signals),
|
|
187
|
+
...extractToxEnvs(signals),
|
|
188
|
+
];
|
|
189
|
+
const { kept, omitted } = filterCommands(allCommands);
|
|
190
|
+
const { commands: ciCommands, omittedCount: omittedCiCount } = extractCiCommands(signals);
|
|
191
|
+
return {
|
|
192
|
+
commands: kept,
|
|
193
|
+
omittedCommands: omitted,
|
|
194
|
+
structure: extractStructure(signals, lang),
|
|
195
|
+
ciCommands,
|
|
196
|
+
omittedCiCount,
|
|
197
|
+
};
|
|
198
|
+
}
|
package/dist/core/scanner.js
CHANGED
|
@@ -28,11 +28,16 @@ function walk(rootPath) {
|
|
|
28
28
|
recurse(rootPath, 0);
|
|
29
29
|
return results;
|
|
30
30
|
}
|
|
31
|
+
// Windows editors (Notepad, PowerShell's Set-Content, some IDE defaults) save UTF-8
|
|
32
|
+
// with a leading BOM, which JSON.parse rejects and line-anchored regexes trip over.
|
|
33
|
+
function stripBom(content) {
|
|
34
|
+
return content.charCodeAt(0) === 0xfeff ? content.slice(1) : content;
|
|
35
|
+
}
|
|
31
36
|
function readJsonIfExists(filePath) {
|
|
32
37
|
if (!fs.existsSync(filePath))
|
|
33
38
|
return undefined;
|
|
34
39
|
try {
|
|
35
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
40
|
+
return JSON.parse(stripBom(fs.readFileSync(filePath, "utf-8")));
|
|
36
41
|
}
|
|
37
42
|
catch {
|
|
38
43
|
return undefined;
|
|
@@ -42,7 +47,7 @@ function readTextIfExists(filePath) {
|
|
|
42
47
|
if (!fs.existsSync(filePath))
|
|
43
48
|
return undefined;
|
|
44
49
|
try {
|
|
45
|
-
return fs.readFileSync(filePath, "utf-8");
|
|
50
|
+
return stripBom(fs.readFileSync(filePath, "utf-8"));
|
|
46
51
|
}
|
|
47
52
|
catch {
|
|
48
53
|
return undefined;
|
|
@@ -121,6 +126,7 @@ export function scanRepo(rootPath) {
|
|
|
121
126
|
? {
|
|
122
127
|
require: rawComposerJson.require ?? {},
|
|
123
128
|
requireDev: rawComposerJson["require-dev"] ?? {},
|
|
129
|
+
scripts: rawComposerJson.scripts ?? {},
|
|
124
130
|
}
|
|
125
131
|
: undefined;
|
|
126
132
|
const pyprojectCandidate = findFirstPreferringRealProjectDirs(files, "pyproject.toml");
|
|
@@ -157,11 +163,21 @@ export function scanRepo(rootPath) {
|
|
|
157
163
|
// packages/*. Aggregate all of them so the actual framework/test runner is found.
|
|
158
164
|
const pubspecYamlPaths = findAllByNames(files, ["pubspec.yaml"]);
|
|
159
165
|
const cmakeListsPath = findFirst(files, "CMakeLists.txt");
|
|
160
|
-
|
|
166
|
+
// Un Makefile que solo existe bajo docs/, tools/, etc. es tooling auxiliar (p. ej. el
|
|
167
|
+
// Makefile de Sphinx en docs/ de Flask): sus targets no se pueden ejecutar desde la
|
|
168
|
+
// raíz y no describen el proyecto. Mismo criterio que los manifiestos Python.
|
|
169
|
+
const makefilePath = findFirstPreferringRealProjectDirs(files, "Makefile") ??
|
|
170
|
+
findFirstPreferringRealProjectDirs(files, "makefile");
|
|
161
171
|
const mixExsPath = findFirst(files, "mix.exs");
|
|
162
172
|
const buildSbtPath = findFirst(files, "build.sbt");
|
|
163
173
|
const rDescriptionPath = findFirst(files, "DESCRIPTION");
|
|
164
174
|
const renvLockPath = findFirst(files, "renv.lock");
|
|
175
|
+
const toxIniPath = findFirst(files, "tox.ini");
|
|
176
|
+
const workflowPaths = files.filter((f) => {
|
|
177
|
+
const normalized = f.split(path.sep).join("/");
|
|
178
|
+
return (normalized.startsWith(".github/workflows/") &&
|
|
179
|
+
(normalized.endsWith(".yml") || normalized.endsWith(".yaml")));
|
|
180
|
+
});
|
|
165
181
|
return {
|
|
166
182
|
rootPath,
|
|
167
183
|
files,
|
|
@@ -191,5 +207,12 @@ export function scanRepo(rootPath) {
|
|
|
191
207
|
buildSbt: buildSbtPath ? readTextIfExists(path.join(rootPath, buildSbtPath)) : undefined,
|
|
192
208
|
rDescription: rDescriptionPath ? readTextIfExists(path.join(rootPath, rDescriptionPath)) : undefined,
|
|
193
209
|
renvLock: renvLockPath ? readTextIfExists(path.join(rootPath, renvLockPath)) : undefined,
|
|
210
|
+
toxIni: toxIniPath ? readTextIfExists(path.join(rootPath, toxIniPath)) : undefined,
|
|
211
|
+
githubWorkflows: workflowPaths
|
|
212
|
+
.map((p) => ({
|
|
213
|
+
path: p.split(path.sep).join("/"),
|
|
214
|
+
content: readTextIfExists(path.join(rootPath, p)),
|
|
215
|
+
}))
|
|
216
|
+
.filter((w) => w.content !== undefined),
|
|
194
217
|
};
|
|
195
218
|
}
|
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,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { UI } from "./i18n.js";
|
|
2
|
+
function renderSection(entries, lang) {
|
|
3
|
+
const ui = UI[lang];
|
|
2
4
|
return entries
|
|
3
5
|
.map(({ detection, ruleSet }) => {
|
|
4
6
|
const conventions = ruleSet.conventions.map((c) => `- ${c}`).join("\n");
|
|
@@ -8,41 +10,64 @@ function renderSection(entries) {
|
|
|
8
10
|
"",
|
|
9
11
|
ruleSet.summary,
|
|
10
12
|
"",
|
|
11
|
-
|
|
13
|
+
`### ${ui.sections.conventions}`,
|
|
12
14
|
conventions,
|
|
13
15
|
"",
|
|
14
|
-
|
|
16
|
+
`### ${ui.sections.architecture}`,
|
|
15
17
|
architecture,
|
|
16
18
|
].join("\n");
|
|
17
19
|
})
|
|
18
20
|
.join("\n\n");
|
|
19
21
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
const SOURCE_FILES = {
|
|
23
|
+
npm: "package.json",
|
|
24
|
+
composer: "composer.json",
|
|
25
|
+
make: "Makefile",
|
|
26
|
+
mix: "mix.exs",
|
|
27
|
+
tox: "tox.ini",
|
|
28
|
+
};
|
|
29
|
+
export function renderRepoFacts(facts, lang) {
|
|
30
|
+
const ui = UI[lang];
|
|
31
|
+
const sections = [];
|
|
32
|
+
if (facts.commands.length > 0) {
|
|
33
|
+
const lines = facts.commands.map((c) => c.detail && c.detail !== c.invocation
|
|
34
|
+
? `- \`${c.invocation}\` → \`${c.detail}\` (${SOURCE_FILES[c.source]})`
|
|
35
|
+
: `- \`${c.invocation}\` (${SOURCE_FILES[c.source]})`);
|
|
36
|
+
for (const o of facts.omittedCommands)
|
|
37
|
+
lines.push(`- ${ui.andMore(o.count, SOURCE_FILES[o.source])}`);
|
|
38
|
+
sections.push([`## ${ui.sections.commands}`, "", ...lines].join("\n"));
|
|
39
|
+
}
|
|
40
|
+
if (facts.structure.length > 0) {
|
|
41
|
+
const lines = facts.structure.map((d) => (d.note ? `- \`${d.dir}\` — ${d.note}` : `- \`${d.dir}\``));
|
|
42
|
+
sections.push([`## ${ui.sections.structure}`, "", ...lines].join("\n"));
|
|
43
|
+
}
|
|
44
|
+
if (facts.ciCommands.length > 0) {
|
|
45
|
+
const lines = facts.ciCommands.map((c) => `- \`${c.command}\` (${c.workflow})`);
|
|
46
|
+
if (facts.omittedCiCount > 0)
|
|
47
|
+
lines.push(`- ${ui.andMore(facts.omittedCiCount)}`);
|
|
48
|
+
sections.push([`## ${ui.sections.ci}`, "", ...lines].join("\n"));
|
|
49
|
+
}
|
|
50
|
+
return sections.join("\n\n");
|
|
28
51
|
}
|
|
29
|
-
|
|
52
|
+
function renderDocument(title, entries, facts, lang) {
|
|
53
|
+
const factsBlock = facts ? renderRepoFacts(facts, lang) : "";
|
|
30
54
|
return [
|
|
31
|
-
|
|
55
|
+
title,
|
|
32
56
|
"",
|
|
33
|
-
|
|
57
|
+
UI[lang].generatedHeader,
|
|
34
58
|
"",
|
|
35
|
-
renderSection(entries),
|
|
59
|
+
renderSection(entries, lang),
|
|
60
|
+
...(factsBlock ? ["", factsBlock] : []),
|
|
36
61
|
].join("\n");
|
|
37
62
|
}
|
|
38
|
-
export function
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
export function renderClaudeMd(entries, facts, lang) {
|
|
64
|
+
return renderDocument("# CLAUDE.md", entries, facts, lang);
|
|
65
|
+
}
|
|
66
|
+
export function renderAgentsMd(entries, facts, lang) {
|
|
67
|
+
return renderDocument("# AGENTS.md", entries, facts, lang);
|
|
68
|
+
}
|
|
69
|
+
export function renderCopilotInstructions(entries, facts, lang) {
|
|
70
|
+
return renderDocument("# Copilot Instructions", entries, facts, lang);
|
|
46
71
|
}
|
|
47
72
|
export function renderPromptFiles(packId, templates) {
|
|
48
73
|
return templates.flatMap((template) => [
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Lang } from "./i18n.js";
|
|
1
2
|
export interface PackageJsonManifest {
|
|
2
3
|
name?: string;
|
|
3
4
|
dependencies: Record<string, string>;
|
|
@@ -8,6 +9,7 @@ export interface PackageJsonManifest {
|
|
|
8
9
|
export interface ComposerJsonManifest {
|
|
9
10
|
require: Record<string, string>;
|
|
10
11
|
requireDev: Record<string, string>;
|
|
12
|
+
scripts?: Record<string, unknown>;
|
|
11
13
|
}
|
|
12
14
|
export interface RepoSignals {
|
|
13
15
|
rootPath: string;
|
|
@@ -33,6 +35,11 @@ export interface RepoSignals {
|
|
|
33
35
|
buildSbt?: string;
|
|
34
36
|
rDescription?: string;
|
|
35
37
|
renvLock?: string;
|
|
38
|
+
toxIni?: string;
|
|
39
|
+
githubWorkflows?: {
|
|
40
|
+
path: string;
|
|
41
|
+
content: string;
|
|
42
|
+
}[];
|
|
36
43
|
}
|
|
37
44
|
export type Confidence = "high" | "low";
|
|
38
45
|
export interface DetectionField<T> {
|
|
@@ -59,9 +66,33 @@ export interface PromptTemplate {
|
|
|
59
66
|
title: string;
|
|
60
67
|
body: string;
|
|
61
68
|
}
|
|
69
|
+
export type CommandSource = "npm" | "composer" | "make" | "mix" | "tox";
|
|
70
|
+
export interface CommandEntry {
|
|
71
|
+
source: CommandSource;
|
|
72
|
+
invocation: string;
|
|
73
|
+
detail?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface DirEntry {
|
|
76
|
+
dir: string;
|
|
77
|
+
note?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface CiCommand {
|
|
80
|
+
command: string;
|
|
81
|
+
workflow: string;
|
|
82
|
+
}
|
|
83
|
+
export interface RepoFacts {
|
|
84
|
+
commands: CommandEntry[];
|
|
85
|
+
omittedCommands: {
|
|
86
|
+
source: CommandSource;
|
|
87
|
+
count: number;
|
|
88
|
+
}[];
|
|
89
|
+
structure: DirEntry[];
|
|
90
|
+
ciCommands: CiCommand[];
|
|
91
|
+
omittedCiCount: number;
|
|
92
|
+
}
|
|
62
93
|
export interface Pack {
|
|
63
94
|
id: string;
|
|
64
95
|
detect(signals: RepoSignals): DetectionResult | null;
|
|
65
|
-
rules(detection: DetectionResult): RuleSet;
|
|
66
|
-
promptTemplates(detection: DetectionResult): PromptTemplate[];
|
|
96
|
+
rules(detection: DetectionResult, lang: Lang): RuleSet;
|
|
97
|
+
promptTemplates(detection: DetectionResult, lang: Lang): PromptTemplate[];
|
|
67
98
|
}
|
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,8 +4,10 @@ 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
|
+
// An already-existing file is the expected outcome on a re-run (the tool's
|
|
8
|
+
// no-overwrite guarantee), not a failure — report it as skipped, exit 0.
|
|
7
9
|
if (fs.existsSync(absolutePath)) {
|
|
8
|
-
return { path: relativePath, status: "
|
|
10
|
+
return { path: relativePath, status: "skipped" };
|
|
9
11
|
}
|
|
10
12
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
11
13
|
fs.writeFileSync(absolutePath, content);
|
package/dist/packs/cpp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, unknownRunnerLabel, } from "../core/i18n.js";
|
|
1
2
|
const FRAMEWORKS = [
|
|
2
3
|
["find_package(qt", "qt"],
|
|
3
4
|
["find_package(boost", "boost"],
|
|
@@ -46,39 +47,44 @@ function detect(signals) {
|
|
|
46
47
|
: { value: "make", confidence: "high" };
|
|
47
48
|
return { packId: "cpp", language: "C/C++", framework, testRunner, packageManager };
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"Sigue el estilo de formato ya usado en el proyecto (revisa si hay un `.clang-format`).",
|
|
55
|
-
`Compila y ejecuta los tests con ${detection.packageManager?.value === "cmake" ? "cmake --build . && ctest" : "make test"} antes de terminar una tarea.`,
|
|
56
|
-
"Gestiona la memoria con cuidado: prefiere RAII/smart pointers sobre `new`/`delete` manuales cuando el proyecto ya lo haga.",
|
|
57
|
-
],
|
|
58
|
-
architectureNotes: [
|
|
50
|
+
const TEXTS = {
|
|
51
|
+
es: {
|
|
52
|
+
style: "Sigue el estilo de formato ya usado en el proyecto (revisa si hay un `.clang-format`).",
|
|
53
|
+
memory: "Gestiona la memoria con cuidado: prefiere RAII/smart pointers sobre `new`/`delete` manuales cuando el proyecto ya lo haga.",
|
|
54
|
+
arch: [
|
|
59
55
|
"Mantén los headers con guardas de inclusión (`#pragma once` o include guards) consistentes con el resto del proyecto.",
|
|
60
56
|
"Declara toda dependencia nueva en el sistema de build existente (CMakeLists.txt o Makefile).",
|
|
61
57
|
],
|
|
58
|
+
reviewFocus: "fugas de memoria, punteros colgantes",
|
|
59
|
+
},
|
|
60
|
+
en: {
|
|
61
|
+
style: "Follow the formatting style already used in the project (check for a `.clang-format`).",
|
|
62
|
+
memory: "Manage memory carefully: prefer RAII/smart pointers over manual `new`/`delete` when the project already does.",
|
|
63
|
+
arch: [
|
|
64
|
+
"Keep header include guards (`#pragma once` or include guards) consistent with the rest of the project.",
|
|
65
|
+
"Declare every new dependency in the existing build system (CMakeLists.txt or Makefile).",
|
|
66
|
+
],
|
|
67
|
+
reviewFocus: "memory leaks, dangling pointers",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
function rules(detection, lang) {
|
|
71
|
+
const t = TEXTS[lang];
|
|
72
|
+
const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
|
|
73
|
+
const testCmd = detection.packageManager?.value === "cmake" ? "cmake --build . && ctest" : "make test";
|
|
74
|
+
return {
|
|
75
|
+
summary: summarySentence(lang, "C/C++", framework, detection.packageManager?.value),
|
|
76
|
+
conventions: [t.style, runTestsConvention(lang, testCmd), t.memory],
|
|
77
|
+
architectureNotes: t.arch,
|
|
62
78
|
};
|
|
63
79
|
}
|
|
64
|
-
function promptTemplates(detection) {
|
|
65
|
-
const
|
|
80
|
+
function promptTemplates(detection, lang) {
|
|
81
|
+
const t = TEXTS[lang];
|
|
82
|
+
const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
|
|
83
|
+
const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner.value : unknownRunnerLabel(lang);
|
|
66
84
|
return [
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
body: `Revisa el diff actual buscando fugas de memoria, punteros colgantes y desviaciones de las convenciones de ${framework}. Señala solo problemas concretos con línea de archivo.`,
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
id: "refactor",
|
|
74
|
-
title: "Refactor (C/C++)",
|
|
75
|
-
body: `Propón refactors que reduzcan duplicación y mejoren la legibilidad sin cambiar comportamiento observable.`,
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: "testing",
|
|
79
|
-
title: "Testing (C/C++)",
|
|
80
|
-
body: `Escribe tests con ${detection.testRunner?.value !== "unknown" ? detection.testRunner?.value : "el framework de tests del proyecto"} para el código señalado. Cubre el camino feliz y al menos un caso límite.`,
|
|
81
|
-
},
|
|
85
|
+
{ id: "review", title: "Code Review (C/C++)", body: reviewBody(lang, t.reviewFocus, framework) },
|
|
86
|
+
{ id: "refactor", title: "Refactor (C/C++)", body: refactorBody(lang) },
|
|
87
|
+
{ id: "testing", title: "Testing (C/C++)", body: testingBody(lang, runner) },
|
|
82
88
|
];
|
|
83
89
|
}
|
|
84
90
|
export const cppPack = { id: "cpp", detect, rules, promptTemplates };
|
package/dist/packs/csharp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { refactorBody, reviewBody, runTestsConvention, summarySentence, testingBody, unknownFrameworkLabel, unknownRunnerLabel, } from "../core/i18n.js";
|
|
1
2
|
function detect(signals) {
|
|
2
3
|
const source = signals.csproj;
|
|
3
4
|
if (!source)
|
|
@@ -21,39 +22,43 @@ function detect(signals) {
|
|
|
21
22
|
packageManager: { value: "nuget", confidence: "high" },
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"Sigue las convenciones de nombrado de .NET (PascalCase para clases/métodos públicos, camelCase para variables locales).",
|
|
30
|
-
`Ejecuta los tests con \`dotnet test\` antes de terminar una tarea.`,
|
|
31
|
-
"Declara toda dependencia nueva vía NuGet en el .csproj, nunca la añadas manualmente sin registrarla.",
|
|
32
|
-
],
|
|
33
|
-
architectureNotes: [
|
|
25
|
+
const TEXTS = {
|
|
26
|
+
es: {
|
|
27
|
+
naming: "Sigue las convenciones de nombrado de .NET (PascalCase para clases/métodos públicos, camelCase para variables locales).",
|
|
28
|
+
deps: "Declara toda dependencia nueva vía NuGet en el .csproj, nunca la añadas manualmente sin registrarla.",
|
|
29
|
+
arch: [
|
|
34
30
|
"Respeta la separación en capas (controller/service/repository) si el proyecto ya la usa.",
|
|
35
31
|
"Prefiere inyección de dependencias sobre instanciación manual cuando el framework ya la ofrezca.",
|
|
36
32
|
],
|
|
33
|
+
reviewFocus: "null-safety",
|
|
34
|
+
},
|
|
35
|
+
en: {
|
|
36
|
+
naming: ".NET naming conventions apply (PascalCase for classes/public methods, camelCase for local variables).",
|
|
37
|
+
deps: "Declare every new dependency via NuGet in the .csproj; never add it manually without registering it.",
|
|
38
|
+
arch: [
|
|
39
|
+
"Respect the layered separation (controller/service/repository) if the project already uses it.",
|
|
40
|
+
"Prefer dependency injection over manual instantiation when the framework already provides it.",
|
|
41
|
+
],
|
|
42
|
+
reviewFocus: "null-safety",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
function rules(detection, lang) {
|
|
46
|
+
const t = TEXTS[lang];
|
|
47
|
+
const framework = detection.framework?.value !== "none" ? detection.framework?.value : undefined;
|
|
48
|
+
return {
|
|
49
|
+
summary: summarySentence(lang, "C#/.NET", framework, "NuGet"),
|
|
50
|
+
conventions: [t.naming, runTestsConvention(lang, "`dotnet test`"), t.deps],
|
|
51
|
+
architectureNotes: t.arch,
|
|
37
52
|
};
|
|
38
53
|
}
|
|
39
|
-
function promptTemplates(detection) {
|
|
40
|
-
const
|
|
54
|
+
function promptTemplates(detection, lang) {
|
|
55
|
+
const t = TEXTS[lang];
|
|
56
|
+
const framework = detection.framework?.value !== "none" ? detection.framework.value : unknownFrameworkLabel(lang);
|
|
57
|
+
const runner = detection.testRunner?.value !== "unknown" ? detection.testRunner.value : unknownRunnerLabel(lang);
|
|
41
58
|
return [
|
|
42
|
-
{
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
body: `Revisa el diff actual buscando bugs, null-safety y desviaciones de las convenciones de ${framework}. Señala solo problemas concretos con línea de archivo.`,
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
id: "refactor",
|
|
49
|
-
title: "Refactor (C#/.NET)",
|
|
50
|
-
body: `Propón refactors que reduzcan duplicación y mejoren la legibilidad sin cambiar comportamiento observable.`,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
id: "testing",
|
|
54
|
-
title: "Testing (C#/.NET)",
|
|
55
|
-
body: `Escribe tests con ${detection.testRunner?.value !== "unknown" ? detection.testRunner?.value : "el test runner del proyecto"} para el código señalado. Cubre el camino feliz y al menos un caso límite.`,
|
|
56
|
-
},
|
|
59
|
+
{ id: "review", title: "Code Review (C#/.NET)", body: reviewBody(lang, t.reviewFocus, framework) },
|
|
60
|
+
{ id: "refactor", title: "Refactor (C#/.NET)", body: refactorBody(lang) },
|
|
61
|
+
{ id: "testing", title: "Testing (C#/.NET)", body: testingBody(lang, runner) },
|
|
57
62
|
];
|
|
58
63
|
}
|
|
59
64
|
export const csharpPack = { id: "csharp", detect, rules, promptTemplates };
|