agent-rules-init 0.6.2 → 0.7.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/README.md +30 -19
- package/dist/bin.js +1 -1
- package/dist/cli.d.ts +13 -33
- package/dist/cli.js +138 -153
- package/dist/core/cli-options.d.ts +43 -0
- package/dist/core/cli-options.js +70 -0
- package/dist/core/config.d.ts +12 -0
- package/dist/core/config.js +57 -4
- package/dist/core/enrichment-cache.d.ts +11 -0
- package/dist/core/enrichment-cache.js +93 -0
- package/dist/core/existing-docs.d.ts +2 -0
- package/dist/core/existing-docs.js +26 -0
- package/dist/core/generation-state.d.ts +8 -2
- package/dist/core/generation-state.js +15 -3
- package/dist/core/i18n.d.ts +9 -0
- package/dist/core/i18n.js +64 -48
- package/dist/core/llm-bridge.d.ts +11 -0
- package/dist/core/llm-bridge.js +162 -38
- package/dist/core/project-units.js +2 -1
- package/dist/core/scanner-async.d.ts +4 -0
- package/dist/core/scanner-async.js +55 -0
- package/dist/core/scanner-worker.d.ts +1 -0
- package/dist/core/scanner-worker.js +17 -0
- package/dist/core/scanner.d.ts +5 -1
- package/dist/core/scanner.js +37 -10
- package/dist/core/types.d.ts +8 -0
- package/dist/packs/index.d.ts +3 -0
- package/dist/packs/index.js +25 -0
- package/dist/packs/java.js +1 -1
- package/dist/packs/python.js +5 -2
- package/package.json +60 -54
package/README.md
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
# agent-rules-init
|
|
2
|
-
|
|
1
|
+
# agent-rules-init
|
|
2
|
+
|
|
3
|
+
This is the publishable CLI package from the
|
|
4
|
+
[`agent-rules-init` repository](https://github.com/racama29/agent-rules-init).
|
|
5
|
+
The root README is the canonical source for the complete feature, configuration,
|
|
6
|
+
security and contribution documentation.
|
|
7
|
+
|
|
8
|
+
The operating model is intentionally simple: generate staging files, review them,
|
|
9
|
+
then activate them with `npx agent-rules-init --apply`. Existing active files are
|
|
10
|
+
backed up before replacement.
|
|
11
|
+
|
|
3
12
|
Generate repository-specific instructions for Claude Code, Codex, GitHub Copilot, Cursor, Gemini CLI and Windsurf 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
|
-
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx agent-rules-init
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
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.
|
|
19
|
+
|
|
11
20
|
All output is written with a `.generated` suffix and existing files are never overwritten during generation. Review them and run `--apply` to activate with safe backups.
|
|
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
|
-
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
15
24
|
Automation is supported through `--dry-run`, `--force`, `--apply`, `--check`, `--json` and `--non-interactive`. Repository defaults, enrichment assistant/model and per-project overrides can be stored in `.agent-rules-init.yml`.
|
|
16
25
|
|
|
17
26
|
Use `--apply` after review to activate staging files; replaced finals are backed up under `.agent-rules-init/backups/`.
|
|
@@ -20,10 +29,12 @@ Complete generations store a git-ignored hash receipt so `--check` can verify de
|
|
|
20
29
|
|
|
21
30
|
AI enrichment runs assistants with read-only restrictions and falls back to deterministic output when invocation or validation fails. Review generated files before activation, especially when the repository did not come from a trusted source.
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
Verified enrichment is reused when repository inputs, assistant/model and accepted staging hashes are unchanged. Use `--no-enrich-cache` for a deliberate fresh run, and bound latency with `--enrich-timeout <seconds>` plus `--enrich-retries <0..2>`.
|
|
33
|
+
|
|
34
|
+
Semantic validation rejects prompt-injection language, new sections, destructive instructions, commands absent from repository facts and evidence paths outside the repository. Metrics report cache hits, changed lines and security rejections in addition to batch, retry, input-size, fallback and duration data.
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
MIT
|
package/dist/bin.js
CHANGED
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { type Lang } from "./core/i18n.js";
|
|
|
3
3
|
import { type AgentRulesConfig } from "./core/config.js";
|
|
4
4
|
import { type PromptFn } from "./core/prompt-engine.js";
|
|
5
5
|
import { type AssistantId, type EnrichMetrics, type ExecFn } from "./core/llm-bridge.js";
|
|
6
|
-
import type { RepoFacts } from "./core/types.js";
|
|
6
|
+
import type { RepoFacts, RepoSignals } from "./core/types.js";
|
|
7
|
+
export { resolveCliAction } from "./core/cli-options.js";
|
|
8
|
+
export type { CliAction, CliRunOptions } from "./core/cli-options.js";
|
|
7
9
|
export interface RunCliOptions {
|
|
8
10
|
/** @deprecated Project metadata is no longer requested interactively. */
|
|
9
11
|
promptFn?: PromptFn;
|
|
@@ -22,6 +24,12 @@ export interface RunCliOptions {
|
|
|
22
24
|
assistant?: AssistantId;
|
|
23
25
|
/** Model forwarded verbatim to the assistant CLI; its default when omitted. */
|
|
24
26
|
model?: string;
|
|
27
|
+
/** Per-assistant-attempt timeout in seconds. */
|
|
28
|
+
enrichTimeoutSeconds?: number;
|
|
29
|
+
/** Disable reuse of verified enriched staging for this invocation. */
|
|
30
|
+
noEnrichCache?: boolean;
|
|
31
|
+
/** Number of retries after the first assistant attempt (0..2). */
|
|
32
|
+
enrichRetries?: number;
|
|
25
33
|
/** Receives the rendered files before they are written (or planned). */
|
|
26
34
|
onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
|
|
27
35
|
/** Receives the deterministic generation fingerprint before optional enrichment. */
|
|
@@ -32,39 +40,11 @@ export interface RunCliOptions {
|
|
|
32
40
|
onConfigWarnings?: (warnings: readonly string[]) => void;
|
|
33
41
|
/** Receives the facts extracted from the repo (including canonical commands). */
|
|
34
42
|
onFacts?: (facts: RepoFacts) => void;
|
|
43
|
+
onScanWarnings?: (warnings: readonly string[]) => void;
|
|
44
|
+
onScanStats?: (stats: NonNullable<RepoSignals["scanStats"]>) => void;
|
|
45
|
+
/** Offload repository traversal to a worker thread. Enabled by the published binary. */
|
|
46
|
+
useScannerWorker?: boolean;
|
|
35
47
|
}
|
|
36
48
|
export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
|
|
37
|
-
export interface CliRunOptions {
|
|
38
|
-
lang?: Lang;
|
|
39
|
-
dryRun?: true;
|
|
40
|
-
force?: true;
|
|
41
|
-
apply?: true;
|
|
42
|
-
check?: true;
|
|
43
|
-
json?: true;
|
|
44
|
-
nonInteractive?: true;
|
|
45
|
-
enrich?: true;
|
|
46
|
-
assistant?: AssistantId;
|
|
47
|
-
model?: string;
|
|
48
|
-
}
|
|
49
|
-
export type CliAction = ({
|
|
50
|
-
kind: "run";
|
|
51
|
-
} & CliRunOptions) | {
|
|
52
|
-
kind: "help";
|
|
53
|
-
} | {
|
|
54
|
-
kind: "version";
|
|
55
|
-
} | {
|
|
56
|
-
kind: "invalid-lang";
|
|
57
|
-
value: string;
|
|
58
|
-
} | {
|
|
59
|
-
kind: "invalid-assistant";
|
|
60
|
-
value: string;
|
|
61
|
-
} | {
|
|
62
|
-
kind: "missing-value";
|
|
63
|
-
flag: string;
|
|
64
|
-
} | {
|
|
65
|
-
kind: "unknown";
|
|
66
|
-
flag: string;
|
|
67
|
-
};
|
|
68
|
-
export declare function resolveCliAction(argv: string[]): CliAction;
|
|
69
49
|
export declare function getVersion(): string;
|
|
70
50
|
export declare function main(): Promise<void>;
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import * as clack from "@clack/prompts";
|
|
5
5
|
import { scanRepo } from "./core/scanner.js";
|
|
6
|
+
import { scanRepoInWorker } from "./core/scanner-async.js";
|
|
6
7
|
import { writeGeneratedFiles } from "./core/writer.js";
|
|
7
8
|
import { renderClaudeMd, renderAgentsMd, renderCopilotInstructions, renderCursorRules, renderGeminiMd, renderPromptFiles, } from "./core/templates.js";
|
|
8
9
|
import { buildRepoFacts } from "./core/repo-facts.js";
|
|
@@ -11,73 +12,48 @@ import { loadConfig } from "./core/config.js";
|
|
|
11
12
|
import { applyProjectExcludes, buildPackageUnits } from "./core/project-units.js";
|
|
12
13
|
import { renderProjectUnitAgents } from "./core/project-unit-output.js";
|
|
13
14
|
import { hasInteractiveTty, } from "./core/prompt-engine.js";
|
|
14
|
-
import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment,
|
|
15
|
+
import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment, createDefaultExecFn, DEFAULT_EXEC_TIMEOUT_MS, summarizeEnrichmentChanges, } from "./core/llm-bridge.js";
|
|
15
16
|
import { hashGeneratedFiles, makeGenerationState, writeGenerationState, } from "./core/generation-state.js";
|
|
17
|
+
import { loadCachedEnrichment, makeEnrichmentState } from "./core/enrichment-cache.js";
|
|
16
18
|
import { evaluateGenerationCheck } from "./core/check-state.js";
|
|
17
19
|
import { applyGeneratedFiles } from "./core/activation.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
import { rubyPack } from "./packs/ruby.js";
|
|
23
|
-
import { goPack } from "./packs/go.js";
|
|
24
|
-
import { rustPack } from "./packs/rust.js";
|
|
25
|
-
import { csharpPack } from "./packs/csharp.js";
|
|
26
|
-
import { kotlinPack } from "./packs/kotlin.js";
|
|
27
|
-
import { swiftPack } from "./packs/swift.js";
|
|
28
|
-
import { dartPack } from "./packs/dart.js";
|
|
29
|
-
import { cppPack } from "./packs/cpp.js";
|
|
30
|
-
import { elixirPack } from "./packs/elixir.js";
|
|
31
|
-
import { scalaPack } from "./packs/scala.js";
|
|
32
|
-
import { rPack } from "./packs/r.js";
|
|
33
|
-
const ALL_PACKS = [
|
|
34
|
-
jsTsPack,
|
|
35
|
-
pythonPack,
|
|
36
|
-
javaPack,
|
|
37
|
-
phpPack,
|
|
38
|
-
rubyPack,
|
|
39
|
-
goPack,
|
|
40
|
-
rustPack,
|
|
41
|
-
csharpPack,
|
|
42
|
-
kotlinPack,
|
|
43
|
-
swiftPack,
|
|
44
|
-
dartPack,
|
|
45
|
-
cppPack,
|
|
46
|
-
elixirPack,
|
|
47
|
-
scalaPack,
|
|
48
|
-
rPack,
|
|
49
|
-
];
|
|
50
|
-
// Final-name docs the tool itself would generate; when they already exist they carry the
|
|
51
|
-
// team's hand-written intent, so enrichment must integrate them instead of ignoring them.
|
|
52
|
-
const EXISTING_DOC_PATHS = [
|
|
53
|
-
"CLAUDE.md",
|
|
54
|
-
"AGENTS.md",
|
|
55
|
-
"GEMINI.md",
|
|
56
|
-
".github/copilot-instructions.md",
|
|
57
|
-
".cursor/rules/repository.mdc",
|
|
58
|
-
];
|
|
59
|
-
const MAX_EXISTING_DOC_CHARS = 20_000;
|
|
60
|
-
function readExistingDocs(rootPath) {
|
|
61
|
-
const docs = [];
|
|
62
|
-
for (const relativePath of EXISTING_DOC_PATHS) {
|
|
63
|
-
const absolutePath = path.join(rootPath, relativePath);
|
|
64
|
-
if (!fs.existsSync(absolutePath))
|
|
65
|
-
continue;
|
|
66
|
-
const content = fs.readFileSync(absolutePath, "utf8");
|
|
67
|
-
if (content.trim() === "")
|
|
68
|
-
continue;
|
|
69
|
-
docs.push({ path: relativePath, content: content.slice(0, MAX_EXISTING_DOC_CHARS) });
|
|
70
|
-
}
|
|
71
|
-
return docs;
|
|
72
|
-
}
|
|
20
|
+
import { ALL_PACKS, findPack } from "./packs/index.js";
|
|
21
|
+
import { readExistingDocs } from "./core/existing-docs.js";
|
|
22
|
+
import { resolveCliAction } from "./core/cli-options.js";
|
|
23
|
+
export { resolveCliAction } from "./core/cli-options.js";
|
|
73
24
|
export async function runCli(rootPath, options = {}) {
|
|
74
25
|
const loadedConfig = options.config ? { config: options.config, warnings: [] } : loadConfig(rootPath);
|
|
75
26
|
const config = loadedConfig.config;
|
|
76
27
|
options.onConfigWarnings?.(loadedConfig.warnings);
|
|
77
|
-
const
|
|
28
|
+
const timeoutSeconds = options.enrichTimeoutSeconds ?? config.enrichTimeoutSeconds ?? DEFAULT_EXEC_TIMEOUT_MS / 1000;
|
|
29
|
+
const execFn = options.execFn ?? createDefaultExecFn(timeoutSeconds * 1000);
|
|
78
30
|
const lang = options.lang ?? config.lang ?? detectLang();
|
|
79
31
|
const ui = UI[lang];
|
|
80
|
-
const
|
|
32
|
+
const scanOptions = {
|
|
33
|
+
maxDepth: config.scanMaxDepth,
|
|
34
|
+
maxFiles: config.scanMaxFiles,
|
|
35
|
+
};
|
|
36
|
+
let scanned;
|
|
37
|
+
if (options.useScannerWorker) {
|
|
38
|
+
try {
|
|
39
|
+
scanned = await scanRepoInWorker(rootPath, scanOptions, (config.scanWorkerTimeoutSeconds ?? 30) * 1_000);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
scanned = scanRepo(rootPath, scanOptions);
|
|
43
|
+
scanned.scanWarnings = [
|
|
44
|
+
...(scanned.scanWarnings ?? []),
|
|
45
|
+
`Scanner worker unavailable; used synchronous fallback: ${error.message}`,
|
|
46
|
+
];
|
|
47
|
+
if (scanned.scanStats)
|
|
48
|
+
scanned.scanStats.mode = "sync-fallback";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
scanned = scanRepo(rootPath, scanOptions);
|
|
53
|
+
const signals = applyProjectExcludes(scanned, config.exclude ?? []);
|
|
54
|
+
options.onScanWarnings?.(signals.scanWarnings ?? []);
|
|
55
|
+
if (signals.scanStats)
|
|
56
|
+
options.onScanStats?.(signals.scanStats);
|
|
81
57
|
const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
|
|
82
58
|
// Missing framework/tooling signals are valid facts, not questions the user should
|
|
83
59
|
// have to answer. Keep the low-confidence `none` value and render conservative,
|
|
@@ -88,7 +64,7 @@ export async function runCli(rootPath, options = {}) {
|
|
|
88
64
|
options.onFacts?.(facts);
|
|
89
65
|
const ctx = { facts, signals };
|
|
90
66
|
const entries = detections.map((detection) => {
|
|
91
|
-
const pack =
|
|
67
|
+
const pack = findPack(detection.packId);
|
|
92
68
|
return { detection, ruleSet: pack.rules(detection, lang, ctx) };
|
|
93
69
|
});
|
|
94
70
|
const files = [];
|
|
@@ -105,7 +81,7 @@ export async function runCli(rootPath, options = {}) {
|
|
|
105
81
|
});
|
|
106
82
|
files.push({ path: "GEMINI.generated.md", content: renderGeminiMd(entries, facts, lang) });
|
|
107
83
|
for (const detection of detections) {
|
|
108
|
-
const pack =
|
|
84
|
+
const pack = findPack(detection.packId);
|
|
109
85
|
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
|
|
110
86
|
files.push(file);
|
|
111
87
|
}
|
|
@@ -130,6 +106,7 @@ export async function runCli(rootPath, options = {}) {
|
|
|
130
106
|
}
|
|
131
107
|
const baselineHash = hashGeneratedFiles(files);
|
|
132
108
|
options.onBaselineHash?.(baselineHash);
|
|
109
|
+
let acceptedEnrichmentState;
|
|
133
110
|
const wantsEnrich = (options.enrich ?? config.enrich) === true;
|
|
134
111
|
const canOfferEnrich = !options.nonInteractive && hasInteractiveTty();
|
|
135
112
|
if (!options.skipLlm && !config.noAi && (wantsEnrich || canOfferEnrich)) {
|
|
@@ -153,30 +130,77 @@ export async function runCli(rootPath, options = {}) {
|
|
|
153
130
|
proceed = (await clack.confirm({ message: ui.enrichConfirm(chosenAssistant) })) === true;
|
|
154
131
|
}
|
|
155
132
|
if (proceed) {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
133
|
+
const existingDocs = readExistingDocs(rootPath);
|
|
134
|
+
const model = options.model ?? config.model;
|
|
135
|
+
const requestedState = makeEnrichmentState(rootPath, signals.files, baselineHash, existingDocs, chosenAssistant, model);
|
|
136
|
+
const useCache = options.noEnrichCache !== true && config.enrichCache !== false;
|
|
137
|
+
const cached = useCache
|
|
138
|
+
? loadCachedEnrichment(rootPath, baselineHash, files, requestedState)
|
|
139
|
+
: undefined;
|
|
140
|
+
if (cached) {
|
|
141
|
+
const changes = summarizeEnrichmentChanges(files, cached);
|
|
142
|
+
options.onEnrichMetrics?.({
|
|
143
|
+
assistant: chosenAssistant,
|
|
144
|
+
model,
|
|
145
|
+
batches: 0,
|
|
146
|
+
attempts: 0,
|
|
147
|
+
fallbackBatches: 0,
|
|
148
|
+
inputChars: 0,
|
|
149
|
+
outputChars: cached.reduce((sum, file) => sum + file.content.length, 0),
|
|
150
|
+
durationMs: 0,
|
|
151
|
+
cacheHit: true,
|
|
152
|
+
...changes,
|
|
153
|
+
securityRejections: 0,
|
|
154
|
+
});
|
|
155
|
+
acceptedEnrichmentState = requestedState;
|
|
156
|
+
files.splice(0, files.length, ...cached);
|
|
157
|
+
notify(ui.enrichCacheHit);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const estimate = estimateEnrichment(files);
|
|
161
|
+
if (estimate.batches > 1)
|
|
162
|
+
notify(ui.enrichLargeInput(estimate.characters, estimate.batches));
|
|
163
|
+
const spinner = canOfferEnrich ? clack.spinner() : undefined;
|
|
164
|
+
const retries = options.enrichRetries ?? config.enrichRetries ?? 1;
|
|
165
|
+
notify(ui.enrichBudget(timeoutSeconds, retries + 1));
|
|
166
|
+
if (spinner)
|
|
167
|
+
spinner.start(ui.enrichWorking(chosenAssistant));
|
|
168
|
+
else
|
|
169
|
+
notify(ui.enrichWorking(chosenAssistant));
|
|
170
|
+
let runMetrics;
|
|
171
|
+
const verifiedCommands = [...new Set([
|
|
172
|
+
...facts.canonical.map((entry) => entry.command),
|
|
173
|
+
...facts.commands.map((entry) => entry.invocation),
|
|
174
|
+
...facts.ciCommands.map((entry) => entry.command),
|
|
175
|
+
])];
|
|
176
|
+
const enriched = await enrichFilesWithAssistant(chosenAssistant, files, {
|
|
177
|
+
execFn,
|
|
178
|
+
lang,
|
|
179
|
+
cwd: rootPath,
|
|
180
|
+
mustKeep: verifiedCommands,
|
|
181
|
+
existingDocs,
|
|
182
|
+
model,
|
|
183
|
+
maxAttempts: retries + 1,
|
|
184
|
+
onMetrics: (metrics) => {
|
|
185
|
+
runMetrics = metrics;
|
|
186
|
+
options.onEnrichMetrics?.(metrics);
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
const changed = enriched.some((file, i) => file.content !== files[i].content);
|
|
190
|
+
const outcome = changed ? ui.enrichDone : ui.enrichKept;
|
|
191
|
+
if (spinner)
|
|
192
|
+
spinner.stop(outcome);
|
|
193
|
+
else
|
|
194
|
+
notify(outcome);
|
|
195
|
+
files.splice(0, files.length, ...enriched);
|
|
196
|
+
if (changed && runMetrics && runMetrics.fallbackBatches === 0) {
|
|
197
|
+
acceptedEnrichmentState = requestedState;
|
|
198
|
+
}
|
|
199
|
+
else if (!changed) {
|
|
200
|
+
// Never cache a deterministic fallback as a successful enrichment.
|
|
201
|
+
acceptedEnrichmentState = undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
206
|
}
|
|
@@ -191,76 +215,10 @@ export async function runCli(rootPath, options = {}) {
|
|
|
191
215
|
}
|
|
192
216
|
const results = writeGeneratedFiles(rootPath, files, { force: options.force });
|
|
193
217
|
const completeRefresh = results.every((result) => result.status === "written" || result.status === "overwritten");
|
|
194
|
-
if (completeRefresh)
|
|
195
|
-
writeGenerationState(rootPath, makeGenerationState(baselineHash, files));
|
|
196
|
-
return results;
|
|
197
|
-
}
|
|
198
|
-
function isLang(value) {
|
|
199
|
-
return value === "es" || value === "en";
|
|
200
|
-
}
|
|
201
|
-
function isAssistant(value) {
|
|
202
|
-
return value === "claude" || value === "codex";
|
|
203
|
-
}
|
|
204
|
-
export function resolveCliAction(argv) {
|
|
205
|
-
const options = {};
|
|
206
|
-
for (let i = 0; i < argv.length; i++) {
|
|
207
|
-
const arg = argv[i];
|
|
208
|
-
if (arg === "--help" || arg === "-h")
|
|
209
|
-
return { kind: "help" };
|
|
210
|
-
if (arg === "--version" || arg === "-v")
|
|
211
|
-
return { kind: "version" };
|
|
212
|
-
if (arg === "--lang" || arg.startsWith("--lang=")) {
|
|
213
|
-
const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
|
|
214
|
-
if (!isLang(value))
|
|
215
|
-
return { kind: "invalid-lang", value };
|
|
216
|
-
options.lang = value;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
if (arg === "--dry-run") {
|
|
220
|
-
options.dryRun = true;
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (arg === "--force") {
|
|
224
|
-
options.force = true;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (arg === "--apply") {
|
|
228
|
-
options.apply = true;
|
|
229
|
-
continue;
|
|
230
|
-
}
|
|
231
|
-
if (arg === "--check") {
|
|
232
|
-
options.check = true;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
if (arg === "--json") {
|
|
236
|
-
options.json = true;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (arg === "--non-interactive") {
|
|
240
|
-
options.nonInteractive = true;
|
|
241
|
-
continue;
|
|
242
|
-
}
|
|
243
|
-
if (arg === "--enrich") {
|
|
244
|
-
options.enrich = true;
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (arg === "--assistant" || arg.startsWith("--assistant=")) {
|
|
248
|
-
const value = arg.startsWith("--assistant=") ? arg.slice("--assistant=".length) : argv[++i] ?? "";
|
|
249
|
-
if (!isAssistant(value))
|
|
250
|
-
return { kind: "invalid-assistant", value };
|
|
251
|
-
options.assistant = value;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (arg === "--model" || arg.startsWith("--model=")) {
|
|
255
|
-
const value = arg.startsWith("--model=") ? arg.slice("--model=".length) : argv[++i] ?? "";
|
|
256
|
-
if (value === "")
|
|
257
|
-
return { kind: "missing-value", flag: "--model" };
|
|
258
|
-
options.model = value;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
return { kind: "unknown", flag: arg };
|
|
218
|
+
if (completeRefresh) {
|
|
219
|
+
writeGenerationState(rootPath, makeGenerationState(baselineHash, files, acceptedEnrichmentState));
|
|
262
220
|
}
|
|
263
|
-
return
|
|
221
|
+
return results;
|
|
264
222
|
}
|
|
265
223
|
function usageWithAutomationOptions(ui) {
|
|
266
224
|
return `${ui.usage}\n\n${ui.automationUsage}`;
|
|
@@ -298,6 +256,16 @@ export async function main() {
|
|
|
298
256
|
process.exitCode = 1;
|
|
299
257
|
return;
|
|
300
258
|
}
|
|
259
|
+
if (action.kind === "invalid-timeout") {
|
|
260
|
+
console.error(`${ui.invalidTimeout(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
261
|
+
process.exitCode = 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (action.kind === "invalid-retries") {
|
|
265
|
+
console.error(`${ui.invalidRetries(action.value)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
266
|
+
process.exitCode = 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
301
269
|
if (action.kind === "missing-value") {
|
|
302
270
|
console.error(`${ui.missingFlagValue(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
303
271
|
process.exitCode = 1;
|
|
@@ -334,6 +302,8 @@ export async function main() {
|
|
|
334
302
|
let repoFacts;
|
|
335
303
|
let baselineHash;
|
|
336
304
|
let enrichMetrics;
|
|
305
|
+
let scanWarnings = [];
|
|
306
|
+
let scanStats;
|
|
337
307
|
const results = await runCli(process.cwd(), {
|
|
338
308
|
lang,
|
|
339
309
|
config: loadedConfig.config,
|
|
@@ -344,6 +314,9 @@ export async function main() {
|
|
|
344
314
|
enrich,
|
|
345
315
|
assistant: action.assistant ?? loadedConfig.config.assistant,
|
|
346
316
|
model: action.model ?? loadedConfig.config.model,
|
|
317
|
+
enrichTimeoutSeconds: action.enrichTimeoutSeconds ?? loadedConfig.config.enrichTimeoutSeconds,
|
|
318
|
+
noEnrichCache: action.noEnrichCache === true,
|
|
319
|
+
enrichRetries: action.enrichRetries ?? loadedConfig.config.enrichRetries,
|
|
347
320
|
onGeneratedFiles: (files) => {
|
|
348
321
|
generatedFiles = files;
|
|
349
322
|
},
|
|
@@ -356,7 +329,17 @@ export async function main() {
|
|
|
356
329
|
onFacts: (facts) => {
|
|
357
330
|
repoFacts = facts;
|
|
358
331
|
},
|
|
332
|
+
onScanWarnings: (warnings) => {
|
|
333
|
+
scanWarnings = warnings;
|
|
334
|
+
},
|
|
335
|
+
onScanStats: (stats) => {
|
|
336
|
+
scanStats = stats;
|
|
337
|
+
},
|
|
338
|
+
useScannerWorker: true,
|
|
359
339
|
});
|
|
340
|
+
if (!machineOutput)
|
|
341
|
+
for (const warning of scanWarnings)
|
|
342
|
+
console.warn(warning);
|
|
360
343
|
let activationResults = [];
|
|
361
344
|
if (apply) {
|
|
362
345
|
if (!baselineHash)
|
|
@@ -377,6 +360,8 @@ export async function main() {
|
|
|
377
360
|
console.log(JSON.stringify({
|
|
378
361
|
mode: action.check ? "check" : action.dryRun ? "dry-run" : apply ? "apply" : "write",
|
|
379
362
|
configWarnings: loadedConfig.warnings,
|
|
363
|
+
scanWarnings,
|
|
364
|
+
scanStats,
|
|
380
365
|
facts: repoFacts,
|
|
381
366
|
wouldCreate: action.check ? missing.length : written.length,
|
|
382
367
|
missing: missing.map((file) => file.path),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Lang } from "./i18n.js";
|
|
2
|
+
import type { AssistantId } from "./llm-bridge.js";
|
|
3
|
+
export interface CliRunOptions {
|
|
4
|
+
lang?: Lang;
|
|
5
|
+
dryRun?: true;
|
|
6
|
+
force?: true;
|
|
7
|
+
apply?: true;
|
|
8
|
+
check?: true;
|
|
9
|
+
json?: true;
|
|
10
|
+
nonInteractive?: true;
|
|
11
|
+
enrich?: true;
|
|
12
|
+
assistant?: AssistantId;
|
|
13
|
+
model?: string;
|
|
14
|
+
enrichTimeoutSeconds?: number;
|
|
15
|
+
noEnrichCache?: true;
|
|
16
|
+
enrichRetries?: number;
|
|
17
|
+
}
|
|
18
|
+
export type CliAction = ({
|
|
19
|
+
kind: "run";
|
|
20
|
+
} & CliRunOptions) | {
|
|
21
|
+
kind: "help";
|
|
22
|
+
} | {
|
|
23
|
+
kind: "version";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "invalid-lang";
|
|
26
|
+
value: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "invalid-assistant";
|
|
29
|
+
value: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "invalid-timeout";
|
|
32
|
+
value: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "invalid-retries";
|
|
35
|
+
value: string;
|
|
36
|
+
} | {
|
|
37
|
+
kind: "missing-value";
|
|
38
|
+
flag: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "unknown";
|
|
41
|
+
flag: string;
|
|
42
|
+
};
|
|
43
|
+
export declare function resolveCliAction(argv: string[]): CliAction;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function optionValue(argv, index, name) {
|
|
2
|
+
const argument = argv[index];
|
|
3
|
+
return argument.startsWith(`${name}=`)
|
|
4
|
+
? [argument.slice(name.length + 1), index]
|
|
5
|
+
: [argv[index + 1] ?? "", index + 1];
|
|
6
|
+
}
|
|
7
|
+
export function resolveCliAction(argv) {
|
|
8
|
+
const options = {};
|
|
9
|
+
for (let index = 0; index < argv.length; index++) {
|
|
10
|
+
const argument = argv[index];
|
|
11
|
+
if (argument === "--help" || argument === "-h")
|
|
12
|
+
return { kind: "help" };
|
|
13
|
+
if (argument === "--version" || argument === "-v")
|
|
14
|
+
return { kind: "version" };
|
|
15
|
+
if (argument === "--lang" || argument.startsWith("--lang=")) {
|
|
16
|
+
const [value, consumed] = optionValue(argv, index, "--lang");
|
|
17
|
+
index = consumed;
|
|
18
|
+
if (value !== "es" && value !== "en")
|
|
19
|
+
return { kind: "invalid-lang", value };
|
|
20
|
+
options.lang = value;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (argument === "--assistant" || argument.startsWith("--assistant=")) {
|
|
24
|
+
const [value, consumed] = optionValue(argv, index, "--assistant");
|
|
25
|
+
index = consumed;
|
|
26
|
+
if (value !== "claude" && value !== "codex")
|
|
27
|
+
return { kind: "invalid-assistant", value };
|
|
28
|
+
options.assistant = value;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (argument === "--model" || argument.startsWith("--model=")) {
|
|
32
|
+
const [value, consumed] = optionValue(argv, index, "--model");
|
|
33
|
+
index = consumed;
|
|
34
|
+
if (!value)
|
|
35
|
+
return { kind: "missing-value", flag: "--model" };
|
|
36
|
+
options.model = value;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (argument === "--enrich-timeout" || argument.startsWith("--enrich-timeout=")) {
|
|
40
|
+
const [value, consumed] = optionValue(argv, index, "--enrich-timeout");
|
|
41
|
+
index = consumed;
|
|
42
|
+
const parsed = Number(value);
|
|
43
|
+
if (!Number.isInteger(parsed) || parsed < 10 || parsed > 3600)
|
|
44
|
+
return { kind: "invalid-timeout", value };
|
|
45
|
+
options.enrichTimeoutSeconds = parsed;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (argument === "--enrich-retries" || argument.startsWith("--enrich-retries=")) {
|
|
49
|
+
const [value, consumed] = optionValue(argv, index, "--enrich-retries");
|
|
50
|
+
index = consumed;
|
|
51
|
+
const parsed = Number(value);
|
|
52
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 2)
|
|
53
|
+
return { kind: "invalid-retries", value };
|
|
54
|
+
options.enrichRetries = parsed;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const booleanOptions = {
|
|
58
|
+
"--dry-run": "dryRun", "--force": "force", "--apply": "apply", "--check": "check",
|
|
59
|
+
"--json": "json", "--non-interactive": "nonInteractive", "--enrich": "enrich",
|
|
60
|
+
"--no-enrich-cache": "noEnrichCache",
|
|
61
|
+
};
|
|
62
|
+
const key = booleanOptions[argument];
|
|
63
|
+
if (key) {
|
|
64
|
+
Object.assign(options, { [key]: true });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
return { kind: "unknown", flag: argument };
|
|
68
|
+
}
|
|
69
|
+
return { kind: "run", ...options };
|
|
70
|
+
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -13,6 +13,18 @@ export interface AgentRulesConfig {
|
|
|
13
13
|
enrich?: boolean;
|
|
14
14
|
assistant?: "claude" | "codex";
|
|
15
15
|
model?: string;
|
|
16
|
+
/** Reuse verified enriched staging when repository inputs are unchanged. Defaults to true. */
|
|
17
|
+
enrichCache?: boolean;
|
|
18
|
+
/** Per-assistant-attempt timeout in seconds (10..3600). */
|
|
19
|
+
enrichTimeoutSeconds?: number;
|
|
20
|
+
/** Validation retries after the first attempt (0..2). */
|
|
21
|
+
enrichRetries?: number;
|
|
22
|
+
/** Maximum directory nesting inspected by the repository scanner (1..64). */
|
|
23
|
+
scanMaxDepth?: number;
|
|
24
|
+
/** Maximum files collected before stopping the repository scan (100..1,000,000). */
|
|
25
|
+
scanMaxFiles?: number;
|
|
26
|
+
/** Worker scan timeout before a synchronous compatibility fallback (1..300 seconds). */
|
|
27
|
+
scanWorkerTimeoutSeconds?: number;
|
|
16
28
|
}
|
|
17
29
|
export interface LoadedConfig {
|
|
18
30
|
config: AgentRulesConfig;
|