alchemy 0.35.0 → 0.36.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 (59) 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/docker/api.d.ts +185 -0
  7. package/lib/docker/api.d.ts.map +1 -0
  8. package/lib/docker/api.js +288 -0
  9. package/lib/docker/api.js.map +1 -0
  10. package/lib/docker/container.d.ts +147 -0
  11. package/lib/docker/container.d.ts.map +1 -0
  12. package/lib/docker/container.js +109 -0
  13. package/lib/docker/container.js.map +1 -0
  14. package/lib/docker/image.d.ts +92 -0
  15. package/lib/docker/image.d.ts.map +1 -0
  16. package/lib/docker/image.js +92 -0
  17. package/lib/docker/image.js.map +1 -0
  18. package/lib/docker/index.d.ts +7 -0
  19. package/lib/docker/index.d.ts.map +1 -0
  20. package/lib/docker/index.js +7 -0
  21. package/lib/docker/index.js.map +1 -0
  22. package/lib/docker/network.d.ts +62 -0
  23. package/lib/docker/network.d.ts.map +1 -0
  24. package/lib/docker/network.js +49 -0
  25. package/lib/docker/network.js.map +1 -0
  26. package/lib/docker/remote-image.d.ts +45 -0
  27. package/lib/docker/remote-image.d.ts.map +1 -0
  28. package/lib/docker/remote-image.js +36 -0
  29. package/lib/docker/remote-image.js.map +1 -0
  30. package/lib/docker/volume.d.ts +83 -0
  31. package/lib/docker/volume.d.ts.map +1 -0
  32. package/lib/docker/volume.js +76 -0
  33. package/lib/docker/volume.js.map +1 -0
  34. package/lib/fs/file-system-state-store.d.ts.map +1 -1
  35. package/lib/fs/file-system-state-store.js +5 -1
  36. package/lib/fs/file-system-state-store.js.map +1 -1
  37. package/lib/fs/file.d.ts.map +1 -1
  38. package/lib/fs/file.js +6 -3
  39. package/lib/fs/file.js.map +1 -1
  40. package/lib/os/exec.d.ts +19 -1
  41. package/lib/os/exec.d.ts.map +1 -1
  42. package/lib/os/exec.js +28 -4
  43. package/lib/os/exec.js.map +1 -1
  44. package/lib/util/cli.d.ts.map +1 -1
  45. package/lib/util/cli.js +1 -1
  46. package/lib/util/cli.js.map +1 -1
  47. package/package.json +5 -1
  48. package/src/cloudflare/bundle/bundle-worker.ts +1 -1
  49. package/src/docker/api.ts +365 -0
  50. package/src/docker/container.ts +267 -0
  51. package/src/docker/image.ts +200 -0
  52. package/src/docker/index.ts +6 -0
  53. package/src/docker/network.ts +101 -0
  54. package/src/docker/remote-image.ts +83 -0
  55. package/src/docker/volume.ts +154 -0
  56. package/src/fs/file-system-state-store.ts +5 -1
  57. package/src/fs/file.ts +6 -3
  58. package/src/os/exec.ts +48 -6
  59. package/src/util/cli.ts +1 -3
