agent-rules-init 0.6.1 → 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 +14 -33
- package/dist/cli.js +144 -158
- 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/prompt-engine.d.ts +4 -13
- package/dist/core/prompt-engine.js +0 -54
- 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,8 +3,11 @@ 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 {
|
|
10
|
+
/** @deprecated Project metadata is no longer requested interactively. */
|
|
8
11
|
promptFn?: PromptFn;
|
|
9
12
|
execFn?: ExecFn;
|
|
10
13
|
skipLlm?: boolean;
|
|
@@ -21,6 +24,12 @@ export interface RunCliOptions {
|
|
|
21
24
|
assistant?: AssistantId;
|
|
22
25
|
/** Model forwarded verbatim to the assistant CLI; its default when omitted. */
|
|
23
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;
|
|
24
33
|
/** Receives the rendered files before they are written (or planned). */
|
|
25
34
|
onGeneratedFiles?: (files: readonly GeneratedFile[]) => void;
|
|
26
35
|
/** Receives the deterministic generation fingerprint before optional enrichment. */
|
|
@@ -31,39 +40,11 @@ export interface RunCliOptions {
|
|
|
31
40
|
onConfigWarnings?: (warnings: readonly string[]) => void;
|
|
32
41
|
/** Receives the facts extracted from the repo (including canonical commands). */
|
|
33
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;
|
|
34
47
|
}
|
|
35
48
|
export declare function runCli(rootPath: string, options?: RunCliOptions): Promise<WriteResult[]>;
|
|
36
|
-
export interface CliRunOptions {
|
|
37
|
-
lang?: Lang;
|
|
38
|
-
dryRun?: true;
|
|
39
|
-
force?: true;
|
|
40
|
-
apply?: true;
|
|
41
|
-
check?: true;
|
|
42
|
-
json?: true;
|
|
43
|
-
nonInteractive?: true;
|
|
44
|
-
enrich?: true;
|
|
45
|
-
assistant?: AssistantId;
|
|
46
|
-
model?: string;
|
|
47
|
-
}
|
|
48
|
-
export type CliAction = ({
|
|
49
|
-
kind: "run";
|
|
50
|
-
} & CliRunOptions) | {
|
|
51
|
-
kind: "help";
|
|
52
|
-
} | {
|
|
53
|
-
kind: "version";
|
|
54
|
-
} | {
|
|
55
|
-
kind: "invalid-lang";
|
|
56
|
-
value: string;
|
|
57
|
-
} | {
|
|
58
|
-
kind: "invalid-assistant";
|
|
59
|
-
value: string;
|
|
60
|
-
} | {
|
|
61
|
-
kind: "missing-value";
|
|
62
|
-
flag: string;
|
|
63
|
-
} | {
|
|
64
|
-
kind: "unknown";
|
|
65
|
-
flag: string;
|
|
66
|
-
};
|
|
67
|
-
export declare function resolveCliAction(argv: string[]): CliAction;
|
|
68
49
|
export declare function getVersion(): string;
|
|
69
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";
|
|
@@ -10,84 +11,60 @@ import { UI, detectLang } from "./core/i18n.js";
|
|
|
10
11
|
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
|
-
import {
|
|
14
|
-
import { detectAvailableAssistants, enrichFilesWithAssistant, estimateEnrichment,
|
|
14
|
+
import { hasInteractiveTty, } from "./core/prompt-engine.js";
|
|
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
|
-
const promptFn = options.promptFn ?? makeDefaultPromptFn(lang);
|
|
80
31
|
const ui = UI[lang];
|
|
81
|
-
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);
|
|
82
57
|
const rawDetections = ALL_PACKS.map((pack) => pack.detect(signals)).filter((d) => d !== null);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
58
|
+
// Missing framework/tooling signals are valid facts, not questions the user should
|
|
59
|
+
// have to answer. Keep the low-confidence `none` value and render conservative,
|
|
60
|
+
// framework-neutral guidance; explicit repository config can still override project
|
|
61
|
+
// metadata when a team wants to provide it.
|
|
62
|
+
const detections = rawDetections;
|
|
86
63
|
const facts = buildRepoFacts(signals, lang);
|
|
87
64
|
options.onFacts?.(facts);
|
|
88
65
|
const ctx = { facts, signals };
|
|
89
66
|
const entries = detections.map((detection) => {
|
|
90
|
-
const pack =
|
|
67
|
+
const pack = findPack(detection.packId);
|
|
91
68
|
return { detection, ruleSet: pack.rules(detection, lang, ctx) };
|
|
92
69
|
});
|
|
93
70
|
const files = [];
|
|
@@ -104,7 +81,7 @@ export async function runCli(rootPath, options = {}) {
|
|
|
104
81
|
});
|
|
105
82
|
files.push({ path: "GEMINI.generated.md", content: renderGeminiMd(entries, facts, lang) });
|
|
106
83
|
for (const detection of detections) {
|
|
107
|
-
const pack =
|
|
84
|
+
const pack = findPack(detection.packId);
|
|
108
85
|
for (const file of renderPromptFiles(detection.packId, pack.promptTemplates(detection, lang, ctx))) {
|
|
109
86
|
files.push(file);
|
|
110
87
|
}
|
|
@@ -129,6 +106,7 @@ export async function runCli(rootPath, options = {}) {
|
|
|
129
106
|
}
|
|
130
107
|
const baselineHash = hashGeneratedFiles(files);
|
|
131
108
|
options.onBaselineHash?.(baselineHash);
|
|
109
|
+
let acceptedEnrichmentState;
|
|
132
110
|
const wantsEnrich = (options.enrich ?? config.enrich) === true;
|
|
133
111
|
const canOfferEnrich = !options.nonInteractive && hasInteractiveTty();
|
|
134
112
|
if (!options.skipLlm && !config.noAi && (wantsEnrich || canOfferEnrich)) {
|
|
@@ -152,30 +130,77 @@ export async function runCli(rootPath, options = {}) {
|
|
|
152
130
|
proceed = (await clack.confirm({ message: ui.enrichConfirm(chosenAssistant) })) === true;
|
|
153
131
|
}
|
|
154
132
|
if (proceed) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
+
}
|
|
179
204
|
}
|
|
180
205
|
}
|
|
181
206
|
}
|
|
@@ -190,76 +215,10 @@ export async function runCli(rootPath, options = {}) {
|
|
|
190
215
|
}
|
|
191
216
|
const results = writeGeneratedFiles(rootPath, files, { force: options.force });
|
|
192
217
|
const completeRefresh = results.every((result) => result.status === "written" || result.status === "overwritten");
|
|
193
|
-
if (completeRefresh)
|
|
194
|
-
writeGenerationState(rootPath, makeGenerationState(baselineHash, files));
|
|
195
|
-
return results;
|
|
196
|
-
}
|
|
197
|
-
function isLang(value) {
|
|
198
|
-
return value === "es" || value === "en";
|
|
199
|
-
}
|
|
200
|
-
function isAssistant(value) {
|
|
201
|
-
return value === "claude" || value === "codex";
|
|
202
|
-
}
|
|
203
|
-
export function resolveCliAction(argv) {
|
|
204
|
-
const options = {};
|
|
205
|
-
for (let i = 0; i < argv.length; i++) {
|
|
206
|
-
const arg = argv[i];
|
|
207
|
-
if (arg === "--help" || arg === "-h")
|
|
208
|
-
return { kind: "help" };
|
|
209
|
-
if (arg === "--version" || arg === "-v")
|
|
210
|
-
return { kind: "version" };
|
|
211
|
-
if (arg === "--lang" || arg.startsWith("--lang=")) {
|
|
212
|
-
const value = arg.startsWith("--lang=") ? arg.slice("--lang=".length) : argv[++i] ?? "";
|
|
213
|
-
if (!isLang(value))
|
|
214
|
-
return { kind: "invalid-lang", value };
|
|
215
|
-
options.lang = value;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
if (arg === "--dry-run") {
|
|
219
|
-
options.dryRun = true;
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (arg === "--force") {
|
|
223
|
-
options.force = true;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (arg === "--apply") {
|
|
227
|
-
options.apply = true;
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
if (arg === "--check") {
|
|
231
|
-
options.check = true;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
if (arg === "--json") {
|
|
235
|
-
options.json = true;
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
if (arg === "--non-interactive") {
|
|
239
|
-
options.nonInteractive = true;
|
|
240
|
-
continue;
|
|
241
|
-
}
|
|
242
|
-
if (arg === "--enrich") {
|
|
243
|
-
options.enrich = true;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (arg === "--assistant" || arg.startsWith("--assistant=")) {
|
|
247
|
-
const value = arg.startsWith("--assistant=") ? arg.slice("--assistant=".length) : argv[++i] ?? "";
|
|
248
|
-
if (!isAssistant(value))
|
|
249
|
-
return { kind: "invalid-assistant", value };
|
|
250
|
-
options.assistant = value;
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (arg === "--model" || arg.startsWith("--model=")) {
|
|
254
|
-
const value = arg.startsWith("--model=") ? arg.slice("--model=".length) : argv[++i] ?? "";
|
|
255
|
-
if (value === "")
|
|
256
|
-
return { kind: "missing-value", flag: "--model" };
|
|
257
|
-
options.model = value;
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
return { kind: "unknown", flag: arg };
|
|
218
|
+
if (completeRefresh) {
|
|
219
|
+
writeGenerationState(rootPath, makeGenerationState(baselineHash, files, acceptedEnrichmentState));
|
|
261
220
|
}
|
|
262
|
-
return
|
|
221
|
+
return results;
|
|
263
222
|
}
|
|
264
223
|
function usageWithAutomationOptions(ui) {
|
|
265
224
|
return `${ui.usage}\n\n${ui.automationUsage}`;
|
|
@@ -297,6 +256,16 @@ export async function main() {
|
|
|
297
256
|
process.exitCode = 1;
|
|
298
257
|
return;
|
|
299
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
|
+
}
|
|
300
269
|
if (action.kind === "missing-value") {
|
|
301
270
|
console.error(`${ui.missingFlagValue(action.flag)}\n\n${usageWithAutomationOptions(ui)}`);
|
|
302
271
|
process.exitCode = 1;
|
|
@@ -333,6 +302,8 @@ export async function main() {
|
|
|
333
302
|
let repoFacts;
|
|
334
303
|
let baselineHash;
|
|
335
304
|
let enrichMetrics;
|
|
305
|
+
let scanWarnings = [];
|
|
306
|
+
let scanStats;
|
|
336
307
|
const results = await runCli(process.cwd(), {
|
|
337
308
|
lang,
|
|
338
309
|
config: loadedConfig.config,
|
|
@@ -343,6 +314,9 @@ export async function main() {
|
|
|
343
314
|
enrich,
|
|
344
315
|
assistant: action.assistant ?? loadedConfig.config.assistant,
|
|
345
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,
|
|
346
320
|
onGeneratedFiles: (files) => {
|
|
347
321
|
generatedFiles = files;
|
|
348
322
|
},
|
|
@@ -355,7 +329,17 @@ export async function main() {
|
|
|
355
329
|
onFacts: (facts) => {
|
|
356
330
|
repoFacts = facts;
|
|
357
331
|
},
|
|
332
|
+
onScanWarnings: (warnings) => {
|
|
333
|
+
scanWarnings = warnings;
|
|
334
|
+
},
|
|
335
|
+
onScanStats: (stats) => {
|
|
336
|
+
scanStats = stats;
|
|
337
|
+
},
|
|
338
|
+
useScannerWorker: true,
|
|
358
339
|
});
|
|
340
|
+
if (!machineOutput)
|
|
341
|
+
for (const warning of scanWarnings)
|
|
342
|
+
console.warn(warning);
|
|
359
343
|
let activationResults = [];
|
|
360
344
|
if (apply) {
|
|
361
345
|
if (!baselineHash)
|
|
@@ -376,6 +360,8 @@ export async function main() {
|
|
|
376
360
|
console.log(JSON.stringify({
|
|
377
361
|
mode: action.check ? "check" : action.dryRun ? "dry-run" : apply ? "apply" : "write",
|
|
378
362
|
configWarnings: loadedConfig.warnings,
|
|
363
|
+
scanWarnings,
|
|
364
|
+
scanStats,
|
|
379
365
|
facts: repoFacts,
|
|
380
366
|
wouldCreate: action.check ? missing.length : written.length,
|
|
381
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;
|