@intentius/chant 0.79.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.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 +14 -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/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/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/runtimes/local.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.ts +2 -1
- 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 +69 -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 +14 -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/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/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/discover-convergence.test.ts +70 -0
- package/src/op/discover.ts +4 -0
- package/src/op/gate-approval.ts +3 -1
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/runtimes/local.ts +2 -1
- 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
|
@@ -6,14 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { existsSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
9
|
-
import { join } from "path";
|
|
9
|
+
import { join, resolve } from "path";
|
|
10
10
|
import { auditFiles, type AuditInput, type AuditFinding, type ChecksProvider, type SuppressionStats } from "../../audit/core";
|
|
11
|
-
import { AUDIT_LEXICONS, classifyFiles,
|
|
11
|
+
import { AUDIT_LEXICONS, classifyFiles, loadAuditPlugins, unclaimedFiles, walkCandidates, type CandidateWalk, type DetectPlugin, type RepoFile, type UnclaimedFile } from "../../audit/discover";
|
|
12
12
|
import { RULE_CATALOG, resolveAuditCatalog, type RuleMeta } from "../../audit/catalog";
|
|
13
13
|
import { scanForSecrets, parseSecretsConfig, type SecretsScanOptions } from "../../audit/secrets";
|
|
14
14
|
import { auditWranglerConfigs } from "../../audit/wrangler";
|
|
15
15
|
import { auditNginxConfigs } from "../../audit/nginx";
|
|
16
|
-
import { auditTerraformState } from "../../audit/terraform-state";
|
|
16
|
+
import { auditTerraformState, isTerraformStatePath } from "../../audit/terraform-state";
|
|
17
|
+
import { warnDiscoveryChanges } from "../../discovery/convergence";
|
|
17
18
|
import { renderMarkdown } from "../../audit/report";
|
|
18
19
|
import { renderHtml, type ReportTheme } from "../../audit/report-html";
|
|
19
20
|
import { buildReportJson, REPORT_SCHEMA_VERSION, type AuditSnapshot } from "../../audit/report-model";
|
|
@@ -21,6 +22,8 @@ import { fetchRepoFiles, resolveActionSha, resolveImageDigest, resolveRepoCommit
|
|
|
21
22
|
import { extractUnpinnedActions, extractUnpinnedImages } from "../../audit/proof";
|
|
22
23
|
import type { ProveOptions } from "../../audit/proof";
|
|
23
24
|
import type { Severity } from "../../lint/rule";
|
|
25
|
+
import { formatWarning } from "../format";
|
|
26
|
+
import { lexiconModulePath, lexiconPackagesToInstall, lexiconSourceLabel } from "../../lexicon-module";
|
|
24
27
|
|
|
25
28
|
export type AuditFormat = "stylish" | "json" | "sarif" | "markdown" | "html";
|
|
26
29
|
export type AuditTier = "merge-worthy" | "all";
|
|
@@ -68,6 +71,23 @@ export interface AuditCommandOptions {
|
|
|
68
71
|
* present (URL targets have no local config file to read).
|
|
69
72
|
*/
|
|
70
73
|
secretsScan?: SecretsScanOptions;
|
|
74
|
+
/**
|
|
75
|
+
* Files the local walk takes before it stops, counting every file
|
|
76
|
+
* (`--max-files`, #2528). Defaults to 1000. Local paths only.
|
|
77
|
+
*/
|
|
78
|
+
maxFiles?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A diagnostic about the run rather than the repository. Printed on stderr by
|
|
83
|
+
* `printAuditResult`, never part of `output`, so the report on stdout is the
|
|
84
|
+
* same with or without one.
|
|
85
|
+
*/
|
|
86
|
+
export interface AuditWarning {
|
|
87
|
+
/** The path the warning is about, when there is one. */
|
|
88
|
+
file?: string;
|
|
89
|
+
message: string;
|
|
90
|
+
hint?: string;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
export interface AuditCommandResult {
|
|
@@ -96,14 +116,27 @@ export interface AuditCommandResult {
|
|
|
96
116
|
* summary line.
|
|
97
117
|
*/
|
|
98
118
|
suppressedCount?: number;
|
|
119
|
+
/** Run diagnostics for stderr (#2528). */
|
|
120
|
+
warnings?: AuditWarning[];
|
|
99
121
|
}
|
|
100
122
|
|
|
101
123
|
/** Exit code when the audit had no lexicons to look with. Distinct from 1 (findings / failure). */
|
|
102
124
|
export const NO_LEXICONS_EXIT_CODE = 2;
|
|
103
125
|
|
|
104
|
-
/**
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
/**
|
|
127
|
+
* What to tell the user about lexicons that did not load: an `npm i` line for
|
|
128
|
+
* the package-backed ones, and the declared path for any lexicon
|
|
129
|
+
* `chant.config.ts` names by path (#2520), which has no package to install
|
|
130
|
+
* (chant#2578).
|
|
131
|
+
*/
|
|
132
|
+
function missingLexiconRemedy(names: readonly string[]): string {
|
|
133
|
+
const parts: string[] = [];
|
|
134
|
+
const pkgs = lexiconPackagesToInstall(names);
|
|
135
|
+
if (pkgs.length > 0) parts.push(`npm i ${pkgs.join(" ")}`);
|
|
136
|
+
for (const name of names) {
|
|
137
|
+
if (lexiconModulePath(name) !== undefined) parts.push(`${name} is declared by path: ${lexiconSourceLabel(name)}`);
|
|
138
|
+
}
|
|
139
|
+
return parts.join("; ");
|
|
107
140
|
}
|
|
108
141
|
|
|
109
142
|
/** Missing audit lexicons the unclaimed files pointed at, in first-seen order. */
|
|
@@ -117,7 +150,8 @@ function wantedLexicons(unclaimed: UnclaimedFile[]): string[] {
|
|
|
117
150
|
* them on the same resolution path.
|
|
118
151
|
*/
|
|
119
152
|
export function installLine(lexicons: string[], target: string): string {
|
|
120
|
-
|
|
153
|
+
// chant#2578 — a lexicon declared by path has no package for npx to fetch.
|
|
154
|
+
const pkgs = ["@intentius/chant", ...lexiconPackagesToInstall(lexicons)];
|
|
121
155
|
return `npx ${pkgs.map((p) => `-p ${p}`).join(" ")} chant audit ${target}`;
|
|
122
156
|
}
|
|
123
157
|
|
|
@@ -127,7 +161,7 @@ function missingLexiconHint(unclaimed: UnclaimedFile[]): string | undefined {
|
|
|
127
161
|
if (wanted.length === 0) return undefined;
|
|
128
162
|
const n = unclaimed.length;
|
|
129
163
|
return `${n} file${n === 1 ? " looks" : "s look"} like ${wanted.join("/")} but ${wanted.length === 1 ? "that lexicon is" : "those lexicons are"} not installed, so ${n === 1 ? "it was" : "they were"} skipped` +
|
|
130
|
-
` (
|
|
164
|
+
` (${missingLexiconRemedy(wanted)}).`;
|
|
131
165
|
}
|
|
132
166
|
|
|
133
167
|
/** Human-readable diagnostic for the zero-lexicon case. */
|
|
@@ -372,6 +406,34 @@ async function buildSnapshot(options: AuditCommandOptions, files: string[], isUr
|
|
|
372
406
|
};
|
|
373
407
|
}
|
|
374
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Warnings for the two level-0 changes #2528 makes in the next release, one
|
|
411
|
+
* release ahead of it (#2525). Neither changes today's report: a truncated walk
|
|
412
|
+
* is still reported as if it were whole, and TF023 still reads only the root
|
|
413
|
+
* `.gitignore`.
|
|
414
|
+
*/
|
|
415
|
+
export function walkWarnings(walk: CandidateWalk, target: string): AuditWarning[] {
|
|
416
|
+
const warnings: AuditWarning[] = [];
|
|
417
|
+
if (walk.truncated) {
|
|
418
|
+
warnings.push({
|
|
419
|
+
message:
|
|
420
|
+
`the scan of ${target} stopped at ${walk.maxFiles} files, so part of the tree was not audited. ` +
|
|
421
|
+
"The limit counts every file the walk reaches, not only the ones it audits.",
|
|
422
|
+
hint:
|
|
423
|
+
"Raise it with --max-files <n>. From the next release, text and JSON output will also report a truncated scan.",
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
for (const change of walk.nestedGitignore) {
|
|
427
|
+
warnings.push({
|
|
428
|
+
file: change.path,
|
|
429
|
+
message: `TF023 reports this path because chant audit reads only the root .gitignore, but ${change.gitignore} ignores it.`,
|
|
430
|
+
hint:
|
|
431
|
+
"From the next release chant audit reads every .gitignore between a file and the scan root, and TF023 will no longer report this path.",
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
return warnings;
|
|
435
|
+
}
|
|
436
|
+
|
|
375
437
|
/** Run the audit and produce a rendered result. */
|
|
376
438
|
export async function auditCommand(options: AuditCommandOptions): Promise<AuditCommandResult> {
|
|
377
439
|
const format = options.format ?? "stylish";
|
|
@@ -387,6 +449,7 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
387
449
|
// render the same way (#1623).
|
|
388
450
|
const plugins = options.plugins ?? (await loadAuditPlugins());
|
|
389
451
|
let candidates: RepoFile[];
|
|
452
|
+
let warnings: AuditWarning[] = [];
|
|
390
453
|
if (isUrl) {
|
|
391
454
|
try {
|
|
392
455
|
// Fetch the whole repo's candidate files (all lexicons, not just CI) and
|
|
@@ -406,8 +469,21 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
406
469
|
// One walk, plugin-delegated detection. CI (path), Dockerfiles (name), and
|
|
407
470
|
// Helm charts (bundle) are special-cased by the classifier since content
|
|
408
471
|
// shape alone can't disambiguate them.
|
|
409
|
-
|
|
472
|
+
const walk = walkCandidates(options.path, { maxFiles: options.maxFiles });
|
|
473
|
+
candidates = walk.files;
|
|
474
|
+
warnings = walkWarnings(walk, options.path);
|
|
475
|
+
// #2527's warning release: the converged walk skips git-ignored files and,
|
|
476
|
+
// inside a project, child projects. Terraform state paths are TF023's own
|
|
477
|
+
// question and stay out of it. `candidates` is unchanged.
|
|
478
|
+
await warnDiscoveryChanges({
|
|
479
|
+
walker: "audit",
|
|
480
|
+
root: options.path,
|
|
481
|
+
files: candidates.filter((f) => !isTerraformStatePath(f.path)).map((f) => resolve(options.path, f.path)),
|
|
482
|
+
});
|
|
410
483
|
}
|
|
484
|
+
// Only a non-empty list rides on the result, so a run with nothing to warn
|
|
485
|
+
// about returns exactly the object it did before #2528.
|
|
486
|
+
const withWarnings = warnings.length > 0 ? { warnings } : {};
|
|
411
487
|
// A local target hands its directory to the classifier, so an input whose
|
|
412
488
|
// path names a directory carries the directory itself (#2217): terraform's
|
|
413
489
|
// parse-to-graph follows a root module's local `module` calls from there. A
|
|
@@ -439,14 +515,14 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
439
515
|
|
|
440
516
|
if (plugins.length === 0) {
|
|
441
517
|
const output = format === "json" ? renderNoLexiconsJson(options.path, unclaimed) : renderNoLexicons(options.path, unclaimed);
|
|
442
|
-
return { success: true, status: "no-lexicons", output, findings: [...secretsFindings, ...wranglerFindings, ...nginxFindings, ...terraformStateFindings], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr" };
|
|
518
|
+
return { success: true, status: "no-lexicons", output, findings: [...secretsFindings, ...wranglerFindings, ...nginxFindings, ...terraformStateFindings], scanned: [], unclaimed, exitCode: NO_LEXICONS_EXIT_CODE, stream: format === "json" ? "stdout" : "stderr", ...withWarnings };
|
|
443
519
|
}
|
|
444
520
|
|
|
445
521
|
const missingLexiconNote = missingLexiconHint(unclaimed);
|
|
446
522
|
|
|
447
523
|
if (inputs.length === 0 && secretsFindings.length === 0 && wranglerFindings.length === 0 && nginxFindings.length === 0 && terraformStateFindings.length === 0) {
|
|
448
524
|
const output = `No auditable files found under ${options.path}.${missingLexiconNote ? ` ${missingLexiconNote}` : ""}`;
|
|
449
|
-
return { success: true, status: "ok", output, findings: [], scanned: [], unclaimed, exitCode: 0 };
|
|
525
|
+
return { success: true, status: "ok", output, findings: [], scanned: [], unclaimed, exitCode: 0, ...withWarnings };
|
|
450
526
|
}
|
|
451
527
|
|
|
452
528
|
let findings: AuditFinding[] = [];
|
|
@@ -545,22 +621,20 @@ export async function auditCommand(options: AuditCommandOptions): Promise<AuditC
|
|
|
545
621
|
} catch (err) {
|
|
546
622
|
return { success: false, output, findings, scanned, exitCode: 1, error: `Failed to write ${options.output}: ${err instanceof Error ? err.message : String(err)}` };
|
|
547
623
|
}
|
|
548
|
-
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode, wroteTo: options.output, suppressedCount: suppressionStats.count };
|
|
624
|
+
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode, wroteTo: options.output, suppressedCount: suppressionStats.count, ...withWarnings };
|
|
549
625
|
}
|
|
550
626
|
|
|
551
|
-
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode, suppressedCount: suppressionStats.count };
|
|
627
|
+
return { success: true, status: "ok", output, findings, scanned, unclaimed, exitCode, suppressedCount: suppressionStats.count, ...withWarnings };
|
|
552
628
|
}
|
|
553
629
|
|
|
554
|
-
/** Print an audit result to stdout. */
|
|
630
|
+
/** Print an audit result to stdout, and its warnings to stderr. */
|
|
555
631
|
export function printAuditResult(result: AuditCommandResult): void {
|
|
556
632
|
if (!result.success) {
|
|
557
633
|
console.error(result.error ?? "Audit failed");
|
|
558
634
|
return;
|
|
559
635
|
}
|
|
560
|
-
if (result.wroteTo) {
|
|
561
|
-
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
if (result.stream === "stderr") console.error(result.output);
|
|
636
|
+
if (result.wroteTo) console.error(`Wrote report to ${result.wroteTo}`);
|
|
637
|
+
else if (result.stream === "stderr") console.error(result.output);
|
|
565
638
|
else console.log(result.output);
|
|
639
|
+
for (const w of result.warnings ?? []) console.error(formatWarning(w));
|
|
566
640
|
}
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
* validates against its own schemas.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
import { lexiconNames as declaredLexiconNames } from "../../lexicon-module";
|
|
36
37
|
import { existsSync, readdirSync } from "fs";
|
|
37
38
|
import { join } from "path";
|
|
38
39
|
import { build, type BuildResult } from "../../build";
|
|
@@ -141,7 +142,7 @@ export async function checkExamplesBuild(lexiconDir: string): Promise<ExampleBui
|
|
|
141
142
|
const { config: exampleConfig } = await loadChantConfig(join(examplesDir, entry.name));
|
|
142
143
|
const lexiconNames =
|
|
143
144
|
exampleConfig.lexicons && exampleConfig.lexicons.length > 0
|
|
144
|
-
? exampleConfig.lexicons
|
|
145
|
+
? declaredLexiconNames(exampleConfig.lexicons)
|
|
145
146
|
: await detectLexicons(files);
|
|
146
147
|
if (lexiconNames.length === 0) {
|
|
147
148
|
results.push({
|
|
@@ -95,6 +95,9 @@ export async function doctorCommand(path: string): Promise<DoctorReport> {
|
|
|
95
95
|
const lexicons = config?.lexicons as string[] | undefined;
|
|
96
96
|
if (lexicons && Array.isArray(lexicons)) {
|
|
97
97
|
for (const lex of lexicons) {
|
|
98
|
+
// chant #2520 — a lexicon declared by module path is project source,
|
|
99
|
+
// not a package `chant update` copies types from.
|
|
100
|
+
if (typeof lex !== "string") continue;
|
|
98
101
|
const lexDir = join(projectPath, ".chant", "types", `lexicon-${lex}`);
|
|
99
102
|
if (!existsSync(lexDir)) {
|
|
100
103
|
checks.push({ name: `lexicon-${lex}-types`, status: "fail", message: `.chant/types/lexicon-${lex}/ not found — run chant update` });
|
|
@@ -23,7 +23,8 @@ import { scanAgentConfigs } from "../../agents/discover";
|
|
|
23
23
|
import type { AgentRuntime, AgentScope } from "../../agents/types";
|
|
24
24
|
import type { AgentImportOutcome } from "../../agents/importer";
|
|
25
25
|
import type { LexiconPlugin } from "../../lexicon";
|
|
26
|
-
import { loadPlugin } from "../plugins";
|
|
26
|
+
import { loadPlugin, recordProjectLexicons } from "../plugins";
|
|
27
|
+
import { lexiconModulePath, lexiconSourceLabel } from "../../lexicon-module";
|
|
27
28
|
|
|
28
29
|
/** The lexicon used when `--lexicon` isn't given: the one that models agent workloads. */
|
|
29
30
|
export const DEFAULT_AGENT_LEXICON = "fountain";
|
|
@@ -91,18 +92,24 @@ export async function importAgentsCommand(opts: ImportAgentsOptions = {}): Promi
|
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
// chant#2578 — a lexicon the project's chant.config declares by path loads
|
|
96
|
+
// from that module, so read the declarations before loading the plugin.
|
|
97
|
+
if (!opts.pluginLoader) await recordProjectLexicons(process.cwd());
|
|
98
|
+
|
|
94
99
|
let plugin: LexiconPlugin;
|
|
95
100
|
try {
|
|
96
101
|
plugin = await (opts.pluginLoader ?? loadPlugin)(lexiconName);
|
|
97
102
|
} catch (err) {
|
|
103
|
+
const remedy =
|
|
104
|
+
lexiconModulePath(lexiconName) !== undefined
|
|
105
|
+
? `The ${lexiconName} lexicon is declared by path in chant.config.ts: ${lexiconSourceLabel(lexiconName)}`
|
|
106
|
+
: `Install it with: npm i @intentius/chant-lexicon-${lexiconName}`;
|
|
98
107
|
return {
|
|
99
108
|
success: false,
|
|
100
109
|
generatedFiles: [],
|
|
101
110
|
summary: { discovered: scan.sites.length, mapped: 0 },
|
|
102
111
|
warnings,
|
|
103
|
-
error:
|
|
104
|
-
`Could not load the ${lexiconName} lexicon: ${err instanceof Error ? err.message : String(err)}\n` +
|
|
105
|
-
`Install it with: npm i @intentius/chant-lexicon-${lexiconName}`,
|
|
112
|
+
error: `Could not load the ${lexiconName} lexicon: ${err instanceof Error ? err.message : String(err)}\n${remedy}`,
|
|
106
113
|
};
|
|
107
114
|
}
|
|
108
115
|
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { join, resolve, dirname } from "path";
|
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import { formatSuccess, formatWarning } from "../format";
|
|
6
|
-
import { loadPlugin } from "../plugins";
|
|
6
|
+
import { loadPlugin, recordProjectLexicons } from "../plugins";
|
|
7
|
+
import { lexiconModulePath, lexiconSourceLabel } from "../../lexicon-module";
|
|
7
8
|
import { MCP_CONFIG_FILENAME, detectPackageManager, generateMcpConfig, mcpConfigPath } from "../mcp-config";
|
|
8
9
|
|
|
9
10
|
/** Read the current chant package version from our own package.json. */
|
|
@@ -53,16 +54,27 @@ export interface InitResult {
|
|
|
53
54
|
warnings: string[];
|
|
54
55
|
/** Error message if failed */
|
|
55
56
|
error?: string;
|
|
57
|
+
/**
|
|
58
|
+
* chant#2578 — where the lexicon loads from when the directory's existing
|
|
59
|
+
* chant.config declares it by path. There is no package to depend on or
|
|
60
|
+
* install for it.
|
|
61
|
+
*/
|
|
62
|
+
lexiconModule?: string;
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
/**
|
|
59
66
|
* Generate package.json content
|
|
60
67
|
*/
|
|
61
|
-
function generatePackageJson(
|
|
68
|
+
function generatePackageJson(
|
|
69
|
+
lexicon: string,
|
|
70
|
+
extraScripts?: Record<string, string>,
|
|
71
|
+
lexiconIsPackage = true,
|
|
72
|
+
): string {
|
|
62
73
|
const ver = getChantVersion();
|
|
63
74
|
const dependencies: Record<string, string> = {
|
|
64
75
|
"@intentius/chant": `^${ver}`,
|
|
65
|
-
|
|
76
|
+
// chant#2578 — a lexicon declared by path is not a package to depend on.
|
|
77
|
+
...(lexiconIsPackage ? { [`@intentius/chant-lexicon-${lexicon}`]: `^${ver}` } : {}),
|
|
66
78
|
};
|
|
67
79
|
|
|
68
80
|
const pkg = {
|
|
@@ -252,6 +264,17 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
252
264
|
mkdirSync(targetDir, { recursive: true });
|
|
253
265
|
}
|
|
254
266
|
|
|
267
|
+
// chant#2578 — `--force` into an existing project keeps its chant.config.
|
|
268
|
+
// When that config declares the lexicon by path, it loads from the module
|
|
269
|
+
// and there is no package to depend on or install.
|
|
270
|
+
let lexiconModule: string | undefined;
|
|
271
|
+
if (existsSync(join(targetDir, "chant.config.ts")) || existsSync(join(targetDir, "chant.config.json"))) {
|
|
272
|
+
await recordProjectLexicons(targetDir);
|
|
273
|
+
if (lexiconModulePath(options.lexicon) !== undefined) {
|
|
274
|
+
lexiconModule = lexiconSourceLabel(options.lexicon, targetDir);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
255
278
|
// Load plugin early to get template set (used for scripts + source files)
|
|
256
279
|
let templateSet: import("../../lexicon").InitTemplateSet | undefined;
|
|
257
280
|
try {
|
|
@@ -272,7 +295,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
272
295
|
// Generate package.json
|
|
273
296
|
writeIfNotExists(
|
|
274
297
|
join(targetDir, "package.json"),
|
|
275
|
-
generatePackageJson(options.lexicon, templateSet?.scripts),
|
|
298
|
+
generatePackageJson(options.lexicon, templateSet?.scripts, lexiconModule === undefined),
|
|
276
299
|
"package.json",
|
|
277
300
|
createdFiles,
|
|
278
301
|
warnings,
|
|
@@ -361,29 +384,32 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
361
384
|
warnings,
|
|
362
385
|
);
|
|
363
386
|
|
|
364
|
-
// Scaffold .chant/types/lexicon-{lexicon}/ stub
|
|
365
|
-
|
|
366
|
-
|
|
387
|
+
// Scaffold .chant/types/lexicon-{lexicon}/ stub. A lexicon declared by path
|
|
388
|
+
// is imported by its path, so it has no package name to stub (chant#2578).
|
|
389
|
+
if (lexiconModule === undefined) {
|
|
390
|
+
const lexiconTypesDir = join(targetDir, ".chant", "types", `lexicon-${options.lexicon}`);
|
|
391
|
+
mkdirSync(lexiconTypesDir, { recursive: true });
|
|
367
392
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
393
|
+
writeIfNotExists(
|
|
394
|
+
join(lexiconTypesDir, "package.json"),
|
|
395
|
+
JSON.stringify(
|
|
396
|
+
{ name: `@intentius/chant-lexicon-${options.lexicon}`, version: "0.0.0", types: "./index.d.ts" },
|
|
397
|
+
null,
|
|
398
|
+
2,
|
|
399
|
+
),
|
|
400
|
+
`.chant/types/lexicon-${options.lexicon}/package.json`,
|
|
401
|
+
createdFiles,
|
|
402
|
+
warnings,
|
|
403
|
+
);
|
|
379
404
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
405
|
+
writeIfNotExists(
|
|
406
|
+
join(lexiconTypesDir, "index.d.ts"),
|
|
407
|
+
`// Lexicon type stubs — run "chant update" to sync full types\nexport {};\n`,
|
|
408
|
+
`.chant/types/lexicon-${options.lexicon}/index.d.ts`,
|
|
409
|
+
createdFiles,
|
|
410
|
+
warnings,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
387
413
|
|
|
388
414
|
// Generate the project's MCP config. It lands in the directory init was
|
|
389
415
|
// pointed at, like everything else init produces, and at the path
|
|
@@ -425,6 +451,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
|
|
|
425
451
|
success: true,
|
|
426
452
|
createdFiles,
|
|
427
453
|
warnings,
|
|
454
|
+
...(lexiconModule !== undefined ? { lexiconModule } : {}),
|
|
428
455
|
};
|
|
429
456
|
}
|
|
430
457
|
|
|
@@ -453,6 +480,11 @@ export async function printInitResult(
|
|
|
453
480
|
|
|
454
481
|
console.log("");
|
|
455
482
|
|
|
483
|
+
if (result.lexiconModule !== undefined) {
|
|
484
|
+
console.log(`The lexicon loads from ${result.lexiconModule}, as chant.config.ts declares. It has no package to install.`);
|
|
485
|
+
console.log("");
|
|
486
|
+
}
|
|
487
|
+
|
|
456
488
|
const pm = detectPackageManager(options?.cwd);
|
|
457
489
|
|
|
458
490
|
// Interactive install prompt
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "vitest";
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
2
|
import { lintCommand, isLintRule, loadPluginRules, LEXICON_RESOLUTION_RULE_ID, type LintOptions } from "./lint";
|
|
3
3
|
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
4
4
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
@@ -6,6 +6,7 @@ import { join, resolve } from "node:path";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { params, setBuildParams } from "../../params";
|
|
9
|
+
import { resetDiscoveryWarnings } from "../../discovery/convergence";
|
|
9
10
|
|
|
10
11
|
describe("lintCommand", () => {
|
|
11
12
|
let testDir: string;
|
|
@@ -641,6 +642,62 @@ describe("lintCommand — git-ignored files are not linted", () => {
|
|
|
641
642
|
});
|
|
642
643
|
});
|
|
643
644
|
|
|
645
|
+
/**
|
|
646
|
+
* chant#2527's warning release for lint: a child project is linted today and
|
|
647
|
+
* stops the converged walk next release; a scan root that is itself ignored
|
|
648
|
+
* lints nothing today and is linted next release. Both still lint exactly
|
|
649
|
+
* what they did, with a warning naming the files and the glob.
|
|
650
|
+
*/
|
|
651
|
+
describe("lintCommand — files the converged walker reads differently (#2527)", () => {
|
|
652
|
+
let testDir: string;
|
|
653
|
+
let stderr: string[];
|
|
654
|
+
const EVL003 = `const t = { a: 1 };\nexport function g(k: string) { return t[k]; }\n`;
|
|
655
|
+
|
|
656
|
+
beforeEach(async () => {
|
|
657
|
+
testDir = join(tmpdir(), `chant-lint-2527-${Date.now()}-${Math.random()}`);
|
|
658
|
+
await mkdir(testDir, { recursive: true });
|
|
659
|
+
process.env.NO_COLOR = "1";
|
|
660
|
+
resetDiscoveryWarnings();
|
|
661
|
+
stderr = [];
|
|
662
|
+
vi.spyOn(console, "error").mockImplementation((...args: unknown[]) => void stderr.push(args.join(" ")));
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
afterEach(async () => {
|
|
666
|
+
vi.restoreAllMocks();
|
|
667
|
+
await rm(testDir, { recursive: true, force: true });
|
|
668
|
+
delete process.env.NO_COLOR;
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
test("a nested child project is still linted, and named with its include glob", async () => {
|
|
672
|
+
await writeFile(join(testDir, "chant.config.json"), JSON.stringify({ knowledge: { dir: "knowledge" } }));
|
|
673
|
+
await writeFile(join(testDir, "index.ts"), `export const x = 1;\n`);
|
|
674
|
+
await mkdir(join(testDir, "stacks", "east"), { recursive: true });
|
|
675
|
+
await writeFile(join(testDir, "stacks", "east", "chant.config.ts"), `export default {};\n`);
|
|
676
|
+
await writeFile(join(testDir, "stacks", "east", "east.ts"), EVL003);
|
|
677
|
+
|
|
678
|
+
const result = await lintCommand({ path: testDir, format: "stylish" });
|
|
679
|
+
expect(result.diagnostics.some((d) => d.file.includes("east.ts") && d.ruleId === "EVL003")).toBe(true);
|
|
680
|
+
const warning = stderr.find((l) => l.startsWith("warning: chant lint under"));
|
|
681
|
+
expect(warning).toContain(" stacks/east/chant.config.ts, stacks/east/east.ts: stacks/east/ is a child project");
|
|
682
|
+
expect(warning).toContain('To keep reading them after the change, add "stacks/east" to include in');
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("an ignored scan root lints nothing today, and says it will be linted", async () => {
|
|
686
|
+
execFileSync("git", ["init", "-q"], { cwd: testDir });
|
|
687
|
+
await writeFile(join(testDir, ".gitignore"), "/carveout/\n");
|
|
688
|
+
await mkdir(join(testDir, "carveout", "src"), { recursive: true });
|
|
689
|
+
await writeFile(join(testDir, "carveout", "src", "main.ts"), EVL003);
|
|
690
|
+
|
|
691
|
+
const result = await lintCommand({ path: join(testDir, "carveout", "src"), format: "stylish" });
|
|
692
|
+
expect(result.diagnostics.filter((d) => d.file.includes("main.ts"))).toHaveLength(0);
|
|
693
|
+
const warning = stderr.find((l) => l.startsWith("warning: chant lint under"));
|
|
694
|
+
expect(warning).toContain(" main.ts: ");
|
|
695
|
+
expect(warning).toContain("is itself git-ignored, so today nothing under it is linted. ");
|
|
696
|
+
expect(warning).toContain("so the files under it are linted.");
|
|
697
|
+
expect(warning).toContain('To keep skipping it, add "**" to exclude in');
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
644
701
|
/**
|
|
645
702
|
* chant #2222 (epic #2215 sub-issue 7) — `chant lint` used to print "No
|
|
646
703
|
* problems found" and exit 0 on a project whose declared lexicon could not be
|