agent-rules-init 0.2.1 → 0.4.0

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