@intentius/chant 0.12.1 → 0.13.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/audit/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AASD,KAAK,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEhD,UAAU,UAAU;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,YAAY,CAAC;CACvB;AAQD,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,UAAU,UAAU;IAClB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAqBpD;AAuKD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CA6B5H;AAID;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1E,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAsB7B;AAeD,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAsB/D;AAkBD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1D,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1E,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwB7B"}
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/audit/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAE3C,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AASD,KAAK,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEhD,UAAU,UAAU;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,YAAY,CAAC;CACvB;AAQD,qBAAa,UAAW,SAAQ,KAAK;CAAG;AAExC,UAAU,UAAU;IAClB,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAqBpD;AAoND;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CA6B5H;AAID;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1E,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAsB7B;AAeD,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACb;AAED,iFAAiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAsB/D;AAkBD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1D,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA+B7B;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAC1E,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwB7B"}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAKA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAe9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAiJpD"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":";AAKA,OAAO,EAAmC,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAgB9E;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAiJpD"}
@@ -52,7 +52,8 @@ export interface ParsedArgs {
52
52
  verbatim?: boolean;
53
53
  /** `chant lifecycle … --src <dir>` — build root override for lifecycle commands */
54
54
  src?: string;
55
- /** `chant build --env <name>` — environment for organizational policy evaluation */
55
+ /** `--env <name>` — active environment: sets CHANT_ENV so env-aware source
56
+ * re-evaluates for that environment (`build` + `graph`), and drives policy. */
56
57
  env?: string;
57
58
  /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
58
59
  stacks?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uEAAuE;IACvE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mFAAmF;IACnF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;uFACmF;IACnF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,0CAA0C;IAC1C,OAAO,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACnD;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,UAAU,CAAC;IAChB,4FAA4F;IAC5F,QAAQ,EAAE,OAAO,CAAC;CACnB;AAWD,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,eAAe,GAAG,IAAI,CAiB/F"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/cli/registry.ts"],"names":[],"mappings":"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"}
