alchemy 0.35.1 → 0.37.0

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 (78) hide show
  1. package/bin/alchemy.mjs +45102 -45100
  2. package/bin/alchemy.ts +1 -1
  3. package/bin/create-alchemy.ts +10 -4
  4. package/lib/cloudflare/bundle/bundle-worker.js +1 -1
  5. package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
  6. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  7. package/lib/cloudflare/d1-database.js +0 -3
  8. package/lib/cloudflare/d1-database.js.map +1 -1
  9. package/lib/cloudflare/do-state-store/internal.d.ts.map +1 -1
  10. package/lib/cloudflare/do-state-store/internal.js +35 -28
  11. package/lib/cloudflare/do-state-store/internal.js.map +1 -1
  12. package/lib/cloudflare/website.d.ts.map +1 -1
  13. package/lib/cloudflare/website.js +28 -13
  14. package/lib/cloudflare/website.js.map +1 -1
  15. package/lib/cloudflare/worker-metadata.d.ts +8 -5
  16. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  17. package/lib/cloudflare/worker-metadata.js +48 -11
  18. package/lib/cloudflare/worker-metadata.js.map +1 -1
  19. package/lib/cloudflare/worker.d.ts +18 -12
  20. package/lib/cloudflare/worker.d.ts.map +1 -1
  21. package/lib/cloudflare/worker.js +74 -68
  22. package/lib/cloudflare/worker.js.map +1 -1
  23. package/lib/docker/api.d.ts +185 -0
  24. package/lib/docker/api.d.ts.map +1 -0
  25. package/lib/docker/api.js +288 -0
  26. package/lib/docker/api.js.map +1 -0
  27. package/lib/docker/container.d.ts +147 -0
  28. package/lib/docker/container.d.ts.map +1 -0
  29. package/lib/docker/container.js +109 -0
  30. package/lib/docker/container.js.map +1 -0
  31. package/lib/docker/image.d.ts +92 -0
  32. package/lib/docker/image.d.ts.map +1 -0
  33. package/lib/docker/image.js +92 -0
  34. package/lib/docker/image.js.map +1 -0
  35. package/lib/docker/index.d.ts +7 -0
  36. package/lib/docker/index.d.ts.map +1 -0
  37. package/lib/docker/index.js +7 -0
  38. package/lib/docker/index.js.map +1 -0
  39. package/lib/docker/network.d.ts +62 -0
  40. package/lib/docker/network.d.ts.map +1 -0
  41. package/lib/docker/network.js +49 -0
  42. package/lib/docker/network.js.map +1 -0
  43. package/lib/docker/remote-image.d.ts +45 -0
  44. package/lib/docker/remote-image.d.ts.map +1 -0
  45. package/lib/docker/remote-image.js +36 -0
  46. package/lib/docker/remote-image.js.map +1 -0
  47. package/lib/docker/volume.d.ts +83 -0
  48. package/lib/docker/volume.d.ts.map +1 -0
  49. package/lib/docker/volume.js +76 -0
  50. package/lib/docker/volume.js.map +1 -0
  51. package/lib/os/exec.d.ts +19 -1
  52. package/lib/os/exec.d.ts.map +1 -1
  53. package/lib/os/exec.js +28 -4
  54. package/lib/os/exec.js.map +1 -1
  55. package/lib/stripe/price.d.ts +24 -0
  56. package/lib/stripe/price.d.ts.map +1 -1
  57. package/lib/stripe/price.js +30 -0
  58. package/lib/stripe/price.js.map +1 -1
  59. package/lib/util/cli.d.ts.map +1 -1
  60. package/lib/util/cli.js +1 -1
  61. package/lib/util/cli.js.map +1 -1
  62. package/package.json +5 -1
  63. package/src/cloudflare/bundle/bundle-worker.ts +1 -1
  64. package/src/cloudflare/d1-database.ts +0 -4
  65. package/src/cloudflare/do-state-store/internal.ts +45 -29
  66. package/src/cloudflare/website.ts +40 -16
  67. package/src/cloudflare/worker-metadata.ts +101 -19
  68. package/src/cloudflare/worker.ts +104 -148
  69. package/src/docker/api.ts +365 -0
  70. package/src/docker/container.ts +267 -0
  71. package/src/docker/image.ts +200 -0
  72. package/src/docker/index.ts +6 -0
  73. package/src/docker/network.ts +101 -0
  74. package/src/docker/remote-image.ts +83 -0
  75. package/src/docker/volume.ts +154 -0
  76. package/src/os/exec.ts +48 -6
  77. package/src/stripe/price.ts +47 -1
  78. package/src/util/cli.ts +1 -3
