agent-rules-init 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -0
- package/dist/cli.d.ts +38 -1
- package/dist/cli.js +203 -40
- package/dist/core/canonical-commands.d.ts +4 -0
- package/dist/core/canonical-commands.js +169 -0
- package/dist/core/config.d.ts +25 -0
- package/dist/core/config.js +125 -0
- package/dist/core/i18n.d.ts +47 -0
- package/dist/core/i18n.js +207 -0
- package/dist/core/llm-bridge.d.ts +5 -1
- package/dist/core/llm-bridge.js +56 -4
- package/dist/core/project-unit-output.d.ts +13 -0
- package/dist/core/project-unit-output.js +25 -0
- package/dist/core/project-units.d.ts +16 -0
- package/dist/core/project-units.js +95 -0
- package/dist/core/prompt-engine.d.ts +3 -1
- package/dist/core/prompt-engine.js +19 -14
- package/dist/core/repo-facts.d.ts +30 -0
- package/dist/core/repo-facts.js +420 -0
- package/dist/core/scanner.js +153 -27
- package/dist/core/templates.d.ts +6 -4
- package/dist/core/templates.js +127 -29
- package/dist/core/types.d.ts +84 -2
- package/dist/core/writer.d.ts +1 -1
- package/dist/core/writer.js +6 -4
- package/dist/packs/cpp.js +33 -27
- package/dist/packs/csharp.js +32 -27
- package/dist/packs/dart.js +30 -31
- package/dist/packs/elixir.js +31 -27
- package/dist/packs/go.js +31 -27
- package/dist/packs/java.js +82 -30
- package/dist/packs/js-ts.js +150 -37
- package/dist/packs/kotlin.js +56 -31
- package/dist/packs/php.js +30 -27
- package/dist/packs/python.js +107 -34
- package/dist/packs/r.js +30 -27
- package/dist/packs/ruby.js +36 -27
- package/dist/packs/rust.js +33 -27
- package/dist/packs/scala.js +32 -27
- package/dist/packs/swift.js +31 -27
- package/package.json +7 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agent-rules-init contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# agent-rules-init
|
|
2
|
+
|
|
3
|
+
Generate repository-specific instructions for Claude Code, Codex and GitHub Copilot from the manifests and commands that already exist in your project.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx agent-rules-init
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The CLI detects JavaScript/TypeScript, Python, Java, PHP, Ruby, Go, Rust, C#/.NET, Kotlin, Swift, Dart/Flutter, C/C++, Elixir, Scala and R projects. It also understands mixed repositories and npm, pnpm, Yarn and Bun workspaces, generating package-scoped `AGENTS.generated.md` files.
|
|
10
|
+
|
|
11
|
+
All output is written with a `.generated` suffix and existing files are never overwritten. Review the generated files and remove the suffix when you are ready to activate them.
|
|
12
|
+
|
|
13
|
+
Use `--lang es` or `--lang en` to select the output language. See the [full documentation](https://github.com/racama29/agent-rules-init#readme) for supported frameworks, generated files and contribution instructions.
|
|
14
|
+
|
|
15
|
+
Automation is supported through `--dry-run`, `--check`, `--json` and `--non-interactive`. Repository defaults and per-project overrides can be stored in `.agent-rules-init.yml`.
|
|
16
|
+
|
|
17
|
+
Generated documents share an evidence-backed model but are not duplicates: Claude receives broad repository context, AGENTS receives operational commands and scope, and Copilot receives concise implementation conventions. Observed architecture and local conventions include their source files so specific claims can be audited.
|
|
18
|
+
|
|
19
|
+
## License
|
|
20
|
+
|
|
21
|
+
MIT
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,10 +1,47 @@
|
|
|
1
|
-
import { type WriteResult } from "./core/writer.js";
|
|
1
|
+
import { type GeneratedFile, type WriteResult } from "./core/writer.js";
|
|
2
|
+
import { type Lang } from "./core/i18n.js";
|
|
3
|
+
import { type AgentRulesConfig } from "./core/config.js";
|
|
2
4
|
import { type PromptFn } from "./core/prompt-engine.js";
|
|
3
5
|
import { type ExecFn } from "./core/llm-bridge.js";
|
|
6
|
+
import type { RepoFacts } from "./core/types.js";
|
|
4
7
|
export interface RunCliOptions {
|
|
5
8
|
promptFn?: PromptFn;
|
|
6
9
|
execFn?: ExecFn;
|
|
7
10
|
skipLlm?: boolean;
|
|
11
|
+
lang?: Lang;
|
|
12
|
+
/** Generate results without changing the filesystem. */
|
|
13
|
+
dryRun?: boolean;
|
|
14
|
+
/** Do not prompt or offer AI polishing. */
|
|
15
|
+
nonInteractive?: boolean;
|
|
16
|
+
/** Receives the rendered files before they are written (or planned). */
|
|
17
|
+
onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
|
|
18
|
+
/** Preloaded repository configuration; loaded from disk when omitted. */
|
|
19
|
+
config?: AgentRulesConfig;
|
|
20
|
+
onConfigWarnings?: (warnings: readonly string[]) => void;
|
|
21
|
+
/** Receives the facts extracted from the repo (including canonical commands). */
|
|
22
|
+
onFacts?: (facts: RepoFacts) => void;
|
|
8
23
|
}
|
|
9
24
|
export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
|
|
25
|
+
export interface CliRunOptions {
|
|
26
|
+
lang?: Lang;
|
|
27
|
+
dryRun?: true;
|
|
28
|
+
check?: true;
|
|
29
|
+
json?: true;
|
|
30
|
+
nonInteractive?: true;
|
|
31
|
+
}
|
|
32
|
+
export type CliAction = ({
|
|
33
|
+
kind: "run";
|
|
34
|
+
} & CliRunOptions) | {
|
|
35
|
+
kind: "help";
|
|
36
|
+
} | {
|
|
37
|
+
kind: "version";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "invalid-lang";
|
|
40
|
+
value: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "unknown";
|
|
43
|
+
flag: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function resolveCliAction(argv: string[]): CliAction;
|
|
46
|
+
export declare function getVersion(): string;
|
|
10
47
|
export declare function main(): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import * as clack from "@clack/prompts";
|
|
2
5
|
import { scanRepo } from "./core/scanner.js";
|
|
3
6
|
import { writeGeneratedFiles } from "./core/writer.js";
|
|
4
7
|
import { renderClaudeMd, renderAgentsMd, renderCopilotInstructions, renderPromptFiles, } from "./core/templates.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
8
|
+
import { buildRepoFacts } from "./core/repo-facts.js";
|
|
9
|
+
import { UI, detectLang } from "./core/i18n.js";
|
|
10
|
+
import { loadConfig } from "./core/config.js";
|
|
11
|
+
import { applyProjectExcludes, buildPackageUnits } from "./core/project-units.js";
|
|
12
|
+
import { renderProjectUnitAgents } from "./core/project-unit-output.js";
|
|
13
|
+
import { collectLowConfidenceQuestions, askQuestions, applyAnswers, makeDefaultPromptFn, hasInteractiveTty, } from "./core/prompt-engine.js";
|
|
14
|
+
import { detectAvailableAssistants, polishFilesWithAssistant, defaultExecFn } from "./core/llm-bridge.js";
|
|
7
15
|
import { jsTsPack } from "./packs/js-ts.js";
|
|
8
16
|
import { pythonPack } from "./packs/python.js";
|
|
9
17
|
import { javaPack } from "./packs/java.js";
|
|
@@ -37,87 +45,242 @@ const ALL_PACKS = [
|
|
|
37
45
|
rPack,
|
|
38
46
|
];
|
|
39
47
|
export async function runCli(rootPath, options = {}) {
|
|
40
|
-
const
|
|
48
|
+
const loadedConfig = options.config ? { config: options.config, warnings: [] } : loadConfig(rootPath);
|
|
49
|
+
const config = loadedConfig.config;
|
|
50
|
+
options.onConfigWarnings?.(loadedConfig.warnings);
|
|
41
51
|
const execFn = options.execFn ?? defaultExecFn;
|
|
42
|
-
const
|
|
52
|
+
const lang = options.lang ?? config.lang ?? detectLang();
|
|
53
|
+
const promptFn = options.promptFn ?? makeDefaultPromptFn(lang);
|
|
54
|
+
const ui = UI[lang];
|
|
55
|
+
const signals = applyProjectExcludes(scanRepo(rootPath), config.exclude ?? []);
|
|
43
56
|
const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
|
|
44
|
-
const questions = collectLowConfidenceQuestions(rawDetections);
|
|
45
|
-
const answers = await askQuestions(questions, promptFn);
|
|
57
|
+
const questions = collectLowConfidenceQuestions(rawDetections, lang);
|
|
58
|
+
const answers = options.nonInteractive ? {} : await askQuestions(questions, promptFn);
|
|
46
59
|
const detections = applyAnswers(rawDetections, answers);
|
|
60
|
+
const facts = buildRepoFacts(signals, lang);
|
|
61
|
+
options.onFacts?.(facts);
|
|
62
|
+
const ctx = { facts, signals };
|
|
47
63
|
const entries = detections.map((detection) => {
|
|
48
64
|
const pack = ALL_PACKS.find((p) => p.id === detection.packId);
|
|
49
|
-
return { detection, ruleSet: pack.rules(detection) };
|
|
65
|
+
return { detection, ruleSet: pack.rules(detection, lang, ctx) };
|
|
50
66
|
});
|
|
51
67
|
const files = [];
|
|
52
68
|
if (entries.length > 0) {
|
|
53
|
-
files.push({ path: "CLAUDE.generated.md", content: renderClaudeMd(entries) });
|
|
54
|
-
files.push({ path: "AGENTS.generated.md", content: renderAgentsMd(entries) });
|
|
69
|
+
files.push({ path: "CLAUDE.generated.md", content: renderClaudeMd(entries, facts, lang) });
|
|
70
|
+
files.push({ path: "AGENTS.generated.md", content: renderAgentsMd(entries, facts, lang) });
|
|
55
71
|
files.push({
|
|
56
72
|
path: ".github/copilot-instructions.generated.md",
|
|
57
|
-
content: renderCopilotInstructions(entries),
|
|
73
|
+
content: renderCopilotInstructions(entries, facts, lang),
|
|
58
74
|
});
|
|
59
75
|
for (const detection of detections) {
|
|
60
76
|
const pack = ALL_PACKS.find((p) => p.id === detection.packId);
|
|
61
|
-
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection))) {
|
|
77
|
+
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
|
|
62
78
|
files.push(file);
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
}
|
|
66
82
|
else {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
const withFallback = (content) => content.replace(ui.generatedHeader, `${ui.generatedHeader}\n\n${ui.noStackFallback}`);
|
|
84
|
+
files.push({ path: "CLAUDE.generated.md", content: withFallback(renderClaudeMd([], facts, lang)) }, { path: "AGENTS.generated.md", content: withFallback(renderAgentsMd([], facts, lang)) }, {
|
|
85
|
+
path: ".github/copilot-instructions.generated.md",
|
|
86
|
+
content: withFallback(renderCopilotInstructions([], facts, lang)),
|
|
70
87
|
});
|
|
71
88
|
}
|
|
72
|
-
|
|
89
|
+
// Nested AGENTS files are intentionally limited to package-local facts and stack
|
|
90
|
+
// signals. The root documents remain the cross-repository overview.
|
|
91
|
+
for (const unit of buildPackageUnits(signals)) {
|
|
92
|
+
const scoped = renderProjectUnitAgents(unit, lang, config.projects?.[unit.path]);
|
|
93
|
+
if (scoped)
|
|
94
|
+
files.push(scoped);
|
|
95
|
+
}
|
|
96
|
+
if (!options.skipLlm && !config.noAi && !options.nonInteractive && hasInteractiveTty()) {
|
|
73
97
|
const assistants = await detectAvailableAssistants(execFn);
|
|
74
98
|
if (assistants.length > 0) {
|
|
75
99
|
const chosenAssistant = assistants[0];
|
|
76
|
-
clack.log.info(
|
|
77
|
-
const usePolish = await clack.confirm({
|
|
78
|
-
message: `Se detectó ${chosenAssistant}. ¿Quieres que pula la redacción final?`,
|
|
79
|
-
});
|
|
100
|
+
clack.log.info(ui.polishDetected(chosenAssistant));
|
|
101
|
+
const usePolish = await clack.confirm({ message: ui.polishConfirm(chosenAssistant) });
|
|
80
102
|
if (usePolish === true) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
103
|
+
const polished = await polishFilesWithAssistant(chosenAssistant, files, execFn, lang);
|
|
104
|
+
files.splice(0, files.length, ...polished);
|
|
84
105
|
}
|
|
85
106
|
}
|
|
86
107
|
}
|
|
108
|
+
options.onGeneratedFiles?.(files);
|
|
109
|
+
if (options.dryRun) {
|
|
110
|
+
return files.map((file) => ({
|
|
111
|
+
path: file.path,
|
|
112
|
+
status: fs.existsSync(path.join(rootPath, file.path)) ? "skipped" : "written",
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
87
115
|
return writeGeneratedFiles(rootPath, files);
|
|
88
116
|
}
|
|
117
|
+
function isLang(value) {
|
|
118
|
+
return value === "es" || value === "en";
|
|
119
|
+
}
|
|
120
|
+
export function resolveCliAction(argv) {
|
|
121
|
+
const options = {};
|
|
122
|
+
for (let i = 0; i < argv.length; i++) {
|
|
123
|
+
const arg = argv[i];
|
|
124
|
+
if (arg === "--help" || arg === "-h")
|
|
125
|
+
return { kind: "help" };
|
|
126
|
+
if (arg === "--version" || arg === "-v")
|
|
127
|
+
return { kind: "version" };
|
|
128
|
+
if (arg === "--lang" || arg.startsWith("--lang=")) {
|
|
129
|
+
const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
|
|
130
|
+
if (!isLang(value))
|
|
131
|
+
return { kind: "invalid-lang", value };
|
|
132
|
+
options.lang = value;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--dry-run") {
|
|
136
|
+
options.dryRun = true;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (arg === "--check") {
|
|
140
|
+
options.check = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (arg === "--json") {
|
|
144
|
+
options.json = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (arg === "--non-interactive") {
|
|
148
|
+
options.nonInteractive = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
return { kind: "unknown", flag: arg };
|
|
152
|
+
}
|
|
153
|
+
return { kind: "run", ...options };
|
|
154
|
+
}
|
|
155
|
+
function usageWithAutomationOptions(ui) {
|
|
156
|
+
return `${ui.usage}\n\n${ui.automationUsage}`;
|
|
157
|
+
}
|
|
158
|
+
export function getVersion() {
|
|
159
|
+
// Works both from src/ (tests) and dist/ (published bin): ../package.json
|
|
160
|
+
// resolves to packages/cli/package.json in either layout.
|
|
161
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
162
|
+
return pkg.version;
|
|
163
|
+
}
|
|
89
164
|
export async function main() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
165
|
+
const action = resolveCliAction(process.argv.slice(2));
|
|
166
|
+
const defaultLang = action.kind === "run" && action.lang ? action.lang : detectLang();
|
|
167
|
+
let ui = UI[defaultLang];
|
|
168
|
+
if (action.kind === "help") {
|
|
169
|
+
console.log(usageWithAutomationOptions(ui));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (action.kind === "version") {
|
|
173
|
+
console.log(getVersion());
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (action.kind === "unknown") {
|
|
177
|
+
console.error(`${ui.unknownOption(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (action.kind === "invalid-lang") {
|
|
182
|
+
console.error(`${ui.invalidLang(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
183
|
+
process.exitCode = 1;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const machineOutput = action.json === true;
|
|
187
|
+
const planningOnly = action.dryRun === true || action.check === true;
|
|
188
|
+
const nonInteractive = action.nonInteractive === true || machineOutput || planningOnly;
|
|
189
|
+
if (!machineOutput)
|
|
190
|
+
clack.intro("agent-rules-init");
|
|
191
|
+
if (!machineOutput && !nonInteractive && !hasInteractiveTty()) {
|
|
192
|
+
console.warn(ui.noTtyWarning);
|
|
94
193
|
}
|
|
95
194
|
try {
|
|
96
|
-
const
|
|
195
|
+
const loadedConfig = loadConfig(process.cwd());
|
|
196
|
+
const lang = action.lang ?? loadedConfig.config.lang ?? defaultLang;
|
|
197
|
+
ui = UI[lang];
|
|
198
|
+
if (!machineOutput) {
|
|
199
|
+
for (const warning of loadedConfig.warnings)
|
|
200
|
+
console.warn(warning);
|
|
201
|
+
}
|
|
202
|
+
let generatedFiles = [];
|
|
203
|
+
let repoFacts;
|
|
204
|
+
const results = await runCli(process.cwd(), {
|
|
205
|
+
lang,
|
|
206
|
+
config: loadedConfig.config,
|
|
207
|
+
dryRun: planningOnly,
|
|
208
|
+
nonInteractive,
|
|
209
|
+
skipLlm: nonInteractive,
|
|
210
|
+
onGeneratedFiles: (files) => {
|
|
211
|
+
generatedFiles = files;
|
|
212
|
+
},
|
|
213
|
+
onFacts: (facts) => {
|
|
214
|
+
repoFacts = facts;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
97
217
|
const written = results.filter((r) => r.status === "written");
|
|
98
218
|
const failures = results.filter((r) => r.status === "error");
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
219
|
+
const outdated = action.check
|
|
220
|
+
? generatedFiles.filter((file) => {
|
|
221
|
+
const absolutePath = path.join(process.cwd(), file.path);
|
|
222
|
+
return fs.existsSync(absolutePath) && fs.readFileSync(absolutePath, "utf8") !== file.content;
|
|
223
|
+
})
|
|
224
|
+
: [];
|
|
225
|
+
const checkIssues = written.length + outdated.length;
|
|
226
|
+
if (machineOutput) {
|
|
227
|
+
const contentByPath = new Map(generatedFiles.map((file) => [file.path, file.content]));
|
|
228
|
+
console.log(JSON.stringify({
|
|
229
|
+
mode: action.check ? "check" : action.dryRun ? "dry-run" : "write",
|
|
230
|
+
configWarnings: loadedConfig.warnings,
|
|
231
|
+
facts: repoFacts,
|
|
232
|
+
wouldCreate: written.length,
|
|
233
|
+
outdated: outdated.map((file) => file.path),
|
|
234
|
+
results: results.map((result) => ({
|
|
235
|
+
...result,
|
|
236
|
+
...(planningOnly ? { content: contentByPath.get(result.path) } : {}),
|
|
237
|
+
})),
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
else if (action.dryRun) {
|
|
241
|
+
const statusByPath = new Map(results.map((result) => [result.path, result.status]));
|
|
242
|
+
for (const file of generatedFiles) {
|
|
243
|
+
console.log(`\n--- ${file.path} (${statusByPath.get(file.path) === "written" ? "would create" : "exists"}) ---\n${file.content}`);
|
|
102
244
|
}
|
|
103
|
-
|
|
104
|
-
|
|
245
|
+
}
|
|
246
|
+
else if (!action.check)
|
|
247
|
+
for (const result of results) {
|
|
248
|
+
if (result.status === "written") {
|
|
249
|
+
clack.log.success(result.path);
|
|
250
|
+
}
|
|
251
|
+
else if (result.status === "skipped") {
|
|
252
|
+
clack.log.info(ui.fileSkipped(result.path));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
clack.log.warn(`${result.path}: ${result.error}`);
|
|
256
|
+
}
|
|
105
257
|
}
|
|
258
|
+
if (!machineOutput && action.check) {
|
|
259
|
+
console.log(checkIssues > 0
|
|
260
|
+
? `${written.length} file(s) missing; ${outdated.length} file(s) outdated.`
|
|
261
|
+
: "Generated files are present and up to date.");
|
|
106
262
|
}
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
'".generated" (ej. "CLAUDE.generated.md" → "CLAUDE.md") para activarlos — ' +
|
|
110
|
-
"tu asistente de IA solo lee el nombre final, no el generado.");
|
|
263
|
+
else if (!machineOutput && action.dryRun) {
|
|
264
|
+
console.log(`\n${written.length} file(s) would be generated.`);
|
|
111
265
|
}
|
|
112
|
-
else {
|
|
113
|
-
clack.outro(
|
|
266
|
+
else if (!machineOutput && written.length > 0) {
|
|
267
|
+
clack.outro(ui.outroWritten);
|
|
114
268
|
}
|
|
115
|
-
if (
|
|
269
|
+
else if (!machineOutput) {
|
|
270
|
+
clack.outro(ui.outroNothing);
|
|
271
|
+
}
|
|
272
|
+
if (failures.length > 0 || (action.check && checkIssues > 0)) {
|
|
116
273
|
process.exitCode = 1;
|
|
117
274
|
}
|
|
118
275
|
}
|
|
119
276
|
catch (err) {
|
|
120
|
-
|
|
277
|
+
const message = ui.unexpectedError(err.message);
|
|
278
|
+
if (machineOutput) {
|
|
279
|
+
console.log(JSON.stringify({ mode: action.check ? "check" : action.dryRun ? "dry-run" : "write", error: message }));
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
clack.log.error(message);
|
|
283
|
+
}
|
|
121
284
|
process.exitCode = 1;
|
|
122
285
|
}
|
|
123
286
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CanonicalCommand, CiCommand, CommandEntry, CommandSource, PackContext, RepoSignals } from "./types.js";
|
|
2
|
+
export declare const SOURCE_FILES: Record<CommandSource, string>;
|
|
3
|
+
export declare function selectCanonicalCommands(signals: RepoSignals, commands: CommandEntry[], ciCommands: CiCommand[]): CanonicalCommand[];
|
|
4
|
+
export declare function canonicalOf(ctx: PackContext | undefined, kind: CanonicalCommand["kind"], family?: "js-ts" | "python" | "java"): CanonicalCommand | undefined;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export const SOURCE_FILES = {
|
|
2
|
+
npm: "package.json",
|
|
3
|
+
pnpm: "package.json",
|
|
4
|
+
yarn: "package.json",
|
|
5
|
+
bun: "package.json",
|
|
6
|
+
composer: "composer.json",
|
|
7
|
+
make: "Makefile",
|
|
8
|
+
mix: "mix.exs",
|
|
9
|
+
tox: "tox.ini",
|
|
10
|
+
};
|
|
11
|
+
const KIND_ORDER = ["test", "lint", "build", "format", "typecheck"];
|
|
12
|
+
// Nombre de script → kind. Solo nombres inequívocos; "check" o "ci" quedan fuera a propósito.
|
|
13
|
+
const SCRIPT_KINDS = {
|
|
14
|
+
test: "test",
|
|
15
|
+
lint: "lint",
|
|
16
|
+
build: "build",
|
|
17
|
+
format: "format",
|
|
18
|
+
fmt: "format",
|
|
19
|
+
typecheck: "typecheck",
|
|
20
|
+
"type-check": "typecheck",
|
|
21
|
+
};
|
|
22
|
+
function scriptName(entry) {
|
|
23
|
+
const parts = entry.invocation.split(" ");
|
|
24
|
+
return parts[parts.length - 1];
|
|
25
|
+
}
|
|
26
|
+
function isRootManifest(entry) {
|
|
27
|
+
return entry.manifestPath === undefined || entry.manifestPath === "package.json";
|
|
28
|
+
}
|
|
29
|
+
function fromScripts(commands) {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const entry of commands) {
|
|
32
|
+
if (!isRootManifest(entry))
|
|
33
|
+
continue;
|
|
34
|
+
const kind = SCRIPT_KINDS[scriptName(entry)];
|
|
35
|
+
if (!kind || found.some((c) => c.kind === kind))
|
|
36
|
+
continue;
|
|
37
|
+
found.push({
|
|
38
|
+
kind,
|
|
39
|
+
command: entry.invocation,
|
|
40
|
+
source: entry.manifestPath ?? SOURCE_FILES[entry.source],
|
|
41
|
+
confidence: "high",
|
|
42
|
+
scope: ".",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
// El comando de CI se conserva textual (evidencia), solo se clasifica su kind.
|
|
48
|
+
const CI_PATTERNS = [
|
|
49
|
+
[/^(?:npm|pnpm|yarn|bun)(?: run)? test\b/, "test"],
|
|
50
|
+
[/^(?:npm|pnpm|yarn|bun) run lint\b/, "lint"],
|
|
51
|
+
[/^(?:npm|pnpm|yarn|bun) run build\b/, "build"],
|
|
52
|
+
[/^(?:npm|pnpm|yarn|bun) run (?:format|fmt)\b/, "format"],
|
|
53
|
+
[/^(?:npm|pnpm|yarn|bun) run (?:typecheck|type-check)\b/, "typecheck"],
|
|
54
|
+
[/^(?:\.[\\/])?mvnw(?:\.cmd)?\b.*\b(?:verify|test)\b/, "test"],
|
|
55
|
+
[/^(?:\.[\\/])?mvnw(?:\.cmd)?\b.*\bpackage\b/, "build"],
|
|
56
|
+
[/^(?:\.[\\/])?gradlew(?:\.bat)?\b.*\b(?:check|test)\b/, "test"],
|
|
57
|
+
[/^(?:\.[\\/])?gradlew(?:\.bat)?\b.*\b(?:build|assemble)\b/, "build"],
|
|
58
|
+
[/^uv run\b.*\b(?:pytest|tox(?:\s+run)?)\b/, "test"],
|
|
59
|
+
[/^poetry run\b.*\b(?:pytest|tox)\b/, "test"],
|
|
60
|
+
[/^pytest\b/, "test"],
|
|
61
|
+
[/^tox(?:\s+run)?\b/, "test"],
|
|
62
|
+
[/^cargo test\b/, "test"],
|
|
63
|
+
[/^go test\b/, "test"],
|
|
64
|
+
];
|
|
65
|
+
function fromCi(ciCommands) {
|
|
66
|
+
const found = [];
|
|
67
|
+
for (const ci of ciCommands) {
|
|
68
|
+
for (const [pattern, kind] of CI_PATTERNS) {
|
|
69
|
+
if (!pattern.test(ci.command))
|
|
70
|
+
continue;
|
|
71
|
+
if (found.some((c) => c.kind === kind))
|
|
72
|
+
break;
|
|
73
|
+
found.push({
|
|
74
|
+
kind,
|
|
75
|
+
command: ci.command,
|
|
76
|
+
source: `ci: ${ci.workflow}`,
|
|
77
|
+
confidence: "high",
|
|
78
|
+
scope: ".",
|
|
79
|
+
});
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
function fromSignals(signals) {
|
|
86
|
+
const out = [];
|
|
87
|
+
if (signals.pomXml) {
|
|
88
|
+
const unixWrapper = signals.hasFile("mvnw");
|
|
89
|
+
const windowsWrapper = !unixWrapper && signals.hasFile("mvnw.cmd");
|
|
90
|
+
const hasWrapper = unixWrapper || windowsWrapper;
|
|
91
|
+
out.push({
|
|
92
|
+
kind: "test",
|
|
93
|
+
command: unixWrapper ? "./mvnw test" : windowsWrapper ? "mvnw.cmd test" : "mvn test",
|
|
94
|
+
source: unixWrapper ? "mvnw" : windowsWrapper ? "mvnw.cmd" : "pom.xml",
|
|
95
|
+
confidence: hasWrapper ? "high" : "low",
|
|
96
|
+
scope: ".",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else if (signals.buildGradle) {
|
|
100
|
+
const unixWrapper = signals.hasFile("gradlew");
|
|
101
|
+
const windowsWrapper = !unixWrapper && signals.hasFile("gradlew.bat");
|
|
102
|
+
const hasWrapper = unixWrapper || windowsWrapper;
|
|
103
|
+
out.push({
|
|
104
|
+
kind: "test",
|
|
105
|
+
command: unixWrapper ? "./gradlew test" : windowsWrapper ? "gradlew.bat test" : "gradle test",
|
|
106
|
+
source: unixWrapper ? "gradlew" : windowsWrapper ? "gradlew.bat" : "build.gradle",
|
|
107
|
+
confidence: hasWrapper ? "high" : "low",
|
|
108
|
+
scope: ".",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const pythonManifest = signals.pyprojectToml ?? signals.requirementsTxt ?? signals.environmentYml;
|
|
112
|
+
const hasPytest = pythonManifest !== undefined && /\bpytest\b/i.test(pythonManifest);
|
|
113
|
+
if (signals.toxIni) {
|
|
114
|
+
out.push({ kind: "test", command: "tox", source: "tox.ini", confidence: "high", scope: "." });
|
|
115
|
+
}
|
|
116
|
+
else if (hasPytest && signals.hasFile("uv.lock")) {
|
|
117
|
+
// El lock demuestra el gestor, no que pytest pertenezca a un grupo instalado por defecto.
|
|
118
|
+
out.push({ kind: "test", command: "uv run pytest", source: "uv.lock", confidence: "low", scope: "." });
|
|
119
|
+
}
|
|
120
|
+
else if (hasPytest && signals.hasFile("poetry.lock")) {
|
|
121
|
+
out.push({ kind: "test", command: "poetry run pytest", source: "poetry.lock", confidence: "low", scope: "." });
|
|
122
|
+
}
|
|
123
|
+
else if (hasPytest) {
|
|
124
|
+
const source = signals.pyprojectToml
|
|
125
|
+
? "pyproject.toml"
|
|
126
|
+
: signals.requirementsTxt
|
|
127
|
+
? "requirements.txt"
|
|
128
|
+
: "environment.yml";
|
|
129
|
+
out.push({ kind: "test", command: "pytest", source, confidence: "low", scope: "." });
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
export function selectCanonicalCommands(signals, commands, ciCommands) {
|
|
134
|
+
const byKind = new Map();
|
|
135
|
+
for (const candidate of [...fromScripts(commands), ...fromCi(ciCommands), ...fromSignals(signals)]) {
|
|
136
|
+
if (!byKind.has(candidate.kind))
|
|
137
|
+
byKind.set(candidate.kind, candidate);
|
|
138
|
+
}
|
|
139
|
+
return KIND_ORDER.flatMap((kind) => byKind.get(kind) ?? []);
|
|
140
|
+
}
|
|
141
|
+
export function canonicalOf(ctx, kind, family) {
|
|
142
|
+
if (family && ctx?.signals) {
|
|
143
|
+
return selectCanonicalForFamily({ ...ctx, signals: ctx.signals }, family).find((c) => c.kind === kind && c.confidence === "high");
|
|
144
|
+
}
|
|
145
|
+
return ctx?.facts.canonical.find((c) => c.kind === kind && c.confidence === "high");
|
|
146
|
+
}
|
|
147
|
+
function selectCanonicalForFamily(ctx, family) {
|
|
148
|
+
const jsSources = new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
149
|
+
const commands = ctx.facts.commands.filter((command) => family === "js-ts" ? jsSources.has(command.source) : family === "python" ? command.source === "tox" : false);
|
|
150
|
+
const ciCommands = ctx.facts.ciCommands.filter(({ command }) => {
|
|
151
|
+
if (family === "js-ts")
|
|
152
|
+
return /^(?:npm|pnpm|yarn|bun)\b/.test(command);
|
|
153
|
+
if (family === "python")
|
|
154
|
+
return /^(?:uv|poetry|pytest|tox)\b/.test(command);
|
|
155
|
+
return /^(?:\.[\\/])?(?:mvnw(?:\.cmd)?|gradlew(?:\.bat)?)\b|^(?:mvn|gradle)\b/.test(command);
|
|
156
|
+
});
|
|
157
|
+
const signals = {
|
|
158
|
+
...ctx.signals,
|
|
159
|
+
packageJson: family === "js-ts" ? ctx.signals.packageJson : undefined,
|
|
160
|
+
packageJsons: family === "js-ts" ? ctx.signals.packageJsons : undefined,
|
|
161
|
+
pyprojectToml: family === "python" ? ctx.signals.pyprojectToml : undefined,
|
|
162
|
+
requirementsTxt: family === "python" ? ctx.signals.requirementsTxt : undefined,
|
|
163
|
+
environmentYml: family === "python" ? ctx.signals.environmentYml : undefined,
|
|
164
|
+
toxIni: family === "python" ? ctx.signals.toxIni : undefined,
|
|
165
|
+
pomXml: family === "java" ? ctx.signals.pomXml : undefined,
|
|
166
|
+
buildGradle: family === "java" ? ctx.signals.buildGradle : undefined,
|
|
167
|
+
};
|
|
168
|
+
return selectCanonicalCommands(signals, commands, ciCommands);
|
|
169
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Lang } from "./i18n.js";
|
|
2
|
+
export interface ProjectConfig {
|
|
3
|
+
framework?: string;
|
|
4
|
+
testRunner?: string;
|
|
5
|
+
linter?: string;
|
|
6
|
+
packageManager?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentRulesConfig {
|
|
9
|
+
lang?: Lang;
|
|
10
|
+
exclude?: string[];
|
|
11
|
+
projects?: Record<string, ProjectConfig>;
|
|
12
|
+
noAi?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface LoadedConfig {
|
|
15
|
+
config: AgentRulesConfig;
|
|
16
|
+
/** Absolute path to the selected config file, if one exists. */
|
|
17
|
+
sourcePath?: string;
|
|
18
|
+
warnings: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare class ConfigError extends Error {
|
|
21
|
+
readonly configPath: string;
|
|
22
|
+
constructor(message: string, configPath: string, options?: ErrorOptions);
|
|
23
|
+
}
|
|
24
|
+
/** Loads and validates the optional repository-local agent-rules configuration. */
|
|
25
|
+
export declare function loadConfig(rootPath: string): LoadedConfig;
|