alchemy 0.61.0 → 0.62.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 (143) hide show
  1. package/README.md +1 -2
  2. package/bin/alchemy.js +1406 -1464
  3. package/bin/commands/init.ts +29 -2
  4. package/bin/services/execute-alchemy.ts +1 -2
  5. package/lib/alchemy.d.ts +2 -2
  6. package/lib/alchemy.d.ts.map +1 -1
  7. package/lib/alchemy.js.map +1 -1
  8. package/lib/aws/client-props.d.ts +149 -0
  9. package/lib/aws/client-props.d.ts.map +1 -0
  10. package/lib/aws/client-props.js +1 -0
  11. package/lib/aws/client-props.js.map +1 -0
  12. package/lib/aws/credentials.d.ts +68 -5
  13. package/lib/aws/credentials.d.ts.map +1 -1
  14. package/lib/aws/credentials.js +148 -0
  15. package/lib/aws/credentials.js.map +1 -1
  16. package/lib/aws/ec2/internet-gateway-attachment.d.ts +57 -3
  17. package/lib/aws/ec2/internet-gateway-attachment.d.ts.map +1 -1
  18. package/lib/aws/ec2/internet-gateway-attachment.js +57 -3
  19. package/lib/aws/ec2/internet-gateway-attachment.js.map +1 -1
  20. package/lib/aws/ec2/internet-gateway.d.ts +68 -40
  21. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -1
  22. package/lib/aws/ec2/internet-gateway.js +68 -40
  23. package/lib/aws/ec2/internet-gateway.js.map +1 -1
  24. package/lib/aws/ec2/nat-gateway.d.ts +99 -18
  25. package/lib/aws/ec2/nat-gateway.d.ts.map +1 -1
  26. package/lib/aws/ec2/nat-gateway.js +113 -37
  27. package/lib/aws/ec2/nat-gateway.js.map +1 -1
  28. package/lib/aws/ec2/route-table-association.d.ts +1 -1
  29. package/lib/aws/ec2/route-table-association.js +1 -1
  30. package/lib/aws/ec2/route-table.d.ts +64 -1
  31. package/lib/aws/ec2/route-table.d.ts.map +1 -1
  32. package/lib/aws/ec2/route-table.js +64 -1
  33. package/lib/aws/ec2/route-table.js.map +1 -1
  34. package/lib/aws/ec2/route.d.ts +60 -1
  35. package/lib/aws/ec2/route.d.ts.map +1 -1
  36. package/lib/aws/ec2/route.js +60 -1
  37. package/lib/aws/ec2/route.js.map +1 -1
  38. package/lib/aws/ec2/security-group.d.ts +96 -3
  39. package/lib/aws/ec2/security-group.d.ts.map +1 -1
  40. package/lib/aws/ec2/security-group.js +96 -3
  41. package/lib/aws/ec2/security-group.js.map +1 -1
  42. package/lib/aws/ec2/subnet.d.ts +61 -3
  43. package/lib/aws/ec2/subnet.d.ts.map +1 -1
  44. package/lib/aws/ec2/subnet.js +61 -3
  45. package/lib/aws/ec2/subnet.js.map +1 -1
  46. package/lib/aws/ec2/utils.d.ts +10 -2
  47. package/lib/aws/ec2/utils.d.ts.map +1 -1
  48. package/lib/aws/ec2/utils.js +68 -7
  49. package/lib/aws/ec2/utils.js.map +1 -1
  50. package/lib/aws/ec2/vpc.d.ts +50 -2
  51. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  52. package/lib/aws/ec2/vpc.js +50 -2
  53. package/lib/aws/ec2/vpc.js.map +1 -1
  54. package/lib/aws/index.d.ts +2 -0
  55. package/lib/aws/index.d.ts.map +1 -1
  56. package/lib/aws/index.js +2 -0
  57. package/lib/aws/index.js.map +1 -1
  58. package/lib/cloudflare/api-response.js +2 -2
  59. package/lib/cloudflare/api-response.js.map +1 -1
  60. package/lib/cloudflare/api.d.ts +19 -0
  61. package/lib/cloudflare/api.d.ts.map +1 -1
  62. package/lib/cloudflare/api.js +4 -0
  63. package/lib/cloudflare/api.js.map +1 -1
  64. package/lib/cloudflare/assets.d.ts.map +1 -1
  65. package/lib/cloudflare/assets.js +3 -4
  66. package/lib/cloudflare/assets.js.map +1 -1
  67. package/lib/cloudflare/bucket.d.ts +5 -3
  68. package/lib/cloudflare/bucket.d.ts.map +1 -1
  69. package/lib/cloudflare/bucket.js +22 -18
  70. package/lib/cloudflare/bucket.js.map +1 -1
  71. package/lib/cloudflare/bundle/plugin-compat-warning.js +7 -7
  72. package/lib/cloudflare/bundle/plugin-compat-warning.js.map +1 -1
  73. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  74. package/lib/cloudflare/kv-namespace.d.ts +2 -2
  75. package/lib/cloudflare/kv-namespace.d.ts.map +1 -1
  76. package/lib/cloudflare/kv-namespace.js.map +1 -1
  77. package/lib/cloudflare/oauth.js +3 -3
  78. package/lib/cloudflare/oauth.js.map +1 -1
  79. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  80. package/lib/cloudflare/website.d.ts.map +1 -1
  81. package/lib/cloudflare/website.js +26 -97
  82. package/lib/cloudflare/website.js.map +1 -1
  83. package/lib/cloudflare/worker-bundle.d.ts.map +1 -1
  84. package/lib/cloudflare/worker-bundle.js +9 -8
  85. package/lib/cloudflare/worker-bundle.js.map +1 -1
  86. package/lib/cloudflare/worker-tail.d.ts.map +1 -1
  87. package/lib/cloudflare/worker-tail.js +6 -6
  88. package/lib/cloudflare/worker-tail.js.map +1 -1
  89. package/lib/resource.d.ts +1 -0
  90. package/lib/resource.d.ts.map +1 -1
  91. package/lib/resource.js +13 -2
  92. package/lib/resource.js.map +1 -1
  93. package/lib/scope.d.ts +24 -23
  94. package/lib/scope.d.ts.map +1 -1
  95. package/lib/scope.js +25 -20
  96. package/lib/scope.js.map +1 -1
  97. package/lib/state/cloudflare-state-store.d.ts.map +1 -1
  98. package/lib/state/cloudflare-state-store.js +2 -1
  99. package/lib/state/cloudflare-state-store.js.map +1 -1
  100. package/lib/util/idempotent-spawn.d.ts +10 -8
  101. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  102. package/lib/util/idempotent-spawn.js +97 -78
  103. package/lib/util/idempotent-spawn.js.map +1 -1
  104. package/lib/util/index.d.ts +2 -0
  105. package/lib/util/index.d.ts.map +1 -0
  106. package/lib/util/index.js +2 -0
  107. package/lib/util/index.js.map +1 -0
  108. package/lib/util/safe-fetch.d.ts +24 -0
  109. package/lib/util/safe-fetch.d.ts.map +1 -1
  110. package/lib/util/safe-fetch.js +72 -0
  111. package/lib/util/safe-fetch.js.map +1 -1
  112. package/package.json +8 -5
  113. package/src/alchemy.ts +2 -2
  114. package/src/aws/client-props.ts +156 -0
  115. package/src/aws/credentials.ts +165 -4
  116. package/src/aws/ec2/internet-gateway-attachment.ts +59 -4
  117. package/src/aws/ec2/internet-gateway.ts +70 -41
  118. package/src/aws/ec2/nat-gateway.ts +123 -33
  119. package/src/aws/ec2/route-table-association.ts +1 -1
  120. package/src/aws/ec2/route-table.ts +66 -2
  121. package/src/aws/ec2/route.ts +62 -2
  122. package/src/aws/ec2/security-group.ts +98 -4
  123. package/src/aws/ec2/subnet.ts +63 -4
  124. package/src/aws/ec2/utils.ts +73 -7
  125. package/src/aws/ec2/vpc.ts +52 -3
  126. package/src/aws/index.ts +2 -0
  127. package/src/cloudflare/api-response.ts +2 -2
  128. package/src/cloudflare/api.ts +23 -0
  129. package/src/cloudflare/assets.ts +3 -4
  130. package/src/cloudflare/bucket.ts +27 -25
  131. package/src/cloudflare/bundle/plugin-compat-warning.ts +7 -7
  132. package/src/cloudflare/cloudflare-env-proxy.ts +2 -2
  133. package/src/cloudflare/kv-namespace.ts +26 -27
  134. package/src/cloudflare/oauth.ts +3 -3
  135. package/src/cloudflare/website.ts +30 -111
  136. package/src/cloudflare/worker-bundle.ts +9 -8
  137. package/src/cloudflare/worker-tail.ts +6 -8
  138. package/src/resource.ts +19 -2
  139. package/src/scope.ts +73 -43
  140. package/src/state/cloudflare-state-store.ts +2 -1
  141. package/src/util/idempotent-spawn.ts +117 -100
  142. package/src/util/index.ts +1 -0
  143. package/src/util/safe-fetch.ts +114 -0
