alchemy 0.43.5 → 0.44.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/bin/alchemy.mjs +430 -343
- package/bin/alchemy.ts +7 -21
- package/bin/commands/create.ts +79 -23
- package/bin/services/template-manager.ts +9 -4
- package/bin/services/vibe-rules.ts +47 -0
- package/bin/types.ts +25 -5
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +8 -11
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.js +2 -0
- package/lib/apply.js.map +1 -1
- package/lib/build-date.d.ts +1 -1
- package/lib/build-date.js +1 -1
- package/lib/cloudflare/assets.d.ts +1 -1
- package/lib/cloudflare/assets.d.ts.map +1 -1
- package/lib/cloudflare/bucket.d.ts.map +1 -1
- package/lib/cloudflare/bucket.js +9 -6
- package/lib/cloudflare/bucket.js.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker-dev.d.ts.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker-dev.js +4 -1
- package/lib/cloudflare/bundle/bundle-worker-dev.js.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
- package/lib/cloudflare/bundle/bundle-worker.js +4 -1
- package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
- package/lib/cloudflare/bundle/external.d.ts +1 -0
- package/lib/cloudflare/bundle/external.d.ts.map +1 -1
- package/lib/cloudflare/bundle/external.js +1 -1
- package/lib/cloudflare/bundle/external.js.map +1 -1
- package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts +10 -0
- package/lib/cloudflare/bundle/nodejs-import-warning-plugin.d.ts.map +1 -0
- package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js +67 -0
- package/lib/cloudflare/bundle/nodejs-import-warning-plugin.js.map +1 -0
- package/lib/cloudflare/container.d.ts +27 -2
- package/lib/cloudflare/container.d.ts.map +1 -1
- package/lib/cloudflare/container.js +82 -24
- package/lib/cloudflare/container.js.map +1 -1
- package/lib/cloudflare/do-state-store/store.d.ts.map +1 -1
- package/lib/cloudflare/do-state-store/store.js +5 -1
- package/lib/cloudflare/do-state-store/store.js.map +1 -1
- package/lib/cloudflare/redirect-rule.js +1 -1
- package/lib/cloudflare/redirect-rule.js.map +1 -1
- package/lib/cloudflare/website.d.ts +17 -0
- package/lib/cloudflare/website.d.ts.map +1 -1
- package/lib/cloudflare/website.js +1 -0
- package/lib/cloudflare/website.js.map +1 -1
- package/lib/cloudflare/worker/miniflare.d.ts +4 -1
- package/lib/cloudflare/worker/miniflare.d.ts.map +1 -1
- package/lib/cloudflare/worker/miniflare.js +22 -2
- package/lib/cloudflare/worker/miniflare.js.map +1 -1
- package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
- package/lib/cloudflare/worker-metadata.js +12 -0
- package/lib/cloudflare/worker-metadata.js.map +1 -1
- package/lib/cloudflare/worker.d.ts +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +24 -0
- package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
- package/lib/cloudflare/wrangler.json.js +20 -2
- package/lib/cloudflare/wrangler.json.js.map +1 -1
- package/lib/context.d.ts +6 -1
- package/lib/context.d.ts.map +1 -1
- package/lib/context.js +2 -1
- package/lib/context.js.map +1 -1
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +1 -0
- package/lib/destroy.js.map +1 -1
- package/lib/docker/api.d.ts +11 -0
- package/lib/docker/api.d.ts.map +1 -1
- package/lib/docker/api.js +41 -1
- package/lib/docker/api.js.map +1 -1
- package/lib/docker/image.d.ts +1 -1
- package/lib/docker/image.d.ts.map +1 -1
- package/lib/docker/image.js +11 -5
- package/lib/docker/image.js.map +1 -1
- package/lib/esbuild/bundle.d.ts +2 -2
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/llms.cloudflare.txt +379 -0
- package/lib/llms.d.ts +10 -0
- package/lib/llms.d.ts.map +1 -0
- package/lib/llms.js +12 -0
- package/lib/llms.js.map +1 -0
- package/lib/planetscale/index.d.ts +3 -2
- package/lib/planetscale/index.d.ts.map +1 -1
- package/lib/planetscale/index.js +3 -2
- package/lib/planetscale/index.js.map +1 -1
- package/lib/planetscale/password.d.ts +233 -0
- package/lib/planetscale/password.d.ts.map +1 -0
- package/lib/planetscale/password.js +238 -0
- package/lib/planetscale/password.js.map +1 -0
- package/lib/scope.d.ts +6 -5
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +18 -7
- package/lib/scope.js.map +1 -1
- package/lib/test/bun.d.ts.map +1 -1
- package/lib/test/bun.js +1 -0
- package/lib/test/bun.js.map +1 -1
- package/lib/test/vitest.d.ts.map +1 -1
- package/lib/test/vitest.js +1 -0
- package/lib/test/vitest.js.map +1 -1
- package/lib/util/nanoid.d.ts +3 -0
- package/lib/util/nanoid.d.ts.map +1 -0
- package/lib/util/nanoid.js +13 -0
- package/lib/util/nanoid.js.map +1 -0
- package/lib/util/safe-fetch.d.ts.map +1 -1
- package/lib/util/safe-fetch.js +0 -1
- package/lib/util/safe-fetch.js.map +1 -1
- package/lib/util/telemetry/client.d.ts.map +1 -1
- package/lib/util/telemetry/client.js +13 -3
- package/lib/util/telemetry/client.js.map +1 -1
- package/lib/util/telemetry/constants.d.ts +2 -1
- package/lib/util/telemetry/constants.d.ts.map +1 -1
- package/lib/util/telemetry/constants.js +3 -2
- package/lib/util/telemetry/constants.js.map +1 -1
- package/package.json +10 -2
- package/src/alchemy.ts +8 -13
- package/src/apply.ts +2 -0
- package/src/build-date.ts +1 -1
- package/src/cloudflare/bucket.ts +22 -14
- package/src/cloudflare/bundle/bundle-worker-dev.ts +4 -1
- package/src/cloudflare/bundle/bundle-worker.ts +4 -1
- package/src/cloudflare/bundle/external.ts +1 -1
- package/src/cloudflare/bundle/nodejs-import-warning-plugin.ts +94 -0
- package/src/cloudflare/container.ts +113 -28
- package/src/cloudflare/do-state-store/store.ts +5 -1
- package/src/cloudflare/redirect-rule.ts +1 -1
- package/src/cloudflare/website.ts +21 -1
- package/src/cloudflare/worker/miniflare.ts +33 -8
- package/src/cloudflare/worker-metadata.ts +15 -0
- package/src/cloudflare/wrangler.json.ts +61 -5
- package/src/context.ts +7 -0
- package/src/destroy.ts +1 -0
- package/src/docker/api.ts +57 -1
- package/src/docker/image.ts +15 -6
- package/src/index.ts +1 -0
- package/src/llms.cloudflare.txt +379 -0
- package/src/llms.ts +25 -0
- package/src/planetscale/index.ts +3 -2
- package/src/planetscale/password.ts +366 -0
- package/src/scope.ts +25 -11
- package/src/test/bun.ts +1 -0
- package/src/test/vitest.ts +1 -0
- package/src/util/nanoid.ts +13 -0
- package/src/util/safe-fetch.ts +0 -1
- package/src/util/telemetry/client.ts +17 -3
- package/src/util/telemetry/constants.ts +5 -3
|
@@ -47,6 +47,25 @@ export interface WranglerJsonProps {
|
|
|
47
47
|
binding: string;
|
|
48
48
|
directory: string;
|
|
49
49
|
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Transform hooks to modify generated configuration files
|
|
53
|
+
*/
|
|
54
|
+
transform?: {
|
|
55
|
+
/**
|
|
56
|
+
* Hook to modify the wrangler.json object before it's written
|
|
57
|
+
*
|
|
58
|
+
* This function receives the generated wrangler.json spec and should return
|
|
59
|
+
* a modified version. It's applied as the final transformation before the
|
|
60
|
+
* file is written to disk.
|
|
61
|
+
*
|
|
62
|
+
* @param spec - The generated wrangler.json specification
|
|
63
|
+
* @returns The modified wrangler.json specification
|
|
64
|
+
*/
|
|
65
|
+
wrangler?: (
|
|
66
|
+
spec: WranglerJsonSpec,
|
|
67
|
+
) => WranglerJsonSpec | Promise<WranglerJsonSpec>;
|
|
68
|
+
};
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
/**
|
|
@@ -156,14 +175,19 @@ export const WranglerJson = Resource(
|
|
|
156
175
|
spec.assets.directory = path.relative(dirname, spec.assets.directory);
|
|
157
176
|
}
|
|
158
177
|
|
|
178
|
+
// Apply the wrangler configuration hook as the final transformation
|
|
179
|
+
const finalSpec = props.transform?.wrangler
|
|
180
|
+
? await props.transform.wrangler(spec)
|
|
181
|
+
: spec;
|
|
182
|
+
|
|
159
183
|
await fs.mkdir(dirname, { recursive: true });
|
|
160
|
-
await fs.writeFile(filePath, await formatJson(
|
|
184
|
+
await fs.writeFile(filePath, await formatJson(finalSpec));
|
|
161
185
|
|
|
162
186
|
// Return the resource
|
|
163
187
|
return this({
|
|
164
188
|
...props,
|
|
165
189
|
path: path.relative(cwd, filePath),
|
|
166
|
-
spec,
|
|
190
|
+
spec: finalSpec,
|
|
167
191
|
createdAt: Date.now(),
|
|
168
192
|
updatedAt: Date.now(),
|
|
169
193
|
});
|
|
@@ -216,6 +240,7 @@ export interface WranglerJsonSpec {
|
|
|
216
240
|
*/
|
|
217
241
|
ai?: {
|
|
218
242
|
binding: string;
|
|
243
|
+
experimental_remote?: boolean;
|
|
219
244
|
};
|
|
220
245
|
|
|
221
246
|
/**
|
|
@@ -223,6 +248,7 @@ export interface WranglerJsonSpec {
|
|
|
223
248
|
*/
|
|
224
249
|
browser?: {
|
|
225
250
|
binding: string;
|
|
251
|
+
experimental_remote?: boolean;
|
|
226
252
|
};
|
|
227
253
|
|
|
228
254
|
/**
|
|
@@ -230,6 +256,7 @@ export interface WranglerJsonSpec {
|
|
|
230
256
|
*/
|
|
231
257
|
images?: {
|
|
232
258
|
binding: string;
|
|
259
|
+
experimental_remote?: boolean;
|
|
233
260
|
};
|
|
234
261
|
|
|
235
262
|
/**
|
|
@@ -242,6 +269,7 @@ export interface WranglerJsonSpec {
|
|
|
242
269
|
* The ID of the KV namespace used during `wrangler dev`
|
|
243
270
|
*/
|
|
244
271
|
preview_id?: string;
|
|
272
|
+
experimental_remote?: boolean;
|
|
245
273
|
}[];
|
|
246
274
|
|
|
247
275
|
/**
|
|
@@ -266,6 +294,7 @@ export interface WranglerJsonSpec {
|
|
|
266
294
|
* The preview name of this R2 bucket at the edge.
|
|
267
295
|
*/
|
|
268
296
|
preview_bucket_name?: string;
|
|
297
|
+
experimental_remote?: boolean;
|
|
269
298
|
}[];
|
|
270
299
|
|
|
271
300
|
/**
|
|
@@ -308,6 +337,7 @@ export interface WranglerJsonSpec {
|
|
|
308
337
|
vectorize?: {
|
|
309
338
|
binding: string;
|
|
310
339
|
index_name: string;
|
|
340
|
+
experimental_remote?: boolean;
|
|
311
341
|
}[];
|
|
312
342
|
|
|
313
343
|
/**
|
|
@@ -327,6 +357,7 @@ export interface WranglerJsonSpec {
|
|
|
327
357
|
* The ID of the D1 database used during `wrangler dev`
|
|
328
358
|
*/
|
|
329
359
|
preview_database_id?: string;
|
|
360
|
+
experimental_remote?: boolean;
|
|
330
361
|
}[];
|
|
331
362
|
|
|
332
363
|
/**
|
|
@@ -398,6 +429,7 @@ export interface WranglerJsonSpec {
|
|
|
398
429
|
dispatch_namespaces?: {
|
|
399
430
|
binding: string;
|
|
400
431
|
namespace: string;
|
|
432
|
+
experimental_remote?: boolean;
|
|
401
433
|
}[];
|
|
402
434
|
}
|
|
403
435
|
|
|
@@ -412,8 +444,12 @@ function processBindings(
|
|
|
412
444
|
workerCwd: string,
|
|
413
445
|
): void {
|
|
414
446
|
// Arrays to collect different binding types
|
|
415
|
-
const kvNamespaces: {
|
|
416
|
-
|
|
447
|
+
const kvNamespaces: {
|
|
448
|
+
binding: string;
|
|
449
|
+
id: string;
|
|
450
|
+
preview_id: string;
|
|
451
|
+
experimental_remote?: boolean;
|
|
452
|
+
}[] = [];
|
|
417
453
|
const durableObjects: {
|
|
418
454
|
name: string;
|
|
419
455
|
class_name: string;
|
|
@@ -440,6 +476,7 @@ function processBindings(
|
|
|
440
476
|
database_name: string;
|
|
441
477
|
migrations_dir?: string;
|
|
442
478
|
preview_database_id: string;
|
|
479
|
+
experimental_remote?: boolean;
|
|
443
480
|
}[] = [];
|
|
444
481
|
const queues: {
|
|
445
482
|
producers: { queue: string; binding: string }[];
|
|
@@ -459,7 +496,11 @@ function processBindings(
|
|
|
459
496
|
const new_sqlite_classes: string[] = [];
|
|
460
497
|
const new_classes: string[] = [];
|
|
461
498
|
|
|
462
|
-
const vectorizeIndexes: {
|
|
499
|
+
const vectorizeIndexes: {
|
|
500
|
+
binding: string;
|
|
501
|
+
index_name: string;
|
|
502
|
+
experimental_remote?: boolean;
|
|
503
|
+
}[] = [];
|
|
463
504
|
const analyticsEngineDatasets: { binding: string; dataset: string }[] = [];
|
|
464
505
|
const hyperdrive: {
|
|
465
506
|
binding: string;
|
|
@@ -475,6 +516,7 @@ function processBindings(
|
|
|
475
516
|
const dispatchNamespaces: {
|
|
476
517
|
binding: string;
|
|
477
518
|
namespace: string;
|
|
519
|
+
experimental_remote?: boolean;
|
|
478
520
|
}[] = [];
|
|
479
521
|
const containers: {
|
|
480
522
|
class_name: string;
|
|
@@ -527,6 +569,9 @@ function processBindings(
|
|
|
527
569
|
binding: bindingName,
|
|
528
570
|
id: id,
|
|
529
571
|
preview_id: id,
|
|
572
|
+
...("dev" in binding && binding.dev?.remote
|
|
573
|
+
? { experimental_remote: true }
|
|
574
|
+
: {}),
|
|
530
575
|
});
|
|
531
576
|
} else if (
|
|
532
577
|
typeof binding === "object" &&
|
|
@@ -550,6 +595,7 @@ function processBindings(
|
|
|
550
595
|
binding: bindingName,
|
|
551
596
|
bucket_name: binding.name,
|
|
552
597
|
preview_bucket_name: binding.name,
|
|
598
|
+
...(binding.dev?.remote ? { experimental_remote: true } : {}),
|
|
553
599
|
});
|
|
554
600
|
} else if (binding.type === "secret") {
|
|
555
601
|
// Secret binding
|
|
@@ -573,6 +619,7 @@ function processBindings(
|
|
|
573
619
|
database_name: binding.name,
|
|
574
620
|
migrations_dir: binding.migrationsDir,
|
|
575
621
|
preview_database_id: binding.id,
|
|
622
|
+
...(binding.dev?.remote ? { experimental_remote: true } : {}),
|
|
576
623
|
});
|
|
577
624
|
} else if (binding.type === "queue") {
|
|
578
625
|
queues.producers.push({
|
|
@@ -583,6 +630,8 @@ function processBindings(
|
|
|
583
630
|
vectorizeIndexes.push({
|
|
584
631
|
binding: bindingName,
|
|
585
632
|
index_name: binding.name,
|
|
633
|
+
// https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
|
|
634
|
+
experimental_remote: true,
|
|
586
635
|
});
|
|
587
636
|
} else if (binding.type === "browser") {
|
|
588
637
|
if (spec.browser) {
|
|
@@ -590,6 +639,8 @@ function processBindings(
|
|
|
590
639
|
}
|
|
591
640
|
spec.browser = {
|
|
592
641
|
binding: bindingName,
|
|
642
|
+
// https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
|
|
643
|
+
experimental_remote: true,
|
|
593
644
|
};
|
|
594
645
|
} else if (binding.type === "ai") {
|
|
595
646
|
if (spec.ai) {
|
|
@@ -597,6 +648,8 @@ function processBindings(
|
|
|
597
648
|
}
|
|
598
649
|
spec.ai = {
|
|
599
650
|
binding: bindingName,
|
|
651
|
+
// https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
|
|
652
|
+
experimental_remote: true,
|
|
600
653
|
};
|
|
601
654
|
} else if (binding.type === "images") {
|
|
602
655
|
if (spec.images) {
|
|
@@ -604,6 +657,8 @@ function processBindings(
|
|
|
604
657
|
}
|
|
605
658
|
spec.images = {
|
|
606
659
|
binding: bindingName,
|
|
660
|
+
// https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
|
|
661
|
+
experimental_remote: true,
|
|
607
662
|
};
|
|
608
663
|
} else if (binding.type === "analytics_engine") {
|
|
609
664
|
analyticsEngineDatasets.push({
|
|
@@ -648,6 +703,7 @@ function processBindings(
|
|
|
648
703
|
dispatchNamespaces.push({
|
|
649
704
|
binding: bindingName,
|
|
650
705
|
namespace: binding.namespaceName,
|
|
706
|
+
experimental_remote: true,
|
|
651
707
|
});
|
|
652
708
|
} else if (binding.type === "secret_key") {
|
|
653
709
|
// no-op
|
package/src/context.ts
CHANGED
|
@@ -46,6 +46,10 @@ export interface BaseContext<Out extends Resource> {
|
|
|
46
46
|
id: ResourceID;
|
|
47
47
|
fqn: ResourceFQN;
|
|
48
48
|
scope: Scope;
|
|
49
|
+
/**
|
|
50
|
+
* Indicates whether this resource is being created as a replacement for another resource
|
|
51
|
+
*/
|
|
52
|
+
isReplacement: boolean;
|
|
49
53
|
get<T>(key: string): Promise<T | undefined>;
|
|
50
54
|
set<T>(key: string, value: T): Promise<void>;
|
|
51
55
|
delete<T>(key: string): Promise<T | undefined>;
|
|
@@ -88,6 +92,7 @@ export function context<
|
|
|
88
92
|
state,
|
|
89
93
|
replace,
|
|
90
94
|
props,
|
|
95
|
+
isReplacement = false,
|
|
91
96
|
}: {
|
|
92
97
|
scope: Scope;
|
|
93
98
|
phase: "create" | "update" | "delete";
|
|
@@ -98,6 +103,7 @@ export function context<
|
|
|
98
103
|
props: Props;
|
|
99
104
|
state: State<Kind, Props, Out>;
|
|
100
105
|
replace: () => never;
|
|
106
|
+
isReplacement?: boolean;
|
|
101
107
|
}): Context<Out> {
|
|
102
108
|
type InternalSymbols =
|
|
103
109
|
| typeof ResourceID
|
|
@@ -133,6 +139,7 @@ export function context<
|
|
|
133
139
|
output: state.output,
|
|
134
140
|
props,
|
|
135
141
|
replace,
|
|
142
|
+
isReplacement,
|
|
136
143
|
get: (key: string) => state.data[key],
|
|
137
144
|
set: async (key: string, value: any) => {
|
|
138
145
|
state.data[key] = value;
|
package/src/destroy.ts
CHANGED
|
@@ -119,6 +119,7 @@ export async function destroy<Type extends string>(
|
|
|
119
119
|
seq: instance[ResourceSeq],
|
|
120
120
|
props: options?.replace?.props ?? state.props,
|
|
121
121
|
state,
|
|
122
|
+
isReplacement: false,
|
|
122
123
|
replace: () => {
|
|
123
124
|
throw new Error("Cannot replace a resource that is being deleted");
|
|
124
125
|
},
|
package/src/docker/api.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
1
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { exec } from "../os/exec.ts";
|
|
3
6
|
|
|
4
7
|
/**
|
|
@@ -9,6 +12,16 @@ export interface DockerApiOptions {
|
|
|
9
12
|
* Custom path to Docker binary
|
|
10
13
|
*/
|
|
11
14
|
dockerPath?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Optional directory that will be used as the Docker CLI configuration
|
|
18
|
+
* directory (equivalent to setting the DOCKER_CONFIG environment variable).
|
|
19
|
+
*
|
|
20
|
+
* This makes authentication actions like `docker login` operate on an
|
|
21
|
+
* isolated credentials store which avoids race-conditions when multiple
|
|
22
|
+
* processes manipulate the default global config simultaneously.
|
|
23
|
+
*/
|
|
24
|
+
configDir?: string;
|
|
12
25
|
}
|
|
13
26
|
|
|
14
27
|
type VolumeInfo = {
|
|
@@ -27,6 +40,8 @@ type VolumeInfo = {
|
|
|
27
40
|
export class DockerApi {
|
|
28
41
|
/** Path to Docker CLI */
|
|
29
42
|
readonly dockerPath: string;
|
|
43
|
+
/** Directory to use for Docker CLI config */
|
|
44
|
+
readonly configDir?: string;
|
|
30
45
|
|
|
31
46
|
/**
|
|
32
47
|
* Create a new Docker API client
|
|
@@ -35,6 +50,7 @@ export class DockerApi {
|
|
|
35
50
|
*/
|
|
36
51
|
constructor(options: DockerApiOptions = {}) {
|
|
37
52
|
this.dockerPath = options.dockerPath || "docker";
|
|
53
|
+
this.configDir = options.configDir;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
/**
|
|
@@ -44,11 +60,17 @@ export class DockerApi {
|
|
|
44
60
|
* @returns Result of the command
|
|
45
61
|
*/
|
|
46
62
|
async exec(args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
63
|
+
// If a custom config directory is provided, ensure all commands use it by
|
|
64
|
+
// setting the DOCKER_CONFIG env variable for the spawned process.
|
|
65
|
+
const env = this.configDir
|
|
66
|
+
? { ...process.env, DOCKER_CONFIG: this.configDir }
|
|
67
|
+
: process.env;
|
|
68
|
+
|
|
47
69
|
const command = `${this.dockerPath} ${args.join(" ")}`;
|
|
48
70
|
const result = (await exec(command, {
|
|
49
71
|
captureOutput: true,
|
|
50
72
|
shell: true,
|
|
51
|
-
env
|
|
73
|
+
env,
|
|
52
74
|
})) as { stdout: string; stderr: string };
|
|
53
75
|
|
|
54
76
|
return result;
|
|
@@ -377,6 +399,27 @@ export class DockerApi {
|
|
|
377
399
|
username: string,
|
|
378
400
|
password: string,
|
|
379
401
|
): Promise<void> {
|
|
402
|
+
// If we have a custom config directory, write credentials directly to
|
|
403
|
+
// config.json to avoid race conditions with the global credential store
|
|
404
|
+
if (this.configDir) {
|
|
405
|
+
const authConfigPath = path.join(this.configDir, "config.json");
|
|
406
|
+
const authToken = Buffer.from(`${username}:${password}`).toString(
|
|
407
|
+
"base64",
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const configJson = {
|
|
411
|
+
auths: {
|
|
412
|
+
[registry]: {
|
|
413
|
+
auth: authToken,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
await fs.writeFile(authConfigPath, JSON.stringify(configJson));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Fallback to original docker login behavior for backwards compatibility
|
|
380
423
|
return new Promise((resolve, reject) => {
|
|
381
424
|
const args = [
|
|
382
425
|
"login",
|
|
@@ -429,6 +472,19 @@ export class DockerApi {
|
|
|
429
472
|
* @param registry Registry URL
|
|
430
473
|
*/
|
|
431
474
|
async logout(registry: string): Promise<void> {
|
|
475
|
+
// If we have a custom config directory, we can just remove the auth entry
|
|
476
|
+
// or delete the config file entirely since it's isolated
|
|
477
|
+
if (this.configDir) {
|
|
478
|
+
try {
|
|
479
|
+
const authConfigPath = path.join(this.configDir, "config.json");
|
|
480
|
+
await fs.unlink(authConfigPath);
|
|
481
|
+
} catch {
|
|
482
|
+
// Ignore errors - file might not exist or already be deleted
|
|
483
|
+
}
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Fallback to original docker logout behavior
|
|
432
488
|
try {
|
|
433
489
|
await this.exec(["logout", registry]);
|
|
434
490
|
} catch (error) {
|
package/src/docker/image.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import type { Context } from "../context.ts";
|
|
4
5
|
import { Resource } from "../resource.ts";
|
|
@@ -31,7 +32,7 @@ export interface DockerBuildOptions {
|
|
|
31
32
|
/**
|
|
32
33
|
* Build arguments as key-value pairs
|
|
33
34
|
*/
|
|
34
|
-
|
|
35
|
+
args?: Record<string, string>;
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Target build stage in multi-stage builds
|
|
@@ -134,8 +135,16 @@ export const Image = Resource(
|
|
|
134
135
|
id: string,
|
|
135
136
|
props: ImageProps,
|
|
136
137
|
): Promise<Image> {
|
|
137
|
-
//
|
|
138
|
-
|
|
138
|
+
// Create a temporary directory that will act as an isolated Docker config
|
|
139
|
+
// (credentials) directory. This prevents race-conditions when multiple
|
|
140
|
+
// concurrent tests perform `docker login` / `logout` by ensuring each
|
|
141
|
+
// Image operation has its own credential store.
|
|
142
|
+
const tempConfigDir = await fs.mkdtemp(
|
|
143
|
+
path.join(os.tmpdir(), "docker-config-"),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// Initialize Docker API client with the isolated config directory
|
|
147
|
+
const api = new DockerApi({ configDir: tempConfigDir });
|
|
139
148
|
|
|
140
149
|
if (this.phase === "delete") {
|
|
141
150
|
// No action needed for delete as Docker images aren't automatically removed
|
|
@@ -166,7 +175,7 @@ export const Image = Resource(
|
|
|
166
175
|
await fs.access(dockerfile);
|
|
167
176
|
|
|
168
177
|
// Prepare build options
|
|
169
|
-
const buildOptions: Record<string, string> = props.build?.
|
|
178
|
+
const buildOptions: Record<string, string> = props.build?.args || {};
|
|
170
179
|
|
|
171
180
|
// Add platform if specified
|
|
172
181
|
let buildArgs = ["build", "-t", imageRef];
|
|
@@ -223,7 +232,7 @@ export const Image = Resource(
|
|
|
223
232
|
: `${registryHost}/${imageRef}`;
|
|
224
233
|
|
|
225
234
|
try {
|
|
226
|
-
// Authenticate to registry
|
|
235
|
+
// Authenticate to registry using the isolated config directory
|
|
227
236
|
await api.login(registryHost, username, password.unencrypted);
|
|
228
237
|
|
|
229
238
|
// Tag local image with fully qualified name if necessary
|
|
@@ -251,7 +260,7 @@ export const Image = Resource(
|
|
|
251
260
|
// Update the final image reference to point at the pushed image
|
|
252
261
|
finalImageRef = targetImage;
|
|
253
262
|
} finally {
|
|
254
|
-
//
|
|
263
|
+
// Clean up credentials from the isolated config
|
|
255
264
|
await api.logout(registryHost);
|
|
256
265
|
}
|
|
257
266
|
}
|