@intentius/chant 0.78.0 → 0.80.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/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +38 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +14 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +27 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +17 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +17 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +9 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +18 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +211 -0
- package/dist/components/promote.d.ts.map +1 -0
- package/dist/components/unbound-gate-approval.d.ts +24 -0
- package/dist/components/unbound-gate-approval.d.ts.map +1 -0
- package/dist/config.d.ts +82 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/convergence.d.ts +97 -0
- package/dist/discovery/convergence.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +41 -2
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +13 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +65 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +24 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +22 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +26 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +25 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +2 -1
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops015-gate-approval.d.ts +14 -0
- package/dist/lint/rules/op/ops015-gate-approval.d.ts.map +1 -0
- package/dist/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +4 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts +142 -0
- package/dist/op/gate-approval.d.ts.map +1 -0
- package/dist/op/gate.d.ts +54 -0
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +4 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/workspace/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +57 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +90 -12
- package/src/audit/terraform-state.test.ts +86 -1
- package/src/audit/terraform-state.ts +26 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-walk-warnings.test.ts +156 -0
- package/src/cli/commands/audit.ts +93 -19
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/doctor.ts +3 -0
- package/src/cli/commands/import-agents.ts +11 -4
- package/src/cli/commands/init.ts +57 -25
- package/src/cli/commands/lint.test.ts +58 -1
- package/src/cli/commands/lint.ts +48 -18
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +5 -1
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- package/src/cli/handlers/build.test.ts +11 -0
- package/src/cli/handlers/build.ts +1 -1
- package/src/cli/handlers/dev.ts +3 -0
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.test.ts +85 -0
- package/src/cli/handlers/operator.ts +118 -2
- package/src/cli/handlers/promote.test.ts +261 -0
- package/src/cli/handlers/promote.ts +325 -0
- package/src/cli/handlers/run-generate.test.ts +160 -0
- package/src/cli/handlers/run-generate.ts +133 -0
- package/src/cli/handlers/run.ts +13 -1
- package/src/cli/main.test.ts +33 -1
- package/src/cli/main.ts +78 -16
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +44 -6
- package/src/cli/registry.ts +18 -1
- package/src/components/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +3 -2
- package/src/components/discover.ts +26 -2
- package/src/components/driver.test.ts +12 -11
- package/src/components/driver.ts +2 -0
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +371 -0
- package/src/components/promote.ts +537 -0
- package/src/components/unbound-gate-approval.test.ts +82 -0
- package/src/components/unbound-gate-approval.ts +51 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +128 -11
- package/src/discovery/convergence.test.ts +231 -0
- package/src/discovery/convergence.ts +399 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +97 -9
- package/src/discovery/fold-import.ts +83 -9
- package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
- package/src/graph-ir.ts +14 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +117 -0
- package/src/lexicon.ts +26 -0
- package/src/lifecycle/build-ledger-store.ts +11 -1
- package/src/lifecycle/gate-ledger.ts +22 -0
- package/src/lifecycle/legacy-digest.test.ts +114 -0
- package/src/lifecycle/legacy-digest.ts +62 -0
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- package/src/lint/rules/op/index.ts +4 -2
- package/src/lint/rules/op/ops015-gate-approval.test.ts +67 -0
- package/src/lint/rules/op/ops015-gate-approval.ts +57 -0
- package/src/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/builders.ts +10 -1
- package/src/op/discover-convergence.test.ts +70 -0
- package/src/op/discover.ts +4 -0
- package/src/op/gate-approval.test.ts +258 -0
- package/src/op/gate-approval.ts +260 -0
- package/src/op/gate.ts +148 -10
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/index.ts +9 -1
- package/src/op/local-executor.test.ts +66 -0
- package/src/op/local-executor.ts +44 -1
- package/src/op/op-ir.ts +4 -0
- package/src/op/runtimes/local.ts +2 -1
- package/src/op/types.ts +7 -0
- package/src/workspace/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +126 -0
- package/src/workspace/records-contract.test.ts +88 -0
- package/src/workspace/records.schema.json +120 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +383 -0
package/src/cli/commands/lint.ts
CHANGED
|
@@ -19,14 +19,15 @@ import { runPostSynthChecks } from "../../lint/post-synth";
|
|
|
19
19
|
import { rule } from "../../lint/declarative";
|
|
20
20
|
import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
|
|
21
21
|
import { formatError, formatInfo } from "../format";
|
|
22
|
-
import { GENERATED_MARKER } from "../../discovery/files";
|
|
22
|
+
import { GENERATED_MARKER, hasSkipMarker, compileDiscoveryFilter } from "../../discovery/files";
|
|
23
|
+
import { dirProbe, warnDiscoveryChanges } from "../../discovery/convergence";
|
|
23
24
|
import { buildParamValues, resolveBuildParams } from "../../build-params";
|
|
24
25
|
import { setBuildParams } from "../../params";
|
|
25
26
|
import { isNoLexiconDetected } from "../../detectLexicon";
|
|
26
27
|
|
|
27
28
|
// Import config loader
|
|
28
29
|
import { loadConfig, resolveRulesForFile, resolveConfiguredSeverity, findProjectRoot } from "../../lint/config";
|
|
29
|
-
import { loadChantConfig, resolveKnowledgeDir } from "../../config";
|
|
30
|
+
import { loadChantConfig, resolveKnowledgeDir, resolveDiscoveryGlobs } from "../../config";
|
|
30
31
|
import { findProjectConfig } from "../../project-root";
|
|
31
32
|
import type { LintProjectConfig } from "../../lint/rule";
|
|
32
33
|
import { loadOkfBundle, type OkfBundle } from "../../okf-read";
|
|
@@ -141,7 +142,7 @@ function lexiconResolutionDiagnostic(projectRoot: string, error: Error): LintDia
|
|
|
141
142
|
*/
|
|
142
143
|
async function loadAllPluginRules(
|
|
143
144
|
projectPath: string,
|
|
144
|
-
): Promise<{ rules: Map<string, LintRule>; intrinsics: IntrinsicDef[]; lexiconError?: Error }> {
|
|
145
|
+
): Promise<{ rules: Map<string, LintRule>; intrinsics: IntrinsicDef[]; plugins: LexiconPlugin[]; lexiconError?: Error }> {
|
|
145
146
|
const rules = new Map<string, LintRule>();
|
|
146
147
|
|
|
147
148
|
// Load core COR/EVL rules directly
|
|
@@ -219,7 +220,7 @@ async function loadAllPluginRules(
|
|
|
219
220
|
rules.set(r.id, r);
|
|
220
221
|
}
|
|
221
222
|
|
|
222
|
-
return { rules, intrinsics, ...(lexiconError ? { lexiconError } : {}) };
|
|
223
|
+
return { rules, intrinsics, plugins, ...(lexiconError ? { lexiconError } : {}) };
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
/**
|
|
@@ -276,12 +277,14 @@ export interface LintResult {
|
|
|
276
277
|
* `git check-ignore` for exact gitignore semantics (nesting, negation) in one
|
|
277
278
|
* batched call; a non-git tree (or absent git) filters nothing.
|
|
278
279
|
*/
|
|
279
|
-
function filterGitIgnored(files: string[], cwd: string): string[] {
|
|
280
|
+
function filterGitIgnored(files: string[], cwd: string, ignoredOut?: Set<string>): string[] {
|
|
280
281
|
if (files.length === 0) return files;
|
|
281
282
|
try {
|
|
282
283
|
const out = execFileSync("git", ["check-ignore", "--stdin"], {
|
|
283
284
|
cwd,
|
|
284
|
-
|
|
285
|
+
// The scan root rides along as one extra probe for #2527's warning
|
|
286
|
+
// release: whether it is itself ignored. It is never a file in `files`.
|
|
287
|
+
input: [...files, dirProbe(cwd)].join("\n"),
|
|
285
288
|
encoding: "utf-8",
|
|
286
289
|
// stdin piped (input), stdout captured, stderr silenced so a non-git tree's
|
|
287
290
|
// "fatal: not a git repository" never leaks to the lint output.
|
|
@@ -290,6 +293,7 @@ function filterGitIgnored(files: string[], cwd: string): string[] {
|
|
|
290
293
|
// catch handles it; exit 128 (not a repo) lands there too and filters none.
|
|
291
294
|
});
|
|
292
295
|
const ignored = new Set(out.split(/\r?\n/).filter(Boolean));
|
|
296
|
+
for (const p of ignored) ignoredOut?.add(p);
|
|
293
297
|
return ignored.size === 0 ? files : files.filter((f) => !ignored.has(f));
|
|
294
298
|
} catch {
|
|
295
299
|
// No git, not a repo, or nothing ignored (exit 1) — keep every file.
|
|
@@ -297,10 +301,19 @@ function filterGitIgnored(files: string[], cwd: string): string[] {
|
|
|
297
301
|
}
|
|
298
302
|
}
|
|
299
303
|
|
|
304
|
+
/** What {@link getTypeScriptFiles} saw before its git-ignore filter, for #2527's warning. */
|
|
305
|
+
interface TypeScriptScan {
|
|
306
|
+
raw: string[];
|
|
307
|
+
ignored: Set<string>;
|
|
308
|
+
}
|
|
309
|
+
|
|
300
310
|
/**
|
|
301
|
-
* Get all TypeScript files recursively, skipping git-ignored paths
|
|
311
|
+
* Get all TypeScript files recursively, skipping git-ignored paths and the
|
|
312
|
+
* files `skip` (the project's `exclude` globs, #2519) names. `scanOut`, when
|
|
313
|
+
* given, receives the files found before the ignore filter and the paths git
|
|
314
|
+
* reported ignored.
|
|
302
315
|
*/
|
|
303
|
-
function getTypeScriptFiles(dir: string): string[] {
|
|
316
|
+
function getTypeScriptFiles(dir: string, skip?: (file: string) => boolean, scanOut?: TypeScriptScan): string[] {
|
|
304
317
|
const files: string[] = [];
|
|
305
318
|
|
|
306
319
|
function scan(currentDir: string): void {
|
|
@@ -315,9 +328,12 @@ function getTypeScriptFiles(dir: string): string[] {
|
|
|
315
328
|
scan(fullPath);
|
|
316
329
|
}
|
|
317
330
|
} else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".spec.ts")) {
|
|
331
|
+
if (skip?.(fullPath)) continue;
|
|
318
332
|
// Skip chant-generated files (worker/workflow/activities bootstrap): they
|
|
319
333
|
// hold no authored source and use runtime patterns the EVL* rules forbid.
|
|
320
|
-
|
|
334
|
+
// A hand-written file opts out with the skip marker (#2519).
|
|
335
|
+
const head = readFileSync(fullPath, "utf-8").slice(0, 1024);
|
|
336
|
+
if (head.slice(0, 256).includes(GENERATED_MARKER) || hasSkipMarker(head)) {
|
|
321
337
|
continue;
|
|
322
338
|
}
|
|
323
339
|
files.push(fullPath);
|
|
@@ -326,7 +342,8 @@ function getTypeScriptFiles(dir: string): string[] {
|
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
scan(dir);
|
|
329
|
-
|
|
345
|
+
scanOut?.raw.push(...files);
|
|
346
|
+
return filterGitIgnored(files, dir, scanOut?.ignored);
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
/**
|
|
@@ -522,8 +539,10 @@ async function runComponentCheckDiagnostics(
|
|
|
522
539
|
*
|
|
523
540
|
* The project's configured lexicon NAMES are read, though (chant #2101), to
|
|
524
541
|
* resolve the activity contracts each one declares at
|
|
525
|
-
* `@intentius/chant-lexicon-<name>/op/activity-contracts
|
|
526
|
-
*
|
|
542
|
+
* `@intentius/chant-lexicon-<name>/op/activity-contracts`. The plugins
|
|
543
|
+
* `lintCommand` already loaded are passed in (chant #2520), so a plugin's
|
|
544
|
+
* `activityContracts()` member counts here as it does in `chant build`, and
|
|
545
|
+
* a lexicon declared by module path is covered. Without them a step calling a
|
|
527
546
|
* lexicon's activity is flagged for the absence of a contract that lexicon
|
|
528
547
|
* does declare, which is the whole point of OPS013 firing outside a build.
|
|
529
548
|
*
|
|
@@ -538,6 +557,7 @@ async function runOpCheckDiagnostics(
|
|
|
538
557
|
infraPath: string,
|
|
539
558
|
files: string[],
|
|
540
559
|
buildParams?: BuildParamProvenance[],
|
|
560
|
+
plugins: readonly LexiconPlugin[] = [],
|
|
541
561
|
): Promise<{ diagnostics: LintDiagnostic[]; suppressed: Array<LintDiagnostic & { reason?: string }> }> {
|
|
542
562
|
const config = loadConfig(findProjectRoot(infraPath));
|
|
543
563
|
const opFiles = files.filter((f) => f.endsWith(".op.ts"));
|
|
@@ -573,8 +593,13 @@ async function runOpCheckDiagnostics(
|
|
|
573
593
|
|
|
574
594
|
if (entities.size === 0) return { diagnostics: [], suppressed: [] };
|
|
575
595
|
|
|
576
|
-
|
|
577
|
-
|
|
596
|
+
// chant #2520 — hand over the loaded plugin objects when there are any, as
|
|
597
|
+
// `chant build` does, so a plugin's `activityContracts()` member is read
|
|
598
|
+
// here too. Names alone are the fallback when no plugin loaded.
|
|
599
|
+
const activityContracts =
|
|
600
|
+
plugins.length > 0
|
|
601
|
+
? await loadActivityContracts(plugins)
|
|
602
|
+
: await loadActivityContracts(await resolveProjectLexicons(infraPath).catch(() => [] as string[]));
|
|
578
603
|
|
|
579
604
|
const raw = runPostSynthChecks(
|
|
580
605
|
coreOpChecks(),
|
|
@@ -698,8 +723,13 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
698
723
|
allRules = new Map([...allRules, ...pluginRules]);
|
|
699
724
|
}
|
|
700
725
|
|
|
701
|
-
// Get all TypeScript files (scan scoped to the lint arg, git-ignored trees
|
|
702
|
-
|
|
726
|
+
// Get all TypeScript files (scan scoped to the lint arg, git-ignored trees
|
|
727
|
+
// and the project's `exclude` globs dropped)
|
|
728
|
+
const scanned: TypeScriptScan = { raw: [], ignored: new Set() };
|
|
729
|
+
const files = getTypeScriptFiles(infraPath, compileDiscoveryFilter(await resolveDiscoveryGlobs(infraPath)), scanned);
|
|
730
|
+
// #2527's warning release: which of these the converged walker reads
|
|
731
|
+
// differently. `files` is today's list, unchanged.
|
|
732
|
+
await warnDiscoveryChanges({ walker: "lint", root: infraPath, files, lintIgnored: scanned });
|
|
703
733
|
|
|
704
734
|
// Run lint — use per-file rules when overrides are present
|
|
705
735
|
let diagnostics: LintDiagnostic[];
|
|
@@ -736,7 +766,7 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
736
766
|
// Run the OPS* Op-model post-synth checks (#2122) over every `*.op.ts`
|
|
737
767
|
// file under the lint target — see runOpCheckDiagnostics's doc for why
|
|
738
768
|
// this needs no lexicon or build to fire.
|
|
739
|
-
const opResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
|
|
769
|
+
const opResult = await runOpCheckDiagnostics(infraPath, files, buildParams, loaded.plugins);
|
|
740
770
|
diagnostics.push(...opResult.diagnostics);
|
|
741
771
|
suppressed.push(...opResult.suppressed);
|
|
742
772
|
|
|
@@ -790,7 +820,7 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
|
|
|
790
820
|
|
|
791
821
|
// OPS* checks have no `.fix` either; re-run for the same consistency
|
|
792
822
|
// reason as the COMP* re-run just above.
|
|
793
|
-
const postOpResult = await runOpCheckDiagnostics(infraPath, files, buildParams);
|
|
823
|
+
const postOpResult = await runOpCheckDiagnostics(infraPath, files, buildParams, loaded.plugins);
|
|
794
824
|
diagnostics.push(...postOpResult.diagnostics);
|
|
795
825
|
suppressed.push(...postOpResult.suppressed);
|
|
796
826
|
}
|
|
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
|
2
2
|
import { join, dirname } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { formatSuccess, formatError, formatWarning } from "../format";
|
|
5
|
+
import { lexiconModulePath, lexiconSourceLabel } from "../../lexicon-module";
|
|
5
6
|
|
|
6
7
|
export interface OnboardOptions {
|
|
7
8
|
name: string;
|
|
@@ -331,6 +332,20 @@ export function patchDockerfile(filePath: string, name: string): PatchResult {
|
|
|
331
332
|
* Execute the onboard command — patches monorepo infrastructure for a new lexicon.
|
|
332
333
|
*/
|
|
333
334
|
export function onboardCommand(options: OnboardOptions): OnboardResult {
|
|
335
|
+
// chant#2578 — onboard wires a lexicon package into the chant monorepo. A
|
|
336
|
+
// lexicon declared by path has no package, so there is nothing to wire and
|
|
337
|
+
// no install step to print; say where it loads from instead.
|
|
338
|
+
if (lexiconModulePath(options.name) !== undefined) {
|
|
339
|
+
return {
|
|
340
|
+
success: false,
|
|
341
|
+
patched: [],
|
|
342
|
+
skipped: [],
|
|
343
|
+
error:
|
|
344
|
+
`lexicon "${options.name}" is declared by path in chant.config.ts and loads from ${lexiconSourceLabel(options.name)}. ` +
|
|
345
|
+
"onboard adds a lexicon package to the chant monorepo, and a lexicon loaded by path has no package to add.",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
334
349
|
const root = options.root ?? findRepoRoot();
|
|
335
350
|
const patched: string[] = [];
|
|
336
351
|
const skipped: string[] = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lexiconModulePath, lexiconNames } from "../../lexicon-module";
|
|
1
2
|
import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync, readFileSync } from "fs";
|
|
2
3
|
import { join, resolve } from "path";
|
|
3
4
|
import { createRequire } from "module";
|
|
@@ -105,7 +106,7 @@ export async function updateCommand(options: UpdateOptions): Promise<UpdateResul
|
|
|
105
106
|
|
|
106
107
|
// Load config to get lexicons
|
|
107
108
|
const { config } = await loadChantConfig(projectDir);
|
|
108
|
-
const lexicons = config.lexicons ?? [];
|
|
109
|
+
const lexicons = lexiconNames(config.lexicons ?? []);
|
|
109
110
|
|
|
110
111
|
if (lexicons.length === 0) {
|
|
111
112
|
return {
|
|
@@ -137,6 +138,9 @@ export async function updateCommand(options: UpdateOptions): Promise<UpdateResul
|
|
|
137
138
|
|
|
138
139
|
// Sync each lexicon
|
|
139
140
|
for (const lexicon of lexicons) {
|
|
141
|
+
// chant #2520 — a lexicon declared by module path is project source, with
|
|
142
|
+
// no package to copy types from.
|
|
143
|
+
if (lexiconModulePath(lexicon) !== undefined) continue;
|
|
140
144
|
const pkgName = `@intentius/chant-lexicon-${lexicon}`;
|
|
141
145
|
const pkgPath = resolvePackagePath(pkgName, projectDir);
|
|
142
146
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { checkConflicts } from "./conflict-check";
|
|
2
|
+
import { checkConflicts, describeConflict } from "./conflict-check";
|
|
3
3
|
import type { LexiconPlugin, CommandGroup } from "../lexicon";
|
|
4
4
|
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -228,6 +228,23 @@ describe("checkConflicts", () => {
|
|
|
228
228
|
expect(report.conflicts).toEqual([{ type: "command-group-name", key: "build", plugins: ["rogue"] }]);
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
+
// #2529 — `workspace` is reserved ahead of core's own command, and the
|
|
232
|
+
// error says it is reserved rather than naming a second lexicon.
|
|
233
|
+
test("a command group named workspace is a hard conflict with a clear message", () => {
|
|
234
|
+
const plugins = [makePlugin("rogue", { commandGroup: { name: "workspace", description: "d", commands: [] } })];
|
|
235
|
+
const report = checkConflicts(plugins);
|
|
236
|
+
expect(report.conflicts).toEqual([{ type: "command-group-name", key: "workspace", plugins: ["rogue"] }]);
|
|
237
|
+
expect(describeConflict(report.conflicts[0])).toMatch(
|
|
238
|
+
/"workspace" is a reserved chant command name, so no lexicon can register a command group with it/,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("a conflict between two lexicons is described without the reserved-name note", () => {
|
|
243
|
+
expect(describeConflict({ type: "command-group-name", key: "kube", plugins: ["k8s", "fly"] })).toBe(
|
|
244
|
+
' command-group-name "kube" from: k8s, fly',
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
231
248
|
test("no conflict for a single lexicon's own, non-reserved command-group name", () => {
|
|
232
249
|
const plugins = [
|
|
233
250
|
makePlugin("k8s", { commandGroup: { name: "kube", description: "d", commands: [] } }),
|
|
@@ -108,3 +108,16 @@ export function checkConflicts(plugins: LexiconPlugin[]): ConflictReport {
|
|
|
108
108
|
|
|
109
109
|
return { conflicts, warnings };
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* One line of the load-time error for a conflict. A command-group name that
|
|
114
|
+
* core reserves says so and says what to do, since the lexicon author reading
|
|
115
|
+
* it may not know the word is core's (#2529).
|
|
116
|
+
*/
|
|
117
|
+
export function describeConflict(c: ConflictEntry): string {
|
|
118
|
+
const line = ` ${c.type} "${c.key}" from: ${c.plugins.join(", ")}`;
|
|
119
|
+
if (c.type === "command-group-name" && RESERVED_COMMAND_NAMES.has(c.key)) {
|
|
120
|
+
return `${line} — "${c.key}" is a reserved chant command name, so no lexicon can register a command group with it. Rename the group.`;
|
|
121
|
+
}
|
|
122
|
+
return line;
|
|
123
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { mkdir, writeFile, rm, symlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { join, dirname, resolve } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { SKIP_MARKER } from "../discovery/files";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* chant #2519 — a project keeps a runnable `.ts` script with top-level side
|
|
12
|
+
* effects next to its declarations, and whole-project commands neither import
|
|
13
|
+
* nor lint it once the project says so, with an `exclude` glob or the skip
|
|
14
|
+
* marker.
|
|
15
|
+
*
|
|
16
|
+
* The script is the one from the issue: it reads `process.argv` as an Op name
|
|
17
|
+
* and exits the process with code 2. Imported by discovery, it reads chant's
|
|
18
|
+
* own argv and ends the command, which is what the first test shows. It also
|
|
19
|
+
* writes a file, so "imported" is visible apart from the exit code, and it
|
|
20
|
+
* indexes `process.env` dynamically, which EVL003 flags, so "linted" is
|
|
21
|
+
* visible in lint output.
|
|
22
|
+
*
|
|
23
|
+
* End-to-end through the real CLI (`main.ts`, spawned as `bin/chant` spawns
|
|
24
|
+
* it): an in-process `build()` would be killed by the script's `process.exit`,
|
|
25
|
+
* and the point is that every command's own path honours the setting.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const mainTsPath = resolve(thisDir, "main.ts");
|
|
30
|
+
const repoRoot = resolve(thisDir, "../../../..");
|
|
31
|
+
/** One CLI run can take a minute on a loaded CI runner, and a test makes up to three. */
|
|
32
|
+
const SPAWN_TIMEOUT = 150_000;
|
|
33
|
+
const TIMEOUT = 3 * SPAWN_TIMEOUT + 30_000;
|
|
34
|
+
|
|
35
|
+
describe("CLI end-to-end — discovery skips a side-effecting script (#2519)", () => {
|
|
36
|
+
let testDir: string;
|
|
37
|
+
let ranPath: string;
|
|
38
|
+
|
|
39
|
+
/** Write the project config; `extra` is spliced into the default export. */
|
|
40
|
+
async function writeConfig(extra: string): Promise<void> {
|
|
41
|
+
await writeFile(join(testDir, "chant.config.ts"), `export default { lexicons: ["k8s"]${extra} };\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The runner from the issue, optionally headed by `header`. */
|
|
45
|
+
async function writeRunner(header = ""): Promise<void> {
|
|
46
|
+
await writeFile(
|
|
47
|
+
join(testDir, "ops", "run.ts"),
|
|
48
|
+
[
|
|
49
|
+
header,
|
|
50
|
+
'import { writeFileSync } from "node:fs";',
|
|
51
|
+
"const op = process.argv[2];",
|
|
52
|
+
"const target = process.env[`OP_${op}`];",
|
|
53
|
+
`writeFileSync(${JSON.stringify(ranPath)}, String(op));`,
|
|
54
|
+
'console.error(`no op named "${op}"`, target ?? "");',
|
|
55
|
+
"process.exit(2);",
|
|
56
|
+
"",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
beforeEach(async () => {
|
|
62
|
+
testDir = await realpath(
|
|
63
|
+
await (async () => {
|
|
64
|
+
const dir = join(tmpdir(), `chant-discovery-skip-test-${Date.now()}-${Math.random()}`);
|
|
65
|
+
await mkdir(join(dir, "src"), { recursive: true });
|
|
66
|
+
await mkdir(join(dir, "ops"), { recursive: true });
|
|
67
|
+
return dir;
|
|
68
|
+
})(),
|
|
69
|
+
);
|
|
70
|
+
ranPath = join(testDir, "runner-ran.txt");
|
|
71
|
+
|
|
72
|
+
await mkdir(join(testDir, "node_modules", "@intentius"), { recursive: true });
|
|
73
|
+
await symlink(
|
|
74
|
+
join(repoRoot, "lexicons", "k8s"),
|
|
75
|
+
join(testDir, "node_modules", "@intentius", "chant-lexicon-k8s"),
|
|
76
|
+
"dir",
|
|
77
|
+
);
|
|
78
|
+
await writeFile(
|
|
79
|
+
join(testDir, "src", "infra.ts"),
|
|
80
|
+
[
|
|
81
|
+
'import { Namespace } from "@intentius/chant-lexicon-k8s";',
|
|
82
|
+
'export const ns = new Namespace({ metadata: { name: "discovery-skip" } });',
|
|
83
|
+
"",
|
|
84
|
+
].join("\n"),
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterEach(async () => {
|
|
89
|
+
await rm(testDir, { recursive: true, force: true });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
function runCli(args: string[]): { status: number | null; stdout: string; stderr: string } {
|
|
93
|
+
const result = spawnSync("npx", ["tsx", mainTsPath, ...args], {
|
|
94
|
+
cwd: repoRoot,
|
|
95
|
+
encoding: "utf-8",
|
|
96
|
+
timeout: SPAWN_TIMEOUT,
|
|
97
|
+
});
|
|
98
|
+
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("without exclude or the marker, the script runs under `chant build` and ends it", async () => {
|
|
102
|
+
await writeConfig("");
|
|
103
|
+
await writeRunner();
|
|
104
|
+
|
|
105
|
+
const { status, stderr } = runCli(["build", testDir]);
|
|
106
|
+
|
|
107
|
+
expect(status).toBe(2);
|
|
108
|
+
expect(stderr).toContain('no op named "build"');
|
|
109
|
+
expect(existsSync(ranPath)).toBe(true);
|
|
110
|
+
}, TIMEOUT);
|
|
111
|
+
|
|
112
|
+
test("an `exclude` glob keeps build and lint out of the script", async () => {
|
|
113
|
+
await writeConfig(', exclude: ["ops/**"]');
|
|
114
|
+
await writeRunner();
|
|
115
|
+
|
|
116
|
+
const build = runCli(["build", testDir]);
|
|
117
|
+
expect(build.stderr).not.toContain("no op named");
|
|
118
|
+
expect(build.status).toBe(0);
|
|
119
|
+
expect(build.stdout).toContain("name: discovery-skip");
|
|
120
|
+
|
|
121
|
+
const lint = runCli(["lint", testDir, "--format", "json"]);
|
|
122
|
+
expect(lint.status).toBe(0);
|
|
123
|
+
const diagnostics = JSON.parse(lint.stdout) as Array<{ file: string }>;
|
|
124
|
+
expect(diagnostics.some((d) => d.file.endsWith("infra.ts"))).toBe(true);
|
|
125
|
+
expect(diagnostics.filter((d) => d.file.includes("ops"))).toEqual([]);
|
|
126
|
+
|
|
127
|
+
expect(existsSync(ranPath), "the script was imported").toBe(false);
|
|
128
|
+
}, TIMEOUT);
|
|
129
|
+
|
|
130
|
+
test("an `exclude` glob keeps list and explain out of the script", async () => {
|
|
131
|
+
await writeConfig(', exclude: ["ops/**"]');
|
|
132
|
+
await writeRunner();
|
|
133
|
+
|
|
134
|
+
for (const command of ["list", "explain"]) {
|
|
135
|
+
const { status, stdout, stderr } = runCli([command, testDir]);
|
|
136
|
+
expect(stderr, command).not.toContain("no op named");
|
|
137
|
+
expect(status, command).toBe(0);
|
|
138
|
+
expect(stdout, command).toContain("ns");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
expect(existsSync(ranPath), "the script was imported").toBe(false);
|
|
142
|
+
}, TIMEOUT);
|
|
143
|
+
|
|
144
|
+
test("the skip marker keeps build and lint out of the script with no config change", async () => {
|
|
145
|
+
await writeConfig("");
|
|
146
|
+
await writeRunner(`// ${SKIP_MARKER}: a runner, not a declaration`);
|
|
147
|
+
|
|
148
|
+
const build = runCli(["build", testDir]);
|
|
149
|
+
expect(build.stderr).not.toContain("no op named");
|
|
150
|
+
expect(build.status).toBe(0);
|
|
151
|
+
|
|
152
|
+
const lint = runCli(["lint", testDir, "--format", "json"]);
|
|
153
|
+
expect(lint.status).toBe(0);
|
|
154
|
+
const diagnostics = JSON.parse(lint.stdout) as Array<{ file: string }>;
|
|
155
|
+
expect(diagnostics.filter((d) => d.file.includes("ops"))).toEqual([]);
|
|
156
|
+
|
|
157
|
+
expect(existsSync(ranPath), "the script was imported").toBe(false);
|
|
158
|
+
}, TIMEOUT);
|
|
159
|
+
|
|
160
|
+
test("the control: without exclude, `chant lint` lints the script", async () => {
|
|
161
|
+
await writeConfig("");
|
|
162
|
+
await writeRunner();
|
|
163
|
+
|
|
164
|
+
const lint = runCli(["lint", testDir, "--format", "json"]);
|
|
165
|
+
const diagnostics = JSON.parse(lint.stdout) as Array<{ file: string; ruleId: string }>;
|
|
166
|
+
expect(diagnostics.some((d) => d.file.endsWith(join("ops", "run.ts")) && d.ruleId === "EVL003")).toBe(true);
|
|
167
|
+
}, TIMEOUT);
|
|
168
|
+
});
|
|
@@ -70,6 +70,17 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
|
|
|
70
70
|
vi.restoreAllMocks();
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
+
test("--promote-to is forwarded to the generator as promoteTo (#2575)", async () => {
|
|
74
|
+
generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "stages: []", stages: [], jobs: [] });
|
|
75
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
76
|
+
|
|
77
|
+
const exit = await runBuild({ args: makeArgs({ env: "staging", promoteTo: "prod" }), plugins: [], serializers: [] });
|
|
78
|
+
|
|
79
|
+
expect(exit).toBe(0);
|
|
80
|
+
expect(generateComponentsPipelineMock).toHaveBeenCalledWith(".", "gitlab", { env: "staging", promoteTo: "prod" }, undefined, []);
|
|
81
|
+
vi.restoreAllMocks();
|
|
82
|
+
});
|
|
83
|
+
|
|
73
84
|
test("chant.config.ts's declared buildParams resolve, log, and are forwarded to generateComponentsPipeline", async () => {
|
|
74
85
|
loadChantConfigUpwardMock.mockResolvedValue({
|
|
75
86
|
config: { buildParams: { tier: { type: "string", default: "light" } } },
|
|
@@ -51,7 +51,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
51
51
|
const result = await generateComponentsPipeline(
|
|
52
52
|
args.path,
|
|
53
53
|
lexicon,
|
|
54
|
-
{ env: args.env },
|
|
54
|
+
{ env: args.env, ...(args.promoteTo ? { promoteTo: args.promoteTo } : {}) },
|
|
55
55
|
args.sandbox,
|
|
56
56
|
paramsResolution.provenance,
|
|
57
57
|
);
|
package/src/cli/handlers/dev.ts
CHANGED
|
@@ -33,6 +33,9 @@ export async function runDevOnboard(ctx: CommandContext): Promise<number> {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const { onboardCommand, printOnboardResult } = await import("../commands/onboard");
|
|
36
|
+
// chant#2578 — so a lexicon the project declares by path is recognised.
|
|
37
|
+
const { recordProjectLexicons } = await import("../plugins");
|
|
38
|
+
await recordProjectLexicons(process.cwd());
|
|
36
39
|
const result = onboardCommand({ name, verbose: ctx.args.verbose });
|
|
37
40
|
await printOnboardResult(result, name);
|
|
38
41
|
return result.success ? 0 : 1;
|
|
@@ -2,6 +2,7 @@ import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import type { ParsedArgs } from "../registry";
|
|
3
3
|
import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
|
|
4
4
|
import { AttrRef } from "../../attrref";
|
|
5
|
+
import { GRAPH_IR_VERSION } from "../../graph-ir";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The aws emulator capability, as the real plugin declares it. `--live`
|
|
@@ -226,6 +227,25 @@ describe("runGraph", () => {
|
|
|
226
227
|
expect(ir.edges).toContainEqual({ from: "subnet", to: "vpc", kind: "ref", viaAttr: "network" });
|
|
227
228
|
});
|
|
228
229
|
|
|
230
|
+
// #2529 — the IR carries its version, as the first key, so a reader can
|
|
231
|
+
// decide how to read the rest before it reads anything else.
|
|
232
|
+
test("--format ir leads with the IR version", async () => {
|
|
233
|
+
lintClean(); discovered();
|
|
234
|
+
const exit = await runGraph({ args: makeArgs({ format: "ir" }), plugins: [], serializers: [] });
|
|
235
|
+
expect(exit).toBe(0);
|
|
236
|
+
const ir = JSON.parse(stdoutBuf.join("\n"));
|
|
237
|
+
expect(ir.version).toBe(GRAPH_IR_VERSION);
|
|
238
|
+
expect(GRAPH_IR_VERSION).toBe(1);
|
|
239
|
+
expect(Object.keys(ir)[0]).toBe("version");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("the other view formats carry no version field", async () => {
|
|
243
|
+
lintClean(); discovered();
|
|
244
|
+
const exit = await runGraph({ args: makeArgs({ format: "mermaid" }), plugins: [], serializers: [] });
|
|
245
|
+
expect(exit).toBe(0);
|
|
246
|
+
expect(stdoutBuf.join("\n")).not.toContain("version");
|
|
247
|
+
});
|
|
248
|
+
|
|
229
249
|
// #1675 — discovery scans sourceDir, but ops live beside it (project root,
|
|
230
250
|
// `ops/`) and are found by discoverOps from the git root. The IR joins them.
|
|
231
251
|
describe("ops outside sourceDir reach the IR (#1675)", () => {
|
|
@@ -2,7 +2,7 @@ import { dirname, resolve } from "node:path";
|
|
|
2
2
|
import { discoverOps } from "../../op/discover";
|
|
3
3
|
import { discover } from "../../discovery/index";
|
|
4
4
|
import { partitionByLexicon, computeStackGraph, build, mergeBuildRootEntities } from "../../build";
|
|
5
|
-
import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
|
|
5
|
+
import { buildGraphIr, buildLiveGraphIr, collectUnobserved, overlayGraphs, sourceOverlayGraphs, GRAPH_IR_VERSION, type GraphIR, type IRPipeline, type LiveObservation } from "../../graph-ir";
|
|
6
6
|
import { behaviourOverlay } from "../../behaviour-overlay";
|
|
7
7
|
import type { SerializerResult } from "../../serializer";
|
|
8
8
|
import { applyBehaviourOverlay, behaviourRequestFromIr } from "../../behaviour-graph";
|
|
@@ -886,7 +886,9 @@ async function emitIr(
|
|
|
886
886
|
}
|
|
887
887
|
case "ir":
|
|
888
888
|
default:
|
|
889
|
-
|
|
889
|
+
// The version leads the document (#2529), so a reader sees it before
|
|
890
|
+
// anything it might not understand.
|
|
891
|
+
console.log(JSON.stringify({ version: GRAPH_IR_VERSION, ...ir }, null, 2));
|
|
890
892
|
return 0;
|
|
891
893
|
}
|
|
892
894
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lexiconNames } from "../../lexicon-module";
|
|
1
2
|
import { resolve } from "node:path";
|
|
2
3
|
import { commandBuildParams } from "../build-params-cli";
|
|
3
4
|
import { build } from "../../build";
|
|
@@ -559,7 +560,7 @@ interface BetweenDiffArgs {
|
|
|
559
560
|
async function runLifecycleDiffBetween(args: BetweenDiffArgs): Promise<number> {
|
|
560
561
|
await fetchLifecycle();
|
|
561
562
|
const { config } = await loadChantConfig(resolve("."));
|
|
562
|
-
const lexicons = args.lexiconFilter ? [args.lexiconFilter] : config.lexicons ?? [];
|
|
563
|
+
const lexicons = args.lexiconFilter ? [args.lexiconFilter] : lexiconNames(config.lexicons ?? []);
|
|
563
564
|
if (lexicons.length === 0) {
|
|
564
565
|
console.error(formatError({ message: "No lexicons to diff — pass a lexicon or declare `lexicons` in chant.config." }));
|
|
565
566
|
return 1;
|
package/src/cli/handlers/misc.ts
CHANGED
|
@@ -114,6 +114,21 @@ export async function runAudit(ctx: CommandContext): Promise<number> {
|
|
|
114
114
|
return result.exitCode;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// `--max-files` (#2528) only means something to the local walk. A URL audit
|
|
118
|
+
// fetches through the host's tree API with its own caps, so the flag is
|
|
119
|
+
// refused there rather than silently ignored.
|
|
120
|
+
const maxFiles = args.maxFiles;
|
|
121
|
+
if (maxFiles !== undefined) {
|
|
122
|
+
if (!Number.isInteger(maxFiles) || maxFiles < 1) {
|
|
123
|
+
console.error(formatError({ message: `--max-files must be a positive integer (got "${maxFiles}")` }));
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
if (/^https?:\/\//.test(args.path)) {
|
|
127
|
+
console.error(formatError({ message: "--max-files applies to a local path; a repository URL is fetched with its own limits." }));
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
117
132
|
// HTML report customization: --template <file> (full override) + --theme <file> (JSON knobs).
|
|
118
133
|
let template: string | undefined;
|
|
119
134
|
let theme: ReportTheme | undefined;
|
|
@@ -146,6 +161,7 @@ export async function runAudit(ctx: CommandContext): Promise<number> {
|
|
|
146
161
|
template,
|
|
147
162
|
theme,
|
|
148
163
|
toolVersion: CHANT_VERSION,
|
|
164
|
+
maxFiles,
|
|
149
165
|
});
|
|
150
166
|
printAuditResult(result);
|
|
151
167
|
return result.exitCode;
|