@@ -0,0 +1,365 @@
1
+ import { exec } from "../os/exec.ts";
2
+
3
+ /**
4
+ * Options for Docker API requests
5
+ */
6
+ export interface DockerApiOptions {
7
+ /**
8
+ * Custom path to Docker binary
9
+ */
10
+ dockerPath?: string;
11
+ }
12
+
13
+ type VolumeInfo = {
14
+ CreatedAt: string;
15
+ Driver: string;
16
+ Labels: Record<string, string>;
17
+ Mountpoint: string;
18
+ Name: string;
19
+ Options: Record<string, string>;
20
+ Scope: string;
21
+ };
22
+
23
+ /**
24
+ * Docker API client that wraps Docker CLI commands
25
+ */
26
+ export class DockerApi {
27
+ /** Path to Docker CLI */
28
+ readonly dockerPath: string;
29
+
30
+ /**
31
+ * Create a new Docker API client
32
+ *
33
+ * @param options Docker API options
34
+ */
35
+ constructor(options: DockerApiOptions = {}) {
36
+ this.dockerPath = options.dockerPath || "docker";
37
+ }
38
+
39
+ /**
40
+ * Run a Docker CLI command
41
+ *
42
+ * @param args Command arguments to pass to Docker CLI
43
+ * @returns Result of the command
44
+ */
45
+ async exec(args: string[]): Promise<{ stdout: string; stderr: string }> {
46
+ const command = `${this.dockerPath} ${args.join(" ")}`;
47
+ const result = (await exec(command, {
48
+ captureOutput: true,
49
+ shell: true,
50
+ env: process.env,
51
+ })) as { stdout: string; stderr: string };
52
+
53
+ return result;
54
+ }
55
+
56
+ /**
57
+ * Check if Docker daemon is running
58
+ *
59
+ * @returns True if Docker daemon is running
60
+ */
61
+ async isRunning(): Promise<boolean> {
62
+ try {
63
+ // Use a quick, lightweight command to test if Docker is running
64
+ await this.exec(["version", "--format", "{{.Server.Version}}"]);
65
+ return true;
66
+ } catch (error) {
67
+ console.log(
68
+ `Docker daemon not running: ${error instanceof Error ? error.message : String(error)}`,
69
+ );
70
+ return false;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Pull Docker image
76
+ *
77
+ * @param image Image name and tag
78
+ * @returns Result of the pull command
79
+ */
80
+ async pullImage(image: string): Promise<{ stdout: string; stderr: string }> {
81
+ return this.exec(["pull", image]);
82
+ }
83
+
84
+ /**
85
+ * Build Docker image
86
+ *
87
+ * @param path Path to Dockerfile directory
88
+ * @param tag Tag for the image
89
+ * @param buildArgs Build arguments
90
+ * @returns Result of the build command
91
+ */
92
+ async buildImage(
93
+ path: string,
94
+ tag: string,
95
+ buildArgs: Record<string, string> = {},
96
+ ): Promise<{ stdout: string; stderr: string }> {
97
+ const args = ["build", "-t", tag, path];
98
+
99
+ for (const [key, value] of Object.entries(buildArgs)) {
100
+ args.push("--build-arg", `${key}=${value}`);
101
+ }
102
+
103
+ return this.exec(args);
104
+ }
105
+
106
+ /**
107
+ * List Docker images
108
+ *
109
+ * @returns JSON string containing image list
110
+ */
111
+ async listImages(): Promise<string> {
112
+ const { stdout } = await this.exec(["images", "--format", "{{json .}}"]);
113
+ return stdout;
114
+ }
115
+
116
+ /**
117
+ * Create Docker container
118
+ *
119
+ * @param image Image name
120
+ * @param name Container name
121
+ * @param options Container options
122
+ * @returns Container ID
123
+ */
124
+ async createContainer(
125
+ image: string,
126
+ name: string,
127
+ options: {
128
+ ports?: Record<string, string>;
129
+ env?: Record<string, string>;
130
+ volumes?: Record<string, string>;
131
+ cmd?: string[];
132
+ } = {},
133
+ ): Promise<string> {
134
+ const args = ["create", "--name", name];
135
+
136
+ // Add port mappings
137
+ if (options.ports) {
138
+ for (const [hostPort, containerPort] of Object.entries(options.ports)) {
139
+ args.push("-p", `${hostPort}:${containerPort}`);
140
+ }
141
+ }
142
+
143
+ // Add environment variables
144
+ if (options.env) {
145
+ for (const [key, value] of Object.entries(options.env)) {
146
+ args.push("-e", `${key}=${value}`);
147
+ }
148
+ }
149
+
150
+ // Add volume mappings
151
+ if (options.volumes) {
152
+ for (const [hostPath, containerPath] of Object.entries(options.volumes)) {
153
+ args.push("-v", `${hostPath}:${containerPath}`);
154
+ }
155
+ }
156
+
157
+ args.push(image);
158
+
159
+ // Add command if specified
160
+ if (options.cmd && options.cmd.length > 0) {
161
+ args.push(...options.cmd);
162
+ }
163
+
164
+ const { stdout } = await this.exec(args);
165
+ return stdout.trim();
166
+ }
167
+
168
+ /**
169
+ * Start Docker container
170
+ *
171
+ * @param containerId Container ID or name
172
+ */
173
+ async startContainer(containerId: string): Promise<void> {
174
+ await this.exec(["start", containerId]);
175
+ }
176
+
177
+ /**
178
+ * Stop Docker container
179
+ *
180
+ * @param containerId Container ID or name
181
+ */
182
+ async stopContainer(containerId: string): Promise<void> {
183
+ await this.exec(["stop", containerId]);
184
+ }
185
+
186
+ /**
187
+ * Remove Docker container
188
+ *
189
+ * @param containerId Container ID or name
190
+ * @param force Force removal
191
+ */
192
+ async removeContainer(containerId: string, force = false): Promise<void> {
193
+ const args = ["rm"];
194
+ if (force) {
195
+ args.push("-f");
196
+ }
197
+ args.push(containerId);
198
+ await this.exec(args);
199
+ }
200
+
201
+ /**
202
+ * Get container logs
203
+ *
204
+ * @param containerId Container ID or name
205
+ * @returns Container logs
206
+ */
207
+ async getContainerLogs(containerId: string): Promise<string> {
208
+ const { stdout } = await this.exec(["logs", containerId]);
209
+ return stdout;
210
+ }
211
+
212
+ /**
213
+ * Check if a container exists
214
+ *
215
+ * @param containerId Container ID or name
216
+ * @returns True if container exists
217
+ */
218
+ async containerExists(containerId: string): Promise<boolean> {
219
+ try {
220
+ await this.exec(["inspect", containerId]);
221
+ return true;
222
+ } catch (_error) {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Create Docker network
229
+ *
230
+ * @param name Network name
231
+ * @param driver Network driver
232
+ * @returns Network ID
233
+ */
234
+ async createNetwork(name: string, driver = "bridge"): Promise<string> {
235
+ const { stdout } = await this.exec([
236
+ "network",
237
+ "create",
238
+ "--driver",
239
+ driver,
240
+ name,
241
+ ]);
242
+ return stdout.trim();
243
+ }
244
+
245
+ /**
246
+ * Remove Docker network
247
+ *
248
+ * @param networkId Network ID or name
249
+ */
250
+ async removeNetwork(networkId: string): Promise<void> {
251
+ await this.exec(["network", "rm", networkId]);
252
+ }
253
+
254
+ /**
255
+ * Connect container to network
256
+ *
257
+ * @param containerId Container ID or name
258
+ * @param networkId Network ID or name
259
+ */
260
+ async connectNetwork(
261
+ containerId: string,
262
+ networkId: string,
263
+ options: {
264
+ aliases?: string[];
265
+ } = {},
266
+ ): Promise<void> {
267
+ const args = ["network", "connect"];
268
+ if (options.aliases) {
269
+ for (const alias of options.aliases) {
270
+ args.push("--alias", alias);
271
+ }
272
+ }
273
+ args.push(networkId, containerId);
274
+ await this.exec(args);
275
+ }
276
+
277
+ /**
278
+ * Disconnect container from network
279
+ *
280
+ * @param containerId Container ID or name
281
+ * @param networkId Network ID or name
282
+ */
283
+ async disconnectNetwork(
284
+ containerId: string,
285
+ networkId: string,
286
+ ): Promise<void> {
287
+ await this.exec(["network", "disconnect", networkId, containerId]);
288
+ }
289
+
290
+ /**
291
+ * Create Docker volume
292
+ *
293
+ * @param name Volume name
294
+ * @param driver Volume driver
295
+ * @param driverOpts Driver options
296
+ * @param labels Volume labels
297
+ * @returns Volume name
298
+ */
299
+ async createVolume(
300
+ name: string,
301
+ driver = "local",
302
+ driverOpts: Record<string, string> = {},
303
+ labels: Record<string, string> = {},
304
+ ): Promise<string> {
305
+ const args = ["volume", "create", "--name", name, "--driver", driver];
306
+
307
+ // Add driver options
308
+ for (const [key, value] of Object.entries(driverOpts)) {
309
+ args.push("--opt", `${key}=${value}`);
310
+ }
311
+
312
+ // Add labels
313
+ for (const [key, value] of Object.entries(labels)) {
314
+ args.push("--label", `${key}=${value}`);
315
+ }
316
+
317
+ const { stdout } = await this.exec(args);
318
+ return stdout.trim();
319
+ }
320
+
321
+ /**
322
+ * Remove Docker volume
323
+ *
324
+ * @param volumeName Volume name
325
+ * @param force Force removal of the volume
326
+ */
327
+ async removeVolume(volumeName: string, force = false): Promise<void> {
328
+ const args = ["volume", "rm"];
329
+ if (force) {
330
+ args.push("--force");
331
+ }
332
+ args.push(volumeName);
333
+ await this.exec(args);
334
+ }
335
+
336
+ /**
337
+ * Get Docker volume information
338
+ *
339
+ * @param volumeName Volume name
340
+ * @returns Volume details in JSON format
341
+ */
342
+ async inspectVolume(volumeName: string): Promise<VolumeInfo[]> {
343
+ const { stdout } = await this.exec(["volume", "inspect", volumeName]);
344
+ try {
345
+ return JSON.parse(stdout.trim()) as VolumeInfo[];
346
+ } catch (_error) {
347
+ return [];
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Check if a volume exists
353
+ *
354
+ * @param volumeName Volume name
355
+ * @returns True if volume exists
356
+ */
357
+ async volumeExists(volumeName: string): Promise<boolean> {
358
+ try {
359
+ await this.inspectVolume(volumeName);
360
+ return true;
361
+ } catch (_error) {
362
+ return false;
363
+ }
364
+ }
365
+ }
@@ -0,0 +1,267 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { DockerApi } from "./api.ts";
4
+ import type { Image } from "./image.ts";
5
+ import type { RemoteImage } from "./remote-image.ts";
6
+
7
+ /**
8
+ * Port mapping configuration
9
+ */
10
+ export interface PortMapping {
11
+ /**
12
+ * External port on the host
13
+ */
14
+ external: number | string;
15
+
16
+ /**
17
+ * Internal port inside the container
18
+ */
19
+ internal: number | string;
20
+
21
+ /**
22
+ * Protocol (tcp or udp)
23
+ */
24
+ protocol?: "tcp" | "udp";
25
+ }
26
+
27
+ /**
28
+ * Volume mapping configuration
29
+ */
30
+ export interface VolumeMapping {
31
+ /**
32
+ * Host path
33
+ */
34
+ hostPath: string;
35
+
36
+ /**
37
+ * Container path
38
+ */
39
+ containerPath: string;
40
+
41
+ /**
42
+ * Read-only flag
43
+ */
44
+ readOnly?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Network mapping configuration
49
+ */
50
+ export interface NetworkMapping {
51
+ /**
52
+ * Network name or ID
53
+ */
54
+ name: string;
55
+
56
+ /**
57
+ * Aliases for the container in the network
58
+ */
59
+ aliases?: string[];
60
+ }
61
+
62
+ /**
63
+ * Properties for creating a Docker container
64
+ */
65
+ export interface ContainerProps {
66
+ /**
67
+ * Image to use for the container
68
+ * Can be an Alchemy Image or RemoteImage resource or a string image reference
69
+ */
70
+ image: Image | RemoteImage | string;
71
+
72
+ /**
73
+ * Container name
74
+ */
75
+ name?: string;
76
+
77
+ /**
78
+ * Command to run in the container
79
+ */
80
+ command?: string[];
81
+
82
+ /**
83
+ * Environment variables
84
+ */
85
+ environment?: Record<string, string>;
86
+
87
+ /**
88
+ * Port mappings
89
+ */
90
+ ports?: PortMapping[];
91
+
92
+ /**
93
+ * Volume mappings
94
+ */
95
+ volumes?: VolumeMapping[];
96
+
97
+ /**
98
+ * Restart policy
99
+ */
100
+ restart?: "no" | "always" | "on-failure" | "unless-stopped";
101
+
102
+ /**
103
+ * Networks to connect to
104
+ */
105
+ networks?: NetworkMapping[];
106
+
107
+ /**
108
+ * Whether to remove the container when it exits
109
+ */
110
+ removeOnExit?: boolean;
111
+
112
+ /**
113
+ * Start the container after creation
114
+ */
115
+ start?: boolean;
116
+ }
117
+
118
+ /**
119
+ * Docker Container resource
120
+ */
121
+ export interface Container
122
+ extends Resource<"docker::Container">,
123
+ ContainerProps {
124
+ /**
125
+ * Container ID
126
+ */
127
+ id: string;
128
+
129
+ /**
130
+ * Container state
131
+ */
132
+ state?: "created" | "running" | "paused" | "stopped" | "exited";
133
+
134
+ /**
135
+ * Time when the container was created
136
+ */
137
+ createdAt: number;
138
+ }
139
+
140
+ /**
141
+ * Create and manage a Docker Container
142
+ *
143
+ * @example
144
+ * // Create a simple Nginx container
145
+ * const webContainer = await Container("web", {
146
+ * image: "nginx:latest",
147
+ * ports: [
148
+ * { external: 8080, internal: 80 }
149
+ * ],
150
+ * start: true
151
+ * });
152
+ *
153
+ * @example
154
+ * // Create a container with environment variables and volume mounts
155
+ * const appContainer = await Container("app", {
156
+ * image: customImage, // Using an Alchemy RemoteImage resource
157
+ * environment: {
158
+ * NODE_ENV: "production",
159
+ * API_KEY: "secret-key"
160
+ * },
161
+ * volumes: [
162
+ * { hostPath: "./data", containerPath: "/app/data" }
163
+ * ],
164
+ * ports: [
165
+ * { external: 3000, internal: 3000 }
166
+ * ],
167
+ * restart: "always",
168
+ * start: true
169
+ * });
170
+ */
171
+ export const Container = Resource(
172
+ "docker::Container",
173
+ async function (
174
+ this: Context<Container>,
175
+ id: string,
176
+ props: ContainerProps,
177
+ ): Promise<Container> {
178
+ // Initialize Docker API client
179
+ const api = new DockerApi();
180
+
181
+ // Get image reference
182
+ const imageRef =
183
+ typeof props.image === "string" ? props.image : props.image.imageRef;
184
+
185
+ // Use provided name or generate one based on resource ID
186
+ const containerName =
187
+ props.name || `alchemy-${id.replace(/[^a-zA-Z0-9_.-]/g, "-")}`;
188
+
189
+ // Handle delete phase
190
+ if (this.phase === "delete") {
191
+ if (this.output?.id) {
192
+ // Stop container if running
193
+ await api.stopContainer(this.output.id);
194
+
195
+ // Remove container
196
+ await api.removeContainer(this.output.id, true);
197
+ }
198
+
199
+ // Return destroyed state
200
+ return this.destroy();
201
+ } else {
202
+ let containerState: NonNullable<Container["state"]> = "created";
203
+
204
+ if (this.phase === "update") {
205
+ // Check if container already exists (for update)
206
+ const containerExists = await api.containerExists(containerName);
207
+
208
+ if (containerExists) {
209
+ // Remove existing container for update
210
+ await api.removeContainer(containerName, true);
211
+ }
212
+ }
213
+
214
+ // Prepare port mappings
215
+ const portMappings: Record<string, string> = {};
216
+ if (props.ports) {
217
+ for (const port of props.ports) {
218
+ const protocol = port.protocol || "tcp";
219
+ portMappings[`${port.external}`] = `${port.internal}/${protocol}`;
220
+ }
221
+ }
222
+
223
+ // Prepare volume mappings
224
+ const volumeMappings: Record<string, string> = {};
225
+ if (props.volumes) {
226
+ for (const volume of props.volumes) {
227
+ const readOnlyFlag = volume.readOnly ? ":ro" : "";
228
+ volumeMappings[volume.hostPath] =
229
+ `${volume.containerPath}${readOnlyFlag}`;
230
+ }
231
+ }
232
+
233
+ // Create new container
234
+ const containerId = await api.createContainer(imageRef, containerName, {
235
+ ports: portMappings,
236
+ env: props.environment,
237
+ volumes: volumeMappings,
238
+ cmd: props.command,
239
+ });
240
+
241
+ // Connect to networks if specified
242
+ if (props.networks) {
243
+ for (const network of props.networks) {
244
+ const networkId =
245
+ typeof network === "string" ? network : network.name;
246
+ await api.connectNetwork(containerId, networkId, {
247
+ aliases: network.aliases,
248
+ });
249
+ }
250
+ }
251
+
252
+ // Start container if requested
253
+ if (props.start) {
254
+ await api.startContainer(containerId);
255
+ containerState = "running";
256
+ }
257
+
258
+ // Return the resource using this() to construct output
259
+ return this({
260
+ ...props,
261
+ id: containerId,
262
+ state: containerState,
263
+ createdAt: Date.now(),
264
+ });
265
+ }
266
+ },
267
+ );