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
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,18 +1,37 @@
|
|
|
1
|
-
import { type WriteResult } from "./core/writer.js";
|
|
1
|
+
import { type GeneratedFile, type WriteResult } from "./core/writer.js";
|
|
2
2
|
import { type Lang } from "./core/i18n.js";
|
|
3
|
+
import { type AgentRulesConfig } from "./core/config.js";
|
|
3
4
|
import { type PromptFn } from "./core/prompt-engine.js";
|
|
4
5
|
import { type ExecFn } from "./core/llm-bridge.js";
|
|
6
|
+
import type { RepoFacts } from "./core/types.js";
|
|
5
7
|
export interface RunCliOptions {
|
|
6
8
|
promptFn?: PromptFn;
|
|
7
9
|
execFn?: ExecFn;
|
|
8
10
|
skipLlm?: boolean;
|
|
9
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;
|
|
10
23
|
}
|
|
11
24
|
export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
|
|
12
|
-
export
|
|
13
|
-
kind: "run";
|
|
25
|
+
export interface CliRunOptions {
|
|
14
26
|
lang?: Lang;
|
|
15
|
-
|
|
27
|
+
dryRun?: true;
|
|
28
|
+
check?: true;
|
|
29
|
+
json?: true;
|
|
30
|
+
nonInteractive?: true;
|
|
31
|
+
}
|
|
32
|
+
export type CliAction = ({
|
|
33
|
+
kind: "run";
|
|
34
|
+
} & CliRunOptions) | {
|
|
16
35
|
kind: "help";
|
|
17
36
|
} | {
|
|
18
37
|
kind: "version";
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
2
4
|
import * as clack from "@clack/prompts";
|
|
3
5
|
import { scanRepo } from "./core/scanner.js";
|
|
4
6
|
import { writeGeneratedFiles } from "./core/writer.js";
|
|
5
|
-
import { renderClaudeMd, renderAgentsMd, renderCopilotInstructions, renderPromptFiles,
|
|
7
|
+
import { renderClaudeMd, renderAgentsMd, renderCopilotInstructions, renderPromptFiles, } from "./core/templates.js";
|
|
6
8
|
import { buildRepoFacts } from "./core/repo-facts.js";
|
|
7
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";
|
|
8
13
|
import { collectLowConfidenceQuestions, askQuestions, applyAnswers, makeDefaultPromptFn, hasInteractiveTty, } from "./core/prompt-engine.js";
|
|
9
|
-
import { detectAvailableAssistants,
|
|
14
|
+
import { detectAvailableAssistants, polishFilesWithAssistant, defaultExecFn } from "./core/llm-bridge.js";
|
|
10
15
|
import { jsTsPack } from "./packs/js-ts.js";
|
|
11
16
|
import { pythonPack } from "./packs/python.js";
|
|
12
17
|
import { javaPack } from "./packs/java.js";
|
|
@@ -40,19 +45,24 @@ const ALL_PACKS = [
|
|
|
40
45
|
rPack,
|
|
41
46
|
];
|
|
42
47
|
export async function runCli(rootPath, options = {}) {
|
|
48
|
+
const loadedConfig = options.config ? { config: options.config, warnings: [] } : loadConfig(rootPath);
|
|
49
|
+
const config = loadedConfig.config;
|
|
50
|
+
options.onConfigWarnings?.(loadedConfig.warnings);
|
|
43
51
|
const execFn = options.execFn ?? defaultExecFn;
|
|
44
|
-
const lang = options.lang ?? detectLang();
|
|
52
|
+
const lang = options.lang ?? config.lang ?? detectLang();
|
|
45
53
|
const promptFn = options.promptFn ?? makeDefaultPromptFn(lang);
|
|
46
54
|
const ui = UI[lang];
|
|
47
|
-
const signals = scanRepo(rootPath);
|
|
55
|
+
const signals = applyProjectExcludes(scanRepo(rootPath), config.exclude ?? []);
|
|
48
56
|
const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
|
|
49
57
|
const questions = collectLowConfidenceQuestions(rawDetections, lang);
|
|
50
|
-
const answers = await askQuestions(questions, promptFn);
|
|
58
|
+
const answers = options.nonInteractive ? {} : await askQuestions(questions, promptFn);
|
|
51
59
|
const detections = applyAnswers(rawDetections, answers);
|
|
52
60
|
const facts = buildRepoFacts(signals, lang);
|
|
61
|
+
options.onFacts?.(facts);
|
|
62
|
+
const ctx = { facts, signals };
|
|
53
63
|
const entries = detections.map((detection) => {
|
|
54
64
|
const pack = ALL_PACKS.find((p) => p.id === detection.packId);
|
|
55
|
-
return { detection, ruleSet: pack.rules(detection, lang) };
|
|
65
|
+
return { detection, ruleSet: pack.rules(detection, lang, ctx) };
|
|
56
66
|
});
|
|
57
67
|
const files = [];
|
|
58
68
|
if (entries.length > 0) {
|
|
@@ -64,38 +74,51 @@ export async function runCli(rootPath, options = {}) {
|
|
|
64
74
|
});
|
|
65
75
|
for (const detection of detections) {
|
|
66
76
|
const pack = ALL_PACKS.find((p) => p.id === detection.packId);
|
|
67
|
-
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang))) {
|
|
77
|
+
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
|
|
68
78
|
files.push(file);
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
}
|
|
72
82
|
else {
|
|
73
|
-
const
|
|
74
|
-
files.push({
|
|
75
|
-
path: "
|
|
76
|
-
content:
|
|
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)),
|
|
77
87
|
});
|
|
78
88
|
}
|
|
79
|
-
|
|
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()) {
|
|
80
97
|
const assistants = await detectAvailableAssistants(execFn);
|
|
81
98
|
if (assistants.length > 0) {
|
|
82
99
|
const chosenAssistant = assistants[0];
|
|
83
100
|
clack.log.info(ui.polishDetected(chosenAssistant));
|
|
84
101
|
const usePolish = await clack.confirm({ message: ui.polishConfirm(chosenAssistant) });
|
|
85
102
|
if (usePolish === true) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
103
|
+
const polished = await polishFilesWithAssistant(chosenAssistant, files, execFn, lang);
|
|
104
|
+
files.splice(0, files.length, ...polished);
|
|
89
105
|
}
|
|
90
106
|
}
|
|
91
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
|
+
}
|
|
92
115
|
return writeGeneratedFiles(rootPath, files);
|
|
93
116
|
}
|
|
94
117
|
function isLang(value) {
|
|
95
118
|
return value === "es" || value === "en";
|
|
96
119
|
}
|
|
97
120
|
export function resolveCliAction(argv) {
|
|
98
|
-
|
|
121
|
+
const options = {};
|
|
99
122
|
for (let i = 0; i < argv.length; i++) {
|
|
100
123
|
const arg = argv[i];
|
|
101
124
|
if (arg === "--help" || arg === "-h")
|
|
@@ -106,12 +129,31 @@ export function resolveCliAction(argv) {
|
|
|
106
129
|
const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
|
|
107
130
|
if (!isLang(value))
|
|
108
131
|
return { kind: "invalid-lang", value };
|
|
109
|
-
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;
|
|
110
149
|
continue;
|
|
111
150
|
}
|
|
112
151
|
return { kind: "unknown", flag: arg };
|
|
113
152
|
}
|
|
114
|
-
return
|
|
153
|
+
return { kind: "run", ...options };
|
|
154
|
+
}
|
|
155
|
+
function usageWithAutomationOptions(ui) {
|
|
156
|
+
return `${ui.usage}\n\n${ui.automationUsage}`;
|
|
115
157
|
}
|
|
116
158
|
export function getVersion() {
|
|
117
159
|
// Works both from src/ (tests) and dist/ (published bin): ../package.json
|
|
@@ -121,10 +163,10 @@ export function getVersion() {
|
|
|
121
163
|
}
|
|
122
164
|
export async function main() {
|
|
123
165
|
const action = resolveCliAction(process.argv.slice(2));
|
|
124
|
-
const
|
|
125
|
-
|
|
166
|
+
const defaultLang = action.kind === "run" && action.lang ? action.lang : detectLang();
|
|
167
|
+
let ui = UI[defaultLang];
|
|
126
168
|
if (action.kind === "help") {
|
|
127
|
-
console.log(ui
|
|
169
|
+
console.log(usageWithAutomationOptions(ui));
|
|
128
170
|
return;
|
|
129
171
|
}
|
|
130
172
|
if (action.kind === "version") {
|
|
@@ -132,46 +174,113 @@ export async function main() {
|
|
|
132
174
|
return;
|
|
133
175
|
}
|
|
134
176
|
if (action.kind === "unknown") {
|
|
135
|
-
console.error(`${ui.unknownOption(action.flag)}\n\n${ui
|
|
177
|
+
console.error(`${ui.unknownOption(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
136
178
|
process.exitCode = 1;
|
|
137
179
|
return;
|
|
138
180
|
}
|
|
139
181
|
if (action.kind === "invalid-lang") {
|
|
140
|
-
console.error(`${ui.invalidLang(action.value)}\n\n${ui
|
|
182
|
+
console.error(`${ui.invalidLang(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
141
183
|
process.exitCode = 1;
|
|
142
184
|
return;
|
|
143
185
|
}
|
|
144
|
-
|
|
145
|
-
|
|
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()) {
|
|
146
192
|
console.warn(ui.noTtyWarning);
|
|
147
193
|
}
|
|
148
194
|
try {
|
|
149
|
-
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
|
+
});
|
|
150
217
|
const written = results.filter((r) => r.status === "written");
|
|
151
218
|
const failures = results.filter((r) => r.status === "error");
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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}`);
|
|
158
244
|
}
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
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.");
|
|
162
262
|
}
|
|
163
|
-
if (
|
|
263
|
+
else if (!machineOutput && action.dryRun) {
|
|
264
|
+
console.log(`\n${written.length} file(s) would be generated.`);
|
|
265
|
+
}
|
|
266
|
+
else if (!machineOutput && written.length > 0) {
|
|
164
267
|
clack.outro(ui.outroWritten);
|
|
165
268
|
}
|
|
166
|
-
else {
|
|
269
|
+
else if (!machineOutput) {
|
|
167
270
|
clack.outro(ui.outroNothing);
|
|
168
271
|
}
|
|
169
|
-
if (failures.length > 0) {
|
|
272
|
+
if (failures.length > 0 || (action.check && checkIssues > 0)) {
|
|
170
273
|
process.exitCode = 1;
|
|
171
274
|
}
|
|
172
275
|
}
|
|
173
276
|
catch (err) {
|
|
174
|
-
|
|
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
|
+
}
|
|
175
284
|
process.exitCode = 1;
|
|
176
285
|
}
|
|
177
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;
|