package/dist/env.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Active environment for a chant run (#505).
3
+ *
4
+ * `chant graph --env prod` / `chant build --env prod` set {@link ENV_VAR} before
5
+ * the project is discovered, so env-aware source can branch on it — producing an
6
+ * environment-specific graph (or build). Environments in chant are **build-context
7
+ * switches**, not a resource filter: there is no per-node env membership, so an
8
+ * "env view" comes from *re-evaluating* the project under the environment, exactly
9
+ * as a deploy of that environment would. Author env-aware source by branching on
10
+ * {@link env}:
11
+ *
12
+ * ```ts
13
+ * import { env } from "@intentius/chant";
14
+ * const replicas = env() === "prod" ? 5 : 1;
15
+ * ```
16
+ *
17
+ * Run `chant graph --env prod` and `chant graph --env dev` to get the two graphs;
18
+ * pinhole renders/diffs them to show environment drift (INTENTIUS/pinhole#3).
19
+ */
20
+ /** The environment variable the CLI sets from `--env`. */
21
+ export declare const ENV_VAR = "CHANT_ENV";
22
+ /** The active environment for this run (from `--env`), or `fallback` if none. */
23
+ export declare function env(fallback?: string): string | undefined;
24
+ /**
25
+ * Validate a requested environment against the project's declared `environments`
26
+ * (`chant.config`). Returns an error message for an unknown env, or `undefined`
27
+ * when it's valid (or when the project declares no environments, in which case
28
+ * any name is accepted).
29
+ */
30
+ export declare function unknownEnvError(requested: string | undefined, declared: string[] | undefined): string | undefined;
31
+ //# sourceMappingURL=env.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,OAAO,cAAc,CAAC;AAEnC,iFAAiF;AACjF,wBAAgB,GAAG,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIjH"}
@@ -60,14 +60,30 @@ export interface IREdge {
60
60
  export interface IRGroups {
61
61
  byLexicon?: Record<string, string[]>;
62
62
  byComposite?: Record<string, string[]>;
63
- /** Reserved for stack grouping (#494). */
63
+ /** Deployable-stack grouping (`stackName → nodeIds`). A stack is a lexicon
64
+ * partition today; #513 phase 2 regroups by nested child-project. Consumers
65
+ * (e.g. pinhole's boundary boxes) read this rather than inferring stacks. */
64
66
  byStack?: Record<string, string[]>;
65
67
  }
68
+ /** A cross-stack export this stack publishes (a `stackOutput`/`output`): its
69
+ * name (the matching key another stack imports by) and the node that produces it. */
70
+ export interface IRExport {
71
+ /** Output name — the cross-stack handle (e.g. "ClusterArn"). */
72
+ name: string;
73
+ /** Logical name of the producing node, when resolvable. */
74
+ node?: string;
75
+ /** Producer-side attribute the output reads (e.g. "Arn"). */
76
+ attr?: string;
77
+ }
66
78
  /** The full graph IR for a project at the default (declarable) detail level. */
67
79
  export interface GraphIR {
68
80
  nodes: IRNode[];
69
81
  edges: IREdge[];
70
82
  groups: IRGroups;
83
+ /** Outputs this stack publishes for other stacks to import. Imports are already
84
+ * nodes (`AWS::CloudFormation::Parameter` etc.); a viewer matches an import's
85
+ * name to an export's name to draw the cross-stack edge to its producer (#513). */
86
+ exports?: IRExport[];
71
87
  }
72
88
  /**
73
89
  * Build the graph IR from resolved entities (the output of `discover`). Pure and
@@ -1 +1 @@
1
- {"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAC;AAK7D;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iCAAiC;AACjC,MAAM,WAAW,MAAM;IACrB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mCAAmC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,KAAK,CAAC;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;CAClB;AA6ID;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAmDT"}
1
+ {"version":3,"file":"graph-ir.d.ts","sourceRoot":"","sources":["../src/graph-ir.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAgB,MAAM,cAAc,CAAC;AAK7D;;;;;;;;GAQG;AAEH,iFAAiF;AACjF,MAAM,WAAW,SAAS;IACxB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd;AAED,iCAAiC;AACjC,MAAM,WAAW,MAAM;IACrB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,mCAAmC;IACnC,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,IAAI,EAAE,KAAK,CAAC;IACZ,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACvC;;iFAE6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACpC;AAED;qFACqF;AACrF,MAAM,WAAW,QAAQ;IACvB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,gFAAgF;AAChF,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;IACjB;;uFAEmF;IACnF,OAAO,CAAC,EAAE,QAAQ,EAAE,CAAC;CACtB;AA6ID;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,EACjC,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CA6ET"}
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export * from "./discovery/graph.js";
24
24
  export * from "./discovery/index.js";
25
25
  export * from "./discovery/cache.js";
26
26
  export * from "./build.js";
27
+ export * from "./env.js";
27
28
  export * from "./graph-ir.js";
28
29
  export * from "./graph-detail.js";
29
30
  export * from "./graph-mermaid.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC;AACvB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAClE,cAAc,EAAE,iBAAiB,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5F,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,SAAS,CAAC;AACxB,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC;AACvB,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,SAAS,CAAC;AACxB,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC;AAC7B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,yBAAyB,CAAC;AACxC,cAAc,aAAa,CAAC;AAC5B,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAClE,cAAc,EAAE,iBAAiB,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC5F,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "description": "Declarative infrastructure-as-code toolkit — TypeScript on Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -54,14 +54,51 @@ function gitTreeMock(files: Record<string, string>, sizes: Record<string, number
54
54
  return { impl, calls };
55
55
  }
56
56
 
57
- /** A GitLab-shaped mock: project default_branch, paginated repository/tree, raw files. */
58
- function gitlabMock(files: Record<string, string>) {
57
+ /**
58
+ * GitLab mock for authenticated search-based discovery. `searchMap` maps search
59
+ * term → array of blob paths the search API returns. `files` maps path → content.
60
+ * Pass `token` to fetchRepoFiles when using this mock so the code takes the
61
+ * search path (unauthenticated calls fall back to BFS).
62
+ */
63
+ function gitlabMock(searchMap: Record<string, string[]>, files: Record<string, string>) {
64
+ const impl = (async (url: string | URL | Request) => {
65
+ const u = String(url);
66
+ if (u.includes("/search")) {
67
+ const params = new URL(u).searchParams;
68
+ const term = params.get("search") ?? "";
69
+ const page = Number(params.get("page") ?? "1");
70
+ const paths = page === 1 ? (searchMap[term] ?? []) : [];
71
+ return new Response(JSON.stringify(paths.map((p) => ({ path: p }))), { status: 200 });
72
+ }
73
+ const rm = u.match(/\/repository\/files\/(.+?)\/raw\?/);
74
+ if (rm) {
75
+ const path = decodeURIComponent(rm[1]);
76
+ if (files[path] === undefined) return new Response("not found", { status: 404 });
77
+ return new Response(files[path], { status: 200 });
78
+ }
79
+ if (/\/projects\/[^/]+(\?|$)/.test(u)) return new Response(JSON.stringify({ default_branch: "main" }), { status: 200 });
80
+ return new Response("not found", { status: 404 });
81
+ }) as unknown as typeof fetch;
82
+ return { impl };
83
+ }
84
+
85
+ /**
86
+ * GitLab mock for the unauthenticated BFS fallback. `dirEntries` maps directory
87
+ * path (empty = root) to entries returned by the non-recursive tree API. `files`
88
+ * maps path → content for the raw endpoint.
89
+ */
90
+ function gitlabBfsMock(
91
+ dirEntries: Record<string, Array<{ path: string; type: "blob" | "tree" }>>,
92
+ files: Record<string, string>,
93
+ ) {
59
94
  const impl = (async (url: string | URL | Request) => {
60
95
  const u = String(url);
61
96
  if (u.includes("/repository/tree")) {
62
- const page = Number(new URL(u).searchParams.get("page") ?? "1");
63
- const all = Object.keys(files).map((path) => ({ path, type: "blob" }));
64
- return new Response(JSON.stringify(page === 1 ? all : []), { status: 200 });
97
+ const params = new URL(u).searchParams;
98
+ const dir = params.get("path") ?? "";
99
+ const page = Number(params.get("page") ?? "1");
100
+ const entries = dirEntries[dir] ?? [];
101
+ return new Response(JSON.stringify(page === 1 ? entries : []), { status: 200 });
65
102
  }
66
103
  const rm = u.match(/\/repository\/files\/(.+?)\/raw\?/);
67
104
  if (rm) {
@@ -174,13 +211,80 @@ describe("fetchRepoFiles", () => {
174
211
  await expect(fetchRepoFiles("https://evil.example.com/o/r")).rejects.toThrow(/Host not allowed/);
175
212
  });
176
213
 
177
- test("gitlab: paginated tree + raw file content", async () => {
178
- const { impl } = gitlabMock({ ".gitlab-ci.yml": "stages:\n - build\n" });
179
- const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl });
214
+ test("gitlab: authenticated search finds .gitlab-ci.yml via stages: term", async () => {
215
+ const { impl } = gitlabMock(
216
+ { "stages:": [".gitlab-ci.yml"] },
217
+ { ".gitlab-ci.yml": "stages:\n - build\n" },
218
+ );
219
+ const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl, token: "tok" });
180
220
  expect(files.map((f) => f.path)).toEqual([".gitlab-ci.yml"]);
181
221
  expect(files[0].content).toContain("stages:");
182
222
  });
183
223
 
224
+ test("gitlab: search finds CI at non-canonical paths (include targets) (#520)", async () => {
225
+ // A .gitlab-ci.yml with `include:` referencing other CI files — the included
226
+ // file also has `stages:` so the search finds it without knowing the path.
227
+ const { impl } = gitlabMock(
228
+ { "stages:": [".gitlab-ci.yml", ".gitlab/ci/build.gitlab-ci.yml"] },
229
+ {
230
+ ".gitlab-ci.yml": "stages:\n - build\ninclude:\n - .gitlab/ci/build.gitlab-ci.yml\n",
231
+ ".gitlab/ci/build.gitlab-ci.yml": "stages:\n - build\nscript:\n - make\n",
232
+ },
233
+ );
234
+ const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl, token: "tok" });
235
+ expect(files.map((f) => f.path)).toContain(".gitlab/ci/build.gitlab-ci.yml");
236
+ });
237
+
238
+ test("gitlab: search finds IaC files by content, unaffected by repo size (#518)", async () => {
239
+ // Previously, gitlab-org/gitlab returned 0 files because the recursive tree
240
+ // API listed thousands of directories before any blobs. Search-based discovery
241
+ // is independent of directory count — it goes directly to content.
242
+ const { impl } = gitlabMock(
243
+ {
244
+ "AWSTemplateFormatVersion": ["infra/stack.json"],
245
+ "stages:": [".gitlab-ci.yml"],
246
+ },
247
+ {
248
+ "infra/stack.json": '{"AWSTemplateFormatVersion":"2010-09-09","Resources":{}}',
249
+ ".gitlab-ci.yml": "stages:\n - deploy\n",
250
+ },
251
+ );
252
+ const files = await fetchRepoFiles("https://gitlab.com/acme/monorepo", { fetchImpl: impl, token: "tok" });
253
+ const paths = files.map((f) => f.path);
254
+ expect(paths).toContain(".gitlab-ci.yml");
255
+ expect(paths).toContain("infra/stack.json");
256
+ });
257
+
258
+ test("gitlab: search deduplicates paths found by multiple terms", async () => {
259
+ // A Helm Chart.yaml matches both "apiVersion: v2" and the broader "apiVersion".
260
+ const { impl } = gitlabMock(
261
+ {
262
+ "apiVersion: v2": ["helm/Chart.yaml"],
263
+ "apiVersion": ["helm/Chart.yaml", "k8s/deploy.yaml"],
264
+ },
265
+ {
266
+ "helm/Chart.yaml": "apiVersion: v2\nname: myapp\nversion: 1.0.0\n",
267
+ "k8s/deploy.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
268
+ },
269
+ );
270
+ const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl, token: "tok" });
271
+ const paths = files.map((f) => f.path);
272
+ expect(paths.filter((p) => p === "helm/Chart.yaml")).toHaveLength(1);
273
+ expect(paths).toContain("k8s/deploy.yaml");
274
+ });
275
+
276
+ test("gitlab: unauthenticated falls back to BFS tree walk", async () => {
277
+ // Without a token, GitLab search returns 401. The BFS fallback walks
278
+ // directories non-recursively so root blobs appear on the first request.
279
+ const { impl } = gitlabBfsMock(
280
+ { "": [{ path: ".gitlab-ci.yml", type: "blob" }] },
281
+ { ".gitlab-ci.yml": "stages:\n - build\n" },
282
+ );
283
+ // No token → BFS path
284
+ const files = await fetchRepoFiles("https://gitlab.com/acme/widgets", { fetchImpl: impl });
285
+ expect(files.map((f) => f.path)).toContain(".gitlab-ci.yml");
286
+ });
287
+
184
288
  test("forgejo (codeberg): gitea tree + contents", async () => {
185
289
  const { impl } = gitTreeMock({ ".forgejo/workflows/ci.yml": CI_YAML });
186
290
  const files = await fetchRepoFiles("https://codeberg.org/acme/widgets", { fetchImpl: impl });
@@ -157,16 +157,61 @@ interface TreeEntry {
157
157
  /** List every blob path in the repo (recursive), with size where the host reports it. */
158
158
  async function listTree(host: HostConfig, owner: string, repo: string, ref: string, doFetch: typeof fetch, headers: Record<string, string>, ms: number): Promise<TreeEntry[]> {
159
159
  if (host.kind === "gitlab") {
160
+ // GitLab search API requires authentication even for public projects. When a
161
+ // token is present, use content search — one query per lexicon finds only
162
+ // relevant files regardless of repo size, and catches non-canonical CI paths
163
+ // that path-based detection misses (#518, #520). Without a token, fall back
164
+ // to the non-recursive BFS which works for public repos.
165
+ if ("PRIVATE-TOKEN" in headers) {
166
+ const out: TreeEntry[] = [];
167
+ const seen = new Set<string>();
168
+ const addPath = (p: string) => { if (typeof p === "string" && !seen.has(p)) { seen.add(p); out.push({ path: p, type: "blob" }); } };
169
+ // Ordered most → least selective so the seen-set dedup reduces noise from
170
+ // broader terms (e.g. `apiVersion` won't re-add files already found by
171
+ // the more specific `apiVersion: v2`).
172
+ const SEARCH_TERMS = [
173
+ "AWSTemplateFormatVersion", // CloudFormation
174
+ "deploymentTemplate", // Azure ARM ($schema substring)
175
+ "cnrm.cloud.google.com", // GCP Config Connector
176
+ "apiVersion: v2", // Helm Chart.yaml
177
+ "stages:", // GitLab CI — root file + non-canonical includes (#520)
178
+ "FROM ", // Dockerfiles (content-based; space avoids false matches)
179
+ "services:", // Docker Compose
180
+ "apiVersion", // k8s manifests (broad; runs last)
181
+ ];
182
+ for (const term of SEARCH_TERMS) {
183
+ for (let page = 1; page <= 3; page++) {
184
+ const url = `${host.api}/projects/${projectId(owner, repo)}/search?scope=blobs&search=${encodeURIComponent(term)}&per_page=100&page=${page}&ref=${encodeURIComponent(ref)}`;
185
+ const { body } = await getJsonAt(url, headers, doFetch, ms);
186
+ if (!Array.isArray(body) || body.length === 0) break;
187
+ for (const e of body as Array<{ path: string }>) addPath(e.path);
188
+ if (body.length < 100) break;
189
+ }
190
+ }
191
+ return out;
192
+ }
193
+ // Unauthenticated fallback: non-recursive BFS. GitLab's recursive tree API
194
+ // lists ALL directories before any blobs for large repos (#518), so we walk
195
+ // directories breadth-first to ensure root blobs appear on the first request.
160
196
  const out: TreeEntry[] = [];
161
- const MAX_PAGES = 20; // 20 * 100 = 2000 entries — bounded; candidate filter + caps trim further
162
- for (let page = 1; page <= MAX_PAGES; page++) {
163
- const url = `${host.api}/projects/${projectId(owner, repo)}/repository/tree?recursive=true&per_page=100&page=${page}&ref=${encodeURIComponent(ref)}`;
164
- const { body } = await getJsonAt(url, headers, doFetch, ms);
165
- if (!Array.isArray(body) || body.length === 0) break;
166
- for (const e of body as Array<{ path: string; type: string }>) {
167
- if (e.type === "blob") out.push({ path: e.path, type: "blob" });
197
+ const queue: string[] = [""]; // "" = repo root
198
+ const MAX_DIRS = 30;
199
+ const MAX_BLOBS = 200;
200
+ let dirs = 0;
201
+ while (queue.length > 0 && dirs < MAX_DIRS && out.length < MAX_BLOBS) {
202
+ const dir = queue.shift()!;
203
+ dirs++;
204
+ const pathParam = dir ? `&path=${encodeURIComponent(dir)}` : "";
205
+ for (let page = 1; page <= 5; page++) {
206
+ const url = `${host.api}/projects/${projectId(owner, repo)}/repository/tree?per_page=100&page=${page}&ref=${encodeURIComponent(ref)}${pathParam}`;
207
+ const { body } = await getJsonAt(url, headers, doFetch, ms);
208
+ if (!Array.isArray(body) || body.length === 0) break;
209
+ for (const e of body as Array<{ path: string; type: string }>) {
210
+ if (e.type === "blob") out.push({ path: e.path, type: "blob" });
211
+ else if (e.type === "tree") queue.push(e.path);
212
+ }
213
+ if (body.length < 100) break;
168
214
  }
169
- if (body.length < 100) break;
170
215
  }
171
216
  return out;
172
217
  }
package/src/cli/main.ts CHANGED
@@ -5,6 +5,7 @@ import { formatSuccess, formatError } from "./format";
5
5
  import { loadPlugins, resolveProjectLexicons } from "./plugins";
6
6
  import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
7
7
  import { loadChantConfig } from "../config";
8
+ import { ENV_VAR, unknownEnvError } from "../env";
8
9
  import { initRuntime } from "../runtime-adapter";
9
10
  import { runBuild } from "./handlers/build";
10
11
  import { runLint } from "./handlers/lint";
@@ -242,7 +243,10 @@ Options:
242
243
  - list: text (default) or json
243
244
  - lint: stylish (default), json, or sarif
244
245
  -d, --lexicon <name> Build only the specified lexicon (e.g. aws, gitlab)
245
- --env <name> Environment for organizational policy evaluation (build)
246
+ --env <name> Active environment: sets CHANT_ENV so env-aware source
247
+ re-evaluates for that environment (build + graph), and
248
+ drives organizational policy. Must be in chant.config
249
+ \`environments\` when declared.
246
250
  -t, --template <name> Init template (e.g. node-pipeline, docker-build)
247
251
  --skill <name> Init: install only this skill from the lexicon
248
252
  --fix Auto-fix fixable issues (lint command)
@@ -369,16 +373,30 @@ async function main(): Promise<void> {
369
373
  process.exit(args.help ? 0 : 1);
370
374
  }
371
375
 
376
+ // `--env <name>` is a build-context switch for the whole invocation: set it
377
+ // *before* anything imports the project so env-aware source (and thus the graph
378
+ // / build) reflects that environment. Set early, before config import, since
379
+ // chant.config itself may branch on the env. (#505)
380
+ if (args.env) process.env[ENV_VAR] = args.env;
381
+
372
382
  // Initialize runtime adapter early — before plugins or commands run
373
383
  const projectPath0 = resolve(args.path === "." ? "." : args.path);
384
+ let loadedConfig;
374
385
  try {
375
- await loadChantConfig(projectPath0);
386
+ loadedConfig = await loadChantConfig(projectPath0);
376
387
  initRuntime();
377
388
  } catch {
378
389
  // Config may not exist yet (e.g. `chant init`)
379
390
  initRuntime();
380
391
  }
381
392
 
393
+ // Reject an --env that isn't among the project's declared `environments`.
394
+ const envErr = unknownEnvError(args.env, loadedConfig?.config.environments);
395
+ if (envErr) {
396
+ console.error(formatError({ message: envErr, hint: 'Declare it in chant.config `environments`, or omit --env.' }));
397
+ process.exit(1);
398
+ }
399
+
382
400
  const match = resolveCommand(args, registry);
383
401
  if (!match) {
384
402
  console.error(formatError({
@@ -53,7 +53,8 @@ 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 */
56
+ /** `--env <name>` — active environment: sets CHANT_ENV so env-aware source
57
+ * re-evaluates for that environment (`build` + `graph`), and drives policy. */
57
58
  env?: string;
58
59
  /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
59
60
  stacks?: boolean;
@@ -0,0 +1,77 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { mkdir, writeFile, rm } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { env, unknownEnvError, ENV_VAR } from "./env";
6
+ import { discover } from "./discovery/index";
7
+
8
+ const withEnv = async (value: string | undefined, fn: () => void | Promise<void>): Promise<void> => {
9
+ const prev = process.env[ENV_VAR];
10
+ try {
11
+ if (value === undefined) delete process.env[ENV_VAR];
12
+ else process.env[ENV_VAR] = value;
13
+ await fn();
14
+ } finally {
15
+ if (prev === undefined) delete process.env[ENV_VAR];
16
+ else process.env[ENV_VAR] = prev;
17
+ }
18
+ };
19
+
20
+ describe("env()", () => {
21
+ test("reads CHANT_ENV, with a fallback", async () => {
22
+ await withEnv(undefined, () => {
23
+ expect(env()).toBeUndefined();
24
+ expect(env("dev")).toBe("dev");
25
+ });
26
+ await withEnv("prod", () => {
27
+ expect(env()).toBe("prod");
28
+ expect(env("dev")).toBe("prod");
29
+ });
30
+ });
31
+ });
32
+
33
+ describe("unknownEnvError", () => {
34
+ test("accepts a declared env, and any env when none are declared", () => {
35
+ expect(unknownEnvError("prod", ["dev", "prod"])).toBeUndefined();
36
+ expect(unknownEnvError("prod", undefined)).toBeUndefined();
37
+ expect(unknownEnvError("prod", [])).toBeUndefined();
38
+ expect(unknownEnvError(undefined, ["dev"])).toBeUndefined();
39
+ });
40
+ test("rejects an undeclared env with a clear message", () => {
41
+ expect(unknownEnvError("stage", ["dev", "prod"])).toMatch(/Unknown environment "stage".*dev, prod/);
42
+ });
43
+ });
44
+
45
+ describe("env-aware discovery (#505)", () => {
46
+ // Env is a build-context switch: the same source, discovered under a different
47
+ // CHANT_ENV, yields a different entity set (here a prod-only resource).
48
+ const src = `
49
+ export const base = { entityType: "Base", [Symbol.for("chant.declarable")]: true };
50
+ export const prodOnly = process.env.CHANT_ENV === "prod"
51
+ ? { entityType: "ProdOnly", [Symbol.for("chant.declarable")]: true }
52
+ : undefined;
53
+ `;
54
+
55
+ test("re-evaluates the project under the active environment", async () => {
56
+ // Distinct dirs → distinct module URLs → fresh import per env (import() caches by path).
57
+ const prodDir = join(tmpdir(), `chant-env-prod-${Date.now()}-${Math.random()}`);
58
+ const devDir = join(tmpdir(), `chant-env-dev-${Date.now()}-${Math.random()}`);
59
+ await mkdir(prodDir, { recursive: true });
60
+ await mkdir(devDir, { recursive: true });
61
+ await writeFile(join(prodDir, "app.ts"), src);
62
+ await writeFile(join(devDir, "app.ts"), src);
63
+ try {
64
+ let prod!: Awaited<ReturnType<typeof discover>>;
65
+ let dev!: Awaited<ReturnType<typeof discover>>;
66
+ await withEnv("prod", async () => { prod = await discover(prodDir); });
67
+ await withEnv("dev", async () => { dev = await discover(devDir); });
68
+ expect(prod.entities.has("base")).toBe(true);
69
+ expect(dev.entities.has("base")).toBe(true);
70
+ expect(prod.entities.has("prodOnly")).toBe(true); // present under prod
71
+ expect(dev.entities.has("prodOnly")).toBe(false); // absent under dev
72
+ } finally {
73
+ await rm(prodDir, { recursive: true, force: true });
74
+ await rm(devDir, { recursive: true, force: true });
75
+ }
76
+ });
77
+ });
package/src/env.ts ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Active environment for a chant run (#505).
3
+ *
4
+ * `chant graph --env prod` / `chant build --env prod` set {@link ENV_VAR} before
5
+ * the project is discovered, so env-aware source can branch on it — producing an
6
+ * environment-specific graph (or build). Environments in chant are **build-context
7
+ * switches**, not a resource filter: there is no per-node env membership, so an
8
+ * "env view" comes from *re-evaluating* the project under the environment, exactly
9
+ * as a deploy of that environment would. Author env-aware source by branching on
10
+ * {@link env}:
11
+ *
12
+ * ```ts
13
+ * import { env } from "@intentius/chant";
14
+ * const replicas = env() === "prod" ? 5 : 1;
15
+ * ```
16
+ *
17
+ * Run `chant graph --env prod` and `chant graph --env dev` to get the two graphs;
18
+ * pinhole renders/diffs them to show environment drift (INTENTIUS/pinhole#3).
19
+ */
20
+
21
+ /** The environment variable the CLI sets from `--env`. */
22
+ export const ENV_VAR = "CHANT_ENV";
23
+
24
+ /** The active environment for this run (from `--env`), or `fallback` if none. */
25
+ export function env(fallback?: string): string | undefined {
26
+ return process.env[ENV_VAR] ?? fallback;
27
+ }
28
+
29
+ /**
30
+ * Validate a requested environment against the project's declared `environments`
31
+ * (`chant.config`). Returns an error message for an unknown env, or `undefined`
32
+ * when it's valid (or when the project declares no environments, in which case
33
+ * any name is accepted).
34
+ */
35
+ export function unknownEnvError(requested: string | undefined, declared: string[] | undefined): string | undefined {
36
+ if (!requested || !declared || declared.length === 0) return undefined;
37
+ if (declared.includes(requested)) return undefined;
38
+ return `Unknown environment "${requested}". Declared environments: ${declared.join(", ")}.`;
39
+ }
@@ -2,6 +2,7 @@ import { describe, test, expect } from "vitest";
2
2
  import { buildGraphIr } from "./graph-ir";
3
3
  import { DECLARABLE_MARKER, type Declarable } from "./declarable";
4
4
  import { AttrRef } from "./attrref";
5
+ import { LexiconOutput } from "./lexicon-output";
5
6
  import { resolveAttrRefs } from "./discovery/resolve";
6
7
  import { setProvenance } from "./provenance";
7
8
 
@@ -66,6 +67,20 @@ describe("buildGraphIr", () => {
66
67
  expect(ir.nodes.find((n) => n.id === "sg")!.attrs).toEqual({ VpcId: { $ref: "vpc.VpcId" } });
67
68
  });
68
69
 
70
+ test("surfaces cross-stack exports with the producing node, not as nodes (#513)", () => {
71
+ const cluster = decl({ lexicon: "aws", entityType: "AWS::ECS::Cluster" });
72
+ const out = new LexiconOutput(new AttrRef(cluster, "Arn"), "ClusterArn");
73
+ const entities = new Map<string, Declarable>([
74
+ ["cluster", cluster],
75
+ ["clusterArn", out as unknown as Declarable],
76
+ ]);
77
+ resolveAttrRefs(entities);
78
+
79
+ const ir = buildGraphIr(entities);
80
+ expect(ir.nodes.map((n) => n.id)).toEqual(["cluster"]); // the output itself is not a node
81
+ expect(ir.exports).toEqual([{ name: "ClusterArn", node: "cluster", attr: "Arn" }]);
82
+ });
83
+
69
84
  test("excludes property-kind declarables and keeps resources", () => {
70
85
  const resource = decl({ lexicon: "k8s", entityType: "Deployment" });
71
86
  const prop = decl({ lexicon: "k8s", entityType: "Probe", kind: "property" as const });
@@ -133,6 +148,9 @@ describe("buildGraphIr", () => {
133
148
  const ir = buildGraphIr(entities);
134
149
  expect(ir.nodes.map((n) => n.id)).toEqual(["alpha", "mid", "zeta"]); // sorted
135
150
  expect(ir.groups.byLexicon).toEqual({ gcp: ["alpha", "zeta"], k8s: ["mid"] });
151
+ // byStack mirrors the lexicon partition today (each lexicon → one deployable
152
+ // stack); emitted as its own axis so consumers read stacks, not lexicons (#513).
153
+ expect(ir.groups.byStack).toEqual({ gcp: ["alpha", "zeta"], k8s: ["mid"] });
136
154
 
137
155
  // Same input (different insertion order) yields identical IR.
138
156
  const reordered = new Map<string, Declarable>([
package/src/graph-ir.ts CHANGED
@@ -2,7 +2,7 @@ import { relative, isAbsolute } from "node:path";
2
2
  import { AttrRef } from "./attrref";
3
3
  import { isAttrRefLike } from "./utils";
4
4
  import { type Declarable, isDeclarable } from "./declarable";
5
- import { isLexiconOutput } from "./lexicon-output";
5
+ import { isLexiconOutput, type LexiconOutput } from "./lexicon-output";
6
6
  import { getProvenance } from "./provenance";
7
7
  import { INTRINSIC_MARKER } from "./intrinsic";
8
8
 
@@ -72,15 +72,32 @@ export interface IREdge {
72
72
  export interface IRGroups {
73
73
  byLexicon?: Record<string, string[]>;
74
74
  byComposite?: Record<string, string[]>;
75
- /** Reserved for stack grouping (#494). */
75
+ /** Deployable-stack grouping (`stackName → nodeIds`). A stack is a lexicon
76
+ * partition today; #513 phase 2 regroups by nested child-project. Consumers
77
+ * (e.g. pinhole's boundary boxes) read this rather than inferring stacks. */
76
78
  byStack?: Record<string, string[]>;
77
79
  }
78
80
 
81
+ /** A cross-stack export this stack publishes (a `stackOutput`/`output`): its
82
+ * name (the matching key another stack imports by) and the node that produces it. */
83
+ export interface IRExport {
84
+ /** Output name — the cross-stack handle (e.g. "ClusterArn"). */
85
+ name: string;
86
+ /** Logical name of the producing node, when resolvable. */
87
+ node?: string;
88
+ /** Producer-side attribute the output reads (e.g. "Arn"). */
89
+ attr?: string;
90
+ }
91
+
79
92
  /** The full graph IR for a project at the default (declarable) detail level. */
80
93
  export interface GraphIR {
81
94
  nodes: IRNode[];
82
95
  edges: IREdge[];
83
96
  groups: IRGroups;
97
+ /** Outputs this stack publishes for other stacks to import. Imports are already
98
+ * nodes (`AWS::CloudFormation::Parameter` etc.); a viewer matches an import's
99
+ * name to an export's name to draw the cross-stack edge to its producer (#513). */
100
+ exports?: IRExport[];
84
101
  }
85
102
 
86
103
  /** A node is anything that serializes to a resource — not a property or output. */
@@ -243,6 +260,7 @@ export function buildGraphIr(
243
260
  const nodes: IRNode[] = [];
244
261
  const byLexicon: Record<string, string[]> = {};
245
262
  const byComposite: Record<string, string[]> = {};
263
+ const byStack: Record<string, string[]> = {};
246
264
 
247
265
  for (const [name, entity] of entities) {
248
266
  if (!nodeIds.has(name)) continue;
@@ -260,6 +278,12 @@ export function buildGraphIr(
260
278
  nodes.push(node);
261
279
 
262
280
  (byLexicon[entity.lexicon] ??= []).push(name);
281
+ // A stack is a lexicon partition (each lexicon serialises to one deployable
282
+ // stack — a CloudFormation template, a CI config). `byStack` mirrors that
283
+ // today; #513 phase 2 will regroup it by nested child-project. It's a
284
+ // distinct axis from `byLexicon` (which is for provenance/colouring), so it's
285
+ // emitted separately even where the two currently coincide.
286
+ (byStack[entity.lexicon] ??= []).push(name);
263
287
  if (prov?.composite) (byComposite[prov.composite] ??= []).push(name);
264
288
  }
265
289
 
@@ -275,12 +299,31 @@ export function buildGraphIr(
275
299
  const edges = [...edgeMap.values()].sort((a, b) => edgeKey(a).localeCompare(edgeKey(b)));
276
300
  for (const ids of Object.values(byLexicon)) ids.sort();
277
301
  for (const ids of Object.values(byComposite)) ids.sort();
302
+ for (const ids of Object.values(byStack)) ids.sort();
278
303
 
279
304
  const groups: IRGroups = {};
280
305
  if (Object.keys(byLexicon).length) groups.byLexicon = sortKeys(byLexicon);
281
306
  if (Object.keys(byComposite).length) groups.byComposite = sortKeys(byComposite);
307
+ if (Object.keys(byStack).length) groups.byStack = sortKeys(byStack);
308
+
309
+ // Cross-stack exports: a stack's `output(ref, name)` LexiconOutputs are dropped
310
+ // from the node set (they're not resources), but their name + producing node is
311
+ // the handle another stack imports by. Surface them so a viewer can draw the
312
+ // cross-stack edge (#513). Resolve the producer via the output's source entity.
313
+ const exports: IRExport[] = [];
314
+ for (const entity of entities.values()) {
315
+ if (!isLexiconOutput(entity)) continue;
316
+ const lo = entity as unknown as LexiconOutput;
317
+ if (!lo.outputName) continue;
318
+ const parent = lo._sourceParent?.deref();
319
+ const node = (parent ? reverse.get(parent) : undefined) ?? (lo.sourceEntity || undefined);
320
+ exports.push({ name: lo.outputName, ...(node ? { node } : {}), ...(lo.sourceAttribute ? { attr: lo.sourceAttribute } : {}) });
321
+ }
322
+ exports.sort((a, b) => a.name.localeCompare(b.name));
282
323
 
283
- return { nodes, edges, groups };
324
+ const ir: GraphIR = { nodes, edges, groups };
325
+ if (exports.length) ir.exports = exports;
326
+ return ir;
284
327
  }
285
328
 
286
329
  function sortKeys(rec: Record<string, string[]>): Record<string, string[]> {
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from "./discovery/graph";
25
25
  export * from "./discovery/index";
26
26
  export * from "./discovery/cache";
27
27
  export * from "./build";
28
+ export * from "./env";
28
29
  export * from "./graph-ir";
29
30
  export * from "./graph-detail";
30
31
  export * from "./graph-mermaid";