@konfig.ts/core 0.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1182 @@
1
+ import { Config, Context, Data, Effect, Layer, Schema, Stream } from "effect";
2
+ import * as YAML from "yaml";
3
+ import { FileSystem } from "effect/FileSystem";
4
+ import { Path } from "effect/Path";
5
+ import { ChildProcess } from "effect/unstable/process";
6
+ import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
7
+ import { NodeRuntime, NodeServices } from "@effect/platform-node";
8
+ //#region \0rolldown/runtime.js
9
+ var __defProp = Object.defineProperty;
10
+ var __exportAll = (all, no_symbols) => {
11
+ let target = {};
12
+ for (var name in all) __defProp(target, name, {
13
+ get: all[name],
14
+ enumerable: true
15
+ });
16
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
17
+ return target;
18
+ };
19
+ //#endregion
20
+ //#region src/_cast.ts
21
+ /**
22
+ * Nominal-typing primitive — attach a phantom brand to a string value.
23
+ * Used by `Dep.*Ref` constructors and the like. Safe by construction:
24
+ * the brand is a type-only label that the caller stamps on themselves.
25
+ */
26
+ const brand = (value) => value;
27
+ /**
28
+ * Unsafe escape hatch — claim a value has type `T` without runtime
29
+ * proof. Every call site MUST pass a one-line `reason` explaining why
30
+ * the cast is sound (e.g. "variance erasure", "runtime narrowed via
31
+ * _tag check", "Object.keys is string[]"). Audit by grepping for
32
+ * `unsafeCoerce(` and reading the reasons.
33
+ *
34
+ * For values crossing a trust boundary (external command output, file
35
+ * contents, network payloads), prefer `boundary` from
36
+ * `@konfig.ts/core/boundary` — it produces a `BoundaryDecodeError`
37
+ * instead of silently accepting the claim.
38
+ *
39
+ * The `reason` parameter is intentionally not used at runtime; it
40
+ * documents intent for readers and audits.
41
+ */
42
+ const unsafeCoerce = (value, _reason) => value;
43
+ //#endregion
44
+ //#region src/RenderError.ts
45
+ var RenderError = class extends Data.TaggedError("RenderError") {};
46
+ var EmbedYamlReadError = class extends Data.TaggedError("EmbedYamlReadError") {};
47
+ var BoundaryDecodeError = class extends Data.TaggedError("BoundaryDecodeError") {};
48
+ var HelmVersionTooLow = class extends Data.TaggedError("HelmVersionTooLow") {
49
+ get message() {
50
+ return `Helm CLI too old: requires >= ${this.required}, found ${this.found}`;
51
+ }
52
+ };
53
+ var HelmRenderError = class extends Data.TaggedError("HelmRenderError") {};
54
+ var HelmDigestMismatch = class extends Data.TaggedError("HelmDigestMismatch") {
55
+ get message() {
56
+ return `Helm chart ${this.chart}@${this.version} digest mismatch: expected ${this.expected}, got ${this.actual}`;
57
+ }
58
+ };
59
+ var CrdExtractError = class extends Data.TaggedError("CrdExtractError") {};
60
+ //#endregion
61
+ //#region src/boundary.ts
62
+ const boundary = (input) => (value) => Schema.decodeUnknownEffect(input.schema)(value).pipe(Effect.mapError((cause) => new BoundaryDecodeError({
63
+ schema: input.label ?? "boundary",
64
+ cause
65
+ })));
66
+ //#endregion
67
+ //#region src/Compose.ts
68
+ var Compose_exports = /* @__PURE__ */ __exportAll({
69
+ composeLayers: () => composeLayers,
70
+ makeResidualEntrypoint: () => makeResidualEntrypoint
71
+ });
72
+ /**
73
+ * Runtime layer composition — `reduce(Layer.provideMerge)`. Each
74
+ * successive module receives every prior module's Out as available
75
+ * services. The runtime type collapses to a bottom Layer; the per-module
76
+ * Out/In is tracked statically by `ResidualIn`.
77
+ */
78
+ const composeLayers = (modules) => {
79
+ return modules.reduce((acc, mod) => unsafeCoerce(Layer.provideMerge(unsafeCoerce(mod.layer, "handle.layer carries its narrow type at the call site; the fold collapses to AnyLayer here"), acc), "Layer.provideMerge's return type is per-call; the fold accumulator stays AnyLayer"), unsafeCoerce(Layer.empty, "Layer.empty has type Layer<never, never, never>; widening to AnyLayer is a no-op at runtime"));
80
+ };
81
+ /**
82
+ * Build an `entrypoint` function bound to a specific API name. The
83
+ * returned function accepts only programs whose `R` channel reduces to
84
+ * `Manifest.RenderServices`; otherwise the call fails at the program
85
+ * argument with a `_konfig_unsatisfied` hint that names the missing
86
+ * provider and the calling API.
87
+ */
88
+ const makeResidualEntrypoint = (_api) => (program) => unsafeCoerce(program, "ResidualHintCheck is a phantom intersection; once the call typechecks, the runtime value is the original Effect");
89
+ //#endregion
90
+ //#region src/deps.ts
91
+ var deps_exports = /* @__PURE__ */ __exportAll({
92
+ App: () => App,
93
+ Application: () => Application,
94
+ BuiltImageRef: () => BuiltImageRef,
95
+ ConfigMap: () => ConfigMap,
96
+ Image: () => Image,
97
+ Namespace: () => Namespace,
98
+ Pvc: () => Pvc,
99
+ Secret: () => Secret,
100
+ SecretValues: () => SecretValues,
101
+ ServiceAccount: () => ServiceAccount,
102
+ provideApplication: () => provideApplication,
103
+ provideConfigMap: () => provideConfigMap,
104
+ provideImage: () => provideImage,
105
+ provideNamespace: () => provideNamespace,
106
+ providePvc: () => providePvc,
107
+ provideSecret: () => provideSecret,
108
+ provideServiceAccount: () => provideServiceAccount
109
+ });
110
+ const Secret = (name) => Context.Service(`Secret:${name}`);
111
+ const SecretValues = (name) => Context.Service(`SecretValues:${name}`);
112
+ const ConfigMap = (name) => Context.Service(`ConfigMap:${name}`);
113
+ const Namespace = (name) => Context.Service(`Namespace:${name}`);
114
+ const ServiceAccount = (name) => Context.Service(`ServiceAccount:${name}`);
115
+ const Application = (name) => Context.Service(`Application:${name}`);
116
+ const Pvc = (name) => Context.Service(`Pvc:${name}`);
117
+ const App = (name) => Context.Service(`App:${name}`);
118
+ /**
119
+ * Context.Service tag for a `BuiltImageRef<App>`. Modules that build an
120
+ * image emit a Layer providing this service; modules that deploy a
121
+ * container yield `Dep.Image(app)` to receive the typed ref. The
122
+ * dep-graph residual at `AppOfApps.entrypoint` catches a missing
123
+ * provider exactly like for `Dep.Secret`.
124
+ */
125
+ const Image = (name) => Context.Service(`Image:${name}`);
126
+ const _fullRef = (input) => brand(`${input.registry}/${input.app}:${input.tag}`);
127
+ /**
128
+ * `BuiltImageRef` value namespace.
129
+ *
130
+ * const apiImage = BuiltImageRef.of({
131
+ * app: "api",
132
+ * registry: "ghcr.io/example",
133
+ * tag: "1.0.0",
134
+ * });
135
+ *
136
+ * `BuiltImageRef.of` constructs the brand from registry + app + tag.
137
+ * The literal `app` is captured in the brand so a workload's
138
+ * `Dep.Need<"Image", App>` matches only the build module that
139
+ * provides this exact app.
140
+ */
141
+ const BuiltImageRef = { of: (input) => _fullRef(input) };
142
+ /**
143
+ * Layer providing `Dep.Image(App)` for downstream consumers. Combine
144
+ * with `Application.define` / `Module.fixedNs`'s `provides` slot to
145
+ * have a build module surface its image to sibling workload modules
146
+ * in the composition.
147
+ */
148
+ const provideImage = (input) => Layer.succeed(Image(input.app))(_fullRef(input));
149
+ const provideSecret = (name) => Layer.succeed(Secret(name))(brand(name));
150
+ const provideConfigMap = (name) => Layer.succeed(ConfigMap(name))(brand(name));
151
+ const provideNamespace = (name) => Layer.succeed(Namespace(name))(name);
152
+ const provideServiceAccount = (name) => Layer.succeed(ServiceAccount(name))(brand(name));
153
+ const provideApplication = (name) => Layer.succeed(Application(name))(name);
154
+ const providePvc = (name) => Layer.succeed(Pvc(name))(brand(name));
155
+ //#endregion
156
+ //#region src/Bundle.ts
157
+ var Bundle_exports = /* @__PURE__ */ __exportAll({
158
+ define: () => define,
159
+ entrypoint: () => entrypoint,
160
+ fromModules: () => fromModules,
161
+ make: () => make$1,
162
+ makeSet: () => makeSet,
163
+ target: () => target
164
+ });
165
+ /**
166
+ * Mutate-attach a `.layer` field to an Effect Context.Tag — same pattern
167
+ * `argocd/Application.ts:_attachLayerToTag` uses. A single unsafe cast
168
+ * in the dep-graph machinery; lives here so the rest of Bundle stays
169
+ * cast-free.
170
+ */
171
+ const _attachLayerToTag = (tag, layer) => unsafeCoerce(Object.assign(tag, { layer }), "Effect Context.Tag is callable + extensible; Object.assign mutates in place and the cast widens the public type");
172
+ const make$1 = (opts) => ({
173
+ name: opts.name,
174
+ manifests: opts.manifests,
175
+ ...opts.namespace !== void 0 ? { namespace: opts.namespace } : {}
176
+ });
177
+ /**
178
+ * Build a typed handle for a manifest bundle — a name + optional
179
+ * namespace + a set of manifests, plus dep-graph wiring. Same
180
+ * compile-time guarantees as argocd's `Application.define` minus
181
+ * `source: ArgoSource` / `syncPolicy` / sync-wave annotations:
182
+ * - the literal `Name` flows into `Dep.Provide<"App", Name>`,
183
+ * - the optional literal namespace flows into `Dep.Provide<"Namespace", Ns>`,
184
+ * - the build callback's `R` channel becomes the handle's `In` after
185
+ * subtracting what this bundle provides itself.
186
+ *
187
+ * Pair with `Bundle.fromModules` to compose multiple bundles
188
+ * and have the dep-graph residual checked at `Bundle.entrypoint`.
189
+ *
190
+ * For `Module.fixedNs` / `Module.dynamicNs` use, see `Bundle.target`
191
+ * — it adapts this `define` so namespace is required (Module wrappers
192
+ * always thread a namespace through), which lets the dep-graph drop
193
+ * the `Provide<"Namespace", never>` cell the optional shape needs.
194
+ */
195
+ const define = (opts) => {
196
+ const name = unsafeCoerce(opts.name, "LiteralName<Name> resolves to Name itself once the call typechecks");
197
+ const namespace = opts.namespace === void 0 ? void 0 : unsafeCoerce(opts.namespace, "LiteralName<Ns> resolves to Ns itself once the call typechecks");
198
+ const tag = App(name);
199
+ const nsLayer = namespace === void 0 ? Layer.empty : Layer.succeed(Namespace(namespace))(namespace);
200
+ const internalLayer = opts.provides !== void 0 ? Layer.mergeAll(nsLayer, opts.provides) : nsLayer;
201
+ const buildEffect = Effect.isEffect(opts.build) ? opts.build : Effect.sync(opts.build);
202
+ const bundleLayer = Layer.effect(tag, buildEffect.pipe(Effect.map((manifests) => make$1({
203
+ name,
204
+ ...namespace !== void 0 ? { namespace } : {},
205
+ manifests
206
+ })))).pipe(Layer.provide(internalLayer));
207
+ return unsafeCoerce(_attachLayerToTag(tag, opts.provides !== void 0 ? Layer.mergeAll(bundleLayer, nsLayer, opts.provides) : Layer.mergeAll(bundleLayer, nsLayer)), "narrow generic BundleHandle from the attachLayerToTag helper's loose Tag arg");
208
+ };
209
+ /**
210
+ * `Module.Target` adapter for `Bundle.define`. `Module.fixedNs` /
211
+ * `Module.dynamicNs` always pass a namespace through, so this adapter
212
+ * requires `namespace` (whereas `Bundle.define` itself accepts it as
213
+ * optional) — pinning `Ns` to a real literal lets the dep-graph emit
214
+ * a concrete `Provide<"Namespace", Ns>` cell on the resulting handle.
215
+ *
216
+ * ```ts
217
+ * const defineCache = Module.fixedNs({
218
+ * target: Bundle.target,
219
+ * namespace: "cache",
220
+ * build: ({ name, namespace }) => [ ... ],
221
+ * });
222
+ * ```
223
+ */
224
+ const target = { define: (args) => unsafeCoerce(define(args), "target requires namespace so Ns is always a string literal; under that constraint Bundle.define's _NsProvides<Ns> reduces to Provide<\"Namespace\", Ns>, matching HandleKind") };
225
+ const makeSet = (opts) => ({
226
+ name: opts.name ?? "bundles",
227
+ bundles: opts.bundles
228
+ });
229
+ /**
230
+ * Phantom check that rejects a `Bundle.fromModules` program whose `R`
231
+ * channel still carries unmet dep-graph Needs. Bound to the
232
+ * "Bundle.fromModules" API label so the `_konfig_unsatisfied` hint
233
+ * guides the user to the right call site.
234
+ */
235
+ const entrypoint = makeResidualEntrypoint("Bundle.fromModules");
236
+ /**
237
+ * One-list composition for a backend-agnostic bundle set. Yields each
238
+ * module's `Bundle` in tuple order, then wires the merged provider layer
239
+ * with `Compose.composeLayers`. The returned Effect's R channel is the
240
+ * residual unmet Needs after the fold (`Compose.ResidualIn<Ms>`),
241
+ * which `entrypoint` rejects unless empty.
242
+ *
243
+ * **Order matters.** List providers before their consumers. A consumer
244
+ * placed before its provider leaves an unmet `Need` in the residual,
245
+ * which surfaces at `entrypoint` as a `_konfig_unsatisfied` hint.
246
+ */
247
+ const fromModules = (opts) => {
248
+ const program = Effect.gen(function* () {
249
+ const bundles = [];
250
+ for (const mod of opts.modules) {
251
+ const b = yield* mod;
252
+ bundles.push(b);
253
+ }
254
+ return makeSet({
255
+ name: opts.name,
256
+ bundles
257
+ });
258
+ });
259
+ const wired = composeLayers(opts.modules);
260
+ return unsafeCoerce(Effect.provide(program, wired), "the runtime Effect is the same; only the static R channel is narrowed to ResidualIn<Ms> by the fold-as-type");
261
+ };
262
+ //#endregion
263
+ //#region src/diff.ts
264
+ const IGNORED_LABEL_KEYS = new Set(["helm.sh/chart"]);
265
+ const IGNORED_ANNOTATION_KEYS = new Set(["meta.helm.sh/release-name", "meta.helm.sh/release-namespace"]);
266
+ const MANAGED_BY_HELM_LABEL = "app.kubernetes.io/managed-by";
267
+ const _redactLabelMap = (labels) => {
268
+ const out = {};
269
+ for (const [k, v] of Object.entries(labels)) {
270
+ if (IGNORED_LABEL_KEYS.has(k)) continue;
271
+ if (k === MANAGED_BY_HELM_LABEL && v === "Helm") continue;
272
+ out[k] = v;
273
+ }
274
+ return out;
275
+ };
276
+ const _redactAnnotationMap = (annotations) => {
277
+ const out = {};
278
+ for (const [k, v] of Object.entries(annotations)) {
279
+ if (IGNORED_ANNOTATION_KEYS.has(k)) continue;
280
+ out[k] = v;
281
+ }
282
+ return out;
283
+ };
284
+ const _isNumericString = (s) => /^-?\d+(\.\d+)?(e[-+]?\d+)?$/i.test(s);
285
+ /**
286
+ * Canonicalize a parsed YAML/JSON value for structural comparison,
287
+ * applying two normalizations:
288
+ *
289
+ * 1. **Null/undefined keys are dropped.** Any object key whose value is
290
+ * `null` or `undefined` is omitted from the redacted output. This is
291
+ * deliberate: it makes an *explicit* `field: null` compare equal to
292
+ * an *absent* `field`, which is what we want when diffing manifests
293
+ * where one side spells out a null default the other side simply
294
+ * omits. The trade-off is that a genuine "field was set to null vs.
295
+ * field removed" distinction is intentionally invisible to the diff.
296
+ * 2. **Numeric normalization** (opt-in via
297
+ * {@link RedactOptions.normalizeNumerics}) — see that field.
298
+ *
299
+ * Helm metadata redaction (dropping chart-churn labels/annotations) is
300
+ * layered on top when a `labels`/`annotations` map appears under
301
+ * `metadata`.
302
+ */
303
+ const redact = (input) => {
304
+ const value = input.value;
305
+ const parentKey = input.parentKey ?? null;
306
+ const options = input.options ?? {};
307
+ if (Array.isArray(value)) return value.map((v) => redact({
308
+ value: v,
309
+ parentKey: null,
310
+ options
311
+ }));
312
+ if (value !== null && typeof value === "object") {
313
+ const obj = unsafeCoerce(value, "typeof === object && !Array.isArray && !== null narrowed above");
314
+ const out = {};
315
+ for (const [k, v] of Object.entries(obj)) {
316
+ if (v === null || v === void 0) continue;
317
+ if (k === "labels" && parentKey === "metadata" && v !== null && typeof v === "object") {
318
+ out[k] = _redactLabelMap(unsafeCoerce(v, "metadata.labels is Record<string, string>"));
319
+ continue;
320
+ }
321
+ if (k === "annotations" && parentKey === "metadata" && v !== null && typeof v === "object") {
322
+ out[k] = _redactAnnotationMap(unsafeCoerce(v, "metadata.annotations is Record<string, string>"));
323
+ continue;
324
+ }
325
+ out[k] = redact({
326
+ value: v,
327
+ parentKey: k,
328
+ options
329
+ });
330
+ }
331
+ return out;
332
+ }
333
+ if (options.normalizeNumerics === true) {
334
+ if (typeof value === "string" && _isNumericString(value)) return Number(value);
335
+ if (typeof value === "number" && Number.isFinite(value)) return value;
336
+ }
337
+ return value;
338
+ };
339
+ const deepEqual = (input) => {
340
+ const { a, b } = input;
341
+ if (a === b) return true;
342
+ if (a === null || b === null) return false;
343
+ if (typeof a !== typeof b) return false;
344
+ if (Array.isArray(a)) {
345
+ if (!Array.isArray(b) || a.length !== b.length) return false;
346
+ for (let i = 0; i < a.length; i++) if (!deepEqual({
347
+ a: a[i],
348
+ b: b[i]
349
+ })) return false;
350
+ return true;
351
+ }
352
+ if (typeof a === "object" && typeof b === "object") {
353
+ const ka = Object.keys(unsafeCoerce(a, "typeof === object branch")).sort();
354
+ const kb = Object.keys(unsafeCoerce(b, "typeof === object branch")).sort();
355
+ if (ka.length !== kb.length) return false;
356
+ for (let i = 0; i < ka.length; i++) if (ka[i] !== kb[i]) return false;
357
+ for (const k of ka) {
358
+ const av = unsafeCoerce(a, "typeof === object branch")[k];
359
+ const bv = unsafeCoerce(b, "typeof === object branch")[k];
360
+ if (!deepEqual({
361
+ a: av,
362
+ b: bv
363
+ })) return false;
364
+ }
365
+ return true;
366
+ }
367
+ return false;
368
+ };
369
+ const parseYaml = (text) => YAML.parse(text);
370
+ /**
371
+ * Parse a multi-doc YAML file into an ordered list of documents. Each
372
+ * document's parsed structure is preserved as-is. Empty / whitespace-only
373
+ * segments are dropped, but their position is *not* preserved — only
374
+ * present documents are returned. Use the returned index to identify
375
+ * documents inside one file (alongside the filename) when reporting.
376
+ */
377
+ const parseYamlAll = (text) => {
378
+ const docs = YAML.parseAllDocuments(text);
379
+ const out = [];
380
+ for (const d of docs) {
381
+ const value = d.toJS();
382
+ if (value === null || value === void 0) continue;
383
+ out.push(value);
384
+ }
385
+ return out;
386
+ };
387
+ /**
388
+ * Identifier for a document inside a multi-doc YAML file. We key by
389
+ * (kind, name, namespace) when those fields are present so a
390
+ * label-only edit on a Service inside a 12-doc file diffs against the
391
+ * same-kind/name Service in the other side, regardless of position
392
+ * shuffle.
393
+ */
394
+ const _docKey = (value, fallbackIdx) => {
395
+ if (value === null || typeof value !== "object") return `:doc:${fallbackIdx}`;
396
+ const v = unsafeCoerce(value, "typeof === object && !== null branch above narrowed value; every field access below is guarded by a typeof check");
397
+ const kind = typeof v.kind === "string" ? v.kind : "";
398
+ const name = typeof v.metadata?.name === "string" ? v.metadata.name : "";
399
+ const ns = typeof v.metadata?.namespace === "string" ? v.metadata.namespace : "";
400
+ if (kind || name) return `${kind}|${ns}|${name}`;
401
+ return `:doc:${fallbackIdx}`;
402
+ };
403
+ const _diffOne = (file, leftText, rightText, options) => {
404
+ const lDocs = parseYamlAll(leftText).map((v, i) => [_docKey(v, i), redact({
405
+ value: v,
406
+ options
407
+ })]);
408
+ const rDocs = parseYamlAll(rightText).map((v, i) => [_docKey(v, i), redact({
409
+ value: v,
410
+ options
411
+ })]);
412
+ if (lDocs.length <= 1 && rDocs.length <= 1) {
413
+ const l = lDocs[0]?.[1];
414
+ const r = rDocs[0]?.[1];
415
+ if (deepEqual({
416
+ a: l,
417
+ b: r
418
+ })) return {
419
+ _tag: "Same",
420
+ file
421
+ };
422
+ return {
423
+ _tag: "Changed",
424
+ file,
425
+ left: l,
426
+ right: r
427
+ };
428
+ }
429
+ const lByKey = new Map(lDocs);
430
+ const rByKey = new Map(rDocs);
431
+ const keys = Array.from(new Set([...lByKey.keys(), ...rByKey.keys()])).sort();
432
+ const docs = [];
433
+ let anyChange = false;
434
+ for (const key of keys) {
435
+ const l = lByKey.get(key);
436
+ const r = rByKey.get(key);
437
+ if (l === void 0) {
438
+ docs.push({
439
+ _tag: "MissingLeft",
440
+ key,
441
+ right: r
442
+ });
443
+ anyChange = true;
444
+ } else if (r === void 0) {
445
+ docs.push({
446
+ _tag: "MissingRight",
447
+ key,
448
+ left: l
449
+ });
450
+ anyChange = true;
451
+ } else if (deepEqual({
452
+ a: l,
453
+ b: r
454
+ })) docs.push({
455
+ _tag: "Same",
456
+ key
457
+ });
458
+ else {
459
+ docs.push({
460
+ _tag: "Changed",
461
+ key,
462
+ left: l,
463
+ right: r
464
+ });
465
+ anyChange = true;
466
+ }
467
+ }
468
+ if (!anyChange) return {
469
+ _tag: "Same",
470
+ file
471
+ };
472
+ return {
473
+ _tag: "Changed",
474
+ file,
475
+ left: lDocs.map((d) => d[1]),
476
+ right: rDocs.map((d) => d[1]),
477
+ docs
478
+ };
479
+ };
480
+ const diffFiles = (input) => {
481
+ const { left, right } = input;
482
+ const options = input.options ?? {};
483
+ const files = Array.from(new Set([...Object.keys(left), ...Object.keys(right)])).sort();
484
+ const entries = [];
485
+ for (const file of files) {
486
+ const hasL = Object.hasOwn(left, file);
487
+ const hasR = Object.hasOwn(right, file);
488
+ if (!hasL) {
489
+ entries.push({
490
+ _tag: "MissingLeft",
491
+ file
492
+ });
493
+ continue;
494
+ }
495
+ if (!hasR) {
496
+ entries.push({
497
+ _tag: "MissingRight",
498
+ file
499
+ });
500
+ continue;
501
+ }
502
+ entries.push(_diffOne(file, left[file] ?? "", right[file] ?? "", options));
503
+ }
504
+ return { entries };
505
+ };
506
+ const hasDifferences = (result) => result.entries.some((e) => e._tag !== "Same");
507
+ const formatDiff = (input) => {
508
+ const { result } = input;
509
+ const format = input.format ?? "summary";
510
+ if (format === "json") return JSON.stringify(result, null, 2);
511
+ const changes = result.entries.filter((e) => e._tag !== "Same");
512
+ if (changes.length === 0) return "";
513
+ const lines = [];
514
+ for (const e of changes) {
515
+ if (e._tag === "MissingLeft") lines.push(`+ ${e.file}`);
516
+ else if (e._tag === "MissingRight") lines.push(`- ${e.file}`);
517
+ else lines.push(`~ ${e.file}`);
518
+ if (format === "detail" && e._tag === "Changed") if (e.docs && e.docs.length > 0) for (const d of e.docs) {
519
+ if (d._tag === "Same") continue;
520
+ lines.push(` [doc ${d.key}] ${d._tag}`);
521
+ if (d._tag === "Changed") {
522
+ lines.push(" left:");
523
+ lines.push(...YAML.stringify(d.left, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
524
+ lines.push(" right:");
525
+ lines.push(...YAML.stringify(d.right, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
526
+ }
527
+ }
528
+ else {
529
+ lines.push(" left:");
530
+ lines.push(...YAML.stringify(e.left, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
531
+ lines.push(" right:");
532
+ lines.push(...YAML.stringify(e.right, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
533
+ }
534
+ }
535
+ return lines.join("\n");
536
+ };
537
+ //#endregion
538
+ //#region src/Manifest.ts
539
+ var Manifest_exports = /* @__PURE__ */ __exportAll({
540
+ ManifestTypeId: () => ManifestTypeId,
541
+ combine: () => combine,
542
+ concat: () => concat,
543
+ embedYaml: () => embedYaml,
544
+ make: () => make,
545
+ whenever: () => whenever
546
+ });
547
+ const ManifestTypeId = Symbol.for("@konfig.ts/core/Manifest");
548
+ const variance = { _A: (_) => _ };
549
+ const make = (run) => ({
550
+ [ManifestTypeId]: unsafeCoerce(variance, "phantom variance witness — A only appears in covariant position"),
551
+ render: (ctx) => {
552
+ const result = run(ctx);
553
+ return Effect.isEffect(result) ? unsafeCoerce(result, "Effect.isEffect narrowed `result` to an Effect; TS's narrowing doesn't carry the Effect's full type parameters") : Effect.succeed(result);
554
+ }
555
+ });
556
+ const combine = (input) => make((ctx) => Effect.all([input.a.render(ctx), input.b.render(ctx)], { concurrency: "unbounded" }));
557
+ const concat = (...manifests) => make((ctx) => Effect.all(manifests.map((m) => m.render(ctx)), { concurrency: "unbounded" }).pipe(Effect.map((results) => results.flatMap((r) => Array.isArray(r) ? unsafeCoerce(r, "Array.isArray narrowed; element type is A by render contract") : [unsafeCoerce(r, "non-array branch carries a single A")]))));
558
+ const whenever = (input) => make((ctx) => input.cond ? input.thunk().render(ctx) : unsafeCoerce(Effect.succeed(void 0), "undefined branch — A is the type seen by the consumer when cond=false"));
559
+ const embedYaml = (source) => make((_ctx) => {
560
+ if ("literal" in source) return Effect.succeed({
561
+ _tag: "RawYaml",
562
+ content: source.literal
563
+ });
564
+ const path = source.path;
565
+ return Effect.gen(function* () {
566
+ return {
567
+ _tag: "RawYaml",
568
+ content: yield* (yield* FileSystem).readFileString(path),
569
+ origin: path
570
+ };
571
+ }).pipe(Effect.mapError((cause) => new EmbedYamlReadError({
572
+ path,
573
+ cause
574
+ })));
575
+ });
576
+ //#endregion
577
+ //#region src/subprocess.ts
578
+ /**
579
+ * Exit code stamped on a `ProcessError` when the process never produced
580
+ * one — i.e. the spawn itself failed (binary not found, EACCES, a stream
581
+ * read error). Distinct from any real OS exit code.
582
+ */
583
+ const SPAWN_FAILED_EXIT = -1;
584
+ /**
585
+ * Upper bound (in UTF-16 code units, a close proxy for bytes here) on the
586
+ * stderr tail retained by `ProcessError` — roughly the last 2KB. Keeps a
587
+ * failing command's diagnostics readable without pinning a runaway log in
588
+ * memory or in the serialized error.
589
+ */
590
+ const STDERR_TAIL_LIMIT = 2048;
591
+ const _tail = (text) => text.length > STDERR_TAIL_LIMIT ? text.slice(text.length - STDERR_TAIL_LIMIT) : text;
592
+ const _commandLabel = (command) => ChildProcess.isStandardCommand(command) ? [command.command, ...command.args].join(" ") : "<pipeline>";
593
+ /**
594
+ * Failure of a spawned subprocess: a non-zero exit, a spawn that never
595
+ * started, or (for `runProcessString`) an empty stdout when a payload was
596
+ * required. Carries the exit code and a bounded tail of stderr so callers
597
+ * can surface a diagnostic without re-running the command.
598
+ *
599
+ * `command` is the program plus its already-parsed arguments; it must
600
+ * never be interpolated with secret material by callers.
601
+ */
602
+ var ProcessError = class extends Data.TaggedError("ProcessError") {
603
+ get message() {
604
+ const tail = this.stderrTail.trim();
605
+ const suffix = tail.length > 0 ? `: ${tail}` : "";
606
+ return `command \`${this.command}\` failed (exit ${this.exitCode})${suffix}`;
607
+ }
608
+ };
609
+ const _collect = (stream) => Stream.mkString(Stream.decodeText(stream));
610
+ /**
611
+ * Spawn `command` exactly once and, concurrently, drain stdout and stderr
612
+ * to strings while awaiting the exit code. Draining both pipes alongside
613
+ * `exitCode` avoids the classic pipe-buffer deadlock. A spawn/stream
614
+ * failure (PlatformError) is folded into a `ProcessError` so callers see a
615
+ * single error channel.
616
+ */
617
+ const _spawnCollect = (command) => Effect.scoped(Effect.gen(function* () {
618
+ const handle = yield* (yield* ChildProcessSpawner).spawn(command);
619
+ const [exitCode, stdout, stderr] = yield* Effect.all([
620
+ handle.exitCode,
621
+ _collect(handle.stdout),
622
+ _collect(handle.stderr)
623
+ ], { concurrency: "unbounded" });
624
+ return {
625
+ exitCode,
626
+ stdout,
627
+ stderr
628
+ };
629
+ })).pipe(Effect.mapError((cause) => new ProcessError({
630
+ command: _commandLabel(command),
631
+ exitCode: SPAWN_FAILED_EXIT,
632
+ stderrTail: _tail(String(cause))
633
+ })));
634
+ /**
635
+ * Run `command` and return its stdout, checking the exit code — the guard
636
+ * that `spawner.string` omits. Fails with `ProcessError` (carrying the
637
+ * stderr tail) on any non-zero exit. Unless `allowEmptyStdout` is `true`,
638
+ * also fails when the trimmed stdout is empty, so a silently-failing
639
+ * command can never masquerade as an empty-but-successful result.
640
+ */
641
+ const runProcessString = (command, options) => Effect.gen(function* () {
642
+ const result = yield* _spawnCollect(command);
643
+ if (result.exitCode !== 0) return yield* Effect.fail(new ProcessError({
644
+ command: _commandLabel(command),
645
+ exitCode: result.exitCode,
646
+ stderrTail: _tail(result.stderr)
647
+ }));
648
+ if (options?.allowEmptyStdout !== true && result.stdout.trim().length === 0) return yield* Effect.fail(new ProcessError({
649
+ command: _commandLabel(command),
650
+ exitCode: result.exitCode,
651
+ stderrTail: _tail(result.stderr)
652
+ }));
653
+ return result.stdout;
654
+ });
655
+ /**
656
+ * Run `command` for its success/failure only — stdout is drained (to avoid
657
+ * a pipe deadlock) but discarded. Fails with `ProcessError` on any
658
+ * non-zero exit, attaching the stderr tail when present.
659
+ */
660
+ const runProcessExit = (command) => Effect.gen(function* () {
661
+ const result = yield* _spawnCollect(command);
662
+ if (result.exitCode !== 0) return yield* Effect.fail(new ProcessError({
663
+ command: _commandLabel(command),
664
+ exitCode: result.exitCode,
665
+ stderrTail: _tail(result.stderr)
666
+ }));
667
+ });
668
+ //#endregion
669
+ //#region src/Helm.ts
670
+ var Helm_exports = /* @__PURE__ */ __exportAll({ release: () => release });
671
+ const CLUSTER_SCOPED_KINDS = new Set([
672
+ "APIService",
673
+ "ClusterRole",
674
+ "ClusterRoleBinding",
675
+ "ComponentStatus",
676
+ "CSIDriver",
677
+ "CSINode",
678
+ "CustomResourceDefinition",
679
+ "FlowSchema",
680
+ "IngressClass",
681
+ "MutatingWebhookConfiguration",
682
+ "Namespace",
683
+ "Node",
684
+ "PersistentVolume",
685
+ "PodSecurityPolicy",
686
+ "PriorityClass",
687
+ "PriorityLevelConfiguration",
688
+ "RuntimeClass",
689
+ "StorageClass",
690
+ "ValidatingAdmissionPolicy",
691
+ "ValidatingAdmissionPolicyBinding",
692
+ "ValidatingWebhookConfiguration",
693
+ "VolumeAttachment"
694
+ ]);
695
+ const _asDocShape = (value) => value !== null && typeof value === "object" ? unsafeCoerce(value, "parseYamlAll returned a parsed document object; the kind/metadata reads below are each typeof-guarded") : null;
696
+ /**
697
+ * Turn `helm template` stdout into individual `RawYaml` docs.
698
+ *
699
+ * Document boundaries come from the shared `parseYamlAll`
700
+ * (`YAML.parseAllDocuments`) helper rather than a naive `/^---$/m` split,
701
+ * so a `---` appearing inside a block scalar can't spuriously split one
702
+ * manifest into two. `parseYamlAll` also drops empty / comment-only
703
+ * segments for us.
704
+ *
705
+ * When a `namespace` is supplied, each namespaced-kind document that
706
+ * lacks an explicit namespace is patched to carry it (the re-parse
707
+ * branch behavior). Cluster-scoped kinds and documents that already pin
708
+ * a namespace are left untouched.
709
+ */
710
+ const _parseHelmOutput = (input) => Effect.sync(() => {
711
+ const { output, chart, version, namespace } = input;
712
+ const origin = `helm:${chart}@${version}`;
713
+ const results = [];
714
+ for (const parsed of parseYamlAll(output)) {
715
+ let value = parsed;
716
+ if (namespace !== void 0) {
717
+ const shape = _asDocShape(parsed);
718
+ if (shape !== null && typeof shape.kind === "string" && !CLUSTER_SCOPED_KINDS.has(shape.kind) && (shape.metadata?.namespace === void 0 || shape.metadata.namespace === "")) value = {
719
+ ...shape,
720
+ metadata: {
721
+ ...shape.metadata,
722
+ namespace
723
+ }
724
+ };
725
+ }
726
+ let content = `---\n${YAML.stringify(value, { lineWidth: 0 })}`;
727
+ if (!content.endsWith("\n")) content += "\n";
728
+ results.push({
729
+ _tag: "RawYaml",
730
+ content,
731
+ origin
732
+ });
733
+ }
734
+ return results;
735
+ });
736
+ const _HELM_VERSION_RE = /v?(\d+)\.(\d+)\.(\d+)/;
737
+ /** Extract a `major.minor.patch` triple from a version string, or `null`. */
738
+ const _parseVersionTriple = (text) => {
739
+ const m = _HELM_VERSION_RE.exec(text.trim());
740
+ if (m === null) return null;
741
+ return [
742
+ Number(m[1]),
743
+ Number(m[2]),
744
+ Number(m[3])
745
+ ];
746
+ };
747
+ /** True when `found` is strictly older than `min` (release triple compare). */
748
+ const _isBelow = (found, min) => {
749
+ for (let i = 0; i < 3; i++) {
750
+ const f = found[i] ?? 0;
751
+ const m = min[i] ?? 0;
752
+ if (f < m) return true;
753
+ if (f > m) return false;
754
+ }
755
+ return false;
756
+ };
757
+ /**
758
+ * One-shot `helm version --short` preflight. Fails `HelmVersionTooLow`
759
+ * when helm is absent/unparseable or older than `minVersion`. Pre-release
760
+ * / build metadata on the installed version is ignored — only the release
761
+ * triple gates the check.
762
+ */
763
+ const _assertHelmMinVersion = (minVersion) => Effect.gen(function* () {
764
+ const stdout = yield* runProcessString(ChildProcess.make("helm", ["version", "--short"]), { allowEmptyStdout: false }).pipe(Effect.mapError(() => new HelmVersionTooLow({
765
+ required: minVersion,
766
+ found: "not found"
767
+ })));
768
+ const found = _parseVersionTriple(stdout);
769
+ const min = _parseVersionTriple(minVersion);
770
+ if (found === null || min !== null && _isBelow(found, min)) return yield* Effect.fail(new HelmVersionTooLow({
771
+ required: minVersion,
772
+ found: stdout.trim()
773
+ }));
774
+ });
775
+ const _normalizeDigest = (digest) => digest.startsWith("sha256:") ? digest : `sha256:${digest}`;
776
+ const _toHex = (buf) => {
777
+ const view = new Uint8Array(buf);
778
+ let hex = "";
779
+ for (let i = 0; i < view.length; i++) hex += (view[i] ?? 0).toString(16).padStart(2, "0");
780
+ return hex;
781
+ };
782
+ /**
783
+ * SHA-256 the file at `filePath` via Web Crypto API (`crypto.subtle.digest`).
784
+ * `crypto.subtle` is a runtime global on Node ≥ 20 and on Bun; no
785
+ * `node:crypto` import is needed, which keeps the file portable across
786
+ * the runtimes Effect targets.
787
+ */
788
+ const _hashFile = (filePath) => Effect.gen(function* () {
789
+ const bytes = yield* (yield* FileSystem).readFile(filePath);
790
+ const subtle = unsafeCoerce(globalThis, "globalThis.crypto is provided by the runtime (Node ≥ 20, Bun) — typed via local _CryptoGlobal interface").crypto.subtle;
791
+ return `sha256:${_toHex(yield* Effect.promise(() => subtle.digest("SHA-256", bytes)))}`;
792
+ });
793
+ const _verifyDigest = (input) => Effect.gen(function* () {
794
+ const fs = yield* FileSystem;
795
+ const expected = _normalizeDigest(input.opts.digest);
796
+ const actual = yield* _hashFile(input.cachedTgz);
797
+ if (expected !== actual) {
798
+ yield* fs.remove(input.cachedTgz).pipe(Effect.ignore);
799
+ return yield* Effect.fail(new HelmDigestMismatch({
800
+ chart: input.opts.chart,
801
+ version: input.opts.version,
802
+ expected,
803
+ actual
804
+ }));
805
+ }
806
+ });
807
+ const _ensureCachedTarball = (input) => Effect.gen(function* () {
808
+ const { opts, cacheDir, cachedTgz } = input;
809
+ const fs = yield* FileSystem;
810
+ const path = yield* Path;
811
+ if (yield* fs.exists(cachedTgz)) {
812
+ yield* _verifyDigest({
813
+ opts,
814
+ cachedTgz
815
+ });
816
+ return;
817
+ }
818
+ const beforeFiles = new Set(yield* fs.readDirectory(cacheDir).pipe(Effect.orElseSucceed(() => [])));
819
+ yield* runProcessExit(ChildProcess.make("helm", [
820
+ "pull",
821
+ "--repo",
822
+ opts.repo,
823
+ opts.chart,
824
+ "--version",
825
+ opts.version,
826
+ "--destination",
827
+ cacheDir
828
+ ]));
829
+ const candidates = (yield* fs.readDirectory(cacheDir)).filter((f) => f.endsWith(".tgz") && f.startsWith(opts.chart) && !beforeFiles.has(f) && path.join(cacheDir, f) !== cachedTgz);
830
+ if (candidates.length > 0) yield* fs.rename(path.join(cacheDir, candidates[0] ?? ""), cachedTgz);
831
+ yield* _verifyDigest({
832
+ opts,
833
+ cachedTgz
834
+ });
835
+ });
836
+ const release = (opts) => {
837
+ const extraOpts = opts.extraOpts ?? [];
838
+ return make(() => Effect.gen(function* () {
839
+ const fs = yield* FileSystem;
840
+ const path = yield* Path;
841
+ if (opts.minVersion !== void 0) yield* _assertHelmMinVersion(opts.minVersion);
842
+ const cacheDir = yield* Config.string("KONFIG_HELM_CACHE").pipe(Config.withDefault(path.resolve(".konfig", "helm-cache")));
843
+ yield* fs.makeDirectory(cacheDir, { recursive: true });
844
+ const digestSuffix = opts.digest.replace(/^sha256:/, "").slice(0, 12);
845
+ const cachedTgz = path.join(cacheDir, `${opts.chart}-${opts.version}-${digestSuffix}.tgz`);
846
+ yield* _ensureCachedTarball({
847
+ opts,
848
+ cacheDir,
849
+ cachedTgz
850
+ });
851
+ const tmpDir = yield* fs.makeTempDirectoryScoped({ prefix: "konfig-helm-" });
852
+ const valuesFile = path.join(tmpDir, "values.yaml");
853
+ yield* fs.writeFileString(valuesFile, YAML.stringify(opts.values, { lineWidth: 0 }));
854
+ const releaseName = opts.releaseName ?? opts.chart;
855
+ return yield* _parseHelmOutput({
856
+ output: yield* runProcessString(ChildProcess.make("helm", [
857
+ "template",
858
+ releaseName,
859
+ cachedTgz,
860
+ "--values",
861
+ valuesFile,
862
+ ...opts.namespace !== void 0 ? ["--namespace", opts.namespace] : [],
863
+ ...extraOpts
864
+ ]), { allowEmptyStdout: false }),
865
+ chart: opts.chart,
866
+ version: opts.version,
867
+ namespace: opts.namespace
868
+ });
869
+ }).pipe(Effect.scoped, Effect.mapError((cause) => cause instanceof HelmVersionTooLow ? cause : new HelmRenderError({
870
+ chart: opts.chart,
871
+ version: opts.version,
872
+ cause
873
+ }))));
874
+ };
875
+ //#endregion
876
+ //#region src/images.ts
877
+ const EnvImages = Schema.Record(Schema.String, Schema.String);
878
+ const ImagesConfig = Schema.Struct({ envs: Schema.Record(Schema.String, EnvImages) });
879
+ const decodeEff$1 = Schema.decodeUnknownEffect(ImagesConfig);
880
+ const strict$1 = { onExcessProperty: "error" };
881
+ const decodeImagesSync = (input) => Effect.runSync(decodeEff$1(input, strict$1));
882
+ const decodeImagesEffect = (input) => decodeEff$1(input, strict$1);
883
+ var ImagesEnvMissing = class extends Data.TaggedError("ImagesEnvMissing") {};
884
+ const lookupEnv = (input) => input.cfg.envs[input.env];
885
+ const lookupEnvEffect = (input) => {
886
+ const e = input.cfg.envs[input.env];
887
+ return e === void 0 ? Effect.fail(new ImagesEnvMissing({ env: input.env })) : Effect.succeed(e);
888
+ };
889
+ const imagesFor = (input) => {
890
+ const e = input.cfg.envs[input.env];
891
+ if (e === void 0) throw new ImagesEnvMissing({ env: input.env });
892
+ return e;
893
+ };
894
+ var ImagesAppMissing = class extends Data.TaggedError("ImagesAppMissing") {};
895
+ const requireImage = (input) => {
896
+ const v = input.e[input.app];
897
+ if (v === void 0) throw new ImagesAppMissing({
898
+ env: input.envName,
899
+ app: input.app
900
+ });
901
+ return v;
902
+ };
903
+ //#endregion
904
+ //#region src/konfigConfig.ts
905
+ const _stringWithKeyDefault = (def) => Schema.String.pipe(Schema.optionalKey, Schema.withDecodingDefaultKey(Effect.succeed(def)));
906
+ const _stringArrayWithKeyDefault = (def) => Schema.Array(Schema.String).pipe(Schema.optionalKey, Schema.withDecodingDefaultKey(Effect.succeed(def)));
907
+ const EnvEntry = Schema.Struct({ entry: Schema.String });
908
+ const OutDir = Schema.Struct({ manifests: Schema.String });
909
+ const CrdConfig = Schema.Struct({ outDir: _stringWithKeyDefault(".generated/crd") });
910
+ const HelmConfig = Schema.Struct({
911
+ cacheDir: _stringWithKeyDefault(".konfig/helm-cache"),
912
+ minVersion: _stringWithKeyDefault("3.16.0")
913
+ });
914
+ const ClusterSpec = Schema.Struct({
915
+ registry: Schema.optionalKey(Schema.String),
916
+ ingressClass: Schema.optionalKey(Schema.String),
917
+ storageClass: Schema.optionalKey(Schema.String),
918
+ repositoryUrl: Schema.optionalKey(Schema.String)
919
+ });
920
+ const DiffConfig = Schema.Struct({ baseline: Schema.String });
921
+ const ServicesConfig = Schema.Struct({
922
+ outFile: Schema.optionalKey(Schema.String),
923
+ globalPaths: _stringArrayWithKeyDefault([])
924
+ });
925
+ const KonfigConfig = Schema.Struct({
926
+ root: Schema.String,
927
+ cluster: _stringWithKeyDefault("cluster.ts"),
928
+ modules: _stringWithKeyDefault("modules"),
929
+ charts: _stringWithKeyDefault("charts"),
930
+ envs: Schema.Record(Schema.String, EnvEntry),
931
+ outDir: OutDir,
932
+ crd: Schema.optionalKey(CrdConfig).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ outDir: ".generated/crd" }))),
933
+ helm: Schema.optionalKey(HelmConfig).pipe(Schema.withDecodingDefaultKey(Effect.succeed({
934
+ cacheDir: ".konfig/helm-cache",
935
+ minVersion: "3.16.0"
936
+ }))),
937
+ diff: Schema.optionalKey(DiffConfig),
938
+ services: Schema.optionalKey(ServicesConfig),
939
+ clusters: Schema.optionalKey(Schema.Record(Schema.String, ClusterSpec))
940
+ });
941
+ const strict = { onExcessProperty: "error" };
942
+ const decodeEff = Schema.decodeUnknownEffect(KonfigConfig);
943
+ const decodeKonfigConfigSync = (input) => Effect.runSync(decodeEff(input, strict));
944
+ const decodeKonfigConfigEffect = (input) => decodeEff(input, strict);
945
+ //#endregion
946
+ //#region src/Module.ts
947
+ var Module_exports = /* @__PURE__ */ __exportAll({
948
+ dynamicNs: () => dynamicNs,
949
+ fixedNs: () => fixedNs
950
+ });
951
+ const _liftBuild = (result) => Effect.isEffect(result) ? result : Effect.succeed(result);
952
+ /**
953
+ * Build a typed wrapper for a module whose namespace is part of its
954
+ * identity (e.g. `cert-manager` always installs into `cert-manager`).
955
+ *
956
+ * `target` is the backend adapter — typically `Application.target`
957
+ * (argocd) or `Bundle.target` (k8s). The wrapper's call signature
958
+ * merges the backend's per-instance fields (`source` for argocd,
959
+ * none for bundle) with the user-defined `Opts`.
960
+ *
961
+ * ```ts
962
+ * const defineSops = Module.fixedNs({
963
+ * target: Application.target,
964
+ * namespace: "sops",
965
+ * annotations: Sync.wave(-1),
966
+ * build: ({ namespace }, _opts: Record<never, never>) => [
967
+ * Namespace.make({ name: namespace }),
968
+ * Helm.release({ ... }),
969
+ * ],
970
+ * });
971
+ *
972
+ * const sops = defineSops({ name: "sops-operator", source: src("sops") });
973
+ * ```
974
+ */
975
+ const fixedNs = (config) => {
976
+ const { target, namespace, provides, build, ...extraConfig } = unsafeCoerce(config, "Record spread shape mirrors the FixedNsConfig & ExtraConfig intersection");
977
+ const adapter = unsafeCoerce(target, "target was destructured from config without preserving its typed shape; reattach the constraint");
978
+ return (args) => {
979
+ const { name, ...rest } = unsafeCoerce(args, "destructuring the wrapper args; rest carries ExtraCallArgs & Opts as a flat record");
980
+ const buildResult = build({
981
+ name: unsafeCoerce(name, "LiteralName<Name> resolves to Name itself once the wrapper call typechecks"),
982
+ namespace
983
+ }, unsafeCoerce(rest, "rest carries Opts fields; ExtraCallArgs flow to target.define below"));
984
+ return adapter.define(unsafeCoerce({
985
+ ...extraConfig,
986
+ ...rest,
987
+ name,
988
+ namespace: unsafeCoerce(namespace, "Ns is a const string literal; LiteralName<Ns> resolves to Ns itself"),
989
+ build: _liftBuild(buildResult),
990
+ ...provides !== void 0 ? { provides } : {}
991
+ }, "the assembled object structurally matches the target's define-args; spread layout matches the intersection"));
992
+ };
993
+ };
994
+ /**
995
+ * Build a typed wrapper for a module whose namespace is chosen per
996
+ * instance (e.g. an `api` module deployed into different namespaces
997
+ * per env).
998
+ *
999
+ * ```ts
1000
+ * const defineApi = Module.dynamicNs({
1001
+ * target: Application.target,
1002
+ * annotations: Sync.wave(1),
1003
+ * build: ({ name, namespace }, opts: ApiOpts) => [ ... ],
1004
+ * });
1005
+ *
1006
+ * const api = defineApi({
1007
+ * name: "api",
1008
+ * namespace: "prod",
1009
+ * source: src("api"),
1010
+ * replicas: 2,
1011
+ * });
1012
+ * ```
1013
+ */
1014
+ const dynamicNs = (config) => {
1015
+ const { target, provides, build, ...extraConfig } = unsafeCoerce(config, "Record spread shape mirrors the DynamicNsConfig & ExtraConfig intersection");
1016
+ const adapter = unsafeCoerce(target, "target was destructured from config without preserving its typed shape; reattach the constraint");
1017
+ return (args) => {
1018
+ const { name, namespace, ...rest } = unsafeCoerce(args, "destructuring the wrapper args; rest carries ExtraCallArgs & Opts as a flat record");
1019
+ const buildResult = build({
1020
+ name: unsafeCoerce(name, "LiteralName<Name> resolves to Name itself once the wrapper call typechecks"),
1021
+ namespace: unsafeCoerce(namespace, "LiteralName<Ns> resolves to Ns itself once the wrapper call typechecks")
1022
+ }, unsafeCoerce(rest, "rest carries Opts fields; ExtraCallArgs flow to target.define below"));
1023
+ return adapter.define(unsafeCoerce({
1024
+ ...extraConfig,
1025
+ ...rest,
1026
+ name,
1027
+ namespace,
1028
+ build: _liftBuild(buildResult),
1029
+ ...provides !== void 0 ? { provides } : {}
1030
+ }, "the assembled object structurally matches the target's define-args; spread layout matches the intersection"));
1031
+ };
1032
+ };
1033
+ //#endregion
1034
+ //#region src/RenderContext.ts
1035
+ const RenderContext = {
1036
+ make: (env) => ({ env }),
1037
+ makeFull: (input) => ({
1038
+ env: input.env,
1039
+ ...input.cluster !== void 0 ? { cluster: input.cluster } : {},
1040
+ ...input.k8sVersion !== void 0 ? { k8sVersion: input.k8sVersion } : {},
1041
+ ...input.flags !== void 0 ? { flags: input.flags } : {}
1042
+ })
1043
+ };
1044
+ //#endregion
1045
+ //#region src/render.ts
1046
+ /** @internal */
1047
+ const _resolveEnv = (env) => env ?? "prod";
1048
+ /** @internal */
1049
+ const _buildLayers = (extra) => extra === void 0 ? NodeServices.layer : Layer.mergeAll(NodeServices.layer, extra);
1050
+ /**
1051
+ * @internal
1052
+ * Compose the ctx + layers wiring `render` hands to `NodeRuntime.runMain`
1053
+ * into a single runnable Effect (context fully provided). Extracted so
1054
+ * tests can exercise the composition with `Effect.runPromise` instead of
1055
+ * `runMain`, which would exit the process.
1056
+ */
1057
+ const _compose = (program, options = {}) => {
1058
+ const ctx = RenderContext.make(_resolveEnv(options.env));
1059
+ const layers = _buildLayers(options.layers);
1060
+ return program(ctx).pipe(Effect.scoped, Effect.provide(layers));
1061
+ };
1062
+ /**
1063
+ * Run a render program against `NodeRuntime`.
1064
+ *
1065
+ * The callback receives a `RenderContext` keyed on `options.env`
1066
+ * (default `"prod"`) and returns an Effect whose only required
1067
+ * services are `NodeServices` and whatever the caller supplies via
1068
+ * `options.layers`. `render` provides both, wraps in `Effect.scoped`,
1069
+ * and hands off to `NodeRuntime.runMain`.
1070
+ *
1071
+ * Replaces the per-file `NodeRuntime.runMain(program.pipe(...))`
1072
+ * boilerplate every example used to repeat.
1073
+ */
1074
+ const render = (program, options = {}) => {
1075
+ NodeRuntime.runMain(_compose(program, options));
1076
+ };
1077
+ //#endregion
1078
+ //#region src/renderManifest.ts
1079
+ const renderManifest = (input) => input.manifest.render(input.ctx);
1080
+ //#endregion
1081
+ //#region src/yaml/serialize.ts
1082
+ const ORDER_ROOT = [
1083
+ "apiVersion",
1084
+ "kind",
1085
+ "metadata",
1086
+ "spec",
1087
+ "status"
1088
+ ];
1089
+ const ORDER_METADATA = [
1090
+ "name",
1091
+ "namespace",
1092
+ "labels",
1093
+ "annotations"
1094
+ ];
1095
+ const _reorderObject = (input) => {
1096
+ const { obj, depth, parentKey } = input;
1097
+ const keys = Object.keys(obj).filter((k) => obj[k] !== null && obj[k] !== void 0);
1098
+ const order = depth === 0 ? ORDER_ROOT : depth === 1 && parentKey === "metadata" ? ORDER_METADATA : null;
1099
+ let sortedKeys;
1100
+ if (order !== null) {
1101
+ const known = order.filter((k) => keys.includes(k));
1102
+ const rest = keys.filter((k) => !order.includes(k)).sort();
1103
+ sortedKeys = [...known, ...rest];
1104
+ } else sortedKeys = keys.slice().sort();
1105
+ const out = {};
1106
+ for (const k of sortedKeys) out[k] = _normalize({
1107
+ value: obj[k],
1108
+ depth: depth + 1,
1109
+ parentKey: k
1110
+ });
1111
+ return out;
1112
+ };
1113
+ const _normalize = (input) => {
1114
+ const { value, depth, parentKey } = input;
1115
+ if (value === null || value === void 0) return value;
1116
+ if (Array.isArray(value)) return value.map((v) => _normalize({
1117
+ value: v,
1118
+ depth: depth + 1,
1119
+ parentKey: null
1120
+ }));
1121
+ if (typeof value === "object") return _reorderObject({
1122
+ obj: unsafeCoerce(value, "typeof === object branch (value !== null, !Array.isArray above) — treated as a keyed record for reordering"),
1123
+ depth,
1124
+ parentKey
1125
+ });
1126
+ return value;
1127
+ };
1128
+ const serialize = (input) => {
1129
+ const normalized = _normalize({
1130
+ value: input.value,
1131
+ depth: 0,
1132
+ parentKey: null
1133
+ });
1134
+ const lf = YAML.stringify(normalized, {
1135
+ indent: 2,
1136
+ indentSeq: true,
1137
+ lineWidth: 0,
1138
+ minContentWidth: 0,
1139
+ defaultStringType: "PLAIN",
1140
+ defaultKeyType: "PLAIN",
1141
+ nullStr: "null",
1142
+ version: "1.1"
1143
+ }).replace(/\r\n/g, "\n").replace(/\s+$/g, "");
1144
+ return input.trailingNewline ?? true ? `${lf}\n` : lf;
1145
+ };
1146
+ /**
1147
+ * Derive the on-disk filename (`<Kind>-<name>.yaml`) for a rendered
1148
+ * resource.
1149
+ *
1150
+ * **Precondition (caller invariant):** `resource.kind` and
1151
+ * `resource.metadata.name` MUST both be non-empty strings. A rendered
1152
+ * Kubernetes object always satisfies this, so callers are expected to
1153
+ * validate/decode the object *before* reaching this point (the CLI keys
1154
+ * files by kind+name only for objects it has already confirmed carry
1155
+ * both fields).
1156
+ *
1157
+ * Because the precondition is a caller invariant rather than runtime
1158
+ * input, a violation is a programming error: this throws synchronously
1159
+ * (a defect) instead of returning a typed error. It is deliberately kept
1160
+ * out of the `AnyRenderError` channel — do not route untrusted resources
1161
+ * through it; narrow them first.
1162
+ *
1163
+ * @throws {Error} if `kind` or `metadata.name` is missing or empty.
1164
+ */
1165
+ const filenameFor = (resource) => {
1166
+ const kind = resource.kind;
1167
+ const name = resource.metadata?.name;
1168
+ if (typeof kind !== "string" || kind.length === 0) throw new Error(`filenameFor: resource has no string kind`);
1169
+ if (typeof name !== "string" || name.length === 0) throw new Error(`filenameFor: resource '${kind}' has no metadata.name`);
1170
+ return `${kind}-${_sanitizeNameForFilename(name)}.yaml`;
1171
+ };
1172
+ const _sanitizeNameForFilename = (name) => name.replace(/[./]/g, "-");
1173
+ //#endregion
1174
+ //#region src/yaml/index.ts
1175
+ var yaml_exports = /* @__PURE__ */ __exportAll({
1176
+ filenameFor: () => filenameFor,
1177
+ serialize: () => serialize
1178
+ });
1179
+ //#endregion
1180
+ export { BoundaryDecodeError, BuiltImageRef, Bundle_exports as Bundle, ClusterSpec, Compose_exports as Compose, CrdConfig, CrdExtractError, deps_exports as Dep, DiffConfig, EmbedYamlReadError, EnvEntry, EnvImages, Helm_exports as Helm, HelmConfig, HelmDigestMismatch, HelmRenderError, HelmVersionTooLow, ImagesAppMissing, ImagesConfig, ImagesEnvMissing, KonfigConfig, Manifest_exports as Manifest, Module_exports as Module, OutDir, ProcessError, RenderContext, RenderError, ServicesConfig, yaml_exports as Yaml, boundary, brand, decodeImagesEffect, decodeImagesSync, decodeKonfigConfigEffect, decodeKonfigConfigSync, deepEqual, diffFiles, formatDiff, hasDifferences, imagesFor, lookupEnv, lookupEnvEffect, parseYaml, parseYamlAll, redact, render, renderManifest, requireImage, runProcessExit, runProcessString, unsafeCoerce };
1181
+
1182
+ //# sourceMappingURL=index.mjs.map