agent-rules-init 0.2.0 → 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 +3 -2
- package/dist/core/llm-bridge.js +10 -8
- 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 +86 -16
- package/dist/core/templates.d.ts +6 -4
- package/dist/core/templates.js +48 -23
- package/dist/core/types.d.ts +36 -2
- package/dist/core/writer.d.ts +1 -1
- package/dist/core/writer.js +3 -1
- package/dist/packs/cpp.js +48 -28
- package/dist/packs/csharp.js +32 -27
- package/dist/packs/dart.js +30 -31
- package/dist/packs/elixir.js +36 -28
- package/dist/packs/go.js +44 -28
- package/dist/packs/java.js +38 -28
- package/dist/packs/js-ts.js +54 -29
- package/dist/packs/kotlin.js +36 -28
- package/dist/packs/php.js +30 -27
- package/dist/packs/python.js +57 -29
- 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 +34 -27
- package/dist/packs/swift.js +37 -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;
|
|
@@ -56,8 +61,43 @@ function shallowest(matches) {
|
|
|
56
61
|
function findFirst(files, fileName) {
|
|
57
62
|
return shallowest(files.filter((f) => path.basename(f) === fileName));
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
// Directories that routinely carry their own throwaway Python tooling manifest (a
|
|
65
|
+
// mkdocs/sphinx docs build, a one-off script, a benchmark harness) in projects whose
|
|
66
|
+
// primary language is something else entirely (e.g. nlohmann/json, a C++ library, ships
|
|
67
|
+
// docs/mkdocs/requirements.txt). Prefer a candidate outside these dirs when one exists.
|
|
68
|
+
const NON_PROJECT_DIRS = new Set([
|
|
69
|
+
"docs",
|
|
70
|
+
"doc",
|
|
71
|
+
"tools",
|
|
72
|
+
"tool",
|
|
73
|
+
"scripts",
|
|
74
|
+
"script",
|
|
75
|
+
"examples",
|
|
76
|
+
"example",
|
|
77
|
+
"benchmark",
|
|
78
|
+
"benchmarks",
|
|
79
|
+
]);
|
|
80
|
+
function isUnderNonProjectDir(relativePath) {
|
|
81
|
+
const segments = relativePath.split(path.sep).slice(0, -1);
|
|
82
|
+
return segments.some((segment) => NON_PROJECT_DIRS.has(segment.toLowerCase()));
|
|
83
|
+
}
|
|
84
|
+
function findFirstPreferringRealProjectDirs(files, fileName) {
|
|
85
|
+
const matches = files.filter((f) => path.basename(f) === fileName && !isUnderNonProjectDir(f));
|
|
86
|
+
return shallowest(matches);
|
|
87
|
+
}
|
|
88
|
+
function findAllByExtension(files, extension) {
|
|
89
|
+
return files.filter((f) => f.toLowerCase().endsWith(extension));
|
|
90
|
+
}
|
|
91
|
+
function findAllByNames(files, names) {
|
|
92
|
+
return files.filter((f) => names.includes(path.basename(f)));
|
|
93
|
+
}
|
|
94
|
+
function readAllConcatenated(rootPath, relativePaths) {
|
|
95
|
+
if (relativePaths.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
const contents = relativePaths
|
|
98
|
+
.map((p) => readTextIfExists(path.join(rootPath, p)))
|
|
99
|
+
.filter((content) => content !== undefined);
|
|
100
|
+
return contents.length > 0 ? contents.join("\n") : undefined;
|
|
61
101
|
}
|
|
62
102
|
function pickShallowest(paths) {
|
|
63
103
|
return shallowest(paths.filter((p) => p !== undefined));
|
|
@@ -75,6 +115,7 @@ export function scanRepo(rootPath) {
|
|
|
75
115
|
dependencies: rawPackageJson.dependencies ?? {},
|
|
76
116
|
devDependencies: rawPackageJson.devDependencies ?? {},
|
|
77
117
|
scripts: rawPackageJson.scripts ?? {},
|
|
118
|
+
moduleType: rawPackageJson.type === "module" ? "module" : "commonjs",
|
|
78
119
|
}
|
|
79
120
|
: undefined;
|
|
80
121
|
const composerJsonPath = findFirst(files, "composer.json");
|
|
@@ -85,11 +126,13 @@ export function scanRepo(rootPath) {
|
|
|
85
126
|
? {
|
|
86
127
|
require: rawComposerJson.require ?? {},
|
|
87
128
|
requireDev: rawComposerJson["require-dev"] ?? {},
|
|
129
|
+
scripts: rawComposerJson.scripts ?? {},
|
|
88
130
|
}
|
|
89
131
|
: undefined;
|
|
90
|
-
const pyprojectCandidate =
|
|
91
|
-
const requirementsCandidate =
|
|
92
|
-
const environmentYmlCandidate =
|
|
132
|
+
const pyprojectCandidate = findFirstPreferringRealProjectDirs(files, "pyproject.toml");
|
|
133
|
+
const requirementsCandidate = findFirstPreferringRealProjectDirs(files, "requirements.txt");
|
|
134
|
+
const environmentYmlCandidate = findFirstPreferringRealProjectDirs(files, "environment.yml") ??
|
|
135
|
+
findFirstPreferringRealProjectDirs(files, "environment.yaml");
|
|
93
136
|
// Only the shallowest of the three "wins" as the project's Python manifest — otherwise an
|
|
94
137
|
// unrelated pyproject.toml nested inside a vendored/data subdirectory (e.g. a bundled dataset
|
|
95
138
|
// package) would always outrank a real root-level environment.yml just by file type.
|
|
@@ -101,20 +144,40 @@ export function scanRepo(rootPath) {
|
|
|
101
144
|
const pyprojectPath = primaryPythonManifest === pyprojectCandidate ? pyprojectCandidate : undefined;
|
|
102
145
|
const requirementsPath = primaryPythonManifest === requirementsCandidate ? requirementsCandidate : undefined;
|
|
103
146
|
const environmentYmlPath = primaryPythonManifest === environmentYmlCandidate ? environmentYmlCandidate : undefined;
|
|
104
|
-
|
|
105
|
-
|
|
147
|
+
// Multi-module Maven/Gradle projects (parent + child modules) each have their own
|
|
148
|
+
// pom.xml/build.gradle, and the module that actually declares the framework/Kotlin
|
|
149
|
+
// plugin/test runner isn't necessarily the shallowest one. Aggregate all of them.
|
|
150
|
+
const pomPaths = findAllByNames(files, ["pom.xml"]);
|
|
151
|
+
const buildGradlePaths = findAllByNames(files, ["build.gradle", "build.gradle.kts"]);
|
|
106
152
|
const gemfilePath = findFirst(files, "Gemfile");
|
|
107
153
|
const goModPath = findFirst(files, "go.mod");
|
|
108
154
|
const cargoTomlPath = findFirst(files, "Cargo.toml");
|
|
109
|
-
|
|
155
|
+
// .NET solutions routinely split into several .csproj files (domain/infra/web/tests);
|
|
156
|
+
// picking just the "shallowest" one is close to arbitrary and often lands on a plain
|
|
157
|
+
// class library that has neither the web framework nor the test runner reference.
|
|
158
|
+
// Concatenate all of them so detection can find those references wherever they live.
|
|
159
|
+
const csprojPaths = findAllByExtension(files, ".csproj");
|
|
110
160
|
const packageSwiftPath = findFirst(files, "Package.swift");
|
|
111
|
-
|
|
161
|
+
// Melos/pub workspaces have a root pubspec.yaml that's just workspace glue (no
|
|
162
|
+
// `flutter`/`flutter_test` dependency) plus one real pubspec.yaml per package under
|
|
163
|
+
// packages/*. Aggregate all of them so the actual framework/test runner is found.
|
|
164
|
+
const pubspecYamlPaths = findAllByNames(files, ["pubspec.yaml"]);
|
|
112
165
|
const cmakeListsPath = findFirst(files, "CMakeLists.txt");
|
|
113
|
-
|
|
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");
|
|
114
171
|
const mixExsPath = findFirst(files, "mix.exs");
|
|
115
172
|
const buildSbtPath = findFirst(files, "build.sbt");
|
|
116
173
|
const rDescriptionPath = findFirst(files, "DESCRIPTION");
|
|
117
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
|
+
});
|
|
118
181
|
return {
|
|
119
182
|
rootPath,
|
|
120
183
|
files,
|
|
@@ -129,20 +192,27 @@ export function scanRepo(rootPath) {
|
|
|
129
192
|
environmentYml: environmentYmlPath
|
|
130
193
|
? readTextIfExists(path.join(rootPath, environmentYmlPath))
|
|
131
194
|
: undefined,
|
|
132
|
-
pomXml:
|
|
133
|
-
buildGradle:
|
|
195
|
+
pomXml: readAllConcatenated(rootPath, pomPaths),
|
|
196
|
+
buildGradle: readAllConcatenated(rootPath, buildGradlePaths),
|
|
134
197
|
composerJson,
|
|
135
198
|
gemfile: gemfilePath ? readTextIfExists(path.join(rootPath, gemfilePath)) : undefined,
|
|
136
199
|
goMod: goModPath ? readTextIfExists(path.join(rootPath, goModPath)) : undefined,
|
|
137
200
|
cargoToml: cargoTomlPath ? readTextIfExists(path.join(rootPath, cargoTomlPath)) : undefined,
|
|
138
|
-
csproj:
|
|
201
|
+
csproj: readAllConcatenated(rootPath, csprojPaths),
|
|
139
202
|
packageSwift: packageSwiftPath ? readTextIfExists(path.join(rootPath, packageSwiftPath)) : undefined,
|
|
140
|
-
pubspecYaml:
|
|
203
|
+
pubspecYaml: readAllConcatenated(rootPath, pubspecYamlPaths),
|
|
141
204
|
cmakeLists: cmakeListsPath ? readTextIfExists(path.join(rootPath, cmakeListsPath)) : undefined,
|
|
142
205
|
makefile: makefilePath ? readTextIfExists(path.join(rootPath, makefilePath)) : undefined,
|
|
143
206
|
mixExs: mixExsPath ? readTextIfExists(path.join(rootPath, mixExsPath)) : undefined,
|
|
144
207
|
buildSbt: buildSbtPath ? readTextIfExists(path.join(rootPath, buildSbtPath)) : undefined,
|
|
145
208
|
rDescription: rDescriptionPath ? readTextIfExists(path.join(rootPath, rDescriptionPath)) : undefined,
|
|
146
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),
|
|
147
217
|
};
|
|
148
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,12 +1,15 @@
|
|
|
1
|
+
import type { Lang } from "./i18n.js";
|
|
1
2
|
export interface PackageJsonManifest {
|
|
2
3
|
name?: string;
|
|
3
4
|
dependencies: Record<string, string>;
|
|
4
5
|
devDependencies: Record<string, string>;
|
|
5
6
|
scripts: Record<string, string>;
|
|
7
|
+
moduleType: "module" | "commonjs";
|
|
6
8
|
}
|
|
7
9
|
export interface ComposerJsonManifest {
|
|
8
10
|
require: Record<string, string>;
|
|
9
11
|
requireDev: Record<string, string>;
|
|
12
|
+
scripts?: Record<string, unknown>;
|
|
10
13
|
}
|
|
11
14
|
export interface RepoSignals {
|
|
12
15
|
rootPath: string;
|
|
@@ -32,6 +35,11 @@ export interface RepoSignals {
|
|
|
32
35
|
buildSbt?: string;
|
|
33
36
|
rDescription?: string;
|
|
34
37
|
renvLock?: string;
|
|
38
|
+
toxIni?: string;
|
|
39
|
+
githubWorkflows?: {
|
|
40
|
+
path: string;
|
|
41
|
+
content: string;
|
|
42
|
+
}[];
|
|
35
43
|
}
|
|
36
44
|
export type Confidence = "high" | "low";
|
|
37
45
|
export interface DetectionField<T> {
|
|
@@ -45,6 +53,8 @@ export interface DetectionResult {
|
|
|
45
53
|
packageManager?: DetectionField<string>;
|
|
46
54
|
testRunner?: DetectionField<string>;
|
|
47
55
|
linter?: DetectionField<string>;
|
|
56
|
+
usesTypeScript?: boolean;
|
|
57
|
+
moduleFormat?: "module" | "commonjs";
|
|
48
58
|
}
|
|
49
59
|
export interface RuleSet {
|
|
50
60
|
summary: string;
|
|
@@ -56,9 +66,33 @@ export interface PromptTemplate {
|
|
|
56
66
|
title: string;
|
|
57
67
|
body: string;
|
|
58
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
|
+
}
|
|
59
93
|
export interface Pack {
|
|
60
94
|
id: string;
|
|
61
95
|
detect(signals: RepoSignals): DetectionResult | null;
|
|
62
|
-
rules(detection: DetectionResult): RuleSet;
|
|
63
|
-
promptTemplates(detection: DetectionResult): PromptTemplate[];
|
|
96
|
+
rules(detection: DetectionResult, lang: Lang): RuleSet;
|
|
97
|
+
promptTemplates(detection: DetectionResult, lang: Lang): PromptTemplate[];
|
|
64
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);
|