@intentius/chant 0.41.1 → 0.41.3
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/components/capability-plugin.d.ts +16 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +5 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +1 -1
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/graph-detail.d.ts +16 -0
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts +22 -14
- package/dist/kubectl-context.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/handlers/graph.ts +24 -4
- package/src/components/capability-plugin.test.ts +21 -0
- package/src/components/capability-plugin.ts +33 -0
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +11 -1
- package/src/components/starter-plugin.ts +4 -2
- package/src/graph-detail.test.ts +37 -1
- package/src/graph-detail.ts +28 -0
- package/src/kubectl-context.test.ts +16 -24
- package/src/kubectl-context.ts +22 -19
|
@@ -82,4 +82,20 @@ export interface CapabilityPlugin {
|
|
|
82
82
|
* descriptive error rather than registered half-working.
|
|
83
83
|
*/
|
|
84
84
|
export declare function isCapabilityPlugin(value: unknown): value is CapabilityPlugin;
|
|
85
|
+
/**
|
|
86
|
+
* The calling module's own package version, read from the nearest
|
|
87
|
+
* `package.json` above it (chant #1505).
|
|
88
|
+
*
|
|
89
|
+
* `CapabilityPlugin.version` documents itself as the plugin package's semver,
|
|
90
|
+
* but chant's packages release in lockstep, so a hardcoded literal goes stale
|
|
91
|
+
* on every `just release` — the aws plugin shipped `"1.0.0"` from the day it
|
|
92
|
+
* was extracted from the starter set (#681), and the k8s plugin's authoring-
|
|
93
|
+
* time `"0.41.0"` was stale one release later. Walking up from the module's
|
|
94
|
+
* own URL survives both the `src/` (development condition) and `dist/`
|
|
95
|
+
* layouts, and an npm install, without any build-time stamping.
|
|
96
|
+
*
|
|
97
|
+
* Returns `"0.0.0"` when no versioned `package.json` is found — a visible
|
|
98
|
+
* sentinel rather than a guess; nothing gates on the field.
|
|
99
|
+
*/
|
|
100
|
+
export declare function ownPackageVersion(moduleUrl: string): string;
|
|
85
101
|
//# sourceMappingURL=capability-plugin.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"capability-plugin.d.ts","sourceRoot":"","sources":["../../src/components/capability-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAKH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/C;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,gKAAgK;IAChK,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yIAAyI;IACzI,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAE/B,uFAAuF;IACvF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,mGAAmG;IACnG,YAAY,IAAI,KAAK,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC;IAGlD,qIAAqI;IACrI,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAE/B,gLAAgL;IAChL,QAAQ,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAE/C,2FAA2F;IAC3F,IAAI,CAAC,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAa5E;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAa3D"}
|
|
@@ -72,6 +72,11 @@ export interface ComponentGraphResult {
|
|
|
72
72
|
* so a renderer can deep-link a component node to source (`chant graph
|
|
73
73
|
* --components --format ir` sets `sourceLoc` from this). */
|
|
74
74
|
files?: Record<string, string>;
|
|
75
|
+
/** Component name → the live resource names it owns (#1491): the declared
|
|
76
|
+
* `liveNames`, or `[name]` when undeclared — the identity-join fallback the
|
|
77
|
+
* `Component` contract already specifies. This is what lets a consumer join
|
|
78
|
+
* the component DAG to the resource graph without guessing at kinds. */
|
|
79
|
+
liveNames?: Record<string, string[]>;
|
|
75
80
|
error?: string;
|
|
76
81
|
}
|
|
77
82
|
/** Compute the components' dependency graph under `path`, for `chant graph --components`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli-support.d.ts","sourceRoot":"","sources":["../../src/components/cli-support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAiB,KAAK,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAQL,KAAK,eAAe,EAEpB,KAAK,eAAe,EACrB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAuC,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAChG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAE9D,yDAAyD;AACzD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;IACrB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qFAAqF;AACrF,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAqBnG;AAED,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,0FAA0F;IAC1F,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sFAAsF;AACtF,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAuBvH;AAED,4GAA4G;AAC5G,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC3C;;gEAE4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6FAA6F;AAC7F,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,oBAAoB,CAAC,
|
|
1
|
+
{"version":3,"file":"cli-support.d.ts","sourceRoot":"","sources":["../../src/components/cli-support.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* from verb modules, so neither needed to change for this migration —
|
|
26
26
|
* the compatibility guarantee #559 asks for.
|
|
27
27
|
*/
|
|
28
|
-
import type
|
|
28
|
+
import { type CapabilityPlugin } from "./capability-plugin.js";
|
|
29
29
|
/**
|
|
30
30
|
* Every `kind` in the starter verb set, grouped by family, per epic #551 and
|
|
31
31
|
* docs/components/capabilities.mdx. Source of truth (moved here from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"starter-plugin.d.ts","sourceRoot":"","sources":["../../src/components/starter-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"starter-plugin.d.ts","sourceRoot":"","sources":["../../src/components/starter-plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAkBH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE/E;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB;;;;;;;CAaxB,CAAC;AA2BX;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,gBAOrC,CAAC"}
|
package/dist/graph-detail.d.ts
CHANGED
|
@@ -18,4 +18,20 @@ export declare const DETAIL: {
|
|
|
18
18
|
};
|
|
19
19
|
/** Apply a detail tier to the base (declarable-level) IR. */
|
|
20
20
|
export declare function applyDetail(ir: GraphIR, level: DetailLevel): GraphIR;
|
|
21
|
+
/**
|
|
22
|
+
* chant #1489 — the message to print when `--detail 3` changed nothing.
|
|
23
|
+
*
|
|
24
|
+
* T3's only addition over T2 is the producer attribute on `$ref`-derived
|
|
25
|
+
* edges. A graph whose resources link by name or label convention instead of
|
|
26
|
+
* attribute references (the k8s lexicon end to end) has nothing to annotate,
|
|
27
|
+
* so levels 2 and 3 come out byte-identical — which reads as a broken dial
|
|
28
|
+
* from any consumer stepping through the levels (behold#131 was filed over
|
|
29
|
+
* exactly this). Accepting a value that changes nothing without saying so is
|
|
30
|
+
* the bug; this names it.
|
|
31
|
+
*
|
|
32
|
+
* Returns the warning text, or undefined when detail 3 did add something.
|
|
33
|
+
* Callers print it through their own sink; the compare is over the two IRs
|
|
34
|
+
* the caller already holds, so this stays a pure function.
|
|
35
|
+
*/
|
|
36
|
+
export declare function detailInertNotice(base: GraphIR, detailed: GraphIR): string | undefined;
|
|
21
37
|
//# sourceMappingURL=graph-detail.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-detail.d.ts","sourceRoot":"","sources":["../src/graph-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExC,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAYpE"}
|
|
1
|
+
{"version":3,"file":"graph-detail.d.ts","sourceRoot":"","sources":["../src/graph-detail.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAkB,MAAM,YAAY,CAAC;AAE1D;;;;;;;;;GASG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAExC,eAAO,MAAM,MAAM;;;;;CAKT,CAAC;AAEX,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,OAAO,CAYpE;AA+HD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWtF"}
|
|
@@ -30,11 +30,13 @@ export interface K8sConfigShape {
|
|
|
30
30
|
profiles?: Record<string, K8sClusterProfile>;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
* kubectl context
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Historically thrown when an environment declared a cluster binding but the
|
|
34
|
+
* ambient kubectl context disagreed with it (#1100/#1155). As of #1488 the
|
|
35
|
+
* resolver no longer polices ambient at all — a declared binding is USED, not
|
|
36
|
+
* checked — so {@link resolveClusterTarget} never throws this anymore. The
|
|
37
|
+
* class stays exported because the k8s lexicon's read/write paths still catch
|
|
38
|
+
* it (converting a refusal into NOT-OBSERVED), and a custom connector may
|
|
39
|
+
* still throw it to get that classification.
|
|
38
40
|
*/
|
|
39
41
|
export declare class ClusterBindingMismatchError extends Error {
|
|
40
42
|
readonly environment: string;
|
|
@@ -57,8 +59,11 @@ export interface ResolvedClusterTarget {
|
|
|
57
59
|
* `kubectl config current-context`; the k8s lexicon's typed API client
|
|
58
60
|
* (chant #1074) supplies one that reads the parsed kubeconfig instead, so a
|
|
59
61
|
* client that never needs the `kubectl` binary does not acquire a dependency
|
|
60
|
-
* on it just to check the binding.
|
|
61
|
-
*
|
|
62
|
+
* on it just to check the binding.
|
|
63
|
+
*
|
|
64
|
+
* As of #1488 the resolver itself no longer reads ambient (a declared binding
|
|
65
|
+
* is used directly), so this hook only matters to callers that surface the
|
|
66
|
+
* ambient context in their own messages.
|
|
62
67
|
*/
|
|
63
68
|
export type AmbientContextReader = () => Promise<string | undefined>;
|
|
64
69
|
/** Options for {@link resolveClusterTarget}. */
|
|
@@ -79,13 +84,16 @@ export interface ResolveClusterTargetOptions {
|
|
|
79
84
|
* - No binding declared: returns `{ source: "ambient" }` — unchanged
|
|
80
85
|
* behavior — but logs a visible warning identifying the caller and
|
|
81
86
|
* environment, so the fallback is never silent (#1100 acceptance).
|
|
82
|
-
* - Binding declared
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
87
|
+
* - Binding declared: returns `{ context: bound, source: "bound" }`,
|
|
88
|
+
* regardless of what is ambient (#1488). The declared binding is the
|
|
89
|
+
* selection, not a check: callers pass this context explicitly on every
|
|
90
|
+
* read and write, so whichever cluster `kubectl` happens to be pointed at
|
|
91
|
+
* is irrelevant. Any k3d cluster another project creates mid-session used
|
|
92
|
+
* to steal the ambient context and turn a healthy estate grey with
|
|
93
|
+
* `read-failed` as the only explanation; now it cannot. A bound context
|
|
94
|
+
* that is absent from the kubeconfig fails downstream in the typed client
|
|
95
|
+
* with an error naming the context and the `k8s.profiles.<env>.context`
|
|
96
|
+
* binding — never by falling back to ambient.
|
|
89
97
|
*/
|
|
90
98
|
export declare function resolveClusterTarget(config: Record<string, unknown>, environment: string, lexiconName: string, options?: ResolveClusterTargetOptions): Promise<ResolvedClusterTarget>;
|
|
91
99
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kubectl-context.d.ts","sourceRoot":"","sources":["../src/kubectl-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC9C;AAED
|
|
1
|
+
{"version":3,"file":"kubectl-context.d.ts","sourceRoot":"","sources":["../src/kubectl-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAItD,wFAAwF;AACxF,MAAM,WAAW,iBAAiB;IAChC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC9C;AAED;;;;;;;;GAQG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;aAElC,WAAW,EAAE,MAAM;aACnB,eAAe,EAAE,MAAM;aACvB,cAAc,EAAE,MAAM;gBAFtB,WAAW,EAAE,MAAM,EACnB,eAAe,EAAE,MAAM,EACvB,cAAc,EAAE,MAAM;CAWzC;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,MAAM,EAAE,OAAO,GAAG,SAAS,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,oBAAoB,GAAG,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;AAarE,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,qBAAqB,CAAC,CAiBhC;AAID;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB;AAC5B,kFAAkF;AAChF;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE;AACpB,4DAA4D;GAC1D;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAmBrE;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,kBAAkB,CAuCvE"}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ import { observeResources } from "../../lifecycle/observe";
|
|
|
9
9
|
import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
|
|
10
10
|
import { loadChantConfig, environmentNames, loadChantConfigUpward, type ChantConfig } from "../../config";
|
|
11
11
|
import { applyLiveEndpoint } from "../../live-endpoint";
|
|
12
|
-
import { applyDetail, type DetailLevel } from "../../graph-detail";
|
|
12
|
+
import { applyDetail, detailInertNotice, type DetailLevel } from "../../graph-detail";
|
|
13
13
|
import { applyLens, parseLens } from "../../graph-lens";
|
|
14
14
|
import { toMermaid } from "../../graph-mermaid";
|
|
15
15
|
import { toDot } from "../../graph-dot";
|
|
@@ -343,7 +343,16 @@ async function runGraphLive(
|
|
|
343
343
|
return 1;
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
|
-
|
|
346
|
+
{
|
|
347
|
+
const base = ir;
|
|
348
|
+
ir = applyDetail(ir, (args.detail ?? 2) as DetailLevel);
|
|
349
|
+
// #1489 — a --detail 3 that changed nothing says so instead of silently
|
|
350
|
+
// emitting the same bytes as --detail 2.
|
|
351
|
+
if (args.detail === 3) {
|
|
352
|
+
const notice = detailInertNotice(base, ir);
|
|
353
|
+
if (notice) console.error(formatWarning({ message: notice }));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
347
356
|
|
|
348
357
|
// Containment grouping (#779) → boundary boxes. Built after lens/detail and
|
|
349
358
|
// filtered to surviving nodes, so a lens can't leave dangling group refs.
|
|
@@ -452,7 +461,10 @@ async function runComponentGraphView(
|
|
|
452
461
|
id: name,
|
|
453
462
|
kind: "Component",
|
|
454
463
|
lexicon: "chant",
|
|
455
|
-
|
|
464
|
+
// `liveNames` (#1491): the resources this component owns — declared, or
|
|
465
|
+
// the contract's `[name]` identity fallback — so a consumer can join
|
|
466
|
+
// the component DAG to the resource graph instead of guessing at kinds.
|
|
467
|
+
attrs: { wave: waveOf.get(name) ?? null, liveNames: graph.liveNames?.[name] ?? [name] },
|
|
456
468
|
// Deep-link the node to its `*.component.ts` (behold's inspect panel).
|
|
457
469
|
...(graph.files?.[name] ? { sourceLoc: { file: graph.files[name] } } : {}),
|
|
458
470
|
})),
|
|
@@ -558,7 +570,15 @@ async function runGraphView(
|
|
|
558
570
|
return 1;
|
|
559
571
|
}
|
|
560
572
|
}
|
|
561
|
-
|
|
573
|
+
{
|
|
574
|
+
const base = ir;
|
|
575
|
+
ir = applyDetail(ir, level as DetailLevel);
|
|
576
|
+
// #1489 — same say-so as the live path: an inert --detail 3 names itself.
|
|
577
|
+
if (level === 3) {
|
|
578
|
+
const notice = detailInertNotice(base, ir);
|
|
579
|
+
if (notice) console.error(formatWarning({ message: notice }));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
562
582
|
return emitIr(ir, ctx, format);
|
|
563
583
|
}
|
|
564
584
|
|
|
@@ -233,3 +233,24 @@ describe("MalformedCapabilityPluginError", () => {
|
|
|
233
233
|
expect(err.name).toBe("MalformedCapabilityPluginError");
|
|
234
234
|
});
|
|
235
235
|
});
|
|
236
|
+
|
|
237
|
+
// #1505 — plugin versions track the lockstep release instead of a literal that
|
|
238
|
+
// goes stale on every `just release` (aws shipped "1.0.0" from its extraction;
|
|
239
|
+
// the k8s plugin's authoring-time literal was stale one release later).
|
|
240
|
+
describe("ownPackageVersion (#1505)", () => {
|
|
241
|
+
test("resolves this module's own package version — core's package.json, exactly", async () => {
|
|
242
|
+
const { ownPackageVersion } = await import("./capability-plugin");
|
|
243
|
+
const { readFileSync } = await import("node:fs");
|
|
244
|
+
const { version } = JSON.parse(
|
|
245
|
+
readFileSync(new URL("../../package.json", import.meta.url), "utf-8"),
|
|
246
|
+
) as { version: string };
|
|
247
|
+
expect(ownPackageVersion(import.meta.url)).toBe(version);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("the starter plugin reports that version, not a literal", async () => {
|
|
251
|
+
const { starterCapabilityPlugin } = await import("./starter-plugin");
|
|
252
|
+
const { ownPackageVersion } = await import("./capability-plugin");
|
|
253
|
+
expect(starterCapabilityPlugin.version).toBe(ownPackageVersion(import.meta.url));
|
|
254
|
+
expect(starterCapabilityPlugin.version).not.toBe("1.0.0");
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* migration path and the "no behavior change" guarantee.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { readFileSync } from "node:fs";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
31
34
|
import type { Capability } from "./capability";
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -106,3 +109,33 @@ export function isCapabilityPlugin(value: unknown): value is CapabilityPlugin {
|
|
|
106
109
|
const obj = value as Record<string, unknown>;
|
|
107
110
|
return typeof obj.capabilities === "function";
|
|
108
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The calling module's own package version, read from the nearest
|
|
115
|
+
* `package.json` above it (chant #1505).
|
|
116
|
+
*
|
|
117
|
+
* `CapabilityPlugin.version` documents itself as the plugin package's semver,
|
|
118
|
+
* but chant's packages release in lockstep, so a hardcoded literal goes stale
|
|
119
|
+
* on every `just release` — the aws plugin shipped `"1.0.0"` from the day it
|
|
120
|
+
* was extracted from the starter set (#681), and the k8s plugin's authoring-
|
|
121
|
+
* time `"0.41.0"` was stale one release later. Walking up from the module's
|
|
122
|
+
* own URL survives both the `src/` (development condition) and `dist/`
|
|
123
|
+
* layouts, and an npm install, without any build-time stamping.
|
|
124
|
+
*
|
|
125
|
+
* Returns `"0.0.0"` when no versioned `package.json` is found — a visible
|
|
126
|
+
* sentinel rather than a guess; nothing gates on the field.
|
|
127
|
+
*/
|
|
128
|
+
export function ownPackageVersion(moduleUrl: string): string {
|
|
129
|
+
let dir = dirname(fileURLToPath(moduleUrl));
|
|
130
|
+
for (;;) {
|
|
131
|
+
try {
|
|
132
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as { version?: unknown };
|
|
133
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
134
|
+
} catch {
|
|
135
|
+
// No package.json here (or unreadable) — keep walking up.
|
|
136
|
+
}
|
|
137
|
+
const parent = dirname(dir);
|
|
138
|
+
if (parent === dir) return "0.0.0";
|
|
139
|
+
dir = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -275,6 +275,23 @@ describe("computeComponentGraph", () => {
|
|
|
275
275
|
expect(result.success).toBe(false);
|
|
276
276
|
expect(result.error).toMatch(/unknown component|ghost/i);
|
|
277
277
|
});
|
|
278
|
+
|
|
279
|
+
test("carries each component's liveNames, with the [name] identity fallback (#1491)", async () => {
|
|
280
|
+
await writeFile(
|
|
281
|
+
join(testDir, "pg.component.ts"),
|
|
282
|
+
`export const pg = { name: "postgres", liveNames: ["pgDeployment", "pgService", "pgClaim"], dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
|
|
283
|
+
);
|
|
284
|
+
await writeFile(
|
|
285
|
+
join(testDir, "plain.component.ts"),
|
|
286
|
+
`export const plain = { name: "plain", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const result = await computeComponentGraph(testDir);
|
|
290
|
+
|
|
291
|
+
expect(result.success).toBe(true);
|
|
292
|
+
expect(result.liveNames?.postgres).toEqual(["pgDeployment", "pgService", "pgClaim"]);
|
|
293
|
+
expect(result.liveNames?.plain).toEqual(["plain"]);
|
|
294
|
+
});
|
|
278
295
|
});
|
|
279
296
|
|
|
280
297
|
// ── runComponents (#585) ─────────────────────────────────────────────────────
|
|
@@ -136,6 +136,11 @@ export interface ComponentGraphResult {
|
|
|
136
136
|
* so a renderer can deep-link a component node to source (`chant graph
|
|
137
137
|
* --components --format ir` sets `sourceLoc` from this). */
|
|
138
138
|
files?: Record<string, string>;
|
|
139
|
+
/** Component name → the live resource names it owns (#1491): the declared
|
|
140
|
+
* `liveNames`, or `[name]` when undeclared — the identity-join fallback the
|
|
141
|
+
* `Component` contract already specifies. This is what lets a consumer join
|
|
142
|
+
* the component DAG to the resource graph without guessing at kinds. */
|
|
143
|
+
liveNames?: Record<string, string[]>;
|
|
139
144
|
error?: string;
|
|
140
145
|
}
|
|
141
146
|
|
|
@@ -154,8 +159,13 @@ export async function computeComponentGraph(path: string, sandbox?: boolean): Pr
|
|
|
154
159
|
|
|
155
160
|
// component name → its declaring file, relative to `path`, for node deep-links.
|
|
156
161
|
const files: Record<string, string> = {};
|
|
162
|
+
// component name → owned live resource names (#1491), declared or the
|
|
163
|
+
// contract's identity fallback.
|
|
164
|
+
const liveNames: Record<string, string[]> = {};
|
|
157
165
|
for (const [name, discovered] of result.components) {
|
|
158
166
|
files[name] = relative(path, discovered.filePath);
|
|
167
|
+
const declared = discovered.component.liveNames;
|
|
168
|
+
liveNames[name] = declared && declared.length > 0 ? [...declared] : [name];
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
try {
|
|
@@ -164,7 +174,7 @@ export async function computeComponentGraph(path: string, sandbox?: boolean): Pr
|
|
|
164
174
|
for (const c of driverComponents) {
|
|
165
175
|
for (const dep of c.dependsOn ?? []) edges.push({ from: c.name, to: dep });
|
|
166
176
|
}
|
|
167
|
-
return { success: true, order, waves, edges, files };
|
|
177
|
+
return { success: true, order, waves, edges, files, liveNames };
|
|
168
178
|
} catch (err) {
|
|
169
179
|
if (err instanceof UnknownDependencyError || err instanceof DependencyCycleError) {
|
|
170
180
|
return { success: false, order: [], waves: [], edges: [], error: err.message };
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
healthGateCapability,
|
|
43
43
|
shellCapability,
|
|
44
44
|
} from "./verbs/index";
|
|
45
|
-
import type
|
|
45
|
+
import { ownPackageVersion, type CapabilityPlugin } from "./capability-plugin";
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Every `kind` in the starter verb set, grouped by family, per epic #551 and
|
|
@@ -99,7 +99,9 @@ function starterCapabilities(): Array<Capability<never, unknown>> {
|
|
|
99
99
|
*/
|
|
100
100
|
export const starterCapabilityPlugin: CapabilityPlugin = {
|
|
101
101
|
name: "starter",
|
|
102
|
-
version
|
|
102
|
+
// The core package's own version (#1505) — lockstep releases bump it, so a
|
|
103
|
+
// literal here would go stale every `just release`.
|
|
104
|
+
version: ownPackageVersion(import.meta.url),
|
|
103
105
|
capabilities: starterCapabilities,
|
|
104
106
|
families: () => STARTER_VERB_FAMILIES,
|
|
105
107
|
};
|
package/src/graph-detail.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from "vitest";
|
|
2
|
-
import { applyDetail, DETAIL } from "./graph-detail";
|
|
2
|
+
import { applyDetail, DETAIL, detailInertNotice } from "./graph-detail";
|
|
3
3
|
import type { GraphIR } from "./graph-ir";
|
|
4
4
|
|
|
5
5
|
// A small graph: a gcp vpc/subnet pair plus a k8s namespace and deployment that
|
|
@@ -90,3 +90,39 @@ describe("applyDetail", () => {
|
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
// #1489 — an inert --detail 3 names itself instead of silently emitting the
|
|
95
|
+
// same bytes as --detail 2 (the k8s lexicon links by name/label convention, so
|
|
96
|
+
// its graphs never have a producer attribute to annotate).
|
|
97
|
+
describe("detailInertNotice (#1489)", () => {
|
|
98
|
+
test("detail 3 that added toAttr annotations: no notice", () => {
|
|
99
|
+
const detailed = applyDetail(base, DETAIL.ATTRIBUTES);
|
|
100
|
+
expect(detailInertNotice(base, detailed)).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("edges without producer attributes: notice names the convention-linking cause", () => {
|
|
104
|
+
const labelLinked: GraphIR = {
|
|
105
|
+
nodes: [
|
|
106
|
+
{ id: "svc", kind: "Service", lexicon: "k8s", attrs: { spec: { selector: { app: "web" } } } },
|
|
107
|
+
{ id: "web", kind: "Deployment", lexicon: "k8s", attrs: { metadata: { labels: { app: "web" } } } },
|
|
108
|
+
],
|
|
109
|
+
edges: [{ from: "svc", to: "web", kind: "ref", viaAttr: "spec.selector" }],
|
|
110
|
+
groups: {},
|
|
111
|
+
};
|
|
112
|
+
const detailed = applyDetail(labelLinked, DETAIL.ATTRIBUTES);
|
|
113
|
+
const notice = detailInertNotice(labelLinked, detailed);
|
|
114
|
+
expect(notice).toContain("--detail 3");
|
|
115
|
+
expect(notice).toContain("1 edge(s)");
|
|
116
|
+
expect(notice).toContain("identical to --detail 2");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("no edges at all: notice says so", () => {
|
|
120
|
+
const edgeless: GraphIR = {
|
|
121
|
+
nodes: [{ id: "ns", kind: "Namespace", lexicon: "k8s", attrs: {} }],
|
|
122
|
+
edges: [],
|
|
123
|
+
groups: {},
|
|
124
|
+
};
|
|
125
|
+
const detailed = applyDetail(edgeless, DETAIL.ATTRIBUTES);
|
|
126
|
+
expect(detailInertNotice(edgeless, detailed)).toContain("no edges at all");
|
|
127
|
+
});
|
|
128
|
+
});
|
package/src/graph-detail.ts
CHANGED
|
@@ -158,3 +158,31 @@ function findRefAttr(attrs: Record<string, unknown>, producer: string): string |
|
|
|
158
158
|
visit(attrs);
|
|
159
159
|
return found;
|
|
160
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* chant #1489 — the message to print when `--detail 3` changed nothing.
|
|
164
|
+
*
|
|
165
|
+
* T3's only addition over T2 is the producer attribute on `$ref`-derived
|
|
166
|
+
* edges. A graph whose resources link by name or label convention instead of
|
|
167
|
+
* attribute references (the k8s lexicon end to end) has nothing to annotate,
|
|
168
|
+
* so levels 2 and 3 come out byte-identical — which reads as a broken dial
|
|
169
|
+
* from any consumer stepping through the levels (behold#131 was filed over
|
|
170
|
+
* exactly this). Accepting a value that changes nothing without saying so is
|
|
171
|
+
* the bug; this names it.
|
|
172
|
+
*
|
|
173
|
+
* Returns the warning text, or undefined when detail 3 did add something.
|
|
174
|
+
* Callers print it through their own sink; the compare is over the two IRs
|
|
175
|
+
* the caller already holds, so this stays a pure function.
|
|
176
|
+
*/
|
|
177
|
+
export function detailInertNotice(base: GraphIR, detailed: GraphIR): string | undefined {
|
|
178
|
+
if (JSON.stringify(detailed) !== JSON.stringify(base)) return undefined;
|
|
179
|
+
const edges = base.edges.length;
|
|
180
|
+
const why =
|
|
181
|
+
edges === 0
|
|
182
|
+
? "this graph has no edges at all"
|
|
183
|
+
: `none of this graph's ${edges} edge(s) reference a producer attribute — they link by name or label convention`;
|
|
184
|
+
return (
|
|
185
|
+
`--detail 3 adds the producer attribute to reference edges, but ${why}, ` +
|
|
186
|
+
`so the output is identical to --detail 2.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -16,7 +16,7 @@ vi.mock("node:child_process", async () => {
|
|
|
16
16
|
|
|
17
17
|
const { resolveClusterTarget, ClusterBindingMismatchError } = await import("./kubectl-context");
|
|
18
18
|
|
|
19
|
-
describe("resolveClusterTarget (chant #1100)", () => {
|
|
19
|
+
describe("resolveClusterTarget (chant #1100, #1488)", () => {
|
|
20
20
|
beforeEach(() => {
|
|
21
21
|
execMock.mockReset();
|
|
22
22
|
});
|
|
@@ -34,9 +34,7 @@ describe("resolveClusterTarget (chant #1100)", () => {
|
|
|
34
34
|
warnSpy.mockRestore();
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
test("bound
|
|
38
|
-
execMock.mockResolvedValue({ stdout: "prod-eks\n", stderr: "" });
|
|
39
|
-
|
|
37
|
+
test("bound: returns the bound context without ever probing the ambient one (#1488)", async () => {
|
|
40
38
|
const target = await resolveClusterTarget(
|
|
41
39
|
{ k8s: { profiles: { prod: { context: "prod-eks" } } } },
|
|
42
40
|
"prod",
|
|
@@ -44,11 +42,14 @@ describe("resolveClusterTarget (chant #1100)", () => {
|
|
|
44
42
|
);
|
|
45
43
|
|
|
46
44
|
expect(target).toEqual({ context: "prod-eks", source: "bound" });
|
|
47
|
-
expect(execMock).
|
|
45
|
+
expect(execMock).not.toHaveBeenCalled();
|
|
48
46
|
});
|
|
49
47
|
|
|
50
|
-
test("bound
|
|
51
|
-
|
|
48
|
+
test("bound while a different context is ambient: the binding wins, no refusal (#1488)", async () => {
|
|
49
|
+
// Ambient says staging-eks; the declared binding must be used regardless.
|
|
50
|
+
// Before #1488 this threw ClusterBindingMismatchError, which turned a
|
|
51
|
+
// healthy estate grey the moment any other project switched the context.
|
|
52
|
+
execMock.mockResolvedValue({ stdout: "staging-eks\n", stderr: "" });
|
|
52
53
|
|
|
53
54
|
const target = await resolveClusterTarget(
|
|
54
55
|
{ k8s: { profiles: { prod: { context: "prod-eks" } } } },
|
|
@@ -59,23 +60,14 @@ describe("resolveClusterTarget (chant #1100)", () => {
|
|
|
59
60
|
expect(target).toEqual({ context: "prod-eks", source: "bound" });
|
|
60
61
|
});
|
|
61
62
|
|
|
62
|
-
test("
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
).
|
|
70
|
-
|
|
71
|
-
expect(err).toBeInstanceOf(ClusterBindingMismatchError);
|
|
72
|
-
const mismatch = err as InstanceType<typeof ClusterBindingMismatchError>;
|
|
73
|
-
expect(mismatch.environment).toBe("prod");
|
|
74
|
-
expect(mismatch.expectedContext).toBe("prod-eks");
|
|
75
|
-
expect(mismatch.ambientContext).toBe("staging-eks");
|
|
76
|
-
expect(mismatch.message).toContain('environment "prod"');
|
|
77
|
-
expect(mismatch.message).toContain('"prod-eks"');
|
|
78
|
-
expect(mismatch.message).toContain('"staging-eks"');
|
|
63
|
+
test("ClusterBindingMismatchError still constructs and names all three parts (catchers rely on it)", () => {
|
|
64
|
+
const err = new ClusterBindingMismatchError("prod", "prod-eks", "staging-eks");
|
|
65
|
+
expect(err.environment).toBe("prod");
|
|
66
|
+
expect(err.expectedContext).toBe("prod-eks");
|
|
67
|
+
expect(err.ambientContext).toBe("staging-eks");
|
|
68
|
+
expect(err.message).toContain('environment "prod"');
|
|
69
|
+
expect(err.message).toContain('"prod-eks"');
|
|
70
|
+
expect(err.message).toContain('"staging-eks"');
|
|
79
71
|
});
|
|
80
72
|
|
|
81
73
|
test("bound for a different environment than the one requested: treated as unbound for this environment", async () => {
|
package/src/kubectl-context.ts
CHANGED
|
@@ -38,11 +38,13 @@ export interface K8sConfigShape {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
* kubectl context
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
41
|
+
* Historically thrown when an environment declared a cluster binding but the
|
|
42
|
+
* ambient kubectl context disagreed with it (#1100/#1155). As of #1488 the
|
|
43
|
+
* resolver no longer polices ambient at all — a declared binding is USED, not
|
|
44
|
+
* checked — so {@link resolveClusterTarget} never throws this anymore. The
|
|
45
|
+
* class stays exported because the k8s lexicon's read/write paths still catch
|
|
46
|
+
* it (converting a refusal into NOT-OBSERVED), and a custom connector may
|
|
47
|
+
* still throw it to get that classification.
|
|
46
48
|
*/
|
|
47
49
|
export class ClusterBindingMismatchError extends Error {
|
|
48
50
|
constructor(
|
|
@@ -77,8 +79,11 @@ export interface ResolvedClusterTarget {
|
|
|
77
79
|
* `kubectl config current-context`; the k8s lexicon's typed API client
|
|
78
80
|
* (chant #1074) supplies one that reads the parsed kubeconfig instead, so a
|
|
79
81
|
* client that never needs the `kubectl` binary does not acquire a dependency
|
|
80
|
-
* on it just to check the binding.
|
|
81
|
-
*
|
|
82
|
+
* on it just to check the binding.
|
|
83
|
+
*
|
|
84
|
+
* As of #1488 the resolver itself no longer reads ambient (a declared binding
|
|
85
|
+
* is used directly), so this hook only matters to callers that surface the
|
|
86
|
+
* ambient context in their own messages.
|
|
82
87
|
*/
|
|
83
88
|
export type AmbientContextReader = () => Promise<string | undefined>;
|
|
84
89
|
|
|
@@ -112,13 +117,16 @@ export interface ResolveClusterTargetOptions {
|
|
|
112
117
|
* - No binding declared: returns `{ source: "ambient" }` — unchanged
|
|
113
118
|
* behavior — but logs a visible warning identifying the caller and
|
|
114
119
|
* environment, so the fallback is never silent (#1100 acceptance).
|
|
115
|
-
* - Binding declared
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
120
|
+
* - Binding declared: returns `{ context: bound, source: "bound" }`,
|
|
121
|
+
* regardless of what is ambient (#1488). The declared binding is the
|
|
122
|
+
* selection, not a check: callers pass this context explicitly on every
|
|
123
|
+
* read and write, so whichever cluster `kubectl` happens to be pointed at
|
|
124
|
+
* is irrelevant. Any k3d cluster another project creates mid-session used
|
|
125
|
+
* to steal the ambient context and turn a healthy estate grey with
|
|
126
|
+
* `read-failed` as the only explanation; now it cannot. A bound context
|
|
127
|
+
* that is absent from the kubeconfig fails downstream in the typed client
|
|
128
|
+
* with an error naming the context and the `k8s.profiles.<env>.context`
|
|
129
|
+
* binding — never by falling back to ambient.
|
|
122
130
|
*/
|
|
123
131
|
export async function resolveClusterTarget(
|
|
124
132
|
config: Record<string, unknown>,
|
|
@@ -141,11 +149,6 @@ export async function resolveClusterTarget(
|
|
|
141
149
|
return { source: "ambient" };
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
const ambient = await (options.ambientContext ?? currentAmbientContext)();
|
|
145
|
-
if (ambient && ambient !== bound) {
|
|
146
|
-
throw new ClusterBindingMismatchError(environment, bound, ambient);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
152
|
return { context: bound, source: "bound" };
|
|
150
153
|
}
|
|
151
154
|
|