@intentius/chant 0.1.17 → 0.1.19

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.
@@ -0,0 +1,263 @@
1
+ /**
2
+ * `chant vendor` — pull reusable patterns (composites as source, Ops, init
3
+ * templates, example skeletons) from a remote source into your own repo, pinned
4
+ * and auditable.
5
+ *
6
+ * This is for patterns you copy in and **own/adapt**, recorded in a manifest
7
+ * (`vendor.json`) with a checksum so provenance is verifiable. It is NOT a
8
+ * package manager: lexicons stay npm dependencies (the typed API you import,
9
+ * never edit). Vendoring exists for the source npm handles badly — code you want
10
+ * in-repo, reviewable in diffs, and adaptable.
11
+ *
12
+ * Reuses the existing fetch/extract infra (`codegen/fetch.ts`); the only new
13
+ * machinery is the manifest + copy-to-repo + checksum.
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ import {
17
+ existsSync,
18
+ mkdirSync,
19
+ readdirSync,
20
+ readFileSync,
21
+ rmSync,
22
+ statSync,
23
+ writeFileSync,
24
+ } from "node:fs";
25
+ import { dirname, join, relative, resolve, sep } from "node:path";
26
+ import { z } from "zod";
27
+ import { fetchWithRetry, extractFromTar, extractFromZip } from "../../codegen/fetch";
28
+
29
+ // ── Manifest schema ─────────────────────────────────────────────────────────
30
+
31
+ const SourceSchema = z.discriminatedUnion("type", [
32
+ z.object({ type: z.literal("local"), path: z.string().min(1) }),
33
+ z.object({ type: z.literal("archive"), url: z.string().url(), subpath: z.string().optional() }),
34
+ ]);
35
+
36
+ const EntrySchema = z.object({
37
+ /** Stable identity for the vendored artifact (used by `pull <name>`). */
38
+ name: z.string().min(1),
39
+ source: SourceSchema,
40
+ /** Path in your repo to write the pulled content. */
41
+ target: z.string().min(1),
42
+ /** The pin — a git tag / version label, for provenance (informational). */
43
+ ref: z.string().optional(),
44
+ /** sha256 of the pulled content. Written by `pull`, verified by `check`. */
45
+ checksum: z.string().optional(),
46
+ /** Only "pin" (explicit-bump) is supported; floating refs are out of scope. */
47
+ updatePolicy: z.literal("pin").optional(),
48
+ });
49
+
50
+ export const VendorManifestSchema = z.object({
51
+ vendored: z.array(EntrySchema),
52
+ });
53
+
54
+ export type VendorEntry = z.infer<typeof EntrySchema>;
55
+ export type VendorManifest = z.infer<typeof VendorManifestSchema>;
56
+
57
+ export const MANIFEST_FILE = "vendor.json";
58
+
59
+ // ── Content hashing ───────────────────────────────────────────────────────��─
60
+
61
+ /**
62
+ * Deterministic sha256 over a file set — independent of fetch/extract order.
63
+ * Hashes each `path\0content\0` in sorted-path order.
64
+ */
65
+ export function contentHash(files: Map<string, Buffer>): string {
66
+ const h = createHash("sha256");
67
+ for (const path of [...files.keys()].sort()) {
68
+ h.update(path);
69
+ h.update("\0");
70
+ h.update(files.get(path)!);
71
+ h.update("\0");
72
+ }
73
+ return `sha256:${h.digest("hex")}`;
74
+ }
75
+
76
+ // ── Source resolution → a { relpath → bytes } file set ──────────────────────
77
+
78
+ /** Walk a local directory into a relpath→bytes map (or a single file). */
79
+ function readLocal(absPath: string): Map<string, Buffer> {
80
+ const files = new Map<string, Buffer>();
81
+ const stat = statSync(absPath);
82
+ if (stat.isFile()) {
83
+ files.set(absPath.split(sep).pop()!, readFileSync(absPath));
84
+ return files;
85
+ }
86
+ const walk = (dir: string): void => {
87
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
88
+ const full = join(dir, entry.name);
89
+ if (entry.isDirectory()) walk(full);
90
+ else if (entry.isFile()) {
91
+ files.set(relative(absPath, full).split(sep).join("/"), readFileSync(full));
92
+ }
93
+ }
94
+ };
95
+ walk(absPath);
96
+ return files;
97
+ }
98
+
99
+ /** Fetch + extract an archive (tar.gz/tgz/zip), scoped to an optional subpath. */
100
+ async function readArchive(url: string, subpath?: string): Promise<Map<string, Buffer>> {
101
+ const resp = await fetchWithRetry(url);
102
+ const bytes = new Uint8Array(await resp.arrayBuffer());
103
+
104
+ let raw: Map<string, Buffer>;
105
+ if (url.endsWith(".zip")) {
106
+ raw = await extractFromZip(Buffer.from(bytes));
107
+ } else {
108
+ // .tar.gz / .tgz — gunzip then untar (matches fetchAndExtractTar).
109
+ const { gunzipSync } = await import("fflate");
110
+ raw = extractFromTar(gunzipSync(bytes));
111
+ }
112
+
113
+ return scopeArchiveFiles(raw, subpath);
114
+ }
115
+
116
+ /**
117
+ * Normalize an extracted archive: strip the single top-level wrapper directory
118
+ * (e.g. "repo-main/") that archives commonly add, then scope to `subpath`.
119
+ * Pure — separated from the network fetch so the scoping is unit-testable.
120
+ */
121
+ export function scopeArchiveFiles(
122
+ raw: Map<string, Buffer>,
123
+ subpath?: string,
124
+ ): Map<string, Buffer> {
125
+ const prefix = subpath ? subpath.replace(/^\/+|\/+$/g, "") + "/" : "";
126
+ const files = new Map<string, Buffer>();
127
+ for (const [name, data] of raw) {
128
+ const slash = name.indexOf("/");
129
+ const rel = slash >= 0 ? name.slice(slash + 1) : name;
130
+ if (!rel || !rel.startsWith(prefix)) continue;
131
+ const local = rel.slice(prefix.length);
132
+ if (local) files.set(local, data);
133
+ }
134
+ return files;
135
+ }
136
+
137
+ async function resolveSource(entry: VendorEntry, manifestDir: string): Promise<Map<string, Buffer>> {
138
+ if (entry.source.type === "local") {
139
+ const abs = resolve(manifestDir, entry.source.path);
140
+ if (!existsSync(abs)) throw new Error(`local source not found: ${entry.source.path}`);
141
+ return readLocal(abs);
142
+ }
143
+ return readArchive(entry.source.url, entry.source.subpath);
144
+ }
145
+
146
+ // ── File I/O ────────────────────────────────────────────────────────────────
147
+
148
+ function writeFiles(targetDir: string, files: Map<string, Buffer>): void {
149
+ if (existsSync(targetDir)) rmSync(targetDir, { recursive: true });
150
+ for (const [rel, data] of files) {
151
+ const full = join(targetDir, rel);
152
+ mkdirSync(dirname(full), { recursive: true });
153
+ writeFileSync(full, data);
154
+ }
155
+ }
156
+
157
+ function readTarget(targetDir: string): Map<string, Buffer> {
158
+ if (!existsSync(targetDir)) return new Map();
159
+ return readLocal(targetDir);
160
+ }
161
+
162
+ // ── Manifest load/save ────────────────────────────────────────────────────��─
163
+
164
+ export function loadManifest(manifestDir: string): { manifest: VendorManifest; path: string } {
165
+ const path = join(manifestDir, MANIFEST_FILE);
166
+ if (!existsSync(path)) {
167
+ throw new Error(`no ${MANIFEST_FILE} found in ${manifestDir}`);
168
+ }
169
+ const parsed = VendorManifestSchema.safeParse(JSON.parse(readFileSync(path, "utf-8")));
170
+ if (!parsed.success) {
171
+ throw new Error(`invalid ${MANIFEST_FILE}: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`);
172
+ }
173
+ return { manifest: parsed.data, path };
174
+ }
175
+
176
+ function saveManifest(path: string, manifest: VendorManifest): void {
177
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
178
+ }
179
+
180
+ // ── pull ─────────────────────────────────────────────────────────────────��──
181
+
182
+ export interface VendorPullResult {
183
+ success: boolean;
184
+ pulled: Array<{ name: string; target: string; checksum: string; fileCount: number }>;
185
+ output: string;
186
+ }
187
+
188
+ /**
189
+ * Pull each vendored entry (or just `only`): resolve the source, write it into
190
+ * `target`, and record the content checksum back into the manifest.
191
+ */
192
+ export async function vendorPull(manifestDir: string, only?: string): Promise<VendorPullResult> {
193
+ const { manifest, path } = loadManifest(manifestDir);
194
+ const entries = only ? manifest.vendored.filter((e) => e.name === only) : manifest.vendored;
195
+ if (only && entries.length === 0) {
196
+ return { success: false, pulled: [], output: `no vendored entry named "${only}"` };
197
+ }
198
+
199
+ const pulled: VendorPullResult["pulled"] = [];
200
+ for (const entry of entries) {
201
+ const files = await resolveSource(entry, manifestDir);
202
+ const checksum = contentHash(files);
203
+ writeFiles(resolve(manifestDir, entry.target), files);
204
+ entry.checksum = checksum;
205
+ pulled.push({ name: entry.name, target: entry.target, checksum, fileCount: files.size });
206
+ }
207
+ saveManifest(path, manifest);
208
+
209
+ const output = pulled
210
+ .map((p) => ` ${p.name} → ${p.target} (${p.fileCount} file(s), ${p.checksum.slice(0, 19)}…)`)
211
+ .join("\n");
212
+ return { success: true, pulled, output };
213
+ }
214
+
215
+ // ── check ────────────────────────────────────────────────────────────────��──
216
+
217
+ export interface VendorCheckResult {
218
+ success: boolean;
219
+ /** True if any vendored target diverged from its recorded checksum. */
220
+ drift: boolean;
221
+ entries: Array<{ name: string; status: "ok" | "drifted" | "unpinned" | "missing" }>;
222
+ output: string;
223
+ }
224
+
225
+ /**
226
+ * Verify each target's working copy against its recorded checksum. Editing
227
+ * vendored files is allowed — `check` only reports that they diverged from the
228
+ * pin. The caller decides whether drift is fatal (CI) or a warning (local).
229
+ */
230
+ export function vendorCheck(manifestDir: string): VendorCheckResult {
231
+ const { manifest } = loadManifest(manifestDir);
232
+ const entries: VendorCheckResult["entries"] = [];
233
+ let drift = false;
234
+
235
+ for (const entry of manifest.vendored) {
236
+ const targetAbs = resolve(manifestDir, entry.target);
237
+ if (!entry.checksum) {
238
+ entries.push({ name: entry.name, status: "unpinned" });
239
+ continue;
240
+ }
241
+ if (!existsSync(targetAbs)) {
242
+ entries.push({ name: entry.name, status: "missing" });
243
+ drift = true;
244
+ continue;
245
+ }
246
+ const actual = contentHash(readTarget(targetAbs));
247
+ if (actual === entry.checksum) {
248
+ entries.push({ name: entry.name, status: "ok" });
249
+ } else {
250
+ entries.push({ name: entry.name, status: "drifted" });
251
+ drift = true;
252
+ }
253
+ }
254
+
255
+ const label: Record<string, string> = {
256
+ ok: "ok",
257
+ drifted: "DRIFTED (working copy differs from the pin)",
258
+ unpinned: "unpinned — run `chant vendor pull` to record a checksum",
259
+ missing: "MISSING — target not found; run `chant vendor pull`",
260
+ };
261
+ const output = entries.map((e) => ` ${e.name}: ${label[e.status]}`).join("\n");
262
+ return { success: !drift, drift, entries, output };
263
+ }
@@ -44,6 +44,7 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
44
44
  serializers,