@@ -1,15 +1,11 @@
1
1
  import assert from "node:assert";
2
- import { spawn } from "node:child_process";
3
- import { once } from "node:events";
4
2
  import fs from "node:fs/promises";
5
3
  import path from "node:path";
6
4
  import { alchemy } from "../alchemy.ts";
7
5
  import { Exec } from "../os/index.ts";
8
6
  import { Scope } from "../scope.ts";
9
- import { isSecret, type Secret } from "../secret.ts";
7
+ import { isSecret } from "../secret.ts";
10
8
  import { dedent } from "../util/dedent.ts";
11
- import { DeferredPromise } from "../util/deferred-promise.ts";
12
- import { exists } from "../util/exists.ts";
13
9
  import { logger } from "../util/logger.ts";
14
10
  import { Assets } from "./assets.ts";
15
11
  import type { Bindings } from "./bindings.ts";
@@ -304,14 +300,38 @@ export async function Website<B extends Bindings>(
304
300
 
305
301
  let url: string | undefined;
306
302
  if (dev && scope.local) {
307
- url = await runDevCommand(scope, {
308
- id,
309
- command: typeof dev === "string" ? dev : dev.command,
303
+ url = await scope.spawn(name, {
304
+ cmd: typeof dev === "string" ? dev : dev.command,
305
+ cwd: paths.cwd,
306
+ extract: (line) => {
307
+ const URL_REGEX =
308
+ /http:\/\/(localhost|0\.0\.0\.0|127\.0\.0\.1|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):\d+\/?/;
309
+ const match = line
310
+ .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
311
+ .match(URL_REGEX);
312
+ if (match) {
313
+ return match[0];
314
+ }
315
+ },
310
316
  env: {
311
- ...env,
317
+ ...Object.fromEntries(
318
+ Object.entries(env ?? {}).flatMap(([key, value]) => {
319
+ if (isSecret(value)) {
320
+ return [[key, value.unencrypted]];
321
+ }
322
+ if (typeof value === "string") {
323
+ return [[key, value]];
324
+ }
325
+ return [];
326
+ }),
327
+ ),
312
328
  ...(typeof dev === "object" ? dev.env : {}),
329
+ FORCE_COLOR: "1",
330
+ ...process.env,
331
+ // NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
332
+ // which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
333
+ NODE_ENV: "development",
313
334
  },
314
- cwd: paths.cwd,
315
335
  });
316
336
  }
317
337
 
@@ -348,104 +368,3 @@ async function writeMiniflareSymlink(cwd: string) {
348
368
  }
349
369
  });
350
370
  }
