agent-rules-init 0.3.0 → 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 +23 -4
- package/dist/cli.js +147 -38
- 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 +8 -5
- package/dist/core/i18n.js +61 -30
- package/dist/core/llm-bridge.d.ts +3 -0
- package/dist/core/llm-bridge.js +52 -0
- 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/repo-facts.d.ts +12 -2
- package/dist/core/repo-facts.js +233 -11
- package/dist/core/scanner.js +128 -25
- package/dist/core/templates.js +99 -26
- package/dist/core/types.d.ts +54 -3
- package/dist/core/writer.js +6 -6
- package/dist/packs/cpp.js +3 -3
- package/dist/packs/csharp.js +3 -3
- package/dist/packs/dart.js +4 -4
- package/dist/packs/elixir.js +2 -2
- package/dist/packs/go.js +2 -2
- package/dist/packs/java.js +57 -11
- package/dist/packs/js-ts.js +126 -23
- package/dist/packs/kotlin.js +29 -9
- package/dist/packs/php.js +4 -4
- package/dist/packs/python.js +81 -16
- package/dist/packs/r.js +4 -4
- package/dist/packs/ruby.js +6 -6
- package/dist/packs/rust.js +2 -2
- package/dist/packs/scala.js +3 -3
- package/dist/packs/swift.js +2 -2
- package/package.json +5 -3
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
const CONFIG_FILENAMES = [".agent-rules-init.yml", ".agent-rules-init.yaml"];
|
|
5
|
+
const ROOT_KEYS = new Set(["lang", "exclude", "projects", "noAi"]);
|
|
6
|
+
const PROJECT_KEYS = ["framework", "testRunner", "linter", "packageManager"];
|
|
7
|
+
const PROJECT_KEY_SET = new Set(PROJECT_KEYS);
|
|
8
|
+
export class ConfigError extends Error {
|
|
9
|
+
configPath;
|
|
10
|
+
constructor(message, configPath, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "ConfigError";
|
|
13
|
+
this.configPath = configPath;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function unknownKeyWarnings(value, allowed, location) {
|
|
20
|
+
return Object.keys(value)
|
|
21
|
+
.filter((key) => !allowed.has(key))
|
|
22
|
+
.map((key) => `Unknown configuration key \"${location}${key}\"; it was ignored.`);
|
|
23
|
+
}
|
|
24
|
+
function optionalNonEmptyString(value, location, warnings) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
28
|
+
warnings.push(`Configuration key \"${location}\" must be a non-empty string; it was ignored.`);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return value.trim();
|
|
32
|
+
}
|
|
33
|
+
function validateProjects(value, warnings) {
|
|
34
|
+
if (value === undefined)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (!isRecord(value)) {
|
|
37
|
+
warnings.push('Configuration key "projects" must be an object keyed by project path; it was ignored.');
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const projects = {};
|
|
41
|
+
for (const [projectPath, rawProject] of Object.entries(value)) {
|
|
42
|
+
if (projectPath.trim().length === 0) {
|
|
43
|
+
warnings.push("A project path must not be empty; that project was ignored.");
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(rawProject)) {
|
|
47
|
+
warnings.push(`Configuration key \"projects.${projectPath}\" must be an object; it was ignored.`);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
warnings.push(...unknownKeyWarnings(rawProject, PROJECT_KEY_SET, `projects.${projectPath}.`));
|
|
51
|
+
const project = {};
|
|
52
|
+
for (const key of PROJECT_KEYS) {
|
|
53
|
+
const parsed = optionalNonEmptyString(rawProject[key], `projects.${projectPath}.${key}`, warnings);
|
|
54
|
+
if (parsed !== undefined)
|
|
55
|
+
project[key] = parsed;
|
|
56
|
+
}
|
|
57
|
+
projects[projectPath] = project;
|
|
58
|
+
}
|
|
59
|
+
return projects;
|
|
60
|
+
}
|
|
61
|
+
function validateConfig(value, configPath, warnings) {
|
|
62
|
+
// An empty YAML document is equivalent to an empty configuration.
|
|
63
|
+
if (value === null || value === undefined)
|
|
64
|
+
return {};
|
|
65
|
+
if (!isRecord(value)) {
|
|
66
|
+
throw new ConfigError(`Invalid configuration in ${configPath}: the YAML root must be an object.`, configPath);
|
|
67
|
+
}
|
|
68
|
+
warnings.push(...unknownKeyWarnings(value, ROOT_KEYS, ""));
|
|
69
|
+
const config = {};
|
|
70
|
+
if (value.lang !== undefined) {
|
|
71
|
+
if (value.lang === "es" || value.lang === "en")
|
|
72
|
+
config.lang = value.lang;
|
|
73
|
+
else
|
|
74
|
+
warnings.push('Configuration key "lang" must be "es" or "en"; it was ignored.');
|
|
75
|
+
}
|
|
76
|
+
if (value.exclude !== undefined) {
|
|
77
|
+
if (!Array.isArray(value.exclude)) {
|
|
78
|
+
warnings.push('Configuration key "exclude" must be an array of strings; it was ignored.');
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const exclude = value.exclude.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
82
|
+
if (exclude.length !== value.exclude.length) {
|
|
83
|
+
warnings.push('Configuration key "exclude" contains non-string or empty entries; they were ignored.');
|
|
84
|
+
}
|
|
85
|
+
config.exclude = exclude.map((entry) => entry.trim());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const projects = validateProjects(value.projects, warnings);
|
|
89
|
+
if (projects !== undefined)
|
|
90
|
+
config.projects = projects;
|
|
91
|
+
if (value.noAi !== undefined) {
|
|
92
|
+
if (typeof value.noAi === "boolean")
|
|
93
|
+
config.noAi = value.noAi;
|
|
94
|
+
else
|
|
95
|
+
warnings.push('Configuration key "noAi" must be a boolean; it was ignored.');
|
|
96
|
+
}
|
|
97
|
+
return config;
|
|
98
|
+
}
|
|
99
|
+
/** Loads and validates the optional repository-local agent-rules configuration. */
|
|
100
|
+
export function loadConfig(rootPath) {
|
|
101
|
+
const found = CONFIG_FILENAMES.map((filename) => path.resolve(rootPath, filename)).filter((candidate) => fs.existsSync(candidate));
|
|
102
|
+
if (found.length === 0)
|
|
103
|
+
return { config: {}, warnings: [] };
|
|
104
|
+
const sourcePath = found[0];
|
|
105
|
+
const warnings = [];
|
|
106
|
+
if (found.length > 1) {
|
|
107
|
+
warnings.push(`Both ${CONFIG_FILENAMES.join(" and ")} exist; ${path.basename(sourcePath)} takes precedence.`);
|
|
108
|
+
}
|
|
109
|
+
let raw;
|
|
110
|
+
try {
|
|
111
|
+
raw = fs.readFileSync(sourcePath, "utf8");
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new ConfigError(`Cannot read configuration file ${sourcePath}.`, sourcePath, { cause: error });
|
|
115
|
+
}
|
|
116
|
+
let parsed;
|
|
117
|
+
try {
|
|
118
|
+
parsed = parse(raw);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
const detail = error instanceof Error ? ` ${error.message}` : "";
|
|
122
|
+
throw new ConfigError(`Invalid YAML in ${sourcePath}.${detail}`, sourcePath, { cause: error });
|
|
123
|
+
}
|
|
124
|
+
return { config: validateConfig(parsed, sourcePath, warnings), sourcePath, warnings };
|
|
125
|
+
}
|
package/dist/core/i18n.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
export type Lang = "es" | "en";
|
|
2
2
|
export declare function detectLang(): Lang;
|
|
3
3
|
export declare function summarySentence(lang: Lang, language: string, framework?: string, parenthetical?: string): string;
|
|
4
|
-
export declare function runTestsConvention(lang: Lang, cmd
|
|
5
|
-
export declare function reviewBody(lang: Lang, focus: string, framework
|
|
4
|
+
export declare function runTestsConvention(lang: Lang, cmd?: string): string;
|
|
5
|
+
export declare function reviewBody(lang: Lang, focus: string, framework?: string): string;
|
|
6
6
|
export declare function refactorBody(lang: Lang, extra?: string): string;
|
|
7
|
-
export declare function testingBody(lang: Lang, runner
|
|
8
|
-
export declare function unknownRunnerLabel(lang: Lang): string;
|
|
9
|
-
export declare function unknownFrameworkLabel(lang: Lang): string;
|
|
7
|
+
export declare function testingBody(lang: Lang, runner?: string): string;
|
|
10
8
|
export interface UiTexts {
|
|
11
9
|
generatedHeader: string;
|
|
12
10
|
sections: {
|
|
@@ -15,6 +13,7 @@ export interface UiTexts {
|
|
|
15
13
|
ci: string;
|
|
16
14
|
conventions: string;
|
|
17
15
|
architecture: string;
|
|
16
|
+
canonical: string;
|
|
18
17
|
};
|
|
19
18
|
andMore: (count: number, file?: string) => string;
|
|
20
19
|
noStackFallback: string;
|
|
@@ -26,6 +25,7 @@ export interface UiTexts {
|
|
|
26
25
|
packageManager: string;
|
|
27
26
|
};
|
|
28
27
|
usage: string;
|
|
28
|
+
automationUsage: string;
|
|
29
29
|
unknownOption: (flag: string) => string;
|
|
30
30
|
invalidLang: (value: string) => string;
|
|
31
31
|
noTtyWarning: string;
|
|
@@ -34,11 +34,14 @@ export interface UiTexts {
|
|
|
34
34
|
polishConfirm: (assistant: string) => string;
|
|
35
35
|
polishFailed: (assistant: string, error: string) => string;
|
|
36
36
|
polishPrompt: (content: string) => string;
|
|
37
|
+
polishBatchPrompt: (filesJson: string) => string;
|
|
37
38
|
fileSkipped: (path: string) => string;
|
|
38
39
|
outroWritten: string;
|
|
39
40
|
outroNothing: string;
|
|
40
41
|
unexpectedError: (message: string) => string;
|
|
41
42
|
cancelled: string;
|
|
42
43
|
dirNotes: Record<string, string>;
|
|
44
|
+
testDirNote: string;
|
|
45
|
+
entrypointNote: string;
|
|
43
46
|
}
|
|
44
47
|
export declare const UI: Record<Lang, UiTexts>;
|
package/dist/core/i18n.js
CHANGED
|
@@ -14,16 +14,26 @@ export function summarySentence(lang, language, framework, parenthetical) {
|
|
|
14
14
|
return `${language} project${framework ? ` using ${framework}` : ""}${paren}.`;
|
|
15
15
|
}
|
|
16
16
|
export function runTestsConvention(lang, cmd) {
|
|
17
|
+
if (cmd) {
|
|
18
|
+
return lang === "es"
|
|
19
|
+
? `Ejecuta los tests con ${cmd} antes de terminar una tarea.`
|
|
20
|
+
: `Run the tests with ${cmd} before finishing a task.`;
|
|
21
|
+
}
|
|
17
22
|
return lang === "es"
|
|
18
|
-
?
|
|
19
|
-
:
|
|
23
|
+
? "Ejecuta la suite de tests del repositorio antes de terminar una tarea."
|
|
24
|
+
: "Run the repository's test suite before finishing a task.";
|
|
20
25
|
}
|
|
21
26
|
export function reviewBody(lang, focus, framework) {
|
|
22
27
|
// Con focus vacío la frase colapsa a "bugs y desviaciones" sin coma colgante.
|
|
23
28
|
const focusPart = focus ? `, ${focus}` : "";
|
|
29
|
+
if (framework) {
|
|
30
|
+
return lang === "es"
|
|
31
|
+
? `Revisa el diff actual buscando bugs${focusPart} y desviaciones de las convenciones de ${framework}. Señala solo problemas concretos con línea de archivo.`
|
|
32
|
+
: `Review the current diff looking for bugs${focusPart} and deviations from ${framework} conventions. Point out only concrete issues with file and line.`;
|
|
33
|
+
}
|
|
24
34
|
return lang === "es"
|
|
25
|
-
? `Revisa el diff actual buscando bugs${focusPart}
|
|
26
|
-
: `Review the current diff looking for bugs${focusPart}
|
|
35
|
+
? `Revisa el diff actual buscando bugs${focusPart}. Señala solo problemas concretos con línea de archivo.`
|
|
36
|
+
: `Review the current diff looking for bugs${focusPart}. Point out only concrete issues with file and line.`;
|
|
27
37
|
}
|
|
28
38
|
export function refactorBody(lang, extra) {
|
|
29
39
|
const base = lang === "es"
|
|
@@ -32,15 +42,14 @@ export function refactorBody(lang, extra) {
|
|
|
32
42
|
return extra ? `${base} ${extra}` : base;
|
|
33
43
|
}
|
|
34
44
|
export function testingBody(lang, runner) {
|
|
45
|
+
if (runner) {
|
|
46
|
+
return lang === "es"
|
|
47
|
+
? `Escribe tests con ${runner} para el código señalado. Cubre el camino feliz y al menos un caso límite.`
|
|
48
|
+
: `Write tests with ${runner} for the highlighted code. Cover the happy path and at least one edge case.`;
|
|
49
|
+
}
|
|
35
50
|
return lang === "es"
|
|
36
|
-
?
|
|
37
|
-
:
|
|
38
|
-
}
|
|
39
|
-
export function unknownRunnerLabel(lang) {
|
|
40
|
-
return lang === "es" ? "el test runner del proyecto" : "the project's test runner";
|
|
41
|
-
}
|
|
42
|
-
export function unknownFrameworkLabel(lang) {
|
|
43
|
-
return lang === "es" ? "el framework del proyecto" : "the project's framework";
|
|
51
|
+
? "Escribe tests para el código señalado. Cubre el camino feliz y al menos un caso límite."
|
|
52
|
+
: "Write tests for the highlighted code. Cover the happy path and at least one edge case.";
|
|
44
53
|
}
|
|
45
54
|
export const UI = {
|
|
46
55
|
es: {
|
|
@@ -51,6 +60,7 @@ export const UI = {
|
|
|
51
60
|
ci: "Lo que ejecuta CI (GitHub Actions)",
|
|
52
61
|
conventions: "Convenciones",
|
|
53
62
|
architecture: "Arquitectura",
|
|
63
|
+
canonical: "Comandos canónicos",
|
|
54
64
|
},
|
|
55
65
|
andMore: (count, file) => (file ? `…y ${count} más en ${file}` : `…y ${count} más`),
|
|
56
66
|
noStackFallback: "No se detectó ningún stack conocido. Completa este archivo manualmente.",
|
|
@@ -61,16 +71,21 @@ export const UI = {
|
|
|
61
71
|
linter: "el linter",
|
|
62
72
|
packageManager: "el gestor de paquetes",
|
|
63
73
|
},
|
|
64
|
-
usage: `agent-rules-init — genera CLAUDE.md, AGENTS.md, copilot-instructions y prompts de review/refactor/testing a partir del stack detectado en tu repo.
|
|
65
|
-
|
|
66
|
-
Uso:
|
|
67
|
-
npx agent-rules-init escanea el directorio actual y genera los archivos *.generated.*
|
|
68
|
-
npx agent-rules-init --lang es fuerza el idioma del contenido (es|en); por defecto se detecta del sistema
|
|
69
|
-
npx agent-rules-init --help muestra esta ayuda
|
|
70
|
-
npx agent-rules-init --version muestra la versión
|
|
71
|
-
|
|
72
|
-
Los archivos se crean siempre con sufijo .generated y nunca sobrescriben nada existente:
|
|
74
|
+
usage: `agent-rules-init — genera CLAUDE.md, AGENTS.md, copilot-instructions y prompts de review/refactor/testing a partir del stack detectado en tu repo.
|
|
75
|
+
|
|
76
|
+
Uso:
|
|
77
|
+
npx agent-rules-init escanea el directorio actual y genera los archivos *.generated.*
|
|
78
|
+
npx agent-rules-init --lang es fuerza el idioma del contenido (es|en); por defecto se detecta del sistema
|
|
79
|
+
npx agent-rules-init --help muestra esta ayuda
|
|
80
|
+
npx agent-rules-init --version muestra la versión
|
|
81
|
+
|
|
82
|
+
Los archivos se crean siempre con sufijo .generated y nunca sobrescriben nada existente:
|
|
73
83
|
revisa su contenido y quita el sufijo para activarlos.`,
|
|
84
|
+
automationUsage: `Automatización:
|
|
85
|
+
--dry-run renderiza y muestra archivos sin escribir
|
|
86
|
+
--check termina con error si faltan archivos generados; nunca escribe
|
|
87
|
+
--json emite un único resultado JSON legible por máquinas
|
|
88
|
+
--non-interactive omite preguntas y el pulido con IA`,
|
|
74
89
|
unknownOption: (flag) => `Opción no reconocida: ${flag}`,
|
|
75
90
|
invalidLang: (value) => `Valor de --lang no válido: "${value}" (usa "es" o "en").`,
|
|
76
91
|
noTtyWarning: "No se detectó una terminal interactiva (esto pasa a veces en Git Bash en Windows). " +
|
|
@@ -80,6 +95,9 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
80
95
|
polishConfirm: (assistant) => `Se detectó ${assistant}. ¿Quieres que pula la redacción final?`,
|
|
81
96
|
polishFailed: (assistant, error) => `No se pudo pulir el contenido con ${assistant}, se mantiene el original: ${error}`,
|
|
82
97
|
polishPrompt: (content) => `Pule la redacción del siguiente documento de instrucciones para un agente de IA, sin cambiar su significado ni estructura. Devuelve únicamente el documento pulido, sin comentarios ni explicaciones adicionales:\n\n${content}`,
|
|
98
|
+
polishBatchPrompt: (filesJson) => "Pule la redacción de estos archivos de instrucciones sin cambiar su significado, estructura, rutas ni formato Markdown. " +
|
|
99
|
+
"Devuelve únicamente un array JSON válido con exactamente los mismos objetos path/content y en el mismo orden, sin bloque de código ni comentarios. " +
|
|
100
|
+
`Entrada JSON:\n${filesJson}`,
|
|
83
101
|
fileSkipped: (path) => `${path}: ya existía, se conserva sin cambios.`,
|
|
84
102
|
outroWritten: "Revisa los archivos *.generated.* y, cuando estés conforme, quita el sufijo " +
|
|
85
103
|
'".generated" (ej. "CLAUDE.generated.md" → "CLAUDE.md") para activarlos — ' +
|
|
@@ -107,6 +125,8 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
107
125
|
assets: "activos",
|
|
108
126
|
config: "configuración",
|
|
109
127
|
},
|
|
128
|
+
testDirNote: "tests",
|
|
129
|
+
entrypointNote: "punto de entrada",
|
|
110
130
|
},
|
|
111
131
|
en: {
|
|
112
132
|
generatedHeader: "Generated by agent-rules-init from what was detected in this repo.",
|
|
@@ -116,6 +136,7 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
116
136
|
ci: "What CI runs (GitHub Actions)",
|
|
117
137
|
conventions: "Conventions",
|
|
118
138
|
architecture: "Architecture",
|
|
139
|
+
canonical: "Canonical commands",
|
|
119
140
|
},
|
|
120
141
|
andMore: (count, file) => (file ? `…and ${count} more in ${file}` : `…and ${count} more`),
|
|
121
142
|
noStackFallback: "No known stack was detected. Fill in this file manually.",
|
|
@@ -126,16 +147,21 @@ revisa su contenido y quita el sufijo para activarlos.`,
|
|
|
126
147
|
linter: "the linter",
|
|
127
148
|
packageManager: "the package manager",
|
|
128
149
|
},
|
|
129
|
-
usage: `agent-rules-init — generates CLAUDE.md, AGENTS.md, copilot-instructions and review/refactor/testing prompts from the stack detected in your repo.
|
|
130
|
-
|
|
131
|
-
Usage:
|
|
132
|
-
npx agent-rules-init scan the current directory and generate the *.generated.* files
|
|
133
|
-
npx agent-rules-init --lang en force the content language (es|en); defaults to the system locale
|
|
134
|
-
npx agent-rules-init --help show this help
|
|
135
|
-
npx agent-rules-init --version show the version
|
|
136
|
-
|
|
137
|
-
Files are always created with the .generated suffix and never overwrite anything:
|
|
150
|
+
usage: `agent-rules-init — generates CLAUDE.md, AGENTS.md, copilot-instructions and review/refactor/testing prompts from the stack detected in your repo.
|
|
151
|
+
|
|
152
|
+
Usage:
|
|
153
|
+
npx agent-rules-init scan the current directory and generate the *.generated.* files
|
|
154
|
+
npx agent-rules-init --lang en force the content language (es|en); defaults to the system locale
|
|
155
|
+
npx agent-rules-init --help show this help
|
|
156
|
+
npx agent-rules-init --version show the version
|
|
157
|
+
|
|
158
|
+
Files are always created with the .generated suffix and never overwrite anything:
|
|
138
159
|
review their content and drop the suffix to activate them.`,
|
|
160
|
+
automationUsage: `Automation:
|
|
161
|
+
--dry-run render and print files without writing
|
|
162
|
+
--check exit non-zero if generated files are missing; never write
|
|
163
|
+
--json emit a single machine-readable JSON result
|
|
164
|
+
--non-interactive skip questions and AI polishing`,
|
|
139
165
|
unknownOption: (flag) => `Unknown option: ${flag}`,
|
|
140
166
|
invalidLang: (value) => `Invalid --lang value: "${value}" (use "es" or "en").`,
|
|
141
167
|
noTtyWarning: "No interactive terminal detected (this sometimes happens in Git Bash on Windows). " +
|
|
@@ -145,6 +171,9 @@ review their content and drop the suffix to activate them.`,
|
|
|
145
171
|
polishConfirm: (assistant) => `${assistant} was detected. Do you want it to polish the final wording?`,
|
|
146
172
|
polishFailed: (assistant, error) => `Couldn't polish the content with ${assistant}, keeping the original: ${error}`,
|
|
147
173
|
polishPrompt: (content) => `Polish the wording of the following instructions document for an AI agent, without changing its meaning or structure. Return only the polished document, with no extra comments or explanations:\n\n${content}`,
|
|
174
|
+
polishBatchPrompt: (filesJson) => "Polish the wording of these instruction files without changing their meaning, structure, paths, or Markdown format. " +
|
|
175
|
+
"Return only a valid JSON array with exactly the same path/content objects in the same order, without a code fence or commentary. " +
|
|
176
|
+
`Input JSON:\n${filesJson}`,
|
|
148
177
|
fileSkipped: (path) => `${path}: already existed, left unchanged.`,
|
|
149
178
|
outroWritten: "Review the *.generated.* files and, once you are happy with them, drop the " +
|
|
150
179
|
'".generated" suffix (e.g. "CLAUDE.generated.md" → "CLAUDE.md") to activate them — ' +
|
|
@@ -172,5 +201,7 @@ review their content and drop the suffix to activate them.`,
|
|
|
172
201
|
assets: "assets",
|
|
173
202
|
config: "configuration",
|
|
174
203
|
},
|
|
204
|
+
testDirNote: "tests",
|
|
205
|
+
entrypointNote: "entry point",
|
|
175
206
|
},
|
|
176
207
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Lang } from "./i18n.js";
|
|
2
|
+
import type { GeneratedFile } from "./writer.js";
|
|
2
3
|
export type AssistantId = "claude" | "codex";
|
|
3
4
|
export interface ExecResult {
|
|
4
5
|
stdout: string;
|
|
@@ -8,3 +9,5 @@ export type ExecFn = (command: string, args: string[], stdin?: string) => Promis
|
|
|
8
9
|
export declare const defaultExecFn: ExecFn;
|
|
9
10
|
export declare function detectAvailableAssistants(execFn?: ExecFn): Promise<AssistantId[]>;
|
|
10
11
|
export declare function polishWithAssistant(assistant: AssistantId, content: string, execFn?: ExecFn, lang?: Lang): Promise<string>;
|
|
12
|
+
/** Polishes typical runs in one assistant process, splitting only very large outputs. */
|
|
13
|
+
export declare function polishFilesWithAssistant(assistant: AssistantId, files: GeneratedFile[], execFn?: ExecFn, lang?: Lang): Promise<GeneratedFile[]>;
|
package/dist/core/llm-bridge.js
CHANGED
|
@@ -45,3 +45,55 @@ export async function polishWithAssistant(assistant, content, execFn = defaultEx
|
|
|
45
45
|
return content;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
const MAX_POLISH_BATCH_CHARS = 60_000;
|
|
49
|
+
function makeBatches(files) {
|
|
50
|
+
const batches = [];
|
|
51
|
+
let current = [];
|
|
52
|
+
let currentSize = 2;
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const size = JSON.stringify(file).length + 1;
|
|
55
|
+
if (current.length > 0 && currentSize + size > MAX_POLISH_BATCH_CHARS) {
|
|
56
|
+
batches.push(current);
|
|
57
|
+
current = [];
|
|
58
|
+
currentSize = 2;
|
|
59
|
+
}
|
|
60
|
+
current.push(file);
|
|
61
|
+
currentSize += size;
|
|
62
|
+
}
|
|
63
|
+
if (current.length > 0)
|
|
64
|
+
batches.push(current);
|
|
65
|
+
return batches;
|
|
66
|
+
}
|
|
67
|
+
function parsePolishedBatch(stdout, originals) {
|
|
68
|
+
const trimmed = stdout.trim();
|
|
69
|
+
const withoutFence = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i)?.[1] ?? trimmed;
|
|
70
|
+
const parsed = JSON.parse(withoutFence);
|
|
71
|
+
if (!Array.isArray(parsed) || parsed.length !== originals.length) {
|
|
72
|
+
throw new Error("assistant returned a different number of files");
|
|
73
|
+
}
|
|
74
|
+
return parsed.map((value, index) => {
|
|
75
|
+
if (!value || typeof value !== "object")
|
|
76
|
+
throw new Error("assistant returned an invalid file entry");
|
|
77
|
+
const entry = value;
|
|
78
|
+
if (entry.path !== originals[index].path || typeof entry.content !== "string") {
|
|
79
|
+
throw new Error("assistant changed a path or returned invalid content");
|
|
80
|
+
}
|
|
81
|
+
return { path: originals[index].path, content: entry.content };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Polishes typical runs in one assistant process, splitting only very large outputs. */
|
|
85
|
+
export async function polishFilesWithAssistant(assistant, files, execFn = defaultExecFn, lang = "es") {
|
|
86
|
+
const polished = [];
|
|
87
|
+
for (const batch of makeBatches(files)) {
|
|
88
|
+
try {
|
|
89
|
+
const input = UI[lang].polishBatchPrompt(JSON.stringify(batch));
|
|
90
|
+
const result = await execFn(assistant, ["-p"], input);
|
|
91
|
+
polished.push(...parsePolishedBatch(result.stdout, batch));
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.warn(UI[lang].polishFailed(assistant, err.message));
|
|
95
|
+
polished.push(...batch);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return polished;
|
|
99
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Lang } from "./i18n.js";
|
|
2
|
+
import type { ProjectUnit } from "./project-units.js";
|
|
3
|
+
export interface ProjectOverrides {
|
|
4
|
+
framework?: string;
|
|
5
|
+
testRunner?: string;
|
|
6
|
+
linter?: string;
|
|
7
|
+
packageManager?: string;
|
|
8
|
+
}
|
|
9
|
+
/** Generates the path-scoped AGENTS file for one JS/TS package. */
|
|
10
|
+
export declare function renderProjectUnitAgents(unit: ProjectUnit, lang: Lang, overrides?: ProjectOverrides): {
|
|
11
|
+
path: string;
|
|
12
|
+
content: string;
|
|
13
|
+
} | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { buildRepoFacts } from "./repo-facts.js";
|
|
2
|
+
import { renderAgentsMd } from "./templates.js";
|
|
3
|
+
import { jsTsPack } from "../packs/js-ts.js";
|
|
4
|
+
function applyOverrides(detection, overrides) {
|
|
5
|
+
const updated = { ...detection };
|
|
6
|
+
for (const field of ["framework", "testRunner", "linter", "packageManager"]) {
|
|
7
|
+
const value = overrides[field];
|
|
8
|
+
if (value)
|
|
9
|
+
updated[field] = { value, confidence: "high" };
|
|
10
|
+
}
|
|
11
|
+
return updated;
|
|
12
|
+
}
|
|
13
|
+
/** Generates the path-scoped AGENTS file for one JS/TS package. */
|
|
14
|
+
export function renderProjectUnitAgents(unit, lang, overrides = {}) {
|
|
15
|
+
const rawDetection = jsTsPack.detect(unit.signals);
|
|
16
|
+
if (!rawDetection)
|
|
17
|
+
return null;
|
|
18
|
+
const detection = applyOverrides(rawDetection, overrides);
|
|
19
|
+
const facts = buildRepoFacts(unit.signals, lang);
|
|
20
|
+
const ruleSet = jsTsPack.rules(detection, lang, { facts, signals: unit.signals });
|
|
21
|
+
return {
|
|
22
|
+
path: `${unit.path}/AGENTS.generated.md`,
|
|
23
|
+
content: renderAgentsMd([{ detection, ruleSet }], facts, lang),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RepoSignals } from "./types.js";
|
|
2
|
+
export interface ProjectUnit {
|
|
3
|
+
/** POSIX path relative to the repository root. */
|
|
4
|
+
path: string;
|
|
5
|
+
signals: RepoSignals;
|
|
6
|
+
}
|
|
7
|
+
export declare function isProjectExcluded(projectPath: string, patterns: readonly string[]): boolean;
|
|
8
|
+
/** Removes excluded JS packages before repo-wide aggregation and scoped rendering. */
|
|
9
|
+
export declare function applyProjectExcludes(signals: RepoSignals, patterns: readonly string[]): RepoSignals;
|
|
10
|
+
/**
|
|
11
|
+
* Creates an isolated RepoSignals view for every nested JS/TS package.
|
|
12
|
+
*
|
|
13
|
+
* Files and presence checks become relative to the package, so packs can run without
|
|
14
|
+
* learning workspace-specific rules and cannot accidentally use a sibling's stack.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildPackageUnits(signals: RepoSignals): ProjectUnit[];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
function normalize(value) {
|
|
3
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
4
|
+
}
|
|
5
|
+
function matchesPattern(projectPath, rawPattern) {
|
|
6
|
+
const value = normalize(projectPath);
|
|
7
|
+
const pattern = normalize(rawPattern);
|
|
8
|
+
if (!pattern)
|
|
9
|
+
return false;
|
|
10
|
+
if (pattern.endsWith("/**") && value === pattern.slice(0, -3))
|
|
11
|
+
return true;
|
|
12
|
+
const marker = "\u0000";
|
|
13
|
+
const regex = pattern
|
|
14
|
+
.replace(/\*\*/g, marker)
|
|
15
|
+
.replace(/[.+?^${}()|[\]\\]/g, "\\$&")
|
|
16
|
+
.replace(/\*/g, "[^/]*")
|
|
17
|
+
.replace(new RegExp(marker, "g"), ".*");
|
|
18
|
+
return new RegExp(`^${regex}$`).test(value);
|
|
19
|
+
}
|
|
20
|
+
export function isProjectExcluded(projectPath, patterns) {
|
|
21
|
+
return patterns.some((pattern) => matchesPattern(projectPath, pattern));
|
|
22
|
+
}
|
|
23
|
+
/** Removes excluded JS packages before repo-wide aggregation and scoped rendering. */
|
|
24
|
+
export function applyProjectExcludes(signals, patterns) {
|
|
25
|
+
if (patterns.length === 0 || !signals.packageJsons?.length)
|
|
26
|
+
return signals;
|
|
27
|
+
const packageJsons = signals.packageJsons.filter((manifest) => {
|
|
28
|
+
const packageDir = path.posix.dirname(manifest.path);
|
|
29
|
+
return packageDir === "." || !isProjectExcluded(packageDir, patterns);
|
|
30
|
+
});
|
|
31
|
+
const excludedDirs = (signals.packageJsons ?? [])
|
|
32
|
+
.map((manifest) => path.posix.dirname(manifest.path))
|
|
33
|
+
.filter((dir) => dir !== "." && isProjectExcluded(dir, patterns));
|
|
34
|
+
const isExcludedPath = (relativePath) => {
|
|
35
|
+
const normalized = normalize(relativePath);
|
|
36
|
+
return excludedDirs.some((dir) => normalized === dir || normalized.startsWith(`${dir}/`));
|
|
37
|
+
};
|
|
38
|
+
const primary = packageJsons[0];
|
|
39
|
+
const packageJson = primary
|
|
40
|
+
? {
|
|
41
|
+
name: primary.name,
|
|
42
|
+
main: primary.main,
|
|
43
|
+
dependencies: Object.assign({}, ...packageJsons.map((manifest) => manifest.dependencies)),
|
|
44
|
+
devDependencies: Object.assign({}, ...packageJsons.map((manifest) => manifest.devDependencies)),
|
|
45
|
+
scripts: primary.scripts,
|
|
46
|
+
moduleType: primary.moduleType,
|
|
47
|
+
packageManager: primary.packageManager,
|
|
48
|
+
}
|
|
49
|
+
: undefined;
|
|
50
|
+
return {
|
|
51
|
+
...signals,
|
|
52
|
+
files: signals.files.filter((file) => !isExcludedPath(file)),
|
|
53
|
+
hasFile: (relativePath) => !isExcludedPath(relativePath) && signals.hasFile(relativePath),
|
|
54
|
+
hasDir: (relativeDir) => !isExcludedPath(relativeDir) && signals.hasDir(relativeDir),
|
|
55
|
+
packageJson,
|
|
56
|
+
packageJsons,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function withoutLocation(manifest) {
|
|
60
|
+
const { path: _path, ...packageJson } = manifest;
|
|
61
|
+
return packageJson;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Creates an isolated RepoSignals view for every nested JS/TS package.
|
|
65
|
+
*
|
|
66
|
+
* Files and presence checks become relative to the package, so packs can run without
|
|
67
|
+
* learning workspace-specific rules and cannot accidentally use a sibling's stack.
|
|
68
|
+
*/
|
|
69
|
+
export function buildPackageUnits(signals) {
|
|
70
|
+
return (signals.packageJsons ?? [])
|
|
71
|
+
.filter((manifest) => manifest.path !== "package.json")
|
|
72
|
+
.map((manifest) => {
|
|
73
|
+
const unitPath = path.posix.dirname(manifest.path);
|
|
74
|
+
const prefix = `${unitPath}/`;
|
|
75
|
+
const unitFiles = signals.files
|
|
76
|
+
.map((file) => file.split(path.sep).join("/"))
|
|
77
|
+
.filter((file) => file.startsWith(prefix))
|
|
78
|
+
.map((file) => file.slice(prefix.length));
|
|
79
|
+
const packageJson = withoutLocation(manifest);
|
|
80
|
+
return {
|
|
81
|
+
path: unitPath,
|
|
82
|
+
signals: {
|
|
83
|
+
rootPath: path.join(signals.rootPath, ...unitPath.split("/")),
|
|
84
|
+
files: unitFiles,
|
|
85
|
+
hasFile: (relativePath) => signals.hasFile(`${prefix}${relativePath}`),
|
|
86
|
+
hasDir: (relativeDir) => signals.hasDir(`${prefix}${relativeDir}`),
|
|
87
|
+
packageJson,
|
|
88
|
+
packageJsons: [{ ...packageJson, path: "package.json" }],
|
|
89
|
+
guidanceFiles: (signals.guidanceFiles ?? [])
|
|
90
|
+
.filter((file) => file.path.startsWith(prefix))
|
|
91
|
+
.map((file) => ({ ...file, path: file.path.slice(prefix.length) })),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Lang } from "./i18n.js";
|
|
2
|
-
import type { CiCommand, CommandEntry, CommandSource, DirEntry, RepoFacts, RepoSignals } from "./types.js";
|
|
3
|
-
export declare function
|
|
2
|
+
import type { CiCommand, ArchitectureFact, CommandEntry, CommandSource, ConventionFact, DirEntry, RepoFacts, RepoSignals } from "./types.js";
|
|
3
|
+
export declare function extractJsPackageCommands(signals: RepoSignals): CommandEntry[];
|
|
4
|
+
/** @deprecated Conservado como alias de API; también devuelve comandos pnpm/Yarn/Bun. */
|
|
5
|
+
export declare const extractNpmCommands: typeof extractJsPackageCommands;
|
|
4
6
|
export declare function extractMakeTargets(signals: RepoSignals): CommandEntry[];
|
|
5
7
|
export declare function extractMixAliases(signals: RepoSignals): CommandEntry[];
|
|
6
8
|
export declare function extractToxEnvs(signals: RepoSignals): CommandEntry[];
|
|
@@ -17,4 +19,12 @@ export declare function extractCiCommands(signals: RepoSignals): {
|
|
|
17
19
|
omittedCount: number;
|
|
18
20
|
};
|
|
19
21
|
export declare function extractStructure(signals: RepoSignals, lang: Lang): DirEntry[];
|
|
22
|
+
export declare function detectTestDirs(files: string[]): string[];
|
|
23
|
+
export declare function detectEntrypoints(signals: RepoSignals): RepoFacts["entrypoints"];
|
|
24
|
+
export declare function extractArchitectureFacts(signals: RepoSignals, lang: Lang, testDirs?: string[], entrypoints?: {
|
|
25
|
+
label: string;
|
|
26
|
+
target: string;
|
|
27
|
+
source: string;
|
|
28
|
+
}[]): ArchitectureFact[];
|
|
29
|
+
export declare function extractConventionFacts(signals: RepoSignals, lang: Lang): ConventionFact[];
|
|
20
30
|
export declare function buildRepoFacts(signals: RepoSignals, lang: Lang): RepoFacts;
|