45
45
  plugins,
46
46
  verbose: args.verbose,
47
+ env: args.env,
47
48
  });
48
49
 
49
50
  // When --lexicon filters to a subset, suppress "No serializer" warnings for excluded lexicons
@@ -1,8 +1,21 @@
1
+ import { resolve } from "node:path";
1
2
  import { discoverOps } from "../../op/discover";
2
- import { formatError } from "../format";
3
+ import { discover } from "../../discovery/index";
4
+ import { partitionByLexicon, computeStackGraph } from "../../build";
5
+ import { formatError, formatWarning, formatBold } from "../format";
3
6
  import type { CommandContext } from "../registry";
4
7
 
5
- export async function runGraph(_ctx: CommandContext): Promise<number> {
8
+ /**
9
+ * `chant graph` — the Op dependency graph by default; `--stacks` renders the
10
+ * cross-stack apply-ordering graph (edges, order, waves) chant computes from
11
+ * cross-lexicon references.
12
+ */
13
+ export async function runGraph(ctx: CommandContext): Promise<number> {
14
+ if (ctx.args.stacks) return runStackGraph(ctx);
15
+ return runOpGraph();
16
+ }
17
+
18
+ async function runOpGraph(): Promise<number> {
6
19
  const { ops, errors } = await discoverOps();
7
20
  for (const err of errors) console.error(formatError({ message: err }));
8
21
 
@@ -21,3 +34,43 @@ export async function runGraph(_ctx: CommandContext): Promise<number> {
21
34
  if (!hasEdges) console.log("No Op dependencies");
22
35
  return 0;
23
36
  }
37
+
38
+ async function runStackGraph(ctx: CommandContext): Promise<number> {
39
+ const projectPath = resolve(ctx.args.path === "." ? "." : ctx.args.path);
40
+ const result = await discover(projectPath);
41
+ if (result.errors.length > 0) {
42
+ for (const e of result.errors) console.error(formatError({ message: e.message }));
43
+ return 1;
44
+ }
45
+
46
+ const lexicons = [...partitionByLexicon(result.entities).keys()];
47
+ const graph = computeStackGraph(result.entities, lexicons);
48
+
49
+ if (ctx.args.json) {
50
+ console.log(JSON.stringify(graph, null, 2));
51
+ return graph.cycles.length > 0 ? 1 : 0;
52
+ }
53
+
54
+ if (graph.nodes.length === 0) {
55
+ console.log("No stacks found");
56
+ return 0;
57
+ }
58
+
59
+ console.log(formatBold("Apply order (waves apply top-to-bottom; a wave's stacks are parallel-safe):"));
60
+ graph.waves.forEach((wave, i) => console.log(` ${i + 1}. ${wave.join(", ")}`));
61
+
62
+ if (graph.edges.length > 0) {
63
+ console.log(formatBold("\nDependencies (consumer → producer):"));
64
+ for (const { from, to } of graph.edges) console.log(` ${from} → ${to}`);
65
+ } else {
66
+ console.log("\nNo cross-stack dependencies — all stacks are independent.");
67
+ }
68
+
69
+ if (graph.cycles.length > 0) {
70
+ for (const cycle of graph.cycles) {
71
+ console.error(formatWarning({ message: `Dependency cycle among stacks: ${cycle.join(" ↔ ")}` }));
72
+ }
73
+ return 1;
74
+ }
75
+ return 0;
76
+ }
@@ -5,6 +5,7 @@ import { readSnapshot, readEnvironmentSnapshots, listSnapshots, fetchLifecycle,
5
5
  import { computeBuildDigest, diffDigests } from "../../lifecycle/digest";
6
6
  import { diffLive, diffLiveArtifacts, type LiveDiffResult, type LiveArtifactDiffResult } from "../../lifecycle/live-diff";
7
7
  import { buildChangeSet, renderChangeSet, type ChangeSet } from "../../lifecycle/change-set";
8
+ import { affectedStacks } from "../../lifecycle/affected";
8
9
  import { loadChantConfig } from "../../config";
9
10
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
10
11
  import type { CommandContext } from "../registry";
@@ -576,3 +577,59 @@ function printSnapshotTable(snapshot: LifecycleSnapshot): void {
576
577
  );
577
578
  }
578
579
  }
580
+
581
+ /**
582
+ * chant lifecycle affected --base <ref> [--head <ref>] [--include-dependents] [--json]
583
+ *
584
+ * Read-only: report which stacks a change affects (directly-changed via artifact
585
+ * diff, dependents via the cross-stack graph, external-input as indeterminate).
586
+ * Returns the set; fanning plan/apply over it is an Op the user composes.
587
+ */
588
+ export async function runLifecycleAffected(ctx: CommandContext): Promise<number> {
589
+ const { args, plugins } = ctx;
590
+ if (!args.base) {
591
+ console.error(formatError({
592
+ message: "Base ref is required: chant lifecycle affected --base <ref> [--head <ref>] [--include-dependents]",
593
+ }));
594
+ return 1;
595
+ }
596
+
597
+ const { config } = await loadChantConfig(resolve("."));
598
+ const projectPath = resolveBuildRoot(args, config);
599
+
600
+ let result;
601
+ try {
602
+ result = await affectedStacks({
603
+ projectPath,
604
+ serializers: plugins.map((p) => p.serializer),
605
+ baseRef: args.base,
606
+ headRef: args.head,
607
+ includeDependents: args.includeDependents,
608
+ });
609
+ } catch (err) {
610
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
611
+ return 1;
612
+ }
613
+
614
+ if (args.json) {
615
+ console.log(JSON.stringify(result, null, 2));
616
+ return 0;
617
+ }
618
+
619
+ if (result.changed.length === 0 && result.dependents.length === 0) {
620
+ console.error(formatSuccess("No stacks affected"));
621
+ } else {
622
+ console.log(formatBold("Directly changed:"));
623
+ console.log(result.changed.length ? result.changed.map((s) => ` ${s}`).join("\n") : " (none)");
624
+ if (args.includeDependents) {
625
+ console.log(formatBold("\nDependents (consume a changed stack):"));
626
+ console.log(result.dependents.length ? result.dependents.map((s) => ` ${s}`).join("\n") : " (none)");
627
+ }
628
+ }
629
+ if (result.indeterminate.length > 0) {
630
+ console.error(formatWarning({
631
+ message: `External-input stacks — cannot confirm from source: ${result.indeterminate.join(", ")}`,
632
+ }));
633
+ }
634
+ return 0;
635
+ }
@@ -0,0 +1,61 @@
1
+ import { resolve } from "node:path";
2
+ import { vendorPull, vendorCheck } from "../commands/vendor";
3
+ import { formatError, formatSuccess, formatWarning } from "../format";
4
+ import type { CommandContext } from "../registry";
5
+
6
+ /**
7
+ * `chant vendor [pull|check]` — pull pinned, checksummed patterns into the repo,
8
+ * or check vendored targets against their recorded pin. Defaults to `pull`.
9
+ *
10
+ * The manifest (`vendor.json`) lives in the current directory.
11
+ */
12
+ export async function runVendor(ctx: CommandContext): Promise<number> {
13
+ const { args } = ctx;
14
+ // `chant vendor` → pull; `chant vendor pull|check [name]`.
15
+ const sub = args.path === "." ? "pull" : args.path;
16
+ const manifestDir = resolve(".");
17
+
18
+ if (sub === "pull") {
19
+ try {
20
+ const result = await vendorPull(manifestDir, args.extraPositional);
21
+ if (!result.success) {
22
+ console.error(formatError({ message: result.output }));
23
+ return 1;
24
+ }
25
+ console.log(result.output);
26
+ console.error(formatSuccess(`Vendored ${result.pulled.length} artifact(s)`));
27
+ return 0;
28
+ } catch (err) {
29
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
30
+ return 1;
31
+ }
32
+ }
33
+
34
+ if (sub === "check") {
35
+ try {
36
+ const result = vendorCheck(manifestDir);
37
+ console.log(result.output);
38
+ if (!result.drift) {
39
+ console.error(formatSuccess("All vendored artifacts match their pin"));
40
+ return 0;
41
+ }
42
+ // Drift is allowed (you may edit vendored files); fail only in CI so a
43
+ // pipeline catches unrecorded changes, warn locally.
44
+ if (process.env.CI) {
45
+ console.error(formatError({ message: "Vendored content drifted from the manifest pin" }));
46
+ return 1;
47
+ }
48
+ console.error(formatWarning({ message: "Vendored content drifted from the manifest pin (run `chant vendor pull` to re-pin)" }));
49
+ return 0;
50
+ } catch (err) {
51
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
52
+ return 1;
53
+ }
54
+ }
55
+
56
+ console.error(formatError({
57
+ message: `Unknown vendor subcommand: ${sub}`,
58
+ hint: "Available: chant vendor pull [name], chant vendor check",
59
+ }));
60
+ return 1;
61
+ }
package/src/cli/main.ts CHANGED
@@ -12,8 +12,9 @@ import { runDevGenerate, runDevPublish, runDevOnboard, runDevCheckLexicon, runDe
12
12
  import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
13
13
  import { runInit, runInitLexicon } from "./handlers/init";
14
14
  import { runList, runDescribe, runImport, runUpdate, runDoctor } from "./handlers/misc";
15
+ import { runVendor } from "./handlers/vendor";
15
16
  import { runMigrate } from "./handlers/migrate";
16
- import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecyclePlan, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
17
+ import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
17
18
  import { runGraph } from "./handlers/graph";
18
19
  import { runOp, runOpList, runOpStatus, runOpSignal, runOpCancel, runOpLog } from "./handlers/run";
19
20
 
@@ -50,6 +51,7 @@ export function parseArgs(args: string[]): ParsedArgs {
50
51
  reportFile: undefined,
51
52
  skill: undefined,
52
53
  src: undefined,
54
+ env: undefined,
53
55
  };
54
56
 
55
57
  let i = 0;
@@ -114,6 +116,16 @@ export function parseArgs(args: string[]): ParsedArgs {
114
116
  result.skill = args[++i];
115
117
  } else if (arg === "--src") {
116
118
  result.src = args[++i];
119
+ } else if (arg === "--env") {
120
+ result.env = args[++i];
121
+ } else if (arg === "--stacks") {
122
+ result.stacks = true;
123
+ } else if (arg === "--base") {
124
+ result.base = args[++i];
125
+ } else if (arg === "--head") {
126
+ result.head = args[++i];
127
+ } else if (arg === "--include-dependents") {
128
+ result.includeDependents = true;
117
129
  } else if (arg === "--local") {
118
130
  result.local = true;
119
131
  } else if (arg === "--temporal") {
@@ -155,6 +167,7 @@ Commands:
155
167
  lint Check specifications for issues
156
168
  list List discovered entities
157
169
  describe Show the effective config for one component
170
+ vendor Pull pinned, checksummed patterns into your repo
158
171
  import Import external template into TypeScript
159
172
  migrate <file> Translate a workflow between lexicons
160
173
  (default: --from github --to gitlab)
@@ -167,7 +180,7 @@ Ops:
167
180
  run cancel <name> Cancel the active workflow run (requires --force)
168
181
  run log <name> Show run history for an Op
169
182
 
170
- graph Show Op dependency graph
183
+ graph Show Op dependency graph (--stacks for cross-stack order)
171
184
 
172
185
  Lifecycle (alias: lc):
173
186
  lifecycle snapshot <env> Query API, save metadata to orphan branch
@@ -175,6 +188,7 @@ Lifecycle (alias: lc):
175
188
  lifecycle diff <env> Compare current build against last snapshot
176
189
  --live: query cloud now and detect drift
177
190
  lifecycle plan <env> Typed change set (create/update/delete/adopt) vs live
191
+ lifecycle affected Stacks a change affects (--base <ref> [--include-dependents])
178
192
  --json: emit the ChangeSet as JSON
179
193
  lifecycle log [env] History of lifecycle snapshots
180
194
 
@@ -199,6 +213,7 @@ Options:
199
213
  - list: text (default) or json
200
214
  - lint: stylish (default), json, or sarif
201
215
  -d, --lexicon <name> Build only the specified lexicon (e.g. aws, gitlab)
216
+ --env <name> Environment for organizational policy evaluation (build)
202
217
  -t, --template <name> Init template (e.g. node-pipeline, docker-build)
203
218
  --skill <name> Init: install only this skill from the lexicon
204
219
  --fix Auto-fix fixable issues (lint command)
@@ -291,12 +306,14 @@ const registry: CommandDef[] = [
291
306
  { name: "run", handler: runOp },
292
307
 
293
308
  { name: "graph", handler: runGraph },
309
+ { name: "vendor", handler: runVendor },
294
310
 
295
311
  // State subcommands
296
312
  { name: "lifecycle snapshot", requiresPlugins: true, handler: runLifecycleSnapshot },
297
313
  { name: "lifecycle show", handler: runLifecycleShow },
298
314
  { name: "lifecycle diff", requiresPlugins: true, handler: runLifecycleDiff },
299
315
  { name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
316
+ { name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
300
317
  { name: "lifecycle log", handler: runLifecycleLog },
301
318
 
302
319
  // Serve subcommands
@@ -53,6 +53,16 @@ export interface ParsedArgs {
53
53
  verbatim?: boolean;
54
54
  /** `chant lifecycle … --src <dir>` — build root override for lifecycle commands */
55
55
  src?: string;
56
+ /** `chant build --env <name>` — environment for organizational policy evaluation */
57
+ env?: string;
58
+ /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
59
+ stacks?: boolean;
60
+ /** `chant lifecycle affected --base <ref>` — base git ref to diff against */
61
+ base?: string;
62
+ /** `chant lifecycle affected --head <ref>` — head git ref (default: working tree) */
63
+ head?: string;
64
+ /** `chant lifecycle affected --include-dependents` — add downstream consumers */
65
+ includeDependents?: boolean;
56
66
  }
57
67
 
58
68
  /**
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export * from "./lint/declarative";
32
32
  export * from "./lint/selectors";
33
33
  export * from "./lint/named-checks";
34
34
  export * from "./lint/post-synth";
35
+ export * from "./lint/policy";
35
36
  export * from "./lint/rule-loader";
36
37
  export * from "./lint/discover";
37
38
  export * from "./import/parser";