@@ -0,0 +1,200 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { Context } from "../context.ts";
4
+ import { Resource } from "../resource.ts";
5
+ import { DockerApi } from "./api.ts";
6
+
7
+ /**
8
+ * Options for building a Docker image
9
+ */
10
+ export interface DockerBuildOptions {
11
+ /**
12
+ * Path to the build context directory
13
+ */
14
+ context: string;
15
+
16
+ /**
17
+ * Path to the Dockerfile, relative to context
18
+ */
19
+ dockerfile?: string;
20
+
21
+ /**
22
+ * Target build platform (e.g., linux/amd64)
23
+ */
24
+ platform?: string;
25
+
26
+ /**
27
+ * Build arguments as key-value pairs
28
+ */
29
+ buildArgs?: Record<string, string>;
30
+
31
+ /**
32
+ * Target build stage in multi-stage builds
33
+ */
34
+ target?: string;
35
+
36
+ /**
37
+ * List of images to use for cache
38
+ */
39
+ cacheFrom?: string[];
40
+ }
41
+
42
+ /**
43
+ * Properties for creating a Docker image
44
+ */
45
+ export interface ImageProps {
46
+ /**
47
+ * Repository name for the image (e.g., "username/image")
48
+ */
49
+ name: string;
50
+
51
+ /**
52
+ * Tag for the image (e.g., "latest")
53
+ */
54
+ tag?: string;
55
+
56
+ /**
57
+ * Build configuration
58
+ */
59
+ build: DockerBuildOptions;
60
+
61
+ /**
62
+ * Whether to skip pushing the image to registry
63
+ */
64
+ skipPush?: boolean;
65
+ }
66
+
67
+ /**
68
+ * Docker Image resource
69
+ */
70
+ export interface Image extends Resource<"docker::Image">, ImageProps {
71
+ /**
72
+ * Full image reference (name:tag)
73
+ */
74
+ imageRef: string;
75
+
76
+ /**
77
+ * Image ID
78
+ */
79
+ imageId?: string;
80
+
81
+ /**
82
+ * Repository digest if pushed
83
+ */
84
+ repoDigest?: string;
85
+
86
+ /**
87
+ * Time when the image was built
88
+ */
89
+ builtAt: number;
90
+ }
91
+
92
+ /**
93
+ * Build and manage a Docker image from a Dockerfile
94
+ *
95
+ * @example
96
+ * // Build a Docker image from a Dockerfile
97
+ * const appImage = await Image("app-image", {
98
+ * name: "myapp",
99
+ * tag: "latest",
100
+ * build: {
101
+ * context: "./app",
102
+ * dockerfile: "Dockerfile",
103
+ * buildArgs: {
104
+ * NODE_ENV: "production"
105
+ * }
106
+ * }
107
+ * });
108
+ */
109
+ export const Image = Resource(
110
+ "docker::Image",
111
+ async function (
112
+ this: Context<Image>,
113
+ _id: string,
114
+ props: ImageProps,
115
+ ): Promise<Image> {
116
+ // Initialize Docker API client
117
+ const api = new DockerApi();
118
+
119
+ if (this.phase === "delete") {
120
+ // No action needed for delete as Docker images aren't automatically removed
121
+ // This is intentional as other resources might depend on the same image
122
+ return this.destroy();
123
+ } else {
124
+ // Normalize properties
125
+ const tag = props.tag || "latest";
126
+ const imageRef = `${props.name}:${tag}`;
127
+
128
+ // Validate build context
129
+ const { context } = props.build;
130
+ await fs.access(context);
131
+
132
+ // Determine Dockerfile path
133
+ const dockerfile = props.build.dockerfile || "Dockerfile";
134
+ const dockerfilePath = path.join(context, dockerfile);
135
+ await fs.access(dockerfilePath);
136
+
137
+ // Prepare build options
138
+ const buildOptions: Record<string, string> = props.build.buildArgs || {};
139
+
140
+ // Add platform if specified
141
+ let buildArgs = ["build", "-t", imageRef];
142
+
143
+ if (props.build.platform) {
144
+ buildArgs.push("--platform", props.build.platform);
145
+ }
146
+
147
+ // Add target if specified
148
+ if (props.build.target) {
149
+ buildArgs.push("--target", props.build.target);
150
+ }
151
+
152
+ // Add cache sources if specified
153
+ if (props.build.cacheFrom && props.build.cacheFrom.length > 0) {
154
+ for (const cacheSource of props.build.cacheFrom) {
155
+ buildArgs.push("--cache-from", cacheSource);
156
+ }
157
+ }
158
+
159
+ // Add build arguments
160
+ for (const [key, value] of Object.entries(buildOptions)) {
161
+ buildArgs.push("--build-arg", `${key}="${value}"`);
162
+ }
163
+
164
+ // Add dockerfile if not the default
165
+ if (props.build.dockerfile && props.build.dockerfile !== "Dockerfile") {
166
+ buildArgs.push("-f", props.build.dockerfile);
167
+ }
168
+
169
+ // Add context path
170
+ buildArgs.push(props.build.context);
171
+
172
+ // Execute build command
173
+ console.log(`Building Docker image: ${imageRef}`);
174
+ const { stdout } = await api.exec(buildArgs);
175
+
176
+ // Extract image ID from build output if available
177
+ const imageIdMatch = /Successfully built ([a-f0-9]+)/.exec(stdout);
178
+ const imageId = imageIdMatch ? imageIdMatch[1] : undefined;
179
+
180
+ console.log(`Successfully built Docker image: ${imageRef}`);
181
+
182
+ // Handle push if required
183
+ let repoDigest: string | undefined;
184
+ if (!props.skipPush) {
185
+ console.log(`Pushing Docker image: ${imageRef}`);
186
+ // TODO: Implement push once API supports it
187
+ console.warn("Image pushing is not yet implemented");
188
+ }
189
+
190
+ // Return the resource using this() to construct output
191
+ return this({
192
+ ...props,
193
+ imageRef,
194
+ imageId,
195
+ repoDigest,
196
+ builtAt: Date.now(),
197
+ });
198
+ }
199
+ },
200
+ );
@@ -0,0 +1,6 @@
1
+ export * from "./api.ts";
2
+ export * from "./remote-image.ts";
3
+ export * from "./container.ts";
4
+ export * from "./network.ts";
5
+ export * from "./volume.ts";
6
+ export * from "./image.ts";
@@ -0,0 +1,101 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { DockerApi } from "./api.ts";
4
+
5
+ /**
6
+ * Properties for creating a Docker network
7
+ */
8
+ export interface NetworkProps {
9
+ /**
10
+ * Network name
11
+ */
12
+ name: string;
13
+
14
+ /**
15
+ * Network driver to use
16
+ * @default "bridge"
17
+ */
18
+ driver?: "bridge" | "host" | "none" | "overlay" | "macvlan" | (string & {});
19
+
20
+ /**
21
+ * Enable IPv6 on the network
22
+ * @default false
23
+ */
24
+ enableIPv6?: boolean;
25
+
26
+ /**
27
+ * Network-scoped alias for containers
28
+ */
29
+ labels?: Record<string, string>;
30
+ }
31
+
32
+ /**
33
+ * Docker Network resource
34
+ */
35
+ export interface Network extends Resource<"docker::Network">, NetworkProps {
36
+ /**
37
+ * Network ID
38
+ */
39
+ id: string;
40
+
41
+ /**
42
+ * Time when the network was created
43
+ */
44
+ createdAt: number;
45
+ }
46
+
47
+ /**
48
+ * Create and manage a Docker Network
49
+ *
50
+ * @see https://docs.docker.com/engine/network/
51
+ *
52
+ * @example
53
+ * // Create a simple bridge network
54
+ * const appNetwork = await Network("app-network", {
55
+ * name: "app-network"
56
+ * });
57
+ *
58
+ * @example
59
+ * // Create a custom network with driver
60
+ * const overlayNetwork = await Network("overlay-network", {
61
+ * name: "overlay-network",
62
+ * driver: "overlay",
63
+ * enableIPv6: true,
64
+ * labels: {
65
+ * "com.example.description": "Network for application services"
66
+ * }
67
+ * });
68
+ */
69
+ export const Network = Resource(
70
+ "docker::Network",
71
+ async function (
72
+ this: Context<Network>,
73
+ _id: string,
74
+ props: NetworkProps,
75
+ ): Promise<Network> {
76
+ // Initialize Docker API client
77
+ const api = new DockerApi();
78
+
79
+ // Handle delete phase
80
+ if (this.phase === "delete") {
81
+ if (this.output?.id) {
82
+ // Remove network
83
+ await api.removeNetwork(this.output.id);
84
+ }
85
+
86
+ // Return destroyed state
87
+ return this.destroy();
88
+ } else {
89
+ // Create the network
90
+ props.driver = props.driver || "bridge";
91
+ const networkId = await api.createNetwork(props.name, props.driver);
92
+
93
+ // Return the resource using this() to construct output
94
+ return this({
95
+ ...props,
96
+ id: networkId,
97
+ createdAt: Date.now(),
98
+ });
99
+ }
100
+ },
101
+ );
@@ -0,0 +1,83 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { DockerApi } from "./api.ts";
4
+
5
+ /**
6
+ * Properties for creating a Docker image
7
+ */
8
+ export interface RemoteImageProps {
9
+ /**
10
+ * Docker image name (e.g., "nginx")
11
+ */
12
+ name: string;
13
+
14
+ /**
15
+ * Tag for the image (e.g., "latest" or "1.19-alpine")
16
+ */
17
+ tag?: string;
18
+
19
+ /**
20
+ * Always attempt to pull the image, even if it exists locally
21
+ */
22
+ alwaysPull?: boolean;
23
+ }
24
+
25
+ /**
26
+ * Docker Remote Image resource
27
+ */
28
+ export interface RemoteImage
29
+ extends Resource<"docker::RemoteImage">,
30
+ RemoteImageProps {
31
+ /**
32
+ * Full image reference (name:tag)
33
+ */
34
+ imageRef: string;
35
+
36
+ /**
37
+ * Time when the image was created or pulled
38
+ */
39
+ createdAt: number;
40
+ }
41
+
42
+ /**
43
+ * Create or reference a Docker Remote Image
44
+ *
45
+ * @example
46
+ * // Pull the nginx image
47
+ * const nginxImage = await RemoteImage("nginx", {
48
+ * name: "nginx",
49
+ * tag: "latest"
50
+ * });
51
+ *
52
+ */
53
+ export const RemoteImage = Resource(
54
+ "docker::RemoteImage",
55
+ async function (
56
+ this: Context<RemoteImage>,
57
+ _id: string,
58
+ props: RemoteImageProps,
59
+ ): Promise<RemoteImage> {
60
+ // Initialize Docker API client
61
+ const api = new DockerApi();
62
+
63
+ if (this.phase === "delete") {
64
+ // No action needed for delete as Docker images aren't automatically removed
65
+ // This is intentional as other resources might depend on the same image
66
+ return this.destroy();
67
+ } else {
68
+ // Normalize properties
69
+ const tag = props.tag || "latest";
70
+ const imageRef = `${props.name}:${tag}`;
71
+
72
+ // Pull image
73
+ await api.pullImage(imageRef);
74
+
75
+ // Return the resource using this() to construct output
76
+ return this({
77
+ ...props,
78
+ imageRef,
79
+ createdAt: Date.now(),
80
+ });
81
+ }
82
+ },
83
+ );
@@ -0,0 +1,154 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { DockerApi } from "./api.ts";
4
+
5
+ /**
6
+ * Interface for volume label
7
+ */
8
+ export interface VolumeLabel {
9
+ /**
10
+ * Label name
11
+ */
12
+ name: string;
13
+
14
+ /**
15
+ * Label value
16
+ */
17
+ value: string;
18
+ }
19
+
20
+ /**
21
+ * Properties for creating a Docker volume
22
+ */
23
+ export interface VolumeProps {
24
+ /**
25
+ * Volume name
26
+ */
27
+ name: string;
28
+
29
+ /**
30
+ * Volume driver to use
31
+ * @default "local"
32
+ */
33
+ driver?: string;
34
+
35
+ /**
36
+ * Driver-specific options
37
+ */
38
+ driverOpts?: Record<string, string>;
39
+
40
+ /**
41
+ * Custom metadata labels for the volume
42
+ */
43
+ labels?: VolumeLabel[] | Record<string, string>;
44
+ }
45
+
46
+ /**
47
+ * Docker Volume resource
48
+ */
49
+ export interface Volume extends Resource<"docker::Volume">, VolumeProps {
50
+ /**
51
+ * Volume ID (same as name for Docker volumes)
52
+ */
53
+ id: string;
54
+
55
+ /**
56
+ * Volume mountpoint path on the host
57
+ */
58
+ mountpoint?: string;
59
+
60
+ /**
61
+ * Time when the volume was created
62
+ */
63
+ createdAt: number;
64
+ }
65
+
66
+ /**
67
+ * Create and manage a Docker Volume
68
+ *
69
+ * @see https://docs.docker.com/engine/reference/commandline/volume/
70
+ *
71
+ * @example
72
+ * // Create a simple Docker volume
73
+ * const dataVolume = await Volume("data-volume", {
74
+ * name: "data-volume"
75
+ * });
76
+ *
77
+ * @example
78
+ * // Create a Docker volume with custom driver and options
79
+ * const dbVolume = await Volume("db-data", {
80
+ * name: "db-data",
81
+ * driver: "local",
82
+ * driverOpts: {
83
+ * "type": "nfs",
84
+ * "o": "addr=10.0.0.1,rw",
85
+ * "device": ":/path/to/dir"
86
+ * },
87
+ * labels: [
88
+ * { name: "com.example.usage", value: "database-storage" },
89
+ * { name: "com.example.backup", value: "weekly" }
90
+ * ]
91
+ * });
92
+ */
93
+ export const Volume = Resource(
94
+ "docker::Volume",
95
+ async function (
96
+ this: Context<Volume>,
97
+ _id: string,
98
+ props: VolumeProps,
99
+ ): Promise<Volume> {
100
+ // Initialize Docker API client
101
+ const api = new DockerApi();
102
+
103
+ // Process labels to ensure consistent format
104
+ const processedLabels: Record<string, string> = {};
105
+ if (props.labels) {
106
+ if (Array.isArray(props.labels)) {
107
+ // Convert array of label objects to Record
108
+ for (const label of props.labels) {
109
+ processedLabels[label.name] = label.value;
110
+ }
111
+ } else {
112
+ // Use Record directly
113
+ Object.assign(processedLabels, props.labels);
114
+ }
115
+ }
116
+
117
+ // Handle delete phase
118
+ if (this.phase === "delete") {
119
+ if (this.output?.name) {
120
+ // Remove volume
121
+ await api.removeVolume(this.output.name);
122
+ }
123
+
124
+ // Return destroyed state
125
+ return this.destroy();
126
+ } else {
127
+ // Set default driver if not provided
128
+ props.driver = props.driver || "local";
129
+ const driverOpts = props.driverOpts || {};
130
+
131
+ // Create the volume
132
+ const volumeName = await api.createVolume(
133
+ props.name,
134
+ props.driver,
135
+ driverOpts,
136
+ processedLabels,
137
+ );
138
+
139
+ // Get volume details to retrieve mountpoint
140
+ const volumeInfos = await api.inspectVolume(volumeName);
141
+ const mountpoint = volumeInfos[0].Mountpoint;
142
+
143
+ // Return the resource using this() to construct output
144
+ return this({
145
+ ...props,
146
+ id: volumeName,
147
+ mountpoint,
148
+ createdAt: Date.now(),
149
+ labels: Array.isArray(props.labels) ? props.labels : undefined,
150
+ driverOpts: props.driverOpts,
151
+ });
152
+ }
153
+ },
154
+ );
@@ -7,6 +7,7 @@ import { deserializeState, type State, type StateStore } from "../state.ts";
7
7
  import { ignore } from "../util/ignore.ts";
