@intentius/chant 0.52.1 → 0.52.2
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/agents/checks.d.ts +35 -0
- package/dist/agents/checks.d.ts.map +1 -0
- package/dist/agents/discover.d.ts +86 -0
- package/dist/agents/discover.d.ts.map +1 -0
- package/dist/agents/importer.d.ts +46 -0
- package/dist/agents/importer.d.ts.map +1 -0
- package/dist/agents/index.d.ts +14 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/types.d.ts +196 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/audit/catalog.d.ts +4 -1
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/report.d.ts +8 -0
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/cli/commands/audit-agents.d.ts +83 -0
- package/dist/cli/commands/audit-agents.d.ts.map +1 -0
- package/dist/cli/commands/carve-bridge.d.ts.map +1 -1
- package/dist/cli/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts +64 -0
- package/dist/cli/commands/import-agents.d.ts.map +1 -0
- package/dist/cli/handlers/carve-emit.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +15 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/lexicon.d.ts +11 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/terraform/adopt-state.d.ts +4 -1
- package/dist/terraform/adopt-state.d.ts.map +1 -1
- package/dist/terraform/bridge.d.ts.map +1 -1
- package/dist/terraform/tier-map.d.ts +17 -0
- package/dist/terraform/tier-map.d.ts.map +1 -1
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/agents/checks.test.ts +228 -0
- package/src/agents/checks.ts +429 -0
- package/src/agents/discover.test.ts +310 -0
- package/src/agents/discover.ts +939 -0
- package/src/agents/importer.ts +49 -0
- package/src/agents/index.ts +29 -0
- package/src/agents/types.ts +207 -0
- package/src/audit/catalog.ts +90 -1
- package/src/audit/report.ts +9 -1
- package/src/audit/rules-doc.ts +6 -0
- package/src/cli/commands/audit-agents.test.ts +260 -0
- package/src/cli/commands/audit-agents.ts +387 -0
- package/src/cli/commands/carve-bridge.test.ts +30 -0
- package/src/cli/commands/carve-bridge.ts +19 -1
- package/src/cli/commands/carve-emit.test.ts +72 -1
- package/src/cli/commands/carve-emit.ts +43 -20
- package/src/cli/commands/import-agents.test.ts +208 -0
- package/src/cli/commands/import-agents.ts +196 -0
- package/src/cli/handlers/carve-emit.ts +2 -0
- package/src/cli/handlers/misc.ts +111 -0
- package/src/cli/main.ts +17 -0
- package/src/cli/registry.ts +15 -0
- package/src/lexicon.ts +12 -0
- package/src/terraform/adopt-state.ts +7 -4
- package/src/terraform/aws-resources.test.ts +24 -1
- package/src/terraform/bridge.test.ts +12 -0
- package/src/terraform/bridge.ts +4 -1
- package/src/terraform/tier-map.ts +28 -1
- package/src/yaml.test.ts +54 -0
- package/src/yaml.ts +24 -3
package/src/cli/handlers/misc.ts
CHANGED
|
@@ -5,6 +5,9 @@ import { loadChantConfig } from "../../config";
|
|
|
5
5
|
import { resolve } from "path";
|
|
6
6
|
import { auditCommand, printAuditResult, type AuditFormat, type AuditTier, type AuditFailOn } from "../commands/audit";
|
|
7
7
|
import type { ReportTheme } from "../../audit/report-html";
|
|
8
|
+
import { AGENT_RUNTIMES, AGENT_SCOPES, type AgentRuntime, type AgentScope } from "../../agents/types";
|
|
9
|
+
import { registeredProjectRoots } from "../../agents/discover";
|
|
10
|
+
import { homedir } from "os";
|
|
8
11
|
import type { ResourceSelector } from "../../lexicon";
|
|
9
12
|
import { formatError, formatSuccess, formatWarning, formatBold } from "../format";
|
|
10
13
|
import type { CommandContext } from "../registry";
|
|
@@ -23,6 +26,44 @@ const AUDIT_FORMATS: AuditFormat[] = ["stylish", "json", "sarif", "markdown", "h
|
|
|
23
26
|
const AUDIT_TIERS: AuditTier[] = ["merge-worthy", "all"];
|
|
24
27
|
const AUDIT_FAIL_ON: AuditFailOn[] = ["merge-worthy", "warning", "none"];
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Parse `--scope system,user,project`. Returns `undefined` (meaning "all") when
|
|
31
|
+
* the flag is absent, or an Error the caller reports — an unrecognized scope is
|
|
32
|
+
* rejected rather than ignored, since silently scanning more than the user
|
|
33
|
+
* asked for is exactly the surprise this command exists to prevent.
|
|
34
|
+
*/
|
|
35
|
+
function parseScopes(raw: string | undefined): readonly AgentScope[] | undefined | Error {
|
|
36
|
+
if (!raw) return undefined;
|
|
37
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
38
|
+
const bad = parts.filter((p) => !AGENT_SCOPES.includes(p as AgentScope));
|
|
39
|
+
if (bad.length > 0) return new Error(`Invalid --scope: ${bad.join(", ")}. Expected one or more of ${AGENT_SCOPES.join(", ")}.`);
|
|
40
|
+
return parts.length > 0 ? (parts as AgentScope[]) : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Parse `--runtime claude,codex,...`. Same contract as {@link parseScopes}. */
|
|
44
|
+
function parseRuntimes(raw: string | undefined): readonly AgentRuntime[] | undefined | Error {
|
|
45
|
+
if (!raw) return undefined;
|
|
46
|
+
const parts = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
47
|
+
const bad = parts.filter((p) => !AGENT_RUNTIMES.includes(p as AgentRuntime));
|
|
48
|
+
if (bad.length > 0) return new Error(`Invalid --runtime: ${bad.join(", ")}. Expected one or more of ${AGENT_RUNTIMES.join(", ")}.`);
|
|
49
|
+
return parts.length > 0 ? (parts as AgentRuntime[]) : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Project roots for an `--agents` run.
|
|
54
|
+
*
|
|
55
|
+
* Default: the path argument alone. With `--all-projects`, every project root
|
|
56
|
+
* the harness has registered, *unioned* with the path argument rather than
|
|
57
|
+
* replacing it — a flag that silently discarded an argument the user typed
|
|
58
|
+
* would be the wrong kind of surprise, and a cwd that isn't registered yet is
|
|
59
|
+
* still a project the user is plainly interested in.
|
|
60
|
+
*/
|
|
61
|
+
function resolveProjectRoots(args: { path: string; allProjects?: boolean }): string[] {
|
|
62
|
+
const named = resolve(args.path);
|
|
63
|
+
if (!args.allProjects) return [named];
|
|
64
|
+
return [...new Set([...registeredProjectRoots(homedir()), named])].sort();
|
|
65
|
+
}
|
|
66
|
+
|
|
26
67
|
export async function runAudit(ctx: CommandContext): Promise<number> {
|
|
27
68
|
const { args } = ctx;
|
|
28
69
|
|
|
@@ -42,6 +83,37 @@ export async function runAudit(ctx: CommandContext): Promise<number> {
|
|
|
42
83
|
return 1;
|
|
43
84
|
}
|
|
44
85
|
|
|
86
|
+
// `--agents` switches the subject from a repo to this machine's agent
|
|
87
|
+
// configuration. Same formats, tiers, and fail-on policy — a different thing
|
|
88
|
+
// being audited, not a different report.
|
|
89
|
+
if (args.agents) {
|
|
90
|
+
const scopes = parseScopes(args.scope);
|
|
91
|
+
if (scopes instanceof Error) {
|
|
92
|
+
console.error(formatError({ message: scopes.message }));
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
const runtimes = parseRuntimes(args.runtime);
|
|
96
|
+
if (runtimes instanceof Error) {
|
|
97
|
+
console.error(formatError({ message: runtimes.message }));
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
const { auditAgentsCommand } = await import("../commands/audit-agents");
|
|
101
|
+
const result = auditAgentsCommand({
|
|
102
|
+
format,
|
|
103
|
+
tier,
|
|
104
|
+
failOn,
|
|
105
|
+
scopes,
|
|
106
|
+
runtimes,
|
|
107
|
+
projectRoots: resolveProjectRoots(args),
|
|
108
|
+
output: args.output,
|
|
109
|
+
toolVersion: CHANT_VERSION,
|
|
110
|
+
});
|
|
111
|
+
if (result.error) console.error(formatError({ message: result.error }));
|
|
112
|
+
else if (result.wroteTo) console.log(formatSuccess(`Report written to ${result.wroteTo}`));
|
|
113
|
+
else console.log(result.output);
|
|
114
|
+
return result.exitCode;
|
|
115
|
+
}
|
|
116
|
+
|
|
45
117
|
// HTML report customization: --template <file> (full override) + --theme <file> (JSON knobs).
|
|
46
118
|
let template: string | undefined;
|
|
47
119
|
let theme: ReportTheme | undefined;
|
|
@@ -171,6 +243,45 @@ export async function runDescribe(ctx: CommandContext): Promise<number> {
|
|
|
171
243
|
export async function runImport(ctx: CommandContext): Promise<number> {
|
|
172
244
|
const { args } = ctx;
|
|
173
245
|
|
|
246
|
+
// `--agents` (#chant audit --agents' other half): the source is this
|
|
247
|
+
// machine's agent configuration rather than a template file, so there is
|
|
248
|
+
// nothing to detect — the flag names the subject, like `--kustomize` below
|
|
249
|
+
// names the lexicon.
|
|
250
|
+
if (args.agents) {
|
|
251
|
+
const scopes = parseScopes(args.scope);
|
|
252
|
+
if (scopes instanceof Error) {
|
|
253
|
+
console.error(formatError({ message: scopes.message }));
|
|
254
|
+
return 1;
|
|
255
|
+
}
|
|
256
|
+
const runtimes = parseRuntimes(args.runtime);
|
|
257
|
+
if (runtimes instanceof Error) {
|
|
258
|
+
console.error(formatError({ message: runtimes.message }));
|
|
259
|
+
return 1;
|
|
260
|
+
}
|
|
261
|
+
const { importAgentsCommand } = await import("../commands/import-agents");
|
|
262
|
+
const result = await importAgentsCommand({
|
|
263
|
+
lexicon: args.lexicon,
|
|
264
|
+
output: args.output,
|
|
265
|
+
force: args.force,
|
|
266
|
+
scopes,
|
|
267
|
+
runtimes,
|
|
268
|
+
projectRoots: resolveProjectRoots(args),
|
|
269
|
+
});
|
|
270
|
+
for (const warning of result.warnings) console.warn(formatWarning({ message: warning }));
|
|
271
|
+
if (!result.success) {
|
|
272
|
+
console.error(formatError({ message: result.error ?? "Import failed." }));
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
console.log(
|
|
276
|
+
formatSuccess(
|
|
277
|
+
`Re-expressed ${result.summary.mapped} resource${result.summary.mapped === 1 ? "" : "s"} ` +
|
|
278
|
+
`from ${result.summary.discovered} agent config${result.summary.discovered === 1 ? "" : "s"}:`,
|
|
279
|
+
),
|
|
280
|
+
);
|
|
281
|
+
for (const file of result.generatedFiles) console.log(` ${file}`);
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
174
285
|
// `--kustomize <dir>` (#1548): render the overlay and import the output
|
|
175
286
|
// through the k8s template parser — the flag NAMES the lexicon, so no JSON
|
|
176
287
|
// detection. `kustomize build`, falling back to kubectl's vendored
|
package/src/cli/main.ts
CHANGED
|
@@ -48,6 +48,8 @@ import type { LexiconPlugin } from "../lexicon";
|
|
|
48
48
|
*/
|
|
49
49
|
const BOOLEAN_FLAGS = new Set([
|
|
50
50
|
"--help",
|
|
51
|
+
"--agents",
|
|
52
|
+
"--all-projects",
|
|
51
53
|
"--force",
|
|
52
54
|
"--fix",
|
|
53
55
|
"--watch",
|
|
@@ -254,6 +256,14 @@ export function parseArgs(args: string[]): ParsedArgs {
|
|
|
254
256
|
result.env = args[++i];
|
|
255
257
|
} else if (arg === "--tier") {
|
|
256
258
|
result.tier = args[++i];
|
|
259
|
+
} else if (arg === "--agents") {
|
|
260
|
+
result.agents = true;
|
|
261
|
+
} else if (arg === "--scope") {
|
|
262
|
+
result.scope = args[++i];
|
|
263
|
+
} else if (arg === "--all-projects") {
|
|
264
|
+
result.allProjects = true;
|
|
265
|
+
} else if (arg === "--runtime") {
|
|
266
|
+
result.runtime = args[++i];
|
|
257
267
|
} else if (arg === "--fail-on") {
|
|
258
268
|
result.failOn = args[++i];
|
|
259
269
|
} else if (arg === "--theme") {
|
|
@@ -438,10 +448,17 @@ Commands:
|
|
|
438
448
|
bundle tree, otherwise JSON path→content on stdout)
|
|
439
449
|
vendor Pull pinned, checksummed patterns into your repo
|
|
440
450
|
import Import external template into TypeScript
|
|
451
|
+
(--agents re-expresses this machine's agent config as
|
|
452
|
+
chant code instead of reading a template file)
|
|
441
453
|
audit [path|url] Audit a repo's CI YAML for security issues
|
|
442
454
|
(--format stylish|json|sarif|markdown|html, -o <file>,
|
|
443
455
|
--tier merge-worthy|all, --fail-on merge-worthy|warning|none,
|
|
444
456
|
--template <file> / --theme <file> for the html report)
|
|
457
|
+
--agents audits this machine's agent configuration —
|
|
458
|
+
instruction files, MCP servers, skills, plugins,
|
|
459
|
+
permissions — instead of a repository
|
|
460
|
+
(--scope system,user,project, --runtime claude,codex,...,
|
|
461
|
+
--all-projects)
|
|
445
462
|
migrate <file> Translate a workflow between lexicons
|
|
446
463
|
(default: --from github --to gitlab)
|
|
447
464
|
carve advise Read-only Terraform peelability advisor: rank which
|
package/src/cli/registry.ts
CHANGED
|
@@ -158,6 +158,21 @@ export interface ParsedArgs {
|
|
|
158
158
|
includeDependents?: boolean;
|
|
159
159
|
/** `chant audit --tier merge-worthy|all` */
|
|
160
160
|
tier?: string;
|
|
161
|
+
/**
|
|
162
|
+
* `chant audit --agents` / `chant import --agents` — switch the subject from
|
|
163
|
+
* a repo's pipeline YAML to the agent configuration installed on this
|
|
164
|
+
* machine (skills, MCP servers, instruction files, permissions).
|
|
165
|
+
*/
|
|
166
|
+
agents?: boolean;
|
|
167
|
+
/** `--agents --scope system,user,project` — which scopes to scan (default all). */
|
|
168
|
+
scope?: string;
|
|
169
|
+
/** `--agents --runtime claude,codex,...` — which agent harnesses to scan (default all). */
|
|
170
|
+
runtime?: string;
|
|
171
|
+
/**
|
|
172
|
+
* `--agents --all-projects` — scan every project root the harness has
|
|
173
|
+
* registered in `~/.claude.json`, not just the path argument.
|
|
174
|
+
*/
|
|
175
|
+
allProjects?: boolean;
|
|
161
176
|
/** `chant audit --fail-on merge-worthy|warning|none` */
|
|
162
177
|
failOn?: string;
|
|
163
178
|
/** `chant audit --theme <file>` — JSON theme knobs for the HTML report */
|
package/src/lexicon.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { RuleSpec } from "./lint/declarative";
|
|
|
5
5
|
import type { PostSynthCheck } from "./lint/post-synth";
|
|
6
6
|
import type { TemplateParser, TemplateIR } from "./import/parser";
|
|
7
7
|
import type { TypeScriptGenerator } from "./import/generator";
|
|
8
|
+
import type { AgentConfigImporter } from "./agents/importer";
|
|
8
9
|
import type { ArtifactIntegrity } from "./lexicon-integrity";
|
|
9
10
|
import type { OkfFile } from "./okf";
|
|
10
11
|
import type { CompletionContext, CompletionItem, HoverContext, HoverInfo, CodeActionContext, CodeAction } from "./lsp/types";
|
|
@@ -714,6 +715,17 @@ export interface LexiconPlugin {
|
|
|
714
715
|
/** Return a generator for converting IR to TypeScript */
|
|
715
716
|
templateGenerator?(): TypeScriptGenerator;
|
|
716
717
|
|
|
718
|
+
/**
|
|
719
|
+
* Re-express local agent configuration (skills, MCP servers, instruction
|
|
720
|
+
* files) discovered by `chant audit --agents` as this lexicon's resources.
|
|
721
|
+
*
|
|
722
|
+
* Implement this when the lexicon has types that model an agent's workload —
|
|
723
|
+
* fountain's `Agent`/`Environment` are the motivating case. Core does the
|
|
724
|
+
* harness-neutral discovery; the mapping onto concrete resource types is the
|
|
725
|
+
* lexicon's call, for the same reason `templateParser` is.
|
|
726
|
+
*/
|
|
727
|
+
agentConfigImporter?(): AgentConfigImporter;
|
|
728
|
+
|
|
717
729
|
/** Return skills provided by this lexicon */
|
|
718
730
|
skills?(): SkillDefinition[];
|
|
719
731
|
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import {
|
|
22
|
-
AWS_CARVE_TYPES,
|
|
23
22
|
AWS_LEXICON_IMPORT,
|
|
24
23
|
awsCarveType,
|
|
25
24
|
applyAwsMapper,
|
|
26
25
|
applyAwsFold,
|
|
27
26
|
unmappedFoldAttrs,
|
|
28
27
|
} from "./aws-resources";
|
|
28
|
+
import { canCarveEmit, carveEmitTypes } from "./tier-map";
|
|
29
29
|
import type { StateResource } from "./state";
|
|
30
30
|
|
|
31
31
|
/** The core subpath the emitted source reads build parameters from. */
|
|
@@ -70,14 +70,17 @@ export interface AdoptedSource {
|
|
|
70
70
|
folded: FoldedContribution[];
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Is this Terraform type adoptable from state (has a native constructor)? The
|
|
75
|
+
* same table gates the live path, so both are the shared `canCarveEmit`.
|
|
76
|
+
*/
|
|
74
77
|
export function canAdoptFromState(tfType: string): boolean {
|
|
75
|
-
return
|
|
78
|
+
return canCarveEmit(tfType);
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
/** Terraform types that can currently be adopted from state, for user-facing hints. */
|
|
79
82
|
export function supportedStateAdoptionTypes(): string[] {
|
|
80
|
-
return
|
|
83
|
+
return carveEmitTypes();
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/** Marker for a prop whose value is a `params.<name>` reference, not a literal. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { AWS_CARVE_TYPES, AWS_FOLD_MAPPERS, awsCarveType, applyAwsMapper, applyAwsFold } from "./aws-resources";
|
|
3
|
-
import { TIER_MAP, FOLDS_INTO, IDENTITY_ATTR } from "./tier-map";
|
|
3
|
+
import { TIER_MAP, FOLDS_INTO, IDENTITY_ATTR, canBridge, canCarveEmit, carveEmitTypes } from "./tier-map";
|
|
4
4
|
import { canAdoptFromState } from "./adopt-state";
|
|
5
5
|
|
|
6
6
|
describe("AWS carve-out table", () => {
|
|
@@ -13,6 +13,29 @@ describe("AWS carve-out table", () => {
|
|
|
13
13
|
for (const t of emitAws) expect(canAdoptFromState(t)).toBe(true);
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
+
test("the emit gate is narrower than the tier map, which also ranks k8s (#2015)", () => {
|
|
17
|
+
// `carve advise` bands kubernetes types; emit has no path for them, so the
|
|
18
|
+
// gate both emit paths share must refuse them.
|
|
19
|
+
expect(TIER_MAP.kubernetes_config_map).toBeDefined();
|
|
20
|
+
expect(canCarveEmit("kubernetes_config_map")).toBe(false);
|
|
21
|
+
expect(canCarveEmit("kubernetes_manifest")).toBe(false);
|
|
22
|
+
expect(canCarveEmit("random_pet")).toBe(false);
|
|
23
|
+
expect(canCarveEmit("aws_s3_bucket")).toBe(true);
|
|
24
|
+
expect(carveEmitTypes()).toEqual(AWS_CARVE_TYPES.map((t) => t.tfType).sort());
|
|
25
|
+
// State adoption and live emit gate on the same table.
|
|
26
|
+
for (const t of carveEmitTypes()) expect(canAdoptFromState(t)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("canBridge rejects a dotted identity attribute (#2015)", () => {
|
|
30
|
+
// A data source body is flat `attr = value`; a dotted path is not HCL.
|
|
31
|
+
expect(IDENTITY_ATTR.kubernetes_manifest).toContain(".");
|
|
32
|
+
expect(canBridge("kubernetes_manifest")).toBe(false);
|
|
33
|
+
expect(canBridge("aws_s3_bucket")).toBe(true);
|
|
34
|
+
// No identity entry at all is fine — the bridge writes a TODO comment.
|
|
35
|
+
expect(canBridge("random_pet")).toBe(true);
|
|
36
|
+
for (const t of AWS_CARVE_TYPES) expect(canBridge(t.tfType)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
16
39
|
test("covers the common carve targets across families", () => {
|
|
17
40
|
const types = new Set(AWS_CARVE_TYPES.map((t) => t.tfType));
|
|
18
41
|
for (const t of [
|
|
@@ -51,6 +51,18 @@ describe("generateBridge — inbound (data-source rewrite)", () => {
|
|
|
51
51
|
);
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
test("never renders a dotted identity attribute into the data body (#2015)", () => {
|
|
55
|
+
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
56
|
+
const dotted = new Map<string, CarvedIdentity>([
|
|
57
|
+
["aws_s3_bucket.assets", { attr: "manifest.metadata.name", value: "demo-config" }],
|
|
58
|
+
]);
|
|
59
|
+
const plan = generateBridge(report, [{ path: "api.tf", content: API_TF }], dotted);
|
|
60
|
+
|
|
61
|
+
// `manifest.metadata.name = "demo-config"` is not valid HCL.
|
|
62
|
+
expect(plan.dataSources[0].hcl).not.toContain("manifest.metadata.name =");
|
|
63
|
+
expect(plan.dataSources[0].hcl).toContain("# TODO: identify the resource");
|
|
64
|
+
});
|
|
65
|
+
|
|
54
66
|
test("rewrites survivor references to the data source", () => {
|
|
55
67
|
const report = boundaryReport(buildFixtureGraph(workedExample), "aws_s3_bucket.assets")!;
|
|
56
68
|
const plan = generateBridge(report, [{ path: "api.tf", content: API_TF }], identities);
|
package/src/terraform/bridge.ts
CHANGED
|
@@ -81,8 +81,11 @@ function referenceRegex(address: string): RegExp {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function dataSourceHcl(type: string, name: string, identity: CarvedIdentity): string {
|
|
84
|
+
// A dotted attr is a path into nested blocks; `a.b.c = v` is not valid HCL,
|
|
85
|
+
// so it falls back to the TODO. `carve bridge` refuses such a type outright.
|
|
86
|
+
const flat = identity.attr !== undefined && !identity.attr.includes(".");
|
|
84
87
|
const body =
|
|
85
|
-
|
|
88
|
+
flat && identity.value !== undefined
|
|
86
89
|
? ` ${identity.attr} = ${JSON.stringify(identity.value)}`
|
|
87
90
|
: ` # TODO: identify the resource (its name/id was interpolated in the source)`;
|
|
88
91
|
return `data ${JSON.stringify(type)} ${JSON.stringify(name)} {\n${body}\n}`;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* null — no known native mapping (unsupported provider/type) → score 0
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { AWS_CARVE_TYPES } from "./aws-resources";
|
|
15
|
+
import { AWS_CARVE_TYPES, awsCarveType } from "./aws-resources";
|
|
16
16
|
|
|
17
17
|
export interface TierInfo {
|
|
18
18
|
tier: 1 | 2 | 3;
|
|
@@ -110,3 +110,30 @@ export const IDENTITY_ATTR: Record<string, string> = {
|
|
|
110
110
|
export function resolveTier(tfType: string): TierInfo | null {
|
|
111
111
|
return TIER_MAP[tfType] ?? null;
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Can `chant carve emit` produce chant source for this type? Narrower than
|
|
116
|
+
* `resolveTier`: the tier map also ranks the kubernetes types, which advise
|
|
117
|
+
* bands but no emit path can adopt (#999, blocked on the provider seam #2016).
|
|
118
|
+
* Both emit paths — `--state` and `--env` — gate on this, so a type either
|
|
119
|
+
* command refuses is refused by the other with the same message.
|
|
120
|
+
*/
|
|
121
|
+
export function canCarveEmit(tfType: string): boolean {
|
|
122
|
+
return awsCarveType(tfType) !== undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The types `canCarveEmit` accepts, sorted, for user-facing hints. */
|
|
126
|
+
export function carveEmitTypes(): string[] {
|
|
127
|
+
return AWS_CARVE_TYPES.map((t) => t.tfType).sort();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Can `chant carve bridge` render a Terraform `data` source for this type? A
|
|
132
|
+
* dotted `IDENTITY_ATTR` is a path into nested blocks, and a data source body
|
|
133
|
+
* is flat `attr = value` — `manifest.metadata.name = "x"` is not valid HCL.
|
|
134
|
+
* An absent entry is fine: the bridge writes a TODO comment for the identity.
|
|
135
|
+
*/
|
|
136
|
+
export function canBridge(tfType: string): boolean {
|
|
137
|
+
const attr = IDENTITY_ATTR[tfType];
|
|
138
|
+
return attr === undefined || !attr.includes(".");
|
|
139
|
+
}
|
package/src/yaml.test.ts
CHANGED
|
@@ -484,3 +484,57 @@ describe("parseYAML — sibling keys after a nested block in a sequence item (#1
|
|
|
484
484
|
});
|
|
485
485
|
});
|
|
486
486
|
});
|
|
487
|
+
|
|
488
|
+
// #2013 — a colon opens a mapping only when whitespace or the end of the line
|
|
489
|
+
// follows it (YAML 1.2 §7.4.2). The item/key disambiguation used to accept a
|
|
490
|
+
// colon anywhere, which turned a bare URL in a sequence into a single-key
|
|
491
|
+
// object and made the CRD-schema check reject a string list the emitter here
|
|
492
|
+
// had written itself.
|
|
493
|
+
describe("parseYAML — a colon without following space is not a mapping (#2013)", () => {
|
|
494
|
+
test("a bare URL in a sequence is a string", () => {
|
|
495
|
+
expect(parseYAML("sourceRepos:\n- https://github.com/INTENTIUS/behold\n")).toEqual({
|
|
496
|
+
sourceRepos: ["https://github.com/INTENTIUS/behold"],
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("an AppProject's sourceRepos round-trips through emit and parse", () => {
|
|
501
|
+
const repos = ["https://github.com/INTENTIUS/behold", "ssh://git@github.com:22/INTENTIUS/behold.git"];
|
|
502
|
+
expect(parseYAML(`spec:${emitYAML({ sourceRepos: repos }, 1)}\n`)).toEqual({ spec: { sourceRepos: repos } });
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("a real `- key: value` item is still a mapping", () => {
|
|
506
|
+
expect(parseYAML("items:\n- name: x\n url: https://example.com/a\n")).toEqual({
|
|
507
|
+
items: [{ name: "x", url: "https://example.com/a" }],
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("a mapping item whose first value is a URL keeps its siblings", () => {
|
|
512
|
+
expect(parseYAML("sources:\n- repoURL: https://github.com/o/r\n path: ./chart\n")).toEqual({
|
|
513
|
+
sources: [{ repoURL: "https://github.com/o/r", path: "./chart" }],
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("a quoted URL in a sequence still works", () => {
|
|
518
|
+
expect(parseYAML("sourceRepos:\n- 'https://github.com/o/r'\n- \"https://github.com/o/s\"\n")).toEqual({
|
|
519
|
+
sourceRepos: ["https://github.com/o/r", "https://github.com/o/s"],
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test("`- key:value` with no space after the colon is a scalar, per the spec", () => {
|
|
524
|
+
expect(parseYAML("items:\n- key:value\n")).toEqual({ items: ["key:value"] });
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("`- key:` with nothing after the colon is still a mapping with a null value", () => {
|
|
528
|
+
expect(parseYAML("items:\n- key:\n")).toEqual({ items: [{ key: null }] });
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("a URL on a mapping line, not a sequence item, is unaffected", () => {
|
|
532
|
+
expect(parseYAML("url: https://example.com/a\n")).toEqual({ url: "https://example.com/a" });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("a bare URL among mapping-shaped siblings inside one item", () => {
|
|
536
|
+
expect(parseYAML("items:\n- name: x\n args:\n - --endpoint\n - https://example.com/a\n image: nginx\n")).toEqual({
|
|
537
|
+
items: [{ name: "x", args: ["--endpoint", "https://example.com/a"], image: "nginx" }],
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
});
|
package/src/yaml.ts
CHANGED
|
@@ -155,6 +155,27 @@ export interface ParseResult {
|
|
|
155
155
|
endIndex: number;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* `key: value` on a line of its own (`KEY_LINE`, capturing the indent) and the
|
|
160
|
+
* same after a sequence item's `- ` (`ITEM_KEY`).
|
|
161
|
+
*
|
|
162
|
+
* The lookahead is the rule both patterns used to miss: a `:` opens a mapping
|
|
163
|
+
* only when what follows it is whitespace or the end of the line, and a plain
|
|
164
|
+
* scalar may carry any other colon (YAML 1.2 §7.4.2). Written as `:\s*`, which
|
|
165
|
+
* matches zero whitespace, EVERY colon opened one — so a bare URL in a
|
|
166
|
+
* sequence,
|
|
167
|
+
*
|
|
168
|
+
* sourceRepos:
|
|
169
|
+
* - https://github.com/INTENTIUS/behold
|
|
170
|
+
*
|
|
171
|
+
* came back as `{ https: "//github.com/INTENTIUS/behold" }`, and the CRD-schema
|
|
172
|
+
* post-synth check (#1372) re-reading emitted YAML reported the string list
|
|
173
|
+
* this file's own emitter had just written as a list of objects (#2013). By the
|
|
174
|
+
* same rule `- key:value` is the scalar `"key:value"`, not a mapping.
|
|
175
|
+
*/
|
|
176
|
+
const KEY_LINE = /^(\s*)([^\s:][^:]*?):(?=\s|$)\s*(.*)$/;
|
|
177
|
+
const ITEM_KEY = /^([^\s:][^:]*?):(?=\s|$)\s*(.*)$/;
|
|
178
|
+
|
|
158
179
|
/**
|
|
159
180
|
* Parse a YAML document (or JSON document) into a plain object.
|
|
160
181
|
*
|
|
@@ -274,7 +295,7 @@ export function parseYAMLLines(
|
|
|
274
295
|
if (indent < baseIndent) break; // Dedented — done with this block
|
|
275
296
|
if (indent > baseIndent && startIndex > 0) break; // Unexpected indent
|
|
276
297
|
|
|
277
|
-
const keyMatch = line.match(
|
|
298
|
+
const keyMatch = line.match(KEY_LINE);
|
|
278
299
|
if (keyMatch) {
|
|
279
300
|
const key = keyMatch[2].trim();
|
|
280
301
|
const inlineValue = keyMatch[3].trim();
|
|
@@ -472,7 +493,7 @@ export function parseYAMLArray(
|
|
|
472
493
|
const isQuotedScalar =
|
|
473
494
|
(itemValue.startsWith('"') && itemValue.endsWith('"')) ||
|
|
474
495
|
(itemValue.startsWith("'") && itemValue.endsWith("'"));
|
|
475
|
-
const kvMatch = !isQuotedScalar && itemValue.match(
|
|
496
|
+
const kvMatch = !isQuotedScalar && itemValue.match(ITEM_KEY);
|
|
476
497
|
if (kvMatch) {
|
|
477
498
|
const obj: Record<string, unknown> = {};
|
|
478
499
|
obj[kvMatch[1].trim()] = parseArrayItemValue(kvMatch[2].trim(), lines, i, indent + 2);
|
|
@@ -506,7 +527,7 @@ export function parseYAMLArray(
|
|
|
506
527
|
const ni = nextLine.search(/\S/);
|
|
507
528
|
if (ni < nextIndent) break;
|
|
508
529
|
if (ni > nextIndent) break; // belongs to a nested block already consumed
|
|
509
|
-
const nextKV = nextLine.match(
|
|
530
|
+
const nextKV = nextLine.match(KEY_LINE);
|
|
510
531
|
if (nextKV) {
|
|
511
532
|
const nextVal = nextKV[3].trim();
|
|
512
533
|
obj[nextKV[2].trim()] = parseArrayItemValue(nextVal, lines, j, ni);
|