@alchemy.run/archil 0.0.0 → 2.0.0-beta.86

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.
Files changed (184) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +57 -4
  3. package/lib/AWS/S3Disk.d.ts +68 -0
  4. package/lib/AWS/S3Disk.d.ts.map +1 -0
  5. package/lib/AWS/S3Disk.js +100 -0
  6. package/lib/AWS/S3Disk.js.map +1 -0
  7. package/lib/AWS/index.d.ts +2 -0
  8. package/lib/AWS/index.d.ts.map +1 -0
  9. package/lib/AWS/index.js +2 -0
  10. package/lib/AWS/index.js.map +1 -0
  11. package/lib/ApiToken.d.ts +63 -0
  12. package/lib/ApiToken.d.ts.map +1 -0
  13. package/lib/ApiToken.js +121 -0
  14. package/lib/ApiToken.js.map +1 -0
  15. package/lib/ArchilBindings.d.ts +65 -0
  16. package/lib/ArchilBindings.d.ts.map +1 -0
  17. package/lib/ArchilBindings.js +128 -0
  18. package/lib/ArchilBindings.js.map +1 -0
  19. package/lib/AuthProvider.d.ts +40 -0
  20. package/lib/AuthProvider.d.ts.map +1 -0
  21. package/lib/AuthProvider.js +87 -0
  22. package/lib/AuthProvider.js.map +1 -0
  23. package/lib/Bootstrap.d.ts +8 -0
  24. package/lib/Bootstrap.d.ts.map +1 -0
  25. package/lib/Bootstrap.js +27 -0
  26. package/lib/Bootstrap.js.map +1 -0
  27. package/lib/Branch.d.ts +101 -0
  28. package/lib/Branch.d.ts.map +1 -0
  29. package/lib/Branch.js +117 -0
  30. package/lib/Branch.js.map +1 -0
  31. package/lib/BranchDisk.d.ts +42 -0
  32. package/lib/BranchDisk.d.ts.map +1 -0
  33. package/lib/BranchDisk.js +14 -0
  34. package/lib/BranchDisk.js.map +1 -0
  35. package/lib/Checkpoint.d.ts +78 -0
  36. package/lib/Checkpoint.d.ts.map +1 -0
  37. package/lib/Checkpoint.js +95 -0
  38. package/lib/Checkpoint.js.map +1 -0
  39. package/lib/Clients.d.ts +369 -0
  40. package/lib/Clients.d.ts.map +1 -0
  41. package/lib/Clients.js +334 -0
  42. package/lib/Clients.js.map +1 -0
  43. package/lib/ConnectSandbox.d.ts +69 -0
  44. package/lib/ConnectSandbox.d.ts.map +1 -0
  45. package/lib/ConnectSandbox.js +21 -0
  46. package/lib/ConnectSandbox.js.map +1 -0
  47. package/lib/Contributions.d.ts +7 -0
  48. package/lib/Contributions.d.ts.map +1 -0
  49. package/lib/Contributions.js +6 -0
  50. package/lib/Contributions.js.map +1 -0
  51. package/lib/CreateDisk.d.ts +90 -0
  52. package/lib/CreateDisk.d.ts.map +1 -0
  53. package/lib/CreateDisk.js +47 -0
  54. package/lib/CreateDisk.js.map +1 -0
  55. package/lib/CreateImage.d.ts +42 -0
  56. package/lib/CreateImage.d.ts.map +1 -0
  57. package/lib/CreateImage.js +19 -0
  58. package/lib/CreateImage.js.map +1 -0
  59. package/lib/CreateSandbox.d.ts +104 -0
  60. package/lib/CreateSandbox.d.ts.map +1 -0
  61. package/lib/CreateSandbox.js +98 -0
  62. package/lib/CreateSandbox.js.map +1 -0
  63. package/lib/Credentials.d.ts +44 -0
  64. package/lib/Credentials.d.ts.map +1 -0
  65. package/lib/Credentials.js +57 -0
  66. package/lib/Credentials.js.map +1 -0
  67. package/lib/Disk.d.ts +283 -0
  68. package/lib/Disk.d.ts.map +1 -0
  69. package/lib/Disk.js +294 -0
  70. package/lib/Disk.js.map +1 -0
  71. package/lib/DiskObjectsHttp.d.ts +20 -0
  72. package/lib/DiskObjectsHttp.d.ts.map +1 -0
  73. package/lib/DiskObjectsHttp.js +51 -0
  74. package/lib/DiskObjectsHttp.js.map +1 -0
  75. package/lib/DiskUser.d.ts +189 -0
  76. package/lib/DiskUser.d.ts.map +1 -0
  77. package/lib/DiskUser.js +243 -0
  78. package/lib/DiskUser.js.map +1 -0
  79. package/lib/ExecDisk.d.ts +57 -0
  80. package/lib/ExecDisk.d.ts.map +1 -0
  81. package/lib/ExecDisk.js +3 -0
  82. package/lib/ExecDisk.js.map +1 -0
  83. package/lib/ExecDiskHttp.d.ts +5 -0
  84. package/lib/ExecDiskHttp.d.ts.map +1 -0
  85. package/lib/ExecDiskHttp.js +18 -0
  86. package/lib/ExecDiskHttp.js.map +1 -0
  87. package/lib/ExecDisks.d.ts +54 -0
  88. package/lib/ExecDisks.d.ts.map +1 -0
  89. package/lib/ExecDisks.js +37 -0
  90. package/lib/ExecDisks.js.map +1 -0
  91. package/lib/Image.d.ts +188 -0
  92. package/lib/Image.d.ts.map +1 -0
  93. package/lib/Image.js +191 -0
  94. package/lib/Image.js.map +1 -0
  95. package/lib/InferEnv.d.ts +26 -0
  96. package/lib/InferEnv.d.ts.map +1 -0
  97. package/lib/InferEnv.js +1 -0
  98. package/lib/InferEnv.js.map +1 -0
  99. package/lib/MountDisk.d.ts +190 -0
  100. package/lib/MountDisk.d.ts.map +1 -0
  101. package/lib/MountDisk.js +227 -0
  102. package/lib/MountDisk.js.map +1 -0
  103. package/lib/Providers.d.ts +25 -0
  104. package/lib/Providers.d.ts.map +1 -0
  105. package/lib/Providers.js +48 -0
  106. package/lib/Providers.js.map +1 -0
  107. package/lib/ReadDisk.d.ts +55 -0
  108. package/lib/ReadDisk.d.ts.map +1 -0
  109. package/lib/ReadDisk.js +3 -0
  110. package/lib/ReadDisk.js.map +1 -0
  111. package/lib/ReadWriteDisk.d.ts +26 -0
  112. package/lib/ReadWriteDisk.d.ts.map +1 -0
  113. package/lib/ReadWriteDisk.js +3 -0
  114. package/lib/ReadWriteDisk.js.map +1 -0
  115. package/lib/Sandbox.d.ts +507 -0
  116. package/lib/Sandbox.d.ts.map +1 -0
  117. package/lib/Sandbox.js +729 -0
  118. package/lib/Sandbox.js.map +1 -0
  119. package/lib/SandboxOptions.d.ts +185 -0
  120. package/lib/SandboxOptions.d.ts.map +1 -0
  121. package/lib/SandboxOptions.js +102 -0
  122. package/lib/SandboxOptions.js.map +1 -0
  123. package/lib/SandboxPortToken.d.ts +64 -0
  124. package/lib/SandboxPortToken.d.ts.map +1 -0
  125. package/lib/SandboxPortToken.js +77 -0
  126. package/lib/SandboxPortToken.js.map +1 -0
  127. package/lib/WriteDisk.d.ts +37 -0
  128. package/lib/WriteDisk.d.ts.map +1 -0
  129. package/lib/WriteDisk.js +3 -0
  130. package/lib/WriteDisk.js.map +1 -0
  131. package/lib/index.d.ts +29 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +28 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/internal/DeployerKey.d.ts +21 -0
  136. package/lib/internal/DeployerKey.d.ts.map +1 -0
  137. package/lib/internal/DeployerKey.js +34 -0
  138. package/lib/internal/DeployerKey.js.map +1 -0
  139. package/lib/internal/archil.d.ts +87 -0
  140. package/lib/internal/archil.d.ts.map +1 -0
  141. package/lib/internal/archil.js +154 -0
  142. package/lib/internal/archil.js.map +1 -0
  143. package/lib/internal/hosted.d.ts +90 -0
  144. package/lib/internal/hosted.d.ts.map +1 -0
  145. package/lib/internal/hosted.js +291 -0
  146. package/lib/internal/hosted.js.map +1 -0
  147. package/package.json +79 -3
  148. package/src/AWS/S3Disk.ts +125 -0
  149. package/src/AWS/index.ts +1 -0
  150. package/src/ApiToken.ts +168 -0
  151. package/src/ArchilBindings.ts +179 -0
  152. package/src/AuthProvider.ts +122 -0
  153. package/src/Bootstrap.ts +54 -0
  154. package/src/Branch.ts +199 -0
  155. package/src/BranchDisk.ts +57 -0
  156. package/src/Checkpoint.ts +145 -0
  157. package/src/Clients.ts +954 -0
  158. package/src/ConnectSandbox.ts +91 -0
  159. package/src/Contributions.ts +9 -0
  160. package/src/CreateDisk.ts +165 -0
  161. package/src/CreateImage.ts +73 -0
  162. package/src/CreateSandbox.ts +258 -0
  163. package/src/Credentials.ts +101 -0
  164. package/src/Disk.ts +468 -0
  165. package/src/DiskObjectsHttp.ts +64 -0
  166. package/src/DiskUser.ts +369 -0
  167. package/src/ExecDisk.ts +62 -0
  168. package/src/ExecDiskHttp.ts +25 -0
  169. package/src/ExecDisks.ts +100 -0
  170. package/src/Image.ts +316 -0
  171. package/src/InferEnv.ts +27 -0
  172. package/src/MountDisk.ts +466 -0
  173. package/src/Providers.ts +75 -0
  174. package/src/ReadDisk.ts +61 -0
  175. package/src/ReadWriteDisk.ts +31 -0
  176. package/src/Sandbox.ts +1064 -0
  177. package/src/SandboxOptions.ts +230 -0
  178. package/src/SandboxPortToken.ts +125 -0
  179. package/src/WriteDisk.ts +42 -0
  180. package/src/globals.d.ts +2 -0
  181. package/src/index.ts +59 -0
  182. package/src/internal/DeployerKey.ts +63 -0
  183. package/src/internal/archil.ts +273 -0
  184. package/src/internal/hosted.ts +402 -0