8
8
 
9
9
  const stateRootDir = path.join(process.cwd(), ".alchemy");
10
+ const ALCHEMY_SEPERATOR_CHAR = process.platform === "win32" ? "-" : ":";
10
11
 
11
12
  export class FileSystemStateStore implements StateStore {
12
13
  public readonly dir: string;
@@ -128,7 +129,10 @@ export class FileSystemStateStore implements StateStore {
128
129
  throw new Error(`ID cannot include colons: ${key}`);
129
130
  }
130
131
  if (key.includes("/")) {
131
- key = key.replaceAll("/", ":");
132
+ //todo(michael): remove this next time we do a breaking change
133
+ //* windows doesn't support ":" in file paths, but we already use ":"
134
+ //* so now we use both to prevent breaking changes`
135
+ key = key.replaceAll("/", ALCHEMY_SEPERATOR_CHAR);
132
136
  }
133
137
  return path.join(this.dir, `${key}.json`);
134
138
  }
package/src/fs/file.ts CHANGED
@@ -184,9 +184,12 @@ export const File = Resource(
184
184
  }
185
185
 
186
186
  // Create directory and write file
187
- await fs.promises.mkdir(path.dirname(filePath), {
188
- recursive: true,
189
- });
187
+ const dirName = path.dirname(filePath);
188
+ if (dirName !== ".") {
189
+ await fs.promises.mkdir(dirName, {
190
+ recursive: true,
191
+ });
192
+ }
190
193
 
