agent-rules-init 0.3.0 → 0.5.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 +39 -5
- package/dist/cli.js +242 -43
- 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 +20 -9
- package/dist/core/i18n.js +125 -40
- package/dist/core/llm-bridge.d.ts +20 -2
- package/dist/core/llm-bridge.js +161 -9
- 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 +10 -2
- package/dist/core/repo-facts.js +231 -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 +6 -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,24 +1,58 @@
|
|
|
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
|
-
import { type ExecFn } from "./core/llm-bridge.js";
|
|
5
|
+
import { type AssistantId, 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 enrichment. */
|
|
15
|
+
nonInteractive?: boolean;
|
|
16
|
+
/** Run AI enrichment without asking, even without a TTY. Ignored when skipLlm or noAi apply. */
|
|
17
|
+
enrich?: boolean;
|
|
18
|
+
/** Assistant to enrich with; defaults to the first one installed. */
|
|
19
|
+
assistant?: AssistantId;
|
|
20
|
+
/** Model forwarded verbatim to the assistant CLI; its default when omitted. */
|
|
21
|
+
model?: string;
|
|
22
|
+
/** Receives the rendered files before they are written (or planned). */
|
|
23
|
+
onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
|
|
24
|
+
/** Preloaded repository configuration; loaded from disk when omitted. */
|
|
25
|
+
config?: AgentRulesConfig;
|
|
26
|
+
onConfigWarnings?: (warnings: readonly string[]) => void;
|
|
27
|
+
/** Receives the facts extracted from the repo (including canonical commands). */
|
|
28
|
+
onFacts?: (facts: RepoFacts) => void;
|
|
10
29
|
}
|
|
11
30
|
export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
|
|
12
|
-
export
|
|
13
|
-
kind: "run";
|
|
31
|
+
export interface CliRunOptions {
|
|
14
32
|
lang?: Lang;
|
|
15
|
-
|
|
33
|
+
dryRun?: true;
|
|
34
|
+
check?: true;
|
|
35
|
+
json?: true;
|
|
36
|
+
nonInteractive?: true;
|
|
37
|
+
enrich?: true;
|
|
38
|
+
assistant?: AssistantId;
|
|
39
|
+
model?: string;
|
|
40
|
+
}
|
|
41
|
+
export type CliAction = ({
|
|
42
|
+
kind: "run";
|
|
43
|
+
} & CliRunOptions) | {
|
|
16
44
|
kind: "help";
|
|
17
45
|
} | {
|
|
18
46
|
kind: "version";
|
|
19
47
|
} | {
|
|
20
48
|
kind: "invalid-lang";
|
|
21
49
|
value: string;
|
|
50
|
+
} | {
|
|
51
|
+
kind: "invalid-assistant";
|
|
52
|
+
value: string;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "missing-value";
|
|
55
|
+
flag: string;
|
|
22
56
|
} | {
|
|
23
57
|
kind: "unknown";
|
|
24
58
|
flag: string;
|
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, enrichFilesWithAssistant, 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";
|
|
@@ -39,20 +44,42 @@ const ALL_PACKS = [
|
|
|
39
44
|
scalaPack,
|
|
40
45
|
rPack,
|
|
41
46
|
];
|
|
47
|
+
// Final-name docs the tool itself would generate; when they already exist they carry the
|
|
48
|
+
// team's hand-written intent, so enrichment must integrate them instead of ignoring them.
|
|
49
|
+
const EXISTING_DOC_PATHS = ["CLAUDE.md", "AGENTS.md", ".github/copilot-instructions.md"];
|
|
50
|
+
const MAX_EXISTING_DOC_CHARS = 20_000;
|
|
51
|
+
function readExistingDocs(rootPath) {
|
|
52
|
+
const docs = [];
|
|
53
|
+
for (const relativePath of EXISTING_DOC_PATHS) {
|
|
54
|
+
const absolutePath = path.join(rootPath, relativePath);
|
|
55
|
+
if (!fs.existsSync(absolutePath))
|
|
56
|
+
continue;
|
|
57
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
58
|
+
if (content.trim() === "")
|
|
59
|
+
continue;
|
|
60
|
+
docs.push({ path: relativePath, content: content.slice(0, MAX_EXISTING_DOC_CHARS) });
|
|
61
|
+
}
|
|
62
|
+
return docs;
|
|
63
|
+
}
|
|
42
64
|
export async function runCli(rootPath, options = {}) {
|
|
65
|
+
const loadedConfig = options.config ? { config: options.config, warnings: [] } : loadConfig(rootPath);
|
|
66
|
+
const config = loadedConfig.config;
|
|
67
|
+
options.onConfigWarnings?.(loadedConfig.warnings);
|
|
43
68
|
const execFn = options.execFn ?? defaultExecFn;
|
|
44
|
-
const lang = options.lang ?? detectLang();
|
|
69
|
+
const lang = options.lang ?? config.lang ?? detectLang();
|
|
45
70
|
const promptFn = options.promptFn ?? makeDefaultPromptFn(lang);
|
|
46
71
|
const ui = UI[lang];
|
|
47
|
-
const signals = scanRepo(rootPath);
|
|
72
|
+
const signals = applyProjectExcludes(scanRepo(rootPath), config.exclude ?? []);
|
|
48
73
|
const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
|
|
49
74
|
const questions = collectLowConfidenceQuestions(rawDetections, lang);
|
|
50
|
-
const answers = await askQuestions(questions, promptFn);
|
|
75
|
+
const answers = options.nonInteractive ? {} : await askQuestions(questions, promptFn);
|
|
51
76
|
const detections = applyAnswers(rawDetections, answers);
|
|
52
77
|
const facts = buildRepoFacts(signals, lang);
|
|
78
|
+
options.onFacts?.(facts);
|
|
79
|
+
const ctx = { facts, signals };
|
|
53
80
|
const entries = detections.map((detection) => {
|
|
54
81
|
const pack = ALL_PACKS.find((p) => p.id === detection.packId);
|
|
55
|
-
return { detection, ruleSet: pack.rules(detection, lang) };
|
|
82
|
+
return { detection, ruleSet: pack.rules(detection, lang, ctx) };
|
|
56
83
|
});
|
|
57
84
|
const files = [];
|
|
58
85
|
if (entries.length > 0) {
|
|
@@ -64,38 +91,88 @@ export async function runCli(rootPath, options = {}) {
|
|
|
64
91
|
});
|
|
65
92
|
for (const detection of detections) {
|
|
66
93
|
const pack = ALL_PACKS.find((p) => p.id === detection.packId);
|
|
67
|
-
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang))) {
|
|
94
|
+
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
|
|
68
95
|
files.push(file);
|
|
69
96
|
}
|
|
70
97
|
}
|
|
71
98
|
}
|
|
72
99
|
else {
|
|
73
|
-
const
|
|
74
|
-
files.push({
|
|
75
|
-
path: "
|
|
76
|
-
content:
|
|
100
|
+
const withFallback = (content) => content.replace(ui.generatedHeader, `${ui.generatedHeader}\n\n${ui.noStackFallback}`);
|
|
101
|
+
files.push({ path: "CLAUDE.generated.md", content: withFallback(renderClaudeMd([], facts, lang)) }, { path: "AGENTS.generated.md", content: withFallback(renderAgentsMd([], facts, lang)) }, {
|
|
102
|
+
path: ".github/copilot-instructions.generated.md",
|
|
103
|
+
content: withFallback(renderCopilotInstructions([], facts, lang)),
|
|
77
104
|
});
|
|
78
105
|
}
|
|
79
|
-
|
|
106
|
+
// Nested AGENTS files are intentionally limited to package-local facts and stack
|
|
107
|
+
// signals. The root documents remain the cross-repository overview.
|
|
108
|
+
for (const unit of buildPackageUnits(signals)) {
|
|
109
|
+
const scoped = renderProjectUnitAgents(unit, lang, config.projects?.[unit.path]);
|
|
110
|
+
if (scoped)
|
|
111
|
+
files.push(scoped);
|
|
112
|
+
}
|
|
113
|
+
const wantsEnrich = options.enrich === true;
|
|
114
|
+
const canOfferEnrich = !options.nonInteractive && hasInteractiveTty();
|
|
115
|
+
if (!options.skipLlm && !config.noAi && (wantsEnrich || canOfferEnrich)) {
|
|
116
|
+
// With --enrich there may be no TTY (scripts, CI); route progress through stderr so
|
|
117
|
+
// stdout stays parseable in --json mode.
|
|
118
|
+
const notify = canOfferEnrich ? (message) => clack.log.info(message) : (message) => console.warn(message);
|
|
80
119
|
const assistants = await detectAvailableAssistants(execFn);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
const requested = options.assistant;
|
|
121
|
+
if (requested && !assistants.includes(requested)) {
|
|
122
|
+
console.warn(ui.assistantNotAvailable(requested));
|
|
123
|
+
}
|
|
124
|
+
else if (assistants.length === 0) {
|
|
125
|
+
if (wantsEnrich)
|
|
126
|
+
console.warn(ui.enrichNoAssistant);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
const chosenAssistant = requested ?? assistants[0];
|
|
130
|
+
let proceed = wantsEnrich;
|
|
131
|
+
if (!proceed) {
|
|
132
|
+
clack.log.info(ui.enrichDetected(chosenAssistant));
|
|
133
|
+
proceed = (await clack.confirm({ message: ui.enrichConfirm(chosenAssistant) })) === true;
|
|
134
|
+
}
|
|
135
|
+
if (proceed) {
|
|
136
|
+
const spinner = canOfferEnrich ? clack.spinner() : undefined;
|
|
137
|
+
if (spinner)
|
|
138
|
+
spinner.start(ui.enrichWorking(chosenAssistant));
|
|
139
|
+
else
|
|
140
|
+
notify(ui.enrichWorking(chosenAssistant));
|
|
141
|
+
const enriched = await enrichFilesWithAssistant(chosenAssistant, files, {
|
|
142
|
+
execFn,
|
|
143
|
+
lang,
|
|
144
|
+
cwd: rootPath,
|
|
145
|
+
mustKeep: facts.canonical.map((c) => c.command),
|
|
146
|
+
existingDocs: readExistingDocs(rootPath),
|
|
147
|
+
model: options.model,
|
|
148
|
+
});
|
|
149
|
+
const changed = enriched.some((file, i) => file.content !== files[i].content);
|
|
150
|
+
const outcome = changed ? ui.enrichDone : ui.enrichKept;
|
|
151
|
+
if (spinner)
|
|
152
|
+
spinner.stop(outcome);
|
|
153
|
+
else
|
|
154
|
+
notify(outcome);
|
|
155
|
+
files.splice(0, files.length, ...enriched);
|
|
89
156
|
}
|
|
90
157
|
}
|
|
91
158
|
}
|
|
159
|
+
options.onGeneratedFiles?.(files);
|
|
160
|
+
if (options.dryRun) {
|
|
161
|
+
return files.map((file) => ({
|
|
162
|
+
path: file.path,
|
|
163
|
+
status: fs.existsSync(path.join(rootPath, file.path)) ? "skipped" : "written",
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
92
166
|
return writeGeneratedFiles(rootPath, files);
|
|
93
167
|
}
|
|
94
168
|
function isLang(value) {
|
|
95
169
|
return value === "es" || value === "en";
|
|
96
170
|
}
|
|
171
|
+
function isAssistant(value) {
|
|
172
|
+
return value === "claude" || value === "codex";
|
|
173
|
+
}
|
|
97
174
|
export function resolveCliAction(argv) {
|
|
98
|
-
|
|
175
|
+
const options = {};
|
|
99
176
|
for (let i = 0; i < argv.length; i++) {
|
|
100
177
|
const arg = argv[i];
|
|
101
178
|
if (arg === "--help" || arg === "-h")
|
|
@@ -106,12 +183,49 @@ export function resolveCliAction(argv) {
|
|
|
106
183
|
const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
|
|
107
184
|
if (!isLang(value))
|
|
108
185
|
return { kind: "invalid-lang", value };
|
|
109
|
-
lang = value;
|
|
186
|
+
options.lang = value;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (arg === "--dry-run") {
|
|
190
|
+
options.dryRun = true;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (arg === "--check") {
|
|
194
|
+
options.check = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (arg === "--json") {
|
|
198
|
+
options.json = true;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (arg === "--non-interactive") {
|
|
202
|
+
options.nonInteractive = true;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (arg === "--enrich") {
|
|
206
|
+
options.enrich = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (arg === "--assistant" || arg.startsWith("--assistant=")) {
|
|
210
|
+
const value = arg.startsWith("--assistant=") ? arg.slice("--assistant=".length) : argv[++i] ?? "";
|
|
211
|
+
if (!isAssistant(value))
|
|
212
|
+
return { kind: "invalid-assistant", value };
|
|
213
|
+
options.assistant = value;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (arg === "--model" || arg.startsWith("--model=")) {
|
|
217
|
+
const value = arg.startsWith("--model=") ? arg.slice("--model=".length) : argv[++i] ?? "";
|
|
218
|
+
if (value === "")
|
|
219
|
+
return { kind: "missing-value", flag: "--model" };
|
|
220
|
+
options.model = value;
|
|
110
221
|
continue;
|
|
111
222
|
}
|
|
112
223
|
return { kind: "unknown", flag: arg };
|
|
113
224
|
}
|
|
114
|
-
return
|
|
225
|
+
return { kind: "run", ...options };
|
|
226
|
+
}
|
|
227
|
+
function usageWithAutomationOptions(ui) {
|
|
228
|
+
return `${ui.usage}\n\n${ui.automationUsage}`;
|
|
115
229
|
}
|
|
116
230
|
export function getVersion() {
|
|
117
231
|
// Works both from src/ (tests) and dist/ (published bin): ../package.json
|
|
@@ -121,10 +235,10 @@ export function getVersion() {
|
|
|
121
235
|
}
|
|
122
236
|
export async function main() {
|
|
123
237
|
const action = resolveCliAction(process.argv.slice(2));
|
|
124
|
-
const
|
|
125
|
-
|
|
238
|
+
const defaultLang = action.kind === "run" && action.lang ? action.lang : detectLang();
|
|
239
|
+
let ui = UI[defaultLang];
|
|
126
240
|
if (action.kind === "help") {
|
|
127
|
-
console.log(ui
|
|
241
|
+
console.log(usageWithAutomationOptions(ui));
|
|
128
242
|
return;
|
|
129
243
|
}
|
|
130
244
|
if (action.kind === "version") {
|
|
@@ -132,46 +246,131 @@ export async function main() {
|
|
|
132
246
|
return;
|
|
133
247
|
}
|
|
134
248
|
if (action.kind === "unknown") {
|
|
135
|
-
console.error(`${ui.unknownOption(action.flag)}\n\n${ui
|
|
249
|
+
console.error(`${ui.unknownOption(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
136
250
|
process.exitCode = 1;
|
|
137
251
|
return;
|
|
138
252
|
}
|
|
139
253
|
if (action.kind === "invalid-lang") {
|
|
140
|
-
console.error(`${ui.invalidLang(action.value)}\n\n${ui
|
|
254
|
+
console.error(`${ui.invalidLang(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
255
|
+
process.exitCode = 1;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (action.kind === "invalid-assistant") {
|
|
259
|
+
console.error(`${ui.invalidAssistant(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (action.kind === "missing-value") {
|
|
264
|
+
console.error(`${ui.missingFlagValue(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
141
265
|
process.exitCode = 1;
|
|
142
266
|
return;
|
|
143
267
|
}
|
|
144
|
-
|
|
145
|
-
|
|
268
|
+
const machineOutput = action.json === true;
|
|
269
|
+
const planningOnly = action.dryRun === true || action.check === true;
|
|
270
|
+
const nonInteractive = action.nonInteractive === true || machineOutput || planningOnly;
|
|
271
|
+
// Enrichment output is non-deterministic, so --check (freshness comparison) must stay
|
|
272
|
+
// on the deterministic baseline.
|
|
273
|
+
const enrich = action.enrich === true && action.check !== true;
|
|
274
|
+
if (action.enrich === true && action.check === true)
|
|
275
|
+
console.warn("--enrich is ignored with --check.");
|
|
276
|
+
if (!machineOutput)
|
|
277
|
+
clack.intro("agent-rules-init");
|
|
278
|
+
if (!machineOutput && !nonInteractive && !hasInteractiveTty()) {
|
|
146
279
|
console.warn(ui.noTtyWarning);
|
|
147
280
|
}
|
|
148
281
|
try {
|
|
149
|
-
const
|
|
282
|
+
const loadedConfig = loadConfig(process.cwd());
|
|
283
|
+
const lang = action.lang ?? loadedConfig.config.lang ?? defaultLang;
|
|
284
|
+
ui = UI[lang];
|
|
285
|
+
if (!machineOutput) {
|
|
286
|
+
for (const warning of loadedConfig.warnings)
|
|
287
|
+
console.warn(warning);
|
|
288
|
+
}
|
|
289
|
+
let generatedFiles = [];
|
|
290
|
+
let repoFacts;
|
|
291
|
+
const results = await runCli(process.cwd(), {
|
|
292
|
+
lang,
|
|
293
|
+
config: loadedConfig.config,
|
|
294
|
+
dryRun: planningOnly,
|
|
295
|
+
nonInteractive,
|
|
296
|
+
skipLlm: nonInteractive && !enrich,
|
|
297
|
+
enrich,
|
|
298
|
+
assistant: action.assistant,
|
|
299
|
+
model: action.model,
|
|
300
|
+
onGeneratedFiles: (files) => {
|
|
301
|
+
generatedFiles = files;
|
|
302
|
+
},
|
|
303
|
+
onFacts: (facts) => {
|
|
304
|
+
repoFacts = facts;
|
|
305
|
+
},
|
|
306
|
+
});
|
|
150
307
|
const written = results.filter((r) => r.status === "written");
|
|
151
308
|
const failures = results.filter((r) => r.status === "error");
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
309
|
+
const outdated = action.check
|
|
310
|
+
? generatedFiles.filter((file) => {
|
|
311
|
+
const absolutePath = path.join(process.cwd(), file.path);
|
|
312
|
+
return fs.existsSync(absolutePath) && fs.readFileSync(absolutePath, "utf8") !== file.content;
|
|
313
|
+
})
|
|
314
|
+
: [];
|
|
315
|
+
const checkIssues = written.length + outdated.length;
|
|
316
|
+
if (machineOutput) {
|
|
317
|
+
const contentByPath = new Map(generatedFiles.map((file) => [file.path, file.content]));
|
|
318
|
+
console.log(JSON.stringify({
|
|
319
|
+
mode: action.check ? "check" : action.dryRun ? "dry-run" : "write",
|
|
320
|
+
configWarnings: loadedConfig.warnings,
|
|
321
|
+
facts: repoFacts,
|
|
322
|
+
wouldCreate: written.length,
|
|
323
|
+
outdated: outdated.map((file) => file.path),
|
|
324
|
+
results: results.map((result) => ({
|
|
325
|
+
...result,
|
|
326
|
+
...(planningOnly ? { content: contentByPath.get(result.path) } : {}),
|
|
327
|
+
})),
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
else if (action.dryRun) {
|
|
331
|
+
const statusByPath = new Map(results.map((result) => [result.path, result.status]));
|
|
332
|
+
for (const file of generatedFiles) {
|
|
333
|
+
console.log(`\n--- ${file.path} (${statusByPath.get(file.path) === "written" ? "would create" : "exists"}) ---\n${file.content}`);
|
|
158
334
|
}
|
|
159
|
-
|
|
160
|
-
|
|
335
|
+
}
|
|
336
|
+
else if (!action.check)
|
|
337
|
+
for (const result of results) {
|
|
338
|
+
if (result.status === "written") {
|
|
339
|
+
clack.log.success(result.path);
|
|
340
|
+
}
|
|
341
|
+
else if (result.status === "skipped") {
|
|
342
|
+
clack.log.info(ui.fileSkipped(result.path));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
clack.log.warn(`${result.path}: ${result.error}`);
|
|
346
|
+
}
|
|
161
347
|
}
|
|
348
|
+
if (!machineOutput && action.check) {
|
|
349
|
+
console.log(checkIssues > 0
|
|
350
|
+
? `${written.length} file(s) missing; ${outdated.length} file(s) outdated.`
|
|
351
|
+
: "Generated files are present and up to date.");
|
|
352
|
+
}
|
|
353
|
+
else if (!machineOutput && action.dryRun) {
|
|
354
|
+
console.log(`\n${written.length} file(s) would be generated.`);
|
|
162
355
|
}
|
|
163
|
-
if (written.length > 0) {
|
|
356
|
+
else if (!machineOutput && written.length > 0) {
|
|
164
357
|
clack.outro(ui.outroWritten);
|
|
165
358
|
}
|
|
166
|
-
else {
|
|
359
|
+
else if (!machineOutput) {
|
|
167
360
|
clack.outro(ui.outroNothing);
|
|
168
361
|
}
|
|
169
|
-
if (failures.length > 0) {
|
|
362
|
+
if (failures.length > 0 || (action.check && checkIssues > 0)) {
|
|
170
363
|
process.exitCode = 1;
|
|
171
364
|
}
|
|
172
365
|
}
|
|
173
366
|
catch (err) {
|
|
174
|
-
|
|
367
|
+
const message = ui.unexpectedError(err.message);
|
|
368
|
+
if (machineOutput) {
|
|
369
|
+
console.log(JSON.stringify({ mode: action.check ? "check" : action.dryRun ? "dry-run" : "write", error: message }));
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
clack.log.error(message);
|
|
373
|
+
}
|
|
175
374
|
process.exitCode = 1;
|
|
176
375
|
}
|
|
177
376
|
}
|
|
@@ -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
|
+
}
|