package/src/Image.ts ADDED
@@ -0,0 +1,316 @@
1
+ import * as BuildPlan from "@alchemy.run/docker/BuildPlan";
2
+ import type { InlineDockerfile } from "@alchemy.run/docker/Dockerfile";
3
+ import type { DockerfileBuild } from "@alchemy.run/docker/ImageSource";
4
+ import type { ImageRegistry } from "@alchemy.run/docker/Registry";
5
+ import * as archil from "@distilled.cloud/archil";
6
+ import { isResolved } from "alchemy/Diff";
7
+ import * as Provider from "alchemy/Provider";
8
+ import { Resource } from "alchemy/Resource";
9
+ import * as Data from "effect/Data";
10
+ import * as Effect from "effect/Effect";
11
+ import { waitForImage } from "./internal/archil.ts";
12
+ import type { Providers } from "./Providers.ts";
13
+
14
+ export interface ImageProps {
15
+ /**
16
+ * Linux OCI image reference with a `linux/amd64` manifest, e.g.
17
+ * `ghcr.io/acme/agent:1`. Docker shorthand and tags are accepted.
18
+ * Exclusive with `dockerfile` and `context`. Changing it builds a new
19
+ * image.
20
+ */
21
+ image?: string;
22
+ /**
23
+ * A Dockerfile path (relative to the cwd) or `Dockerfile.inline` content.
24
+ * Archil has no builder of its own: Alchemy builds it with your local
25
+ * Docker and pushes it to `push.repository`, and Archil pulls it from
26
+ * there.
27
+ */
28
+ dockerfile?: string | InlineDockerfile;
29
+ /** Build context for a `dockerfile` path. */
30
+ context?: string;
31
+ /** Docker build args and BuildKit secrets for a `dockerfile` image. */
32
+ build?: DockerfileBuild;
33
+ /**
34
+ * Credentials for a private registry. Archil uses them to pull this
35
+ * image only and doesn't store them.
36
+ */
37
+ registry?: ImageRegistry;
38
+ /**
39
+ * Where a Dockerfile image is pushed for Archil to pull: a repository
40
+ * (no tag; the build's hash is the tag) and credentials that can push to
41
+ * it and pull from it. Required for `dockerfile` images.
42
+ */
43
+ push?: ImagePush;
44
+ }
45
+
46
+ /** A registry repository Alchemy pushes built images to, for Archil to pull. */
47
+ export interface ImagePush {
48
+ /** Repository without a tag, e.g. `ghcr.io/acme/agent`. */
49
+ repository: string;
50
+ /** Credentials that can push to and pull from the repository. */
51
+ credentials: ImageRegistry;
52
+ }
53
+
54
+ export interface ImageAttributes {
55
+ /** Image id (64 hex characters); pass it as a sandbox's `image`. */
56
+ imageId: string;
57
+ /** The reference the image was built from. */
58
+ source: string;
59
+ /** The fully qualified reference Archil resolved, e.g. `docker.io/library/node:24`. */
60
+ canonicalSource: string | undefined;
61
+ /** Digest of the image sandboxes boot. */
62
+ digest: string | undefined;
63
+ /** Whether the image came from a private registry. */
64
+ private: boolean;
65
+ /** Hash of the build plan the image came from. */
66
+ hash: string;
67
+ }
68
+
69
+ export type Image = Resource<
70
+ "Archil.Image",
71
+ ImageProps,
72
+ ImageAttributes,
73
+ never,
74
+ Providers,
75
+ ImageLookup
76
+ >;
77
+
78
+ /**
79
+ * Props for {@link Image.lookup}: find an image this stack
80
+ * doesn't manage by its id.
81
+ */
82
+ export interface ImageLookup {
83
+ /** Archil image id. */
84
+ imageId: string;
85
+ }
86
+
87
+ /**
88
+ * The image needs instructions run on top of its base, and Archil only
89
+ * pulls finished images.
90
+ */
91
+ export class ImageRequiresBuild extends Data.TaggedError("Archil.ImageRequiresBuild")<{
92
+ readonly message: string;
93
+ }> {}
94
+
95
+ /**
96
+ * A sandbox image. Archil pulls it from a public or private registry and
97
+ * sandboxes boot its current digest. Public images can also be used
98
+ * directly as a sandbox's `baseImage`; build an `Image` for a private one,
99
+ * or to pin the digest every sandbox boots.
100
+ *
101
+ * Props follow `@alchemy.run/docker`'s image source, like every Alchemy
102
+ * image resource. Archil has no builder, so a Dockerfile is built with
103
+ * your local Docker and pushed to `push.repository`, and Archil pulls it
104
+ * from there.
105
+ *
106
+ * Archil has no API to delete an image, so destroying the resource only
107
+ * removes it from Alchemy's state.
108
+ * @see https://docs.archil.com/api-reference/images/build-image
109
+ *
110
+ * ### Building an Image
111
+ * **Example:** From a public image
112
+ * ```typescript
113
+ * export const Base = Archil.Image("Base", { image: "node:24-bookworm" });
114
+ * ```
115
+ *
116
+ * **Example:** From a private registry
117
+ * ```typescript
118
+ * export const Agent = Archil.Image("Agent", {
119
+ * image: "ghcr.io/acme/agent:1",
120
+ * registry: {
121
+ * server: "ghcr.io",
122
+ * username: "acme-bot",
123
+ * password: Alchemy.Secret("GHCR_TOKEN"),
124
+ * },
125
+ * });
126
+ * ```
127
+ *
128
+ * **Example:** Build a Dockerfile and push it for Archil to pull
129
+ * ```typescript
130
+ * export const Agent = Archil.Image("Agent", {
131
+ * dockerfile: "./agent/Dockerfile",
132
+ * context: "./agent",
133
+ * push: {
134
+ * repository: "ghcr.io/acme/agent",
135
+ * credentials: { server: "ghcr.io", username: "acme-bot", password: Alchemy.Secret("GHCR_TOKEN") },
136
+ * },
137
+ * });
138
+ * ```
139
+ *
140
+ * ### Booting Sandboxes from an Image
141
+ * **Example:** Sandboxes created at run time
142
+ * ```typescript
143
+ * const sandboxes = yield* Archil.CreateSandbox(Agent);
144
+ * const sandbox = yield* sandboxes.create({ name: "task-1" });
145
+ * ```
146
+ *
147
+ * ### Looking up an existing image
148
+ * `Image.lookup` references an image Alchemy doesn't manage (created in
149
+ * the Archil console or by another stack). It is read during plan/deploy,
150
+ * never created, updated or deleted.
151
+ * `hash` is empty: the image wasn't built by this stack.
152
+ *
153
+ * **Example:** Reference an existing image
154
+ * ```typescript
155
+ * const base = Archil.Image.lookup({ imageId: "img_123" });
156
+ * ```
157
+ *
158
+ * @resource
159
+ * @product Image
160
+ */
161
+ export const Image = Resource<Image>("Archil.Image");
162
+
163
+ /** What Archil's image service can build: nothing beyond pulling a reference. */
164
+ export const capabilities: BuildPlan.Capabilities = {
165
+ target: "Archil.Image",
166
+ platforms: ["linux/amd64"],
167
+ };
168
+
169
+ /** Map an image response to attributes. */
170
+ export const toImageAttributes = (
171
+ image: archil.Image,
172
+ hash: string,
173
+ source: string = image.source,
174
+ ): ImageAttributes => ({
175
+ imageId: image.image_id,
176
+ // Archil may answer with a reference it normalized (or one an earlier
177
+ // build of the same image used), so keep the one that was asked for.
178
+ source,
179
+ canonicalSource: image.canonical_source,
180
+ digest: image.digest,
181
+ private: image.private,
182
+ hash,
183
+ });
184
+
185
+ /** Plan an image source for Archil. */
186
+ export const planImage = (props: ImageProps) =>
187
+ BuildPlan.plan(
188
+ props.image !== undefined
189
+ ? { image: props.image }
190
+ : { dockerfile: props.dockerfile, context: props.context, build: props.build },
191
+ { capabilities, pull: props.registry },
192
+ );
193
+
194
+ /**
195
+ * The reference Archil can pull for a plan: its base, when the plan adds
196
+ * nothing on top of it.
197
+ */
198
+ export const pullableSource = (plan: BuildPlan.BuildPlan) =>
199
+ plan.instructions.length === 0 && plan.files.length === 0
200
+ ? Effect.succeed(plan.base)
201
+ : Effect.fail(
202
+ new ImageRequiresBuild({
203
+ message: `Archil.Image pulls finished images; this Dockerfile adds ${plan.instructions.length} instruction(s) on top of ${plan.base}. Set \`push: { repository, credentials }\` so Alchemy can build it and push it there.`,
204
+ }),
205
+ );
206
+
207
+ /** Ask Archil to pull a reference and wait until the image is ready. */
208
+ export const pullImage = Effect.fn(function* (
209
+ source: string,
210
+ registry: ImageRegistry | undefined,
211
+ hash: string,
212
+ ) {
213
+ const { data } = yield* archil.createImage({
214
+ source,
215
+ registry_auth: registry
216
+ ? { username: registry.username, password: registry.password }
217
+ : undefined,
218
+ });
219
+ return toImageAttributes(yield* waitForImage(data.image_id), hash, source);
220
+ });
221
+
222
+ /**
223
+ * The reference Archil pulls for a plan: its base when the plan adds
224
+ * nothing, else the plan built locally and pushed to `push`.
225
+ */
226
+ export const publishPlan = Effect.fn(function* (
227
+ plan: BuildPlan.BuildPlan,
228
+ push: ImagePush | undefined,
229
+ ) {
230
+ if (plan.instructions.length === 0 && plan.files.length === 0) {
231
+ return { source: plan.base, credentials: undefined };
232
+ }
233
+ if (push === undefined) {
234
+ return yield* new ImageRequiresBuild({
235
+ message: `Archil.Image pulls finished images; this Dockerfile adds ${plan.instructions.length} instruction(s) on top of ${plan.base}. Set \`push: { repository, credentials }\` so Alchemy can build it and push it there.`,
236
+ });
237
+ }
238
+ const { imageRef } = yield* Effect.scoped(
239
+ BuildPlan.push(plan, { repository: push.repository, push: push.credentials }),
240
+ );
241
+ return { source: imageRef, credentials: push.credentials };
242
+ });
243
+
244
+ /** Plan, publish, then pull, an image. */
245
+ export const buildImage = Effect.fn(function* (props: ImageProps) {
246
+ const plan = yield* planImage(props);
247
+ const published = yield* publishPlan(plan, props.push);
248
+ return yield* pullImage(published.source, published.credentials ?? props.registry, plan.hash);
249
+ });
250
+
251
+ export const ImageProvider = () =>
252
+ Provider.effect(
253
+ Image,
254
+ Effect.gen(function* () {
255
+ return {
256
+ stables: ["imageId"],
257
+ // No delete or list API: nuke can't remove images.
258
+ nuke: { skip: true },
259
+ diff: Effect.fn(function* ({ news, output }) {
260
+ if (!isResolved(news) || output === undefined) return undefined;
261
+ // The plan hash covers the reference, the Dockerfile and its context.
262
+ const plan = yield* planImage(news);
263
+ if (plan.hash !== output.hash) return { action: "replace" } as const;
264
+ return undefined;
265
+ }),
266
+ lookup: Effect.fn(function* ({ imageId }) {
267
+ return yield* archil.getImage({ image_id: imageId }).pipe(
268
+ Effect.map(({ data }) => toImageAttributes(data, "")),
269
+ Effect.catchTag("NotFound", () => Effect.undefined),
270
+ );
271
+ }),
272
+
273
+ read: Effect.fn(function* ({ output }) {
274
+ if (!output) return undefined;
275
+ return yield* archil.getImage({ image_id: output.imageId }).pipe(
276
+ Effect.map(({ data }) => toImageAttributes(data, output.hash, output.source)),
277
+ Effect.catchTag("NotFound", () => Effect.undefined),
278
+ );
279
+ }),
280
+ reconcile: Effect.fn(function* ({ news, output, session }) {
281
+ const plan = yield* planImage(news);
282
+ for (const warning of plan.warnings) yield* session.note(warning);
283
+ // Observe — a ready image from the same plan is kept; anything
284
+ // else (missing, failed, interrupted) is requested again.
285
+ if (output && output.hash === plan.hash) {
286
+ const observed = yield* archil.getImage({ image_id: output.imageId }).pipe(
287
+ Effect.map(({ data }) => data),
288
+ Effect.catchTag("NotFound", () => Effect.undefined),
289
+ );
290
+ if (observed?.status === "ready")
291
+ return toImageAttributes(observed, plan.hash, output.source);
292
+ if (observed?.status === "building") {
293
+ return toImageAttributes(
294
+ yield* waitForImage(observed.image_id),
295
+ plan.hash,
296
+ output.source,
297
+ );
298
+ }
299
+ }
300
+ // Ensure
301
+ if (plan.instructions.length > 0 || plan.files.length > 0) {
302
+ yield* session.note(`Building and pushing image to ${news.push?.repository}...`);
303
+ }
304
+ const published = yield* publishPlan(plan, news.push);
305
+ yield* session.note(`Pulling image ${published.source}...`);
306
+ return yield* pullImage(
307
+ published.source,
308
+ published.credentials ?? news.registry,
309
+ plan.hash,
310
+ );
311
+ }),
312
+ // Archil has no image delete API.
313
+ delete: () => Effect.void,
314
+ };
315
+ }),
316
+ );
@@ -0,0 +1,27 @@
1
+ import type * as Effect from "effect/Effect";
2
+
3
+ /**
4
+ * Typed `env` for the async (non-Effect) program of an `Archil.Sandbox`.
5
+ * A program's environment is process environment variables, so every
6
+ * entry is a `string`: the object the handler receives as its second
7
+ * argument.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * export const Hello = Archil.Sandbox("Hello", {
12
+ * main: "./src/hello.ts",
13
+ * env: { GREETING: "hello" },
14
+ * });
15
+ * export type HelloEnv = Archil.InferEnv<typeof Hello>;
16
+ * ```
17
+ */
18
+ export type InferEnv<F> =
19
+ F extends Effect.Effect<infer A, infer _E, infer _R>
20
+ ? InferEnv<A>
21
+ : F extends { readonly Props: { readonly env?: infer E } }
22
+ ? InferEnvRecord<Exclude<E, undefined>>
23
+ : InferEnvRecord<F>;
24
+
25
+ type InferEnvRecord<E> = {
26
+ readonly [K in keyof E]: string;
27
+ };