191
194
  await fs.promises.writeFile(filePath, props.content);
192
195
 
package/src/os/exec.ts CHANGED
@@ -266,30 +266,72 @@ const defaultOptions: SpawnOptions = {
266
266
  shell: true,
267
267
  };
268
268
 
269
+ /**
270
+ * Options for exec function
271
+ */
272
+ export interface ExecOptions extends Partial<SpawnOptions> {
273
+ /**
274
+ * Whether to capture stdout and stderr
275
+ * @default false
276
+ */
277
+ captureOutput?: boolean;
278
+ }
279
+
269
280
  /**
270
281
  * Execute a shell command.
282
+ *
283
+ * @param command The command to execute
284
+ * @param options Options for the command execution
285
+ * @returns Promise that resolves when the command completes.
286
+ * If captureOutput is true, resolves with { stdout, stderr } strings.
271
287
  */
272
288
  export async function exec(
273
289
  command: string,
274
- options?: Partial<SpawnOptions>,
275
- ): Promise<void> {
290
+ options?: ExecOptions,
291
+ ): Promise<{ stdout: string; stderr: string } | undefined> {
276
292
  const [cmd, ...args] = command.split(/\s+/);
293
+ const captureOutput = options?.captureOutput === true;
277
294
 
278
295
  return new Promise((resolve, reject) => {
279
- const child = spawn(cmd, args, {
296
+ // Set stdio to pipe only if we're capturing output
297
+ const spawnOptions = {
280
298
  ...defaultOptions,
281
299
  ...options,
282
300
  env: {
283
301
  ...defaultOptions.env,
284
302
  ...options?.env,
285
303
  },
286
- });
304
+ stdio: captureOutput ? "pipe" : defaultOptions.stdio,
305
+ };
306
+
307
+ const child = spawn(cmd, args, spawnOptions);
308
+
309
+ let stdout = "";
310
+ let stderr = "";
311
+
312
+ if (captureOutput) {
313
+ child.stdout?.on("data", (data) => {
314
+ stdout += data.toString();
315
+ });
316
+
317
+ child.stderr?.on("data", (data) => {
318
+ stderr += data.toString();
319
+ });
320
+ }
287
321
 
288
322
  child.on("close", (code) => {
289
323
  if (code === 0) {
290
- resolve();
324
+ if (captureOutput) {
325
+ resolve({ stdout, stderr });
326
+ } else {
327
+ resolve(undefined);
328
+ }
291
329
  } else {
292
- reject(new Error(`Command failed with exit code ${code}`));
330
+ reject(
331
+ new Error(
332
+ `Command failed with exit code ${code}${stderr ? `: ${stderr}` : ""}`,
333
+ ),
334
+ );
293
335
  }
294
336
  });
295
337
 
package/src/util/cli.ts CHANGED
@@ -17,9 +17,7 @@ type ColorName = keyof typeof colors;
17
17
 
18
18
  // Check if colors should be disabled
19
19
  const shouldDisableColors = (): boolean => {
20
- return Boolean(
21
- process.env.CI || process.env.NO_COLOR || !process.stdout.isTTY,
22
- );
20
+ return Boolean(process.env.NO_COLOR);
23
21
  };
24
22
 
25
23
  // Apply color if colors are enabled