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

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/Branch.ts ADDED
@@ -0,0 +1,199 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import { isResolved } from "alchemy/Diff";
3
+ import { createPhysicalName } from "alchemy/PhysicalName";
4
+ import * as Provider from "alchemy/Provider";
5
+ import { Resource } from "alchemy/Resource";
6
+ import * as Effect from "effect/Effect";
7
+ import type { Providers } from "./Providers.ts";
8
+
9
+ export interface BranchProps {
10
+ /** Id of the disk to branch (e.g. `disk.diskId`). Changing it replaces the branch. */
11
+ disk: string;
12
+ /**
13
+ * Branch name, unique within the disk. If omitted, a unique name is
14
+ * generated from the stack, stage, and logical ID. Changing it replaces
15
+ * the branch.
16
+ */
17
+ name?: string;
18
+ /**
19
+ * Name of the committed checkpoint the branch starts from (e.g.
20
+ * `checkpoint.name`). Changing it replaces the branch.
21
+ */
22
+ fromCheckpoint: string;
23
+ /**
24
+ * Branch the checkpoint was taken on. Omit for a checkpoint on the root
25
+ * disk. Changing it replaces the branch.
26
+ */
27
+ fromBranch?: string;
28
+ }
29
+
30
+ export interface BranchAttributes {
31
+ /** Id of the disk the branch belongs to. */
32
+ diskId: string;
33
+ /** Branch name. Mount it with `archil mount <disk> <path> --branch <name>`. */
34
+ name: string;
35
+ /** Id of the branch's own writable filesystem. */
36
+ filesystemId: string;
37
+ /** Checkpoint the branch started from. */
38
+ fromCheckpoint: string;
39
+ /** Filesystem the checkpoint was taken on. */
40
+ fromFilesystemId: string;
41
+ /** When the branch was created. */
42
+ createdAt: string;
43
+ }
44
+
45
+ export type Branch = Resource<
46
+ "Archil.Branch",
47
+ BranchProps,
48
+ BranchAttributes,
49
+ never,
50
+ Providers,
51
+ BranchLookup
52
+ >;
53
+
54
+ /**
55
+ * Props for {@link Branch.lookup}: find a branch this stack
56
+ * doesn't manage by its disk and name.
57
+ */
58
+ export interface BranchLookup {
59
+ /** Id of the disk the branch belongs to. */
60
+ disk: string;
61
+ /** Branch name. */
62
+ name: string;
63
+ }
64
+
65
+ /**
66
+ * An independent, writable fork of a disk, started from a committed
67
+ * checkpoint in about 35 ms whatever the disk's size. Writes on the branch
68
+ * never reach its source or its siblings. Branches share the disk's
69
+ * credentials, so a disk user can mount any branch of its disk.
70
+ *
71
+ * Archil has no API to delete a branch yet, so destroying the resource
72
+ * only removes it from Alchemy's state. Branches made per session at run
73
+ * time come from {@link BranchDisk}.
74
+ * @see https://docs.archil.com/concepts/branches-and-checkpoints
75
+ *
76
+ * ### Creating a Branch
77
+ * **Example:** A staging copy of a seeded disk
78
+ * ```typescript
79
+ * export const Staging = Effect.gen(function* () {
80
+ * const seed = yield* Seed;
81
+ * const base = yield* Base;
82
+ * return yield* Archil.Branch("Staging", {
83
+ * disk: seed.diskId,
84
+ * fromCheckpoint: base.name,
85
+ * });
86
+ * });
87
+ * ```
88
+ *
89
+ * ### Looking up an existing branch
90
+ * `Branch.lookup` references a branch Alchemy doesn't manage (created in
91
+ * the Archil console or by another stack). It is read during plan/deploy,
92
+ * never created, updated or deleted.
93
+ *
94
+ * **Example:** Reference an existing branch
95
+ * ```typescript
96
+ * const main = Archil.Branch.lookup({ disk: "dsk_123", name: "main" });
97
+ * ```
98
+ *
99
+ * @resource
100
+ * @product Disk
101
+ */
102
+ export const Branch = Resource<Branch>("Archil.Branch");
103
+
104
+ /** Map a branch response to attributes. */
105
+ export const toBranchAttributes = (branch: archil.BranchResponse): BranchAttributes => ({
106
+ diskId: branch.root_filesystem_id,
107
+ name: branch.branch_name,
108
+ filesystemId: branch.filesystem_id,
109
+ fromCheckpoint: branch.from_checkpoint_name,
110
+ fromFilesystemId: branch.from_checkpoint_filesystem_id,
111
+ createdAt: branch.created_at,
112
+ });
113
+
114
+ /** Get a branch; `undefined` when it (or its disk) doesn't exist. */
115
+ export const getBranch = (diskId: string, name: string) =>
116
+ archil.getBranch({ id: diskId, name }).pipe(
117
+ Effect.map(({ data }) => toBranchAttributes(data)),
118
+ Effect.catchTag("NotFound", () => Effect.undefined),
119
+ );
120
+
121
+ /** Create a branch, or return the existing one with that name. */
122
+ export const ensureBranch = Effect.fn(function* (input: {
123
+ disk: string;
124
+ name: string;
125
+ fromCheckpoint: string;
126
+ fromBranch?: string;
127
+ }) {
128
+ const existing = yield* getBranch(input.disk, input.name);
129
+ if (existing) return existing;
130
+ return yield* archil
131
+ .createBranch({
132
+ id: input.disk,
133
+ branch_name: input.name,
134
+ from_checkpoint_name: input.fromCheckpoint,
135
+ from_branch: input.fromBranch,
136
+ })
137
+ .pipe(
138
+ Effect.map(({ data }) => toBranchAttributes(data)),
139
+ // A concurrent create of the same name won the race.
140
+ Effect.catchTag("Conflict", (conflict) =>
141
+ getBranch(input.disk, input.name).pipe(
142
+ Effect.flatMap((found) => (found ? Effect.succeed(found) : Effect.fail(conflict))),
143
+ ),
144
+ ),
145
+ );
146
+ });
147
+
148
+ const branchName = (id: string, name: string | undefined) =>
149
+ name ? Effect.succeed(name) : createPhysicalName({ id, maxLength: 63, delimiter: "-" });
150
+
151
+ export const BranchProvider = () =>
152
+ Provider.effect(
153
+ Branch,
154
+ Effect.gen(function* () {
155
+ return {
156
+ stables: [
157
+ "diskId",
158
+ "name",
159
+ "filesystemId",
160
+ "fromCheckpoint",
161
+ "fromFilesystemId",
162
+ "createdAt",
163
+ ],
164
+ // No delete API: nuke can't remove branches (they go with their disk).
165
+ nuke: { skip: true },
166
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
167
+ if (!isResolved(news) || olds === undefined) return undefined;
168
+ const name = output?.name ?? (yield* branchName(id, olds.name));
169
+ if (
170
+ news.disk !== olds.disk ||
171
+ (news.name !== undefined && news.name !== name) ||
172
+ news.fromCheckpoint !== olds.fromCheckpoint ||
173
+ news.fromBranch !== olds.fromBranch
174
+ ) {
175
+ return { action: "replace" } as const;
176
+ }
177
+ return undefined;
178
+ }),
179
+ lookup: ({ disk, name }) => getBranch(disk, name),
180
+
181
+ read: Effect.fn(function* ({ id, olds, output }) {
182
+ if (!output && !olds) return undefined;
183
+ const diskId = output?.diskId ?? olds!.disk;
184
+ const name = output?.name ?? (yield* branchName(id, olds?.name));
185
+ return yield* getBranch(diskId, name);
186
+ }),
187
+ reconcile: Effect.fn(function* ({ id, news, output }) {
188
+ return yield* ensureBranch({
189
+ disk: news.disk,
190
+ name: output?.name ?? (yield* branchName(id, news.name)),
191
+ fromCheckpoint: news.fromCheckpoint,
192
+ fromBranch: news.fromBranch,
193
+ });
194
+ }),
195
+ // Archil has no branch delete API; the branch goes with its disk.
196
+ delete: () => Effect.void,
197
+ };
198
+ }),
199
+ );
@@ -0,0 +1,57 @@
1
+ import * as Binding from "alchemy/Binding";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import { ApiAccess } from "./ArchilBindings.ts";
5
+ import { makeBranchDiskClient, type BranchDiskClient } from "./Clients.ts";
6
+ import type { Disk } from "./Disk.ts";
7
+
8
+ export type { BranchDiskClient } from "./Clients.ts";
9
+
10
+ /**
11
+ * Fork one disk per session or task at run time: each branch is an
12
+ * independent, writable copy started from a committed checkpoint in about
13
+ * 35 ms, whatever the disk's size. Seed the disk once at deploy time with an
14
+ * {@link Checkpoint}, then branch from it per request.
15
+ *
16
+ * Mount a branch on your own machines with `MountDisk` and `branch`.
17
+ * Branches share the disk's credentials. Sandboxes can't mount a branch.
18
+ *
19
+ * ### Branching per Session
20
+ * **Example:** A fresh workspace for every session
21
+ * ```typescript
22
+ * export default class Api extends Cloudflare.Worker<Api>()(
23
+ * "Api",
24
+ * { main: import.meta.url },
25
+ * Effect.gen(function* () {
26
+ * const seeds = yield* Archil.BranchDisk(Seed);
27
+ * return {
28
+ * fetch: Effect.gen(function* () {
29
+ * const branch = yield* seeds.branch({ name: "session-42", fromCheckpoint: "base" });
30
+ * return yield* HttpServerResponse.json(branch);
31
+ * }),
32
+ * };
33
+ * }).pipe(Effect.provide(Archil.BranchDiskHttp)),
34
+ * ) {}
35
+ * ```
36
+ *
37
+ * @binding
38
+ * @product Disk
39
+ */
40
+ export interface BranchDisk extends Binding.Service<
41
+ BranchDisk,
42
+ "Archil.BranchDisk",
43
+ (disk: Disk) => Effect.Effect<BranchDiskClient>
44
+ > {}
45
+
46
+ export const BranchDisk = Binding.Service<BranchDisk>("Archil.BranchDisk");
47
+
48
+ /** {@link BranchDisk} over Archil's HTTP API, usable from any host. */
49
+ export const BranchDiskHttp = Layer.effect(
50
+ BranchDisk,
51
+ Effect.gen(function* () {
52
+ const access = yield* ApiAccess;
53
+ return Effect.fn(function* (disk: Disk) {
54
+ return makeBranchDiskClient(yield* disk.diskId, yield* access());
55
+ });
56
+ }),
57
+ );
@@ -0,0 +1,145 @@
1
+ import * as archil from "@distilled.cloud/archil";
2
+ import { deepEqual, isResolved } from "alchemy/Diff";
3
+ import { createPhysicalName } from "alchemy/PhysicalName";
4
+ import * as Provider from "alchemy/Provider";
5
+ import { Resource } from "alchemy/Resource";
6
+ import * as Data from "effect/Data";
7
+ import * as Effect from "effect/Effect";
8
+ import type { Providers } from "./Providers.ts";
9
+
10
+ /** A checkpoint's setup command exited non-zero. */
11
+ export class CheckpointSetupFailed extends Data.TaggedError("Archil.CheckpointSetupFailed")<{
12
+ readonly checkpoint: string;
13
+ readonly exitCode: number;
14
+ readonly stderr: string;
15
+ }> {}
16
+
17
+ export interface CheckpointProps {
18
+ /** Id of the disk to checkpoint (e.g. `disk.diskId`). Changing it replaces the checkpoint. */
19
+ disk: string;
20
+ /**
21
+ * Checkpoint name, unique within its branch. If omitted, a unique name
22
+ * is generated from the stack, stage, and logical ID. Changing it
23
+ * replaces the checkpoint.
24
+ */
25
+ name?: string;
26
+ /**
27
+ * Shell commands that seed the disk before the checkpoint is taken. They
28
+ * run in a serverless microVM with the disk mounted at `/mnt/archil` (the
29
+ * working directory). Changing them takes a new checkpoint.
30
+ */
31
+ run?: string[];
32
+ }
33
+
34
+ export interface CheckpointAttributes {
35
+ /** Id of the disk. */
36
+ diskId: string;
37
+ /** Checkpoint name. Pass it to a {@link Branch} or `BranchDisk` as `fromCheckpoint`. */
38
+ name: string;
39
+ }
40
+
41
+ export type Checkpoint = Resource<
42
+ "Archil.Checkpoint",
43
+ CheckpointProps,
44
+ CheckpointAttributes,
45
+ never,
46
+ Providers
47
+ >;
48
+
49
+ /**
50
+ * A named, point-in-time snapshot of a disk, the seeded base that branches
51
+ * fork from. Alchemy runs the `run` commands against the disk, then takes
52
+ * the checkpoint in the same microVM, so the checkpoint holds exactly what
53
+ * they produced. A checkpoint takes about 15 ms whatever the disk's size,
54
+ * and never changes after it's taken.
55
+ *
56
+ * Archil can't delete checkpoints yet, so destroying the resource only
57
+ * removes it from Alchemy's state.
58
+ * @see https://docs.archil.com/concepts/branches-and-checkpoints
59
+ *
60
+ * ### Seeding a Base
61
+ * **Example:** Clone and install once, branch per session
62
+ * ```typescript
63
+ * export const Seed = Archil.Disk("Seed", {});
64
+ * export const Base = Effect.gen(function* () {
65
+ * const seed = yield* Seed;
66
+ * return yield* Archil.Checkpoint("Base", {
67
+ * disk: seed.diskId,
68
+ * name: "base",
69
+ * run: [
70
+ * "git clone --depth 1 https://github.com/alchemy-run/alchemy repo",
71
+ * "cd repo && corepack enable && pnpm install --frozen-lockfile",
72
+ * ],
73
+ * });
74
+ * });
75
+ * ```
76
+ *
77
+ * @resource
78
+ * @product Disk
79
+ */
80
+ export const Checkpoint = Resource<Checkpoint>("Archil.Checkpoint");
81
+
82
+ const checkpointName = (id: string, name: string | undefined) =>
83
+ name ? Effect.succeed(name) : createPhysicalName({ id, maxLength: 63, delimiter: "-" });
84
+
85
+ const shellQuote = (value: string) => `'${value.replaceAll("'", `'"'"'`)}'`;
86
+
87
+ /** Seed a disk and take a checkpoint of it in one serverless exec. */
88
+ export const takeCheckpoint = Effect.fn(function* (
89
+ diskId: string,
90
+ name: string,
91
+ run: ReadonlyArray<string>,
92
+ ) {
93
+ const script = [
94
+ "set -e",
95
+ "cd /mnt/archil",
96
+ ...run.map((command) => `(${command})`),
97
+ "sync",
98
+ `archil checkpoints create /mnt/archil ${shellQuote(name)}`,
99
+ ].join("\n");
100
+ const { data } = yield* archil.execDisk({ id: diskId, command: script });
101
+ if (data.exitCode !== 0) {
102
+ return yield* new CheckpointSetupFailed({
103
+ checkpoint: name,
104
+ exitCode: data.exitCode,
105
+ stderr: data.stderr,
106
+ });
107
+ }
108
+ return { diskId, name } satisfies CheckpointAttributes;
109
+ });
110
+
111
+ export const CheckpointProvider = () =>
112
+ Provider.effect(
113
+ Checkpoint,
114
+ Effect.gen(function* () {
115
+ return {
116
+ stables: ["diskId", "name"],
117
+ // No list or delete API: checkpoints go with their disk.
118
+ nuke: { skip: true },
119
+ diff: Effect.fn(function* ({ id, olds, news, output }) {
120
+ if (!isResolved(news) || olds === undefined) return undefined;
121
+ const name = output?.name ?? (yield* checkpointName(id, olds.name));
122
+ if (
123
+ news.disk !== olds.disk ||
124
+ (news.name !== undefined && news.name !== name) ||
125
+ !deepEqual(news.run ?? [], olds.run ?? [])
126
+ ) {
127
+ return { action: "replace" } as const;
128
+ }
129
+ return undefined;
130
+ }),
131
+ // Checkpoints have no read API; state is authoritative.
132
+ read: Effect.fn(function* ({ output }) {
133
+ return output;
134
+ }),
135
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
136
+ if (output && output.diskId === news.disk) return output;
137
+ const name = yield* checkpointName(id, news.name);
138
+ yield* session.note(`Seeding disk and taking checkpoint ${name}...`);
139
+ return yield* takeCheckpoint(news.disk, name, news.run ?? []);
140
+ }),
141
+ // Archil has no checkpoint delete API.
142
+ delete: () => Effect.void,
143
+ };
144
+ }),
145
+ );