351
-
352
- async function runDevCommand(
353
- scope: Scope,
354
- props: {
355
- id: string;
356
- command: string;
357
- env: Record<string, string | Secret<string> | undefined>;
358
- cwd?: string;
359
- },
360
- ) {
361
- const persistFile = path.join(process.cwd(), ".alchemy", `${props.id}.pid`);
362
- if (await exists(persistFile)) {
363
- const pid = Number.parseInt(await fs.readFile(persistFile, "utf8"));
364
- try {
365
- // Actually kill the process if it's alive
366
- process.kill(pid, "SIGTERM");
367
- } catch {
368
- // ignore
369
- }
370
- try {
371
- await fs.unlink(persistFile);
372
- } catch {
373
- // ignore
374
- }
375
- }
376
- const command = props.command.split(" ");
377
- const [cmd, ...args] = command;
378
-
379
- const childProcess = spawn(cmd, args, {
380
- cwd: props.cwd,
381
- shell: true,
382
- env: {
383
- FORCE_COLOR: "1",
384
- ...process.env,
385
- ...Object.fromEntries(
386
- Object.entries(props.env ?? {}).flatMap(([key, value]) => {
387
- if (isSecret(value)) {
388
- return [[key, value.unencrypted]];
389
- }
390
- if (typeof value === "string") {
391
- return [[key, value]];
392
- }
393
- return [];
394
- }),
395
- ),
396
- // NOTE: we must set this to ensure the user does not accidentally set `NODE_ENV=production`
397
- // which breaks `vite dev` (it won't, for example, re-write `process.env.TSS_APP_BASE` in the `.js` client side bundle)
398
- NODE_ENV: "development",
399
- },
400
- stdio: "pipe",
401
- });
402
- await once(childProcess, "spawn");
403
- scope.onCleanup(async () => {
404
- if (childProcess.exitCode === null && !childProcess.killed) {
405
- childProcess.kill("SIGTERM");
406
- await Promise.any([
407
- once(childProcess, "exit"),
408
- new Promise((resolve) => setTimeout(resolve, 5000)),
409
- ]);
410
- if (!childProcess.killed) {
411
- childProcess.kill("SIGKILL");
412
- }
413
- }
414
- try {
415
- await fs.unlink(persistFile);
416
- } catch {
417
- // ignore
418
- }
419
- });
420
- if (childProcess.pid) {
421
- await fs.mkdir(path.dirname(persistFile), { recursive: true });
422
- await fs.writeFile(persistFile, childProcess.pid.toString());
423
- }
424
- const URL_REGEX =
425
- /http:\/\/(?:(?:localhost|0\.0\.0\.0|127\.0\.0\.1)|(?:\d{1,3}\.){3}\d{1,3}):\d+(?:\/)?/;
426
- const promise = new DeferredPromise<string>();
427
- childProcess.stdout.on("data", (data) => {
428
- if (promise.status === "pending") {
429
- const match = data
430
- .toString()
431
- .replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")
432
- .match(URL_REGEX);
433
- if (match) {
434
- promise.resolve(match[0]);
435
- }
436
- }
437
- process.stdout.write(data);
438
- });
439
- childProcess.stderr.on("data", (data) => {
440
- process.stderr.write(data);
441
- });
442
-
443
- return await Promise.race([
444
- promise.value,
445
- once(childProcess, "exit").then(([code, signal]) => {
446
- throw new Error(
447
- `Dev command "${props.command}" for "${props.id}" failed to start (code: ${code}, signal: ${signal})`,
448
- );
449
- }),
450
- ]);
451
- }
@@ -93,21 +93,22 @@ export namespace WorkerBundle {
93
93
  paths: string[],
94
94
  format: "esm" | "cjs",
95
95
  ): WorkerBundle.Module[] => {
96
- return paths.map((path) => {
97
- const ext = path.split(".").pop();
96
+ return paths.map((filePath) => {
97
+ const normalizedPath = path.normalize(filePath);
98
+ const ext = normalizedPath.split(".").pop();
98
99
  switch (ext) {
99
100
  case "js":
100
- return { type: format, path };
101
+ return { type: format, path: normalizedPath };
101
102
  case "mjs":
102
- return { type: "esm", path };
103
+ return { type: "esm", path: normalizedPath };
103
104
  case "cjs":
104
- return { type: "cjs", path };
105
+ return { type: "cjs", path: normalizedPath };
105
106
  case "wasm":
106
- return { type: "wasm", path };
107
+ return { type: "wasm", path: normalizedPath };
107
108
  case "map":
108
- return { type: "sourcemap", path };
109
+ return { type: "sourcemap", path: normalizedPath };
109
110
  default:
110
- return { type: "text", path };
111
+ return { type: "text", path: normalizedPath };
111
112
  }
112
113
  });
113
114
  };
@@ -1,4 +1,4 @@
1
- import kleur from "kleur";
1
+ import pc from "picocolors";
2
2
  import { WebSocket } from "ws";
3
3
  import { logger } from "../util/logger.ts";
4
4
  import type { CloudflareApiResponse } from "./api-response.ts";
@@ -102,7 +102,7 @@ class TailClient {
102
102
  });
103
103
  ws.on("message", (message) => {
104
104
  const data: TailEventMessage = JSON.parse(message.toString());
105
- const prefix = kleur.blue(`[${this.id}]`);
105
+ const prefix = pc.blue(`[${this.id}]`);
106
106
  if (data.event && "request" in data.event) {
107
107
  // TODO: handle other event types
108
108
  const event = data.event;
@@ -110,18 +110,16 @@ class TailClient {
110
110
  const status = event.response?.status ?? 500;
111
111
  // TODO: make this look nicer
112
112
  logger.log(
113
- `${prefix} ${kleur.gray(event.request.method)} ${url.pathname} ${kleur.dim(">")} ${status >= 200 && status < 300 ? kleur.green(status) : kleur.red(status)} ${kleur.gray(`(cpu: ${Math.round(data.cpuTime)}ms, wall: ${Math.round(data.wallTime)}ms)`)}`,
113
+ `${prefix} ${pc.gray(event.request.method)} ${url.pathname} ${pc.dim(">")} ${status >= 200 && status < 300 ? pc.green(status) : pc.red(status)} ${pc.gray(`(cpu: ${Math.round(data.cpuTime)}ms, wall: ${Math.round(data.wallTime)}ms)`)}`,
114
114
  );
115
115
  }
116
116
  for (const log of data.logs) {
117
- logger.log(
118
- `${prefix} ${kleur.gray(log.level)} ${log.message.join(" ")}`,
119
- );
117
+ logger.log(`${prefix} ${pc.gray(log.level)} ${log.message.join(" ")}`);
120
118
  }
121
119
  for (const exception of data.exceptions) {
122
- const start = `${prefix} ${kleur.red(exception.name)}`;
120
+ const start = `${prefix} ${pc.red(exception.name)}`;
123
121
  logger.log(
124
- `${start} ${exception.message}\n${kleur.gray(exception.stack)}`,
122
+ `${start} ${exception.message}\n${pc.gray(exception.stack)}`,
125
123
  );
126
124
  }
127
125
  });
package/src/resource.ts CHANGED
@@ -5,6 +5,7 @@ import { Scope as _Scope, type Scope } from "./scope.ts";
5
5
 
6
6
  declare global {
7
7
  var ALCHEMY_PROVIDERS: Map<ResourceKind, Provider<string, any>>;
8
+ var ALCHEMY_HANDLERS: Map<ResourceKind, ResourceLifecycleHandler>;
8
9
  var ALCHEMY_DYNAMIC_RESOURCE_RESOLVERS: DynamicResourceResolver[];
9
10
  }
10
11
 
@@ -15,6 +16,12 @@ export const PROVIDERS: Map<
15
16
  ResourceKind,
16
17
  Provider<string, any>
17
18
  >());
19
+ const HANDLERS: Map<ResourceKind, ResourceLifecycleHandler> =
20
+ (globalThis.ALCHEMY_HANDLERS ??= new Map<
21
+ ResourceKind,
22
+ ResourceLifecycleHandler
23
+ >());
24
+
18
25
  const DYNAMIC_RESOURCE_RESOLVERS: DynamicResourceResolver[] =
19
26
  (globalThis.ALCHEMY_DYNAMIC_RESOURCE_RESOLVERS ??= []);
20
27
 
@@ -132,10 +139,20 @@ export function Resource<
132
139
  const Type extends ResourceKind,
133
140
  F extends ResourceLifecycleHandler,
134
141
  >(type: Type, ...args: [Partial<ProviderOptions>, F] | [F]): Handler<F> {
142
+ const [options, handler] = args.length === 2 ? args : [undefined, args[0]];
135
143
  if (PROVIDERS.has(type)) {
136
- throw new Error(`Resource ${type} already exists`);
144
+ // We want Alchemy to work in a PNPM monorepo environment unfortunately,
145
+ // global registries do not work because PNPM's symlinks result in multiple
146
+ // instances of alchemy being loaded even if the package version is the
147
+ // same (peers do not fix this).
148
+ // MITIGATION: to ensure that most cases of accidental double-registration
149
+ // are caught, we're going to check the handler's toString() to see if it's
150
+ // the same as the previous handler. if it is, we're going to overwrite it.
151
+ if (HANDLERS.get(type)!.toString() !== handler.toString()) {
152
+ throw new Error(`Resource ${type} already exists`);
153
+ }
137
154
  }
138
- const [options, handler] = args.length === 2 ? args : [undefined, args[0]];
155
+ HANDLERS.set(type, handler);
139
156
 
140
157
  type Out = Awaited<ReturnType<F>>;
141
158
 
package/src/scope.ts CHANGED
@@ -1,7 +1,7 @@
1
- import kleur from "kleur";
2
1
  import { AsyncLocalStorage } from "node:async_hooks";
3
2
  import path from "node:path";
4
3
  import util from "node:util";
4
+ import pc from "picocolors";
5
5
  import type { Phase } from "./alchemy.ts";
6
6
  import { destroy, destroyAll, DestroyStrategy } from "./destroy.ts";
7
7
  import {
@@ -23,7 +23,10 @@ import {
23
23
  createLoggerInstance,
24
24
  type LoggerApi,
25
25
  } from "./util/cli.ts";
26
- import { idempotentSpawn } from "./util/idempotent-spawn.ts";
26
+ import {
27
+ idempotentSpawn,
28
+ type IdempotentSpawnOptions,
29
+ } from "./util/idempotent-spawn.ts";
27
30
  import { logger } from "./util/logger.ts";
28
31
  import { AsyncMutex } from "./util/mutex.ts";
29
32
  import type { ITelemetryClient } from "./util/telemetry/client.ts";
@@ -34,7 +37,7 @@ export class RootScopeStateAttemptError extends Error {
34
37
  }
35
38
  }
36
39
 
37
- export interface ScopeOptions {
40
+ export interface ScopeOptions extends ProviderCredentials {
38
41
  stage?: string;
39
42
  parent: Scope | undefined;
40
43
  scopeName: string;
@@ -83,6 +86,29 @@ export interface ScopeOptions {
83
86
  dotAlchemy?: string;
84
87
  }
85
88
 
89
+ /**
90
+ * Base interface for provider credentials that can be extended by each provider.
91
+ * This allows providers to add their own credential properties without modifying the core scope interface.
92
+ *
93
+ * Provider credentials cannot conflict with core ScopeOptions properties.
94
+ *
95
+ * Providers can extend this interface using module augmentation:
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // In aws/scope-extensions.ts
100
+ * declare module "../scope.ts" {
101
+ * interface ProviderCredentials {
102
+ * aws?: AwsClientProps;
103
+ * }
104
+ * }
105
+ * ```
106
+ */
107
+ export interface ProviderCredentials extends Record<string, unknown> {
108
+ // Provider credentials should not conflict with core scope properties
109
+ // TypeScript will enforce this at compile time when providers extend this interface
110
+ }
111
+
86
112
  export type PendingDeletions = Array<{
87
113
  resource: Resource<string>;
88
114
  oldProps?: ResourceProps;
@@ -155,6 +181,9 @@ export class Scope {
155
181
  public readonly telemetryClient: ITelemetryClient;
156
182
  public readonly dataMutex: AsyncMutex;
157
183
 
184
+ // Provider credentials for scope-level credential overrides
185
+ public readonly providerCredentials: ProviderCredentials;
186
+
158
187
  private isErrored = false;
159
188
  private isSkipped = false;
160
189
  private finalized = false;
@@ -172,9 +201,31 @@ export class Scope {
172
201
  public readonly dotAlchemy: string;
173
202
 
174
203
  constructor(options: ScopeOptions) {
175
- this.scopeName = options.scopeName;
204
+ // Extract core scope options first
205
+ const {
206
+ scopeName,
207
+ parent,
208
+ stage,
209
+ password,
210
+ stateStore,
211
+ quiet,
212
+ phase,
213
+ local,
214
+ watch,
215
+ force,
216
+ destroyStrategy,
217
+ telemetryClient,
218
+ logger,
219
+ dotAlchemy,
220
+ ...providerCredentials
221
+ } = options;
222
+
223
+ this.scopeName = scopeName;
176
224
  this.name = this.scopeName;
177
- this.parent = options.parent ?? Scope.getScope();
225
+ this.parent = parent ?? Scope.getScope();
226
+
227
+ // Store provider credentials (TypeScript ensures no conflicts with core options)
228
+ this.providerCredentials = providerCredentials as ProviderCredentials;
178
229
 
179
230
  const isChild = this.parent !== undefined;
180
231
  if (this.scopeName?.includes(":") && isChild) {
@@ -188,18 +239,18 @@ export class Scope {
188
239
  this.parent?.dotAlchemy ??
189
240
  path.join(process.cwd(), ".alchemy");
190
241
 
191
- this.stage = options?.stage ?? this.parent?.stage ?? DEFAULT_STAGE;
242
+ this.stage = stage ?? this.parent?.stage ?? DEFAULT_STAGE;
192
243
  this.parent?.children.set(this.scopeName!, this);
193
- this.quiet = options.quiet ?? this.parent?.quiet ?? false;
244
+ this.quiet = quiet ?? this.parent?.quiet ?? false;
194
245
  if (this.parent && !this.scopeName) {
195
246
  throw new Error("Scope name is required when creating a child scope");
196
247
  }
197
- this.password = options.password ?? this.parent?.password;
198
- const phase = options.phase ?? this.parent?.phase;
199
- if (phase === undefined) {
248
+ this.password = password ?? this.parent?.password;
249
+ const resolvedPhase = phase ?? this.parent?.phase;
250
+ if (resolvedPhase === undefined) {
200
251
  throw new Error("Phase is required");
201
252
  }
202
- this.phase = phase;
253
+ this.phase = resolvedPhase;
203
254
 
204
255
  this.logger = this.quiet
205
256
  ? createDummyLogger()
@@ -209,14 +260,14 @@ export class Scope {
209
260
  stage: this.stage,
210
261
  appName: this.appName ?? "",
211
262
  },
212
- options.logger,
263
+ logger,
213
264
  );
214
265
 
215
- this.local = options.local ?? this.parent?.local ?? false;
216
- this.watch = options.watch ?? this.parent?.watch ?? false;
217
- this.force = options.force ?? this.parent?.force ?? false;
266
+ this.local = local ?? this.parent?.local ?? false;
267
+ this.watch = watch ?? this.parent?.watch ?? false;
268
+ this.force = force ?? this.parent?.force ?? false;
218
269
  this.destroyStrategy =
219
- options.destroyStrategy ?? this.parent?.destroyStrategy ?? "sequential";
270
+ destroyStrategy ?? this.parent?.destroyStrategy ?? "sequential";
220
271
  if (this.local) {
221
272
  this.logger.warnOnce(
222
273
  "Development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
@@ -224,14 +275,13 @@ export class Scope {
224
275
  }
225
276
 
226
277
  this.stateStore =
227
- options.stateStore ?? this.parent?.stateStore ?? defaultStateStore;
228
- this.telemetryClient =
229
- options.telemetryClient ?? this.parent?.telemetryClient!;
278
+ stateStore ?? this.parent?.stateStore ?? defaultStateStore;
279
+ this.telemetryClient = telemetryClient ?? this.parent?.telemetryClient!;
230
280
  this.state = new InstrumentedStateStore(
231
281
  this.stateStore(this),
232
282
  this.telemetryClient,
233
283
  );
234
- if (!options.telemetryClient && !this.parent?.telemetryClient) {
284
+ if (!telemetryClient && !this.parent?.telemetryClient) {
235
285
  throw new Error("Telemetry client is required");
236
286
  }
237
287
  this.dataMutex = new AsyncMutex();
@@ -242,28 +292,7 @@ export class Scope {
242
292
  >(
243
293
  // TODO(sam): validate uniqueness? Ensure a flat .logs/${id}.log dir? Or nest in scope dirs?
244
294
  id: string,
245
- options: {
246
- /**
247
- * The command to run (e.g. `cloudflared tunnel --url http://localhost:8080`).
248
- */
249
- cmd: string;
250
- /**
251
- * Function executed on each line of the process's output (stdout and stderr) to extract a value.
252
- */
253
- extract?: E;
254
- /**
255
- * Name of the process - used to check if a PID is a cloudflared process when the parent exits, for resumability
256
- */
257
- processName?: string;
258
- /**
259
- * Function to check if a PID is the same process as the one that was spawned.
260
- *
261
- * Used to check if a PID is a cloudflared process when the parent exits, for resumability.
262
- *
263
- * One of {@link extract} or {@link processName} must be provided.
264
- */
265
- isSameProcess?: (pid: number) => Promise<boolean>;
266
- },
295
+ options: Omit<IdempotentSpawnOptions, "log" | "stateFile">,
267
296
  ) {
268
297
  const dotAlchemy = path.join(process.cwd(), ".alchemy");
269
298
  const logsDir = path.join(dotAlchemy, "logs");
@@ -480,6 +509,7 @@ export class Scope {
480
509
  if (!this.isErrored && !this.isSkipped) {
481
510
  // TODO: need to detect if it is in error
482
511
  const resourceIds = await this.state.list();
512
+
483
513
  const aliveIds = new Set(this.resources.keys());
484
514
  const orphanIds = Array.from(
485
515
  resourceIds.filter((id) => !aliveIds.has(id)),
@@ -607,7 +637,7 @@ export class Scope {
607
637
  */
608
638
  public async cleanup() {
609
639
  if (this.parent || this.cleanups.length === 0) return;
610
- this.logger.log(kleur.gray("Exiting..."));
640
+ this.logger.log(pc.gray("Exiting..."));
611
641
  await Promise.allSettled(this.cleanups.map((cleanup) => cleanup()));
612
642
  }
613
643
 
@@ -15,6 +15,7 @@ import type { Scope } from "../scope.ts";
15
15
  import type { Secret } from "../secret.ts";
16
16
  import { logger } from "../util/logger.ts";
17
17
  import { memoize } from "../util/memoize.ts";
18
+ import { safeFetch } from "../util/safe-fetch.ts";
18
19
  import { StateStoreProxy } from "./proxy.ts";
19
20
 
20
21
  export interface CloudflareStateStoreOptions extends CloudflareApiOptions {
@@ -70,7 +71,7 @@ export class CloudflareStateStore extends StateStoreProxy {
70
71
  params,
71
72
  context: { chain: this.scope.chain },
72
73
  };
73
- const response = await fetch(url, {
74
+ const response = await safeFetch(url, {
74
75
  method: "POST",
75
76
  headers: {
76
77
  Authorization: `Bearer ${token}`,