@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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026-present David Stahl-Gruber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @konfig.ts/core
2
+
3
+ The Kubernetes-agnostic primitives that the rest of konfig.ts builds on:
4
+ the `Manifest<A>` carrier, the `Dep.*` kinds that drive compile-time
5
+ dependency tracking, a stable YAML serializer, structural diff, an Effect
6
+ `Schema` boundary helper, and the `Helm.release` integration.
7
+
8
+ Higher-level surfaces — workloads, services, env contracts, ArgoCD
9
+ Application aggregation — live in `@konfig.ts/k8s`, `@konfig.ts/env`,
10
+ and `@konfig.ts/argocd`.
11
+
12
+ ## `Manifest<A>` — the carrier
13
+
14
+ A `Manifest<A>` is a thunk from a `RenderContext` to an Effect that
15
+ produces an `A`:
16
+
17
+ ```ts
18
+ interface Manifest<A> {
19
+ readonly render: (
20
+ ctx: RenderContext
21
+ ) => Effect.Effect<A, AnyRenderError, RenderServices>
22
+ }
23
+ ```
24
+
25
+ `RenderServices` is `FileSystem | Path | ChildProcessSpawner | Scope`.
26
+ Errors are a tagged union: `RenderError`, `EmbedYamlReadError`,
27
+ `BoundaryDecodeError`, `HelmVersionTooLow`, `HelmRenderError`,
28
+ `HelmDigestMismatch`, `CrdExtractError`.
29
+
30
+ Constructors:
31
+
32
+ | Op | Behaviour |
33
+ | ------------------------------------ | ---------------------------------------------- |
34
+ | `Manifest.make(run)` | Lift a `ctx → A \| Effect<A>` into a Manifest. |
35
+ | `Manifest.combine({ a, b })` | Render `a` and `b` concurrently into a tuple. |
36
+ | `Manifest.concat(...ms)` | Flatten an array of Manifests into one. |
37
+ | `Manifest.whenever({ cond, thunk })` | Optional inclusion. |
38
+ | `Manifest.embedYaml(source)` | Pass through verbatim YAML (file or literal). |
39
+
40
+ The `Manifest<A>` itself is not where dependency-tracking happens — it
41
+ just carries data and effect failures. The compile-time graph lives
42
+ one level up; see below.
43
+
44
+ ## Where the type-safety lives
45
+
46
+ konfig's dep-graph claim has two boundaries:
47
+
48
+ 1. **`AppOfApps.entrypoint(program)`** in `@konfig.ts/argocd`. Every
49
+ `Application.define({...})` produces an Effect `Layer.Layer<Out, _, In>`
50
+ that propagates the `Dep.Provide<K, N>` it produces in `Out` and the
51
+ `Dep.Need<K, N>` it consumes in `In`. Compose with `Layer.provideMerge`,
52
+ pass to `entrypoint`, and TypeScript checks `In === never`. A missing
53
+ provider is a compile error — see
54
+ [`examples/full-stack/infra/envs/broken.ts`](../../examples/full-stack/infra/envs/broken.ts).
55
+
56
+ 2. **`Environment.bind({ env, secrets })`** in `@konfig.ts/env`. The
57
+ bundle's secret atoms describe `{ envName, secret, key }` triples; the
58
+ `bind` checks at the type level that every secret has a backend, the
59
+ backend's `keys` set matches the bundle's keys, and a `requiresSource`
60
+ backend has a `source` of the right key shape.
61
+
62
+ Inside a single `Application.define({ build })` Effect, dep tracking is
63
+ declarative: when you `yield* Dep.Secret("ghcr-pull")` to obtain a
64
+ `SecretRef<"ghcr-pull">`, you add the `Need` to the requirements. Forget
65
+ to yield, and nothing complains — the system catches misses at
66
+ composition, not inside an Application's body.
67
+
68
+ ## `Dep.*` — tracked kinds
69
+
70
+ ```ts
71
+ import { Dep } from "@konfig.ts/core"
72
+ ```
73
+
74
+ | Kind | Service | Provided value |
75
+ | --------------------------------- | ------------------------------ | ----------------------------------------- |
76
+ | `Dep.App<Name>(name)` | `Need<"App", Name>` | `Application` record |
77
+ | `Dep.Application<Name>(name)` | `Need<"Application", Name>` | `Name` literal |
78
+ | `Dep.Namespace<Name>(name)` | `Need<"Namespace", Name>` | `Name` literal |
79
+ | `Dep.Secret<Name>(name)` | `Need<"Secret", Name>` | `SecretRef<Name>` brand |
80
+ | `Dep.SecretValues<Name, K>(name)` | `Need<"SecretValues", Name>` | `{ readonly [k in K]: Redacted<string> }` |
81
+ | `Dep.ConfigMap<Name>(name)` | `Need<"ConfigMap", Name>` | `ConfigMapRef<Name>` brand |
82
+ | `Dep.ServiceAccount<Name>(name)` | `Need<"ServiceAccount", Name>` | `ServiceAccountRef<Name>` brand |
83
+ | `Dep.Pvc<Name>(name)` | `Need<"Pvc", Name>` | `PvcRef<Name>` brand |
84
+
85
+ For each kind there's a matching `provideX` helper (`provideSecret`,
86
+ `provideNamespace`, …) that emits a `Layer.Layer<Provide<K, N>>`.
87
+
88
+ `brand(value)` and `unsafeCoerce(value, reason)` from `@konfig.ts/core`
89
+ are escape hatches. `brand` is fine — it's a nominal-typing primitive
90
+ for the `*Ref` types. `unsafeCoerce` is the unchecked cast and takes a
91
+ mandatory reason string so every call site documents WHY the cast is
92
+ sound; prefer the schema boundary below for values crossing a trust
93
+ boundary. (`coerce` is deprecated and will be removed before 1.0.)
94
+
95
+ ## Stable YAML — `Yaml.serialize({ value })`
96
+
97
+ Output rules:
98
+
99
+ - Top-level keys ordered: `apiVersion`, `kind`, `metadata`, `spec`,
100
+ `status`, then alphabetical.
101
+ - Inside `metadata`: `name`, `namespace`, `labels`, `annotations`, then
102
+ alphabetical.
103
+ - Every other map: alphabetical.
104
+ - Lists preserve insertion order.
105
+ - LF endings, 2-space indent, single trailing newline.
106
+ - YAML 1.1 explicit (kubectl/ArgoCD compatibility).
107
+
108
+ `Yaml.filenameFor(resource)` returns `<Kind>-<metadata.name>.yaml` —
109
+ deterministic per-resource filenames for ArgoCD-friendly diffs.
110
+
111
+ ## Structural diff — `diffFiles({ left, right })`
112
+
113
+ Each file's YAML is parsed and redacted (Helm-volatile metadata stripped)
114
+ before deep-equality. Output is `Map<filename, FileDiff>`, formatted via
115
+ `formatDiff(result, "summary" | "detail" | "json")`. Today's diff is
116
+ two-way only and treats each file as a single document; richer per-doc
117
+ and anchor-aware diffing is on the roadmap.
118
+
119
+ ## `Helm.release({...})`
120
+
121
+ Calls `helm pull` and `helm template`, lifts each emitted YAML document
122
+ as a `RawYaml` Manifest under the parent Application. SHA-256 of the
123
+ `.tgz` is verified against `opts.digest` after pull **and** on every
124
+ cache hit — flipping a byte in a cached tarball fails the next render
125
+ with `HelmDigestMismatch`.
126
+
127
+ `opts.digest` must include the `sha256:` prefix; the cache file name
128
+ includes the first 12 hex digits, but the verifier compares the full
129
+ hash.
130
+
131
+ ## Boundary decode — `boundary({ schema, label })`
132
+
133
+ Wraps `Schema.decodeUnknownEffect` so that any decode failure becomes
134
+ a `BoundaryDecodeError` with `schema: label`. Use at the seams where
135
+ untyped input enters a module:
136
+
137
+ ```ts
138
+ const decodeApiOptions = boundary({ schema: ApiOptions, label: "api" })
139
+ const cfg = yield * decodeApiOptions(input)
140
+ ```
141
+
142
+ Every place that previously did `coerce<T>(YAML.parse(stdout))` over
143
+ untrusted external output is now expected to use this helper.
144
+
145
+ ## Layout
146
+
147
+ ```
148
+ src/
149
+ ├── index.ts barrel
150
+ ├── _cast.ts brand + coerce (escape hatch)
151
+ ├── boundary.ts Schema.decode wrapper → BoundaryDecodeError
152
+ ├── deps.ts Dep.* kinds + provide* helpers
153
+ ├── diff.ts structural diff + redaction
154
+ ├── Helm.ts Helm.release with digest verification
155
+ ├── images.ts ImagesConfig + decoders
156
+ ├── konfigConfig.ts KonfigConfig schema (top-level config)
157
+ ├── Manifest.ts Manifest interface + combinators
158
+ ├── render.ts render entrypoint
159
+ ├── RenderContext.ts threaded through every render
160
+ ├── RenderError.ts tagged error union
161
+ ├── types.ts Kind enum (legacy; superseded by Dep.*)
162
+ └── yaml/
163
+ ├── index.ts
164
+ └── serialize.ts stable YAML serializer + filenameFor
165
+ ```
166
+
167
+ ## Requirements
168
+
169
+ konfig.ts builds on [Effect](https://effect.website/), which is still in
170
+ beta. Until Effect ships a stable 4.x, you must install the exact beta
171
+ konfig is developed against:
172
+
173
+ - **`effect@4.0.0-beta.70`** — required.
174
+ - **`@effect/platform-node@4.0.0-beta.70`** — required only for `render()`
175
+ (the Node filesystem/subprocess entrypoint); manifest-only consumers can
176
+ omit it.
177
+
178
+ The peer dependency is pinned to the exact version on purpose: Effect's beta
179
+ line makes breaking changes between builds, so a looser range would surface
180
+ as `ERESOLVE` install conflicts rather than a working install. This pin will
181
+ relax to a caret range once Effect reaches a stable 4.x.
@@ -0,0 +1,125 @@
1
+ import * as Compose from "./Compose";
2
+ import * as Dep from "./deps";
3
+ import type * as CoreManifest from "./Manifest";
4
+ import type * as Module from "./Module";
5
+ import type { AnyRenderError } from "./RenderError";
6
+ import { type Context, Effect, Layer } from "effect";
7
+ export interface Bundle {
8
+ readonly name: string;
9
+ readonly namespace?: string;
10
+ readonly manifests: ReadonlyArray<unknown>;
11
+ }
12
+ export interface BundleMakeOptions {
13
+ readonly name: string;
14
+ readonly namespace?: string;
15
+ readonly manifests: ReadonlyArray<unknown>;
16
+ }
17
+ export declare const make: (opts: BundleMakeOptions) => Bundle;
18
+ /**
19
+ * Handle returned by `Bundle.define`. Same yieldable-Context-Tag +
20
+ * `.layer` pattern as argocd's `ApplicationHandle`; only the carried
21
+ * value type differs (a plain `Bundle` with no argo source/syncPolicy).
22
+ * `Dep.Need<"App", Name>` keys the dep graph by literal name so
23
+ * sibling modules can `yield* bundleHandle` to consume it.
24
+ */
25
+ export interface BundleHandle<Name extends string, Out, In> extends Context.Service<Dep.Need<"App", Name>, Bundle> {
26
+ readonly layer: Layer.Layer<Out, AnyRenderError, In>;
27
+ }
28
+ /**
29
+ * Higher-kinded handle constructor that maps `Module`'s
30
+ * `(Name, Ns, R, Extra)` slots onto a `BundleHandle`. Lets
31
+ * `Module.fixedNs({ target: Bundle.target, … })` /
32
+ * `Module.dynamicNs({ target: Bundle.target, … })` return
33
+ * strongly-typed `BundleHandle`s.
34
+ */
35
+ export interface HandleKind extends Module.HandleKind {
36
+ readonly Handle: BundleHandle<this["_Name"] & string, Dep.Provide<"App", this["_Name"] & string> | Dep.Provide<"Namespace", this["_Ns"] & string> | (this["_Extra"] & unknown), Exclude<this["_R"] & unknown, Dep.Need<"Namespace", this["_Ns"] & string> | (this["_Extra"] & unknown)>>;
37
+ }
38
+ /**
39
+ * Re-export of {@link Module.LiteralName} — preserved as
40
+ * `Bundle.LiteralName` so existing wrapper code keeps working.
41
+ * konfig's dep graph keys every `Provide<"App", Name>` slot by literal
42
+ * `Name`; a wrapper that lets `Name` widen to `string` collapses every
43
+ * bundle into the same slot.
44
+ */
45
+ export type LiteralName<T extends string> = Module.LiteralName<T>;
46
+ export interface BundleDefineOptions<Name extends string, Ns extends string, R, Extra> {
47
+ readonly name: LiteralName<Name>;
48
+ readonly namespace?: LiteralName<Ns>;
49
+ readonly build: Effect.Effect<ReadonlyArray<unknown>, AnyRenderError, R> | (() => ReadonlyArray<unknown>);
50
+ readonly provides?: Layer.Layer<Extra>;
51
+ }
52
+ type _NsProvides<Ns extends string> = [Ns] extends [never] ? never : Dep.Provide<"Namespace", Ns>;
53
+ type _NsExcludes<Ns extends string> = [Ns] extends [never] ? never : Dep.Need<"Namespace", Ns>;
54
+ /**
55
+ * Build a typed handle for a manifest bundle — a name + optional
56
+ * namespace + a set of manifests, plus dep-graph wiring. Same
57
+ * compile-time guarantees as argocd's `Application.define` minus
58
+ * `source: ArgoSource` / `syncPolicy` / sync-wave annotations:
59
+ * - the literal `Name` flows into `Dep.Provide<"App", Name>`,
60
+ * - the optional literal namespace flows into `Dep.Provide<"Namespace", Ns>`,
61
+ * - the build callback's `R` channel becomes the handle's `In` after
62
+ * subtracting what this bundle provides itself.
63
+ *
64
+ * Pair with `Bundle.fromModules` to compose multiple bundles
65
+ * and have the dep-graph residual checked at `Bundle.entrypoint`.
66
+ *
67
+ * For `Module.fixedNs` / `Module.dynamicNs` use, see `Bundle.target`
68
+ * — it adapts this `define` so namespace is required (Module wrappers
69
+ * always thread a namespace through), which lets the dep-graph drop
70
+ * the `Provide<"Namespace", never>` cell the optional shape needs.
71
+ */
72
+ export declare const define: <const Name extends string, const Ns extends string = never, R = never, Extra = never>(opts: BundleDefineOptions<Name, Ns, R, Extra>) => BundleHandle<Name, Dep.Provide<"App", Name> | _NsProvides<Ns> | Extra, Exclude<R, _NsExcludes<Ns> | Extra>>;
73
+ /**
74
+ * `Module.Target` adapter for `Bundle.define`. `Module.fixedNs` /
75
+ * `Module.dynamicNs` always pass a namespace through, so this adapter
76
+ * requires `namespace` (whereas `Bundle.define` itself accepts it as
77
+ * optional) — pinning `Ns` to a real literal lets the dep-graph emit
78
+ * a concrete `Provide<"Namespace", Ns>` cell on the resulting handle.
79
+ *
80
+ * ```ts
81
+ * const defineCache = Module.fixedNs({
82
+ * target: Bundle.target,
83
+ * namespace: "cache",
84
+ * build: ({ name, namespace }) => [ ... ],
85
+ * });
86
+ * ```
87
+ */
88
+ export declare const target: Module.Target<HandleKind, Record<never, never>, Record<never, never>>;
89
+ export interface BundleSetResult {
90
+ readonly name: string;
91
+ readonly bundles: ReadonlyArray<Bundle>;
92
+ }
93
+ export interface BundleSetMakeOptions {
94
+ readonly name?: string;
95
+ readonly bundles: ReadonlyArray<Bundle>;
96
+ }
97
+ export declare const makeSet: (opts: BundleSetMakeOptions) => BundleSetResult;
98
+ /**
99
+ * Phantom check that rejects a `Bundle.fromModules` program whose `R`
100
+ * channel still carries unmet dep-graph Needs. Bound to the
101
+ * "Bundle.fromModules" API label so the `_konfig_unsatisfied` hint
102
+ * guides the user to the right call site.
103
+ */
104
+ export declare const entrypoint: <A, E, R>(program: Effect.Effect<A, E, R> & ([Exclude<R, CoreManifest.RenderServices>] extends [never] ? unknown : {
105
+ readonly _konfig_unsatisfied: Exclude<R, CoreManifest.RenderServices> extends infer T ? T extends Exclude<R, CoreManifest.RenderServices> ? T extends Dep.Need<infer K extends string, infer V extends string> ? `Missing provider for ${K} "${V}". Add a module that provides it to Bundle.fromModules({ modules }), or check that providers come before consumers in the list.` : "Unsatisfied dep — see the Effect Layer error above." : never : never;
106
+ })) => Effect.Effect<A, E, R & CoreManifest.RenderServices>;
107
+ type AnyHandle = BundleHandle<any, any, any>;
108
+ export type ResidualIn<T extends ReadonlyArray<AnyHandle>> = Compose.ResidualIn<T>;
109
+ export interface FromModulesOptions<Ms extends ReadonlyArray<AnyHandle>> {
110
+ readonly name?: string;
111
+ readonly modules: Ms;
112
+ }
113
+ /**
114
+ * One-list composition for a backend-agnostic bundle set. Yields each
115
+ * module's `Bundle` in tuple order, then wires the merged provider layer
116
+ * with `Compose.composeLayers`. The returned Effect's R channel is the
117
+ * residual unmet Needs after the fold (`Compose.ResidualIn<Ms>`),
118
+ * which `entrypoint` rejects unless empty.
119
+ *
120
+ * **Order matters.** List providers before their consumers. A consumer
121
+ * placed before its provider leaves an unmet `Need` in the residual,
122
+ * which surfaces at `entrypoint` as a `_konfig_unsatisfied` hint.
123
+ */
124
+ export declare const fromModules: <const Ms extends ReadonlyArray<AnyHandle>>(opts: FromModulesOptions<Ms>) => Effect.Effect<BundleSetResult, AnyRenderError, ResidualIn<Ms> | CoreManifest.RenderServices>;
125
+ export {};
@@ -0,0 +1,68 @@
1
+ import { type Effect, Layer } from "effect";
2
+ import type { Need } from "./deps";
3
+ import type { RenderServices } from "./Manifest";
4
+ import type { AnyRenderError } from "./RenderError";
5
+ /**
6
+ * The minimal shape `composeLayers` and the residual fold need: anything
7
+ * with a `.layer` field of the standard `(Out, AnyRenderError, In)`
8
+ * triple. Each runtime (argocd `ApplicationHandle`, k8s `BundleHandle`,
9
+ * …) supplies its own wider handle interface — the carried value type
10
+ * is irrelevant to the fold itself.
11
+ */
12
+ export interface ComposeHandle<Out = any, In = any> {
13
+ readonly layer: Layer.Layer<Out, AnyRenderError, In>;
14
+ }
15
+ type AnyHandle = ComposeHandle<any, any>;
16
+ type OutOfHandle<H> = H extends ComposeHandle<infer Out, any> ? Out : never;
17
+ type InOfHandle<H> = H extends ComposeHandle<any, infer In> ? In : never;
18
+ /**
19
+ * Left-fold over the modules tuple, mirroring the runtime
20
+ * `reduce(Layer.provideMerge)` below. Each module's `In` is filtered
21
+ * against the union of every *prior* module's `Out`; whatever survives
22
+ * is residual. Tuple order matters: a consumer listed before its
23
+ * provider leaves its Need in the residual.
24
+ */
25
+ type FoldResidualIn<T extends ReadonlyArray<AnyHandle>, AccIn, AccOut> = T extends readonly [infer H, ...infer Rest] ? H extends AnyHandle ? Rest extends ReadonlyArray<AnyHandle> ? FoldResidualIn<Rest, AccIn | Exclude<InOfHandle<H>, AccOut>, AccOut | OutOfHandle<H>> : never : never : AccIn;
26
+ /**
27
+ * After folding `Layer.provideMerge` over `Ms` in tuple order, the
28
+ * leftover `RIn` channel — the Needs that no preceding module's Out
29
+ * satisfies. Pair with `makeResidualEntrypoint` to surface a non-empty
30
+ * residual as a compile error at the entrypoint call site.
31
+ */
32
+ export type ResidualIn<T extends ReadonlyArray<AnyHandle>> = FoldResidualIn<T, never, never>;
33
+ /**
34
+ * Runtime layer composition — `reduce(Layer.provideMerge)`. Each
35
+ * successive module receives every prior module's Out as available
36
+ * services. The runtime type collapses to a bottom Layer; the per-module
37
+ * Out/In is tracked statically by `ResidualIn`.
38
+ */
39
+ export declare const composeLayers: (modules: ReadonlyArray<{
40
+ readonly layer: unknown;
41
+ }>) => Layer.Layer<never, AnyRenderError, never>;
42
+ /**
43
+ * Per-Need template-literal hint shown when a residual reaches the
44
+ * entrypoint. `Api` is the calling API's name ("AppOfApps.fromModules",
45
+ * "Bundle.fromModules", …) so the message points the user at the right
46
+ * call site.
47
+ */
48
+ type UnsatisfiedHint<R, Api extends string> = R extends Need<infer K, infer V> ? `Missing provider for ${K} "${V}". Add a module that provides it to ${Api}({ modules }), or check that providers come before consumers in the list.` : "Unsatisfied dep — see the Effect Layer error above.";
49
+ /**
50
+ * Intersects the program type with a phantom `_konfig_unsatisfied`
51
+ * object when the residual `R` carries anything beyond
52
+ * `Manifest.RenderServices`. The program has no such property at runtime,
53
+ * so the call fails to typecheck and the user sees the hint.
54
+ */
55
+ type ResidualHintCheck<R, Api extends string> = [
56
+ Exclude<R, RenderServices>
57
+ ] extends [never] ? unknown : {
58
+ readonly _konfig_unsatisfied: UnsatisfiedHint<Exclude<R, RenderServices>, Api>;
59
+ };
60
+ /**
61
+ * Build an `entrypoint` function bound to a specific API name. The
62
+ * returned function accepts only programs whose `R` channel reduces to
63
+ * `Manifest.RenderServices`; otherwise the call fails at the program
64
+ * argument with a `_konfig_unsatisfied` hint that names the missing
65
+ * provider and the calling API.
66
+ */
67
+ export declare const makeResidualEntrypoint: <const Api extends string>(_api: Api) => <A, E, R>(program: Effect.Effect<A, E, R> & ResidualHintCheck<R, Api>) => Effect.Effect<A, E, R & RenderServices>;
68
+ export {};
package/dist/Helm.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { type Manifest, type RawYaml } from "./Manifest";
2
+ export interface HelmReleaseOptions {
3
+ readonly repo: string;
4
+ readonly chart: string;
5
+ readonly releaseName?: string;
6
+ readonly version: string;
7
+ readonly digest: string;
8
+ readonly namespace?: string;
9
+ readonly values: Record<string, unknown>;
10
+ readonly extraOpts?: readonly string[];
11
+ /**
12
+ * Minimum acceptable `helm` CLI version (semver, e.g. `"3.16.0"`).
13
+ * When set, `release` runs a `helm version --short` preflight before
14
+ * pulling/templating and fails with `HelmVersionTooLow` if the
15
+ * installed helm is older (or absent). Omit to skip the check.
16
+ */
17
+ readonly minVersion?: string;
18
+ }
19
+ export declare const release: (opts: HelmReleaseOptions) => Manifest<RawYaml[]>;
@@ -0,0 +1,42 @@
1
+ import { Effect } from "effect";
2
+ import { FileSystem } from "effect/FileSystem";
3
+ import type { Path } from "effect/Path";
4
+ import type * as Scope from "effect/Scope";
5
+ import type { ChildProcessSpawner } from "./_unstable";
6
+ import type { RenderContext } from "./RenderContext";
7
+ import { type AnyRenderError } from "./RenderError";
8
+ export type RenderServices = FileSystem | Path | ChildProcessSpawner | Scope.Scope;
9
+ export declare const ManifestTypeId: unique symbol;
10
+ export type ManifestTypeId = typeof ManifestTypeId;
11
+ interface Variance<out A> {
12
+ readonly _A: (_: never) => A;
13
+ }
14
+ export interface Manifest<out A> {
15
+ readonly [ManifestTypeId]: Variance<A>;
16
+ readonly render: (ctx: RenderContext) => Effect.Effect<A, AnyRenderError, RenderServices>;
17
+ }
18
+ export type MakeRun<A> = (ctx: RenderContext) => A | Effect.Effect<A, AnyRenderError, RenderServices>;
19
+ export declare const make: <A>(run: MakeRun<A>) => Manifest<A>;
20
+ export interface CombineInput<A1, A2> {
21
+ readonly a: Manifest<A1>;
22
+ readonly b: Manifest<A2>;
23
+ }
24
+ export declare const combine: <A1, A2>(input: CombineInput<A1, A2>) => Manifest<readonly [A1, A2]>;
25
+ export declare const concat: <A>(...manifests: Manifest<A | A[]>[]) => Manifest<A[]>;
26
+ export interface WheneverInput<A> {
27
+ readonly cond: boolean;
28
+ readonly thunk: () => Manifest<A>;
29
+ }
30
+ export declare const whenever: <A>(input: WheneverInput<A>) => Manifest<A | undefined>;
31
+ export type EmbedYamlSource = {
32
+ readonly path: string;
33
+ } | {
34
+ readonly literal: string;
35
+ };
36
+ export interface RawYaml {
37
+ readonly _tag: "RawYaml";
38
+ readonly content: string;
39
+ readonly origin?: string;
40
+ }
41
+ export declare const embedYaml: (source: EmbedYamlSource) => Manifest<RawYaml>;
42
+ export {};
@@ -0,0 +1,146 @@
1
+ import { Effect, type Layer } from "effect";
2
+ import type { AnyRenderError } from "./RenderError";
3
+ /**
4
+ * Resolves to `T` if it is a string literal (or template-literal
5
+ * pattern), and to a structured error type when `T` widens to the bare
6
+ * `string`. Use as the field type on every name/namespace slot any
7
+ * module wrapper forwards.
8
+ *
9
+ * konfig's dep graph keys each provider by literal name; a wrapper that
10
+ * lets `Name` widen to `string` collapses every module into the same
11
+ * slot and silently masks unmet deps. This brand turns that into a
12
+ * compile error at the call site.
13
+ */
14
+ export type LiteralName<T extends string> = string extends T ? {
15
+ readonly _konfig_error: "Module name/namespace must be a string literal. Make the wrapper generic (`<const Name extends string>`) and forward via `Module.LiteralName<Name>`.";
16
+ } : T;
17
+ /**
18
+ * Context passed to a module's `build` callback. Carries the
19
+ * per-instance identity (chosen `name` and the module's `namespace`)
20
+ * so the build can stamp them onto manifests without re-receiving
21
+ * them via `opts`.
22
+ */
23
+ export interface BuildContext<Ns extends string = string> {
24
+ readonly name: string;
25
+ readonly namespace: Ns;
26
+ }
27
+ /**
28
+ * Allowed return shapes from a module `build` callback:
29
+ * - an `Effect` (when the build reads from Layers, files, etc.)
30
+ * - a plain `ReadonlyArray<unknown>` for pure synchronous builds.
31
+ *
32
+ * `Module.fixedNs` / `Module.dynamicNs` lift the array form into an
33
+ * `Effect` internally — wrapper authors don't need to wrap themselves.
34
+ */
35
+ export type BuildResult<R = never> = Effect.Effect<ReadonlyArray<unknown>, AnyRenderError, R> | ReadonlyArray<unknown>;
36
+ /**
37
+ * Higher-kinded handle constructor: each backend declares a sub-interface
38
+ * that maps the four type parameters (`_Name`, `_Ns`, `_R`, `_Extra`)
39
+ * onto its native handle (`ApplicationHandle` / `BundleHandle` / …).
40
+ *
41
+ * `ApplyHandle` substitutes concrete types into the kind's `this`
42
+ * slots — the standard "type lambda" encoding Effect uses for HKTs.
43
+ */
44
+ export interface HandleKind {
45
+ readonly _Name: string;
46
+ readonly _Ns: string;
47
+ readonly _R: unknown;
48
+ readonly _Extra: unknown;
49
+ readonly Handle: unknown;
50
+ }
51
+ export type ApplyHandle<K extends HandleKind, Name extends string, Ns extends string, R, Extra> = (K & {
52
+ readonly _Name: Name;
53
+ readonly _Ns: Ns;
54
+ readonly _R: R;
55
+ readonly _Extra: Extra;
56
+ })["Handle"];
57
+ /**
58
+ * Universal define-args every backend accepts. Each backend layers
59
+ * additional fields on via the `ExtraConfig` (config-time-only) and
60
+ * `ExtraCallArgs` (per-instance) generics on `Target`.
61
+ */
62
+ export interface DefineBaseArgs<Name extends string, Ns extends string, R, Extra> {
63
+ readonly name: LiteralName<Name>;
64
+ readonly namespace: LiteralName<Ns>;
65
+ readonly build: Effect.Effect<ReadonlyArray<unknown>, AnyRenderError, R> | (() => ReadonlyArray<unknown>);
66
+ readonly provides?: Layer.Layer<Extra>;
67
+ }
68
+ /**
69
+ * Adapter contract a backend implements to plug into `Module.fixedNs` /
70
+ * `Module.dynamicNs`. Both `Application` (argocd) and `Bundle` (k8s)
71
+ * satisfy it via their existing `define` exports — pass the namespace
72
+ * itself as the first argument.
73
+ *
74
+ * - `Kind` — HKT mapping `(Name, Ns, R, Extra)` to the
75
+ * backend's native handle type.
76
+ * - `ExtraConfig` — config-time-only fields the backend accepts
77
+ * (e.g. argocd's `syncPolicy`, `annotations`).
78
+ * - `ExtraCallArgs` — per-instance fields the wrapper requires
79
+ * at the call site (e.g. argocd's `source`).
80
+ */
81
+ export interface Target<Kind extends HandleKind = HandleKind, ExtraConfig extends object = Record<string, never>, ExtraCallArgs extends object = Record<string, never>> {
82
+ readonly define: <const Name extends string, const Ns extends string, R = never, Extra = never>(args: DefineBaseArgs<Name, Ns, R, Extra> & ExtraConfig & ExtraCallArgs) => ApplyHandle<Kind, Name, Ns, R, Extra>;
83
+ }
84
+ /** Config accepted by `Module.fixedNs` (namespace baked into the wrapper). */
85
+ export interface FixedNsConfig<Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, Ns extends string, Opts extends object, R, Extra> {
86
+ readonly target: Target<Kind, ExtraConfig, ExtraCallArgs>;
87
+ readonly namespace: Ns;
88
+ readonly provides?: Layer.Layer<Extra>;
89
+ readonly build: (ctx: BuildContext<Ns>, opts: Opts) => BuildResult<R>;
90
+ }
91
+ /**
92
+ * Build a typed wrapper for a module whose namespace is part of its
93
+ * identity (e.g. `cert-manager` always installs into `cert-manager`).
94
+ *
95
+ * `target` is the backend adapter — typically `Application.target`
96
+ * (argocd) or `Bundle.target` (k8s). The wrapper's call signature
97
+ * merges the backend's per-instance fields (`source` for argocd,
98
+ * none for bundle) with the user-defined `Opts`.
99
+ *
100
+ * ```ts
101
+ * const defineSops = Module.fixedNs({
102
+ * target: Application.target,
103
+ * namespace: "sops",
104
+ * annotations: Sync.wave(-1),
105
+ * build: ({ namespace }, _opts: Record<never, never>) => [
106
+ * Namespace.make({ name: namespace }),
107
+ * Helm.release({ ... }),
108
+ * ],
109
+ * });
110
+ *
111
+ * const sops = defineSops({ name: "sops-operator", source: src("sops") });
112
+ * ```
113
+ */
114
+ export declare const fixedNs: <Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, const Ns extends string, Opts extends object = Record<never, never>, R = never, Extra = never>(config: FixedNsConfig<Kind, ExtraConfig, ExtraCallArgs, Ns, Opts, R, Extra> & ExtraConfig) => <const Name extends string>(args: {
115
+ readonly name: LiteralName<Name>;
116
+ } & ExtraCallArgs & Opts) => ApplyHandle<Kind, Name, Ns, R, Extra>;
117
+ /** Config accepted by `Module.dynamicNs` (namespace chosen per instance). */
118
+ export interface DynamicNsConfig<Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, Opts extends object, R, Extra> {
119
+ readonly target: Target<Kind, ExtraConfig, ExtraCallArgs>;
120
+ readonly provides?: Layer.Layer<Extra>;
121
+ readonly build: (ctx: BuildContext, opts: Opts) => BuildResult<R>;
122
+ }
123
+ /**
124
+ * Build a typed wrapper for a module whose namespace is chosen per
125
+ * instance (e.g. an `api` module deployed into different namespaces
126
+ * per env).
127
+ *
128
+ * ```ts
129
+ * const defineApi = Module.dynamicNs({
130
+ * target: Application.target,
131
+ * annotations: Sync.wave(1),
132
+ * build: ({ name, namespace }, opts: ApiOpts) => [ ... ],
133
+ * });
134
+ *
135
+ * const api = defineApi({
136
+ * name: "api",
137
+ * namespace: "prod",
138
+ * source: src("api"),
139
+ * replicas: 2,
140
+ * });
141
+ * ```
142
+ */
143
+ export declare const dynamicNs: <Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, Opts extends object = Record<never, never>, R = never, Extra = never>(config: DynamicNsConfig<Kind, ExtraConfig, ExtraCallArgs, Opts, R, Extra> & ExtraConfig) => <const Name extends string, const Ns extends string>(args: {
144
+ readonly name: LiteralName<Name>;
145
+ readonly namespace: LiteralName<Ns>;
146
+ } & ExtraCallArgs & Opts) => ApplyHandle<Kind, Name, Ns, R, Extra>;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Context object threaded through every `Manifest.render(...)` call.
3
+ *
4
+ * `env` is the single load-bearing field: it keys both the file-output
5
+ * directory under `outDir.manifests` and the choice of bundle entry in
6
+ * `KonfigConfig.envs`. The optional fields below carry per-cluster
7
+ * context for renders where one logical env spans multiple clusters.
8
+ *
9
+ * **Precedence rules:**
10
+ * - The output directory is keyed on `env` alone when `cluster` is
11
+ * `undefined`; when set, builds write to `<outDir>/<env>/<cluster>/`.
12
+ * Tests that bypass the CLI write into the simpler `<outDir>/<env>/`
13
+ * path by construction.
14
+ * - `cluster` is informational inside `Manifest.make((ctx) => ...)` —
15
+ * no constructor mutates a manifest based on it implicitly; the
16
+ * caller branches.
17
+ * - `k8sVersion` is a hint for renderers that emit
18
+ * apiVersion-conditional resources (e.g. `policy/v1beta1` vs
19
+ * `policy/v1`).
20
+ * - `flags` is the escape hatch for one-off "render with X" toggles
21
+ * that don't deserve their own field. Use sparingly.
22
+ */
23
+ export interface RenderContext {
24
+ readonly env: string;
25
+ readonly cluster?: string;
26
+ readonly k8sVersion?: string;
27
+ readonly flags?: ReadonlyMap<string, unknown>;
28
+ }
29
+ export interface RenderContextFullInput {
30
+ readonly env: string;
31
+ readonly cluster?: string;
32
+ readonly k8sVersion?: string;
33
+ readonly flags?: ReadonlyMap<string, unknown>;
34
+ }
35
+ export declare const RenderContext: {
36
+ make: (env: string) => RenderContext;
37
+ makeFull: (input: RenderContextFullInput) => RenderContext;
38
+ };