alchemy 0.70.2 → 0.71.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.
Files changed (131) hide show
  1. package/README.md +1 -1
  2. package/bin/alchemy.js +131 -58
  3. package/bin/commands/init.ts +0 -10
  4. package/bin/services/execute-alchemy.ts +12 -3
  5. package/bin/trpc.ts +12 -12
  6. package/lib/alchemy.d.ts.map +1 -1
  7. package/lib/alchemy.js +21 -6
  8. package/lib/alchemy.js.map +1 -1
  9. package/lib/apply.d.ts.map +1 -1
  10. package/lib/apply.js +3 -1
  11. package/lib/apply.js.map +1 -1
  12. package/lib/cloudflare/bindings.d.ts +12 -2
  13. package/lib/cloudflare/bindings.d.ts.map +1 -1
  14. package/lib/cloudflare/bindings.js.map +1 -1
  15. package/lib/cloudflare/bound.d.ts +3 -2
  16. package/lib/cloudflare/bound.d.ts.map +1 -1
  17. package/lib/cloudflare/bucket.d.ts +6 -0
  18. package/lib/cloudflare/bucket.d.ts.map +1 -1
  19. package/lib/cloudflare/bucket.js +5 -3
  20. package/lib/cloudflare/bucket.js.map +1 -1
  21. package/lib/cloudflare/bun-spa/bun-spa.d.ts +6 -1
  22. package/lib/cloudflare/bun-spa/bun-spa.d.ts.map +1 -1
  23. package/lib/cloudflare/bun-spa/bun-spa.js +29 -9
  24. package/lib/cloudflare/bun-spa/bun-spa.js.map +1 -1
  25. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  26. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  27. package/lib/cloudflare/d1-database.d.ts +1 -1
  28. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  29. package/lib/cloudflare/d1-database.js +1 -1
  30. package/lib/cloudflare/d1-database.js.map +1 -1
  31. package/lib/cloudflare/health-check.d.ts +261 -0
  32. package/lib/cloudflare/health-check.d.ts.map +1 -0
  33. package/lib/cloudflare/health-check.js +277 -0
  34. package/lib/cloudflare/health-check.js.map +1 -0
  35. package/lib/cloudflare/index.d.ts +3 -0
  36. package/lib/cloudflare/index.d.ts.map +1 -1
  37. package/lib/cloudflare/index.js +3 -0
  38. package/lib/cloudflare/index.js.map +1 -1
  39. package/lib/cloudflare/logpush-job.d.ts +349 -0
  40. package/lib/cloudflare/logpush-job.d.ts.map +1 -0
  41. package/lib/cloudflare/logpush-job.js +277 -0
  42. package/lib/cloudflare/logpush-job.js.map +1 -0
  43. package/lib/cloudflare/miniflare/build-worker-options.d.ts +1 -0
  44. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  45. package/lib/cloudflare/miniflare/build-worker-options.js +16 -6
  46. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  47. package/lib/cloudflare/miniflare/remote-binding-proxy.d.ts.map +1 -1
  48. package/lib/cloudflare/miniflare/remote-binding-proxy.js +2 -0
  49. package/lib/cloudflare/miniflare/remote-binding-proxy.js.map +1 -1
  50. package/lib/cloudflare/queue-consumer.js +2 -2
  51. package/lib/cloudflare/queue-consumer.js.map +1 -1
  52. package/lib/cloudflare/worker-loader.d.ts +9 -0
  53. package/lib/cloudflare/worker-loader.d.ts.map +1 -0
  54. package/lib/cloudflare/worker-loader.js +6 -0
  55. package/lib/cloudflare/worker-loader.js.map +1 -0
  56. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  57. package/lib/cloudflare/worker-metadata.js +6 -0
  58. package/lib/cloudflare/worker-metadata.js.map +1 -1
  59. package/lib/cloudflare/worker-subdomain.js +1 -1
  60. package/lib/cloudflare/worker.d.ts.map +1 -1
  61. package/lib/cloudflare/worker.js +7 -0
  62. package/lib/cloudflare/worker.js.map +1 -1
  63. package/lib/cloudflare/wrangler.json.d.ts +11 -8
  64. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  65. package/lib/cloudflare/wrangler.json.js +18 -11
  66. package/lib/cloudflare/wrangler.json.js.map +1 -1
  67. package/lib/cloudflare/zone.js +1 -1
  68. package/lib/destroy.d.ts.map +1 -1
  69. package/lib/destroy.js +27 -0
  70. package/lib/destroy.js.map +1 -1
  71. package/lib/docker/image.js +1 -1
  72. package/lib/docker/image.js.map +1 -1
  73. package/lib/github/comment.d.ts.map +1 -1
  74. package/lib/github/comment.js +14 -7
  75. package/lib/github/comment.js.map +1 -1
  76. package/lib/resource.js +1 -1
  77. package/lib/resource.js.map +1 -1
  78. package/lib/scope.d.ts +6 -1
  79. package/lib/scope.d.ts.map +1 -1
  80. package/lib/scope.js +22 -22
  81. package/lib/scope.js.map +1 -1
  82. package/lib/state/instrumented-state-store.js +1 -1
  83. package/lib/state/instrumented-state-store.js.map +1 -1
  84. package/lib/util/cli-args.d.ts +3 -0
  85. package/lib/util/cli-args.d.ts.map +1 -0
  86. package/lib/util/cli-args.js +6 -0
  87. package/lib/util/cli-args.js.map +1 -0
  88. package/lib/util/retry.d.ts +1 -1
  89. package/lib/util/retry.d.ts.map +1 -1
  90. package/lib/util/retry.js +2 -2
  91. package/lib/util/retry.js.map +1 -1
  92. package/lib/util/telemetry.d.ts +1 -0
  93. package/lib/util/telemetry.d.ts.map +1 -1
  94. package/lib/util/telemetry.js +47 -2
  95. package/lib/util/telemetry.js.map +1 -1
  96. package/lib/util/validate-resource-id.d.ts +2 -0
  97. package/lib/util/validate-resource-id.d.ts.map +1 -0
  98. package/lib/util/validate-resource-id.js +11 -0
  99. package/lib/util/validate-resource-id.js.map +1 -0
  100. package/package.json +11 -9
  101. package/src/alchemy.ts +25 -11
  102. package/src/apply.ts +3 -1
  103. package/src/cloudflare/bindings.ts +13 -0
  104. package/src/cloudflare/bound.ts +36 -32
  105. package/src/cloudflare/bucket.ts +13 -4
  106. package/src/cloudflare/bun-spa/bun-spa.ts +42 -10
  107. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  108. package/src/cloudflare/d1-database.ts +2 -2
  109. package/src/cloudflare/health-check.ts +617 -0
  110. package/src/cloudflare/index.ts +3 -0
  111. package/src/cloudflare/logpush-job.ts +768 -0
  112. package/src/cloudflare/miniflare/build-worker-options.ts +18 -6
  113. package/src/cloudflare/miniflare/remote-binding-proxy.ts +2 -0
  114. package/src/cloudflare/queue-consumer.ts +2 -2
  115. package/src/cloudflare/worker-loader.ts +12 -0
  116. package/src/cloudflare/worker-metadata.ts +5 -0
  117. package/src/cloudflare/worker-subdomain.ts +1 -1
  118. package/src/cloudflare/worker.ts +8 -0
  119. package/src/cloudflare/wrangler.json.ts +36 -23
  120. package/src/cloudflare/zone.ts +1 -1
  121. package/src/destroy.ts +32 -0
  122. package/src/docker/image.ts +1 -1
  123. package/src/github/comment.ts +17 -8
  124. package/src/resource.ts +1 -1
  125. package/src/scope.ts +32 -25
  126. package/src/state/instrumented-state-store.ts +1 -1
  127. package/src/util/cli-args.ts +11 -0
  128. package/src/util/retry.ts +2 -1
  129. package/src/util/telemetry.ts +68 -2
  130. package/src/util/validate-resource-id.ts +10 -0
  131. package/workers/tunnel-proxy.js +1 -1
@@ -1,4 +1,5 @@
1
1
  import * as miniflare from "miniflare";
2
+ import path from "pathe";
2
3
  import { assertNever } from "../../util/assert-never.ts";
3
4
  import type { HTTPServer } from "../../util/http.ts";
4
5
  import { logger } from "../../util/logger.ts";
@@ -26,6 +27,7 @@ export interface MiniflareWorkerInput {
26
27
  bundle: WorkerBundleSource;
27
28
  port: number | undefined;
28
29
  tunnel: boolean | undefined;
30
+ cwd: string;
29
31
  }
30
32
 
31
33
  type RemoteOnlyBindingType =
@@ -78,11 +80,6 @@ export const buildWorkerOptions = async (
78
80
  proxy: true,
79
81
  },
80
82
  ],
81
- containerEngine: {
82
- localDocker: {
83
- socketPath: await getDockerSocketPath(),
84
- },
85
- },
86
83
  // This exposes the worker as a route that can be accessed by setting the MF-Route-Override header.
87
84
  routes: [input.name],
88
85
  };
@@ -119,7 +116,7 @@ export const buildWorkerOptions = async (
119
116
  case "assets": {
120
117
  options.assets = {
121
118
  binding: key,
122
- directory: binding.path,
119
+ directory: path.resolve(input.cwd, binding.path),
123
120
  assetConfig: {
124
121
  html_handling: input.assets?.html_handling,
125
122
  not_found_handling: input.assets?.not_found_handling,
@@ -144,6 +141,11 @@ export const buildWorkerOptions = async (
144
141
  imageName: binding.image.imageRef,
145
142
  },
146
143
  };
144
+ options.containerEngine = {
145
+ localDocker: {
146
+ socketPath: await getDockerSocketPath(),
147
+ },
148
+ };
147
149
  break;
148
150
  }
149
151
  case "d1": {
@@ -268,6 +270,10 @@ export const buildWorkerOptions = async (
268
270
  type: "r2_bucket",
269
271
  name: key,
270
272
  bucket_name: binding.name,
273
+ jurisdiction:
274
+ binding.jurisdiction === "default"
275
+ ? undefined
276
+ : binding.jurisdiction,
271
277
  raw: true,
272
278
  });
273
279
  } else {
@@ -308,6 +314,10 @@ export const buildWorkerOptions = async (
308
314
  };
309
315
  break;
310
316
  }
317
+ case "worker_loader": {
318
+ (options.workerLoaders ??= {})[key] = {};
319
+ break;
320
+ }
311
321
  case "workflow": {
312
322
  (options.workflows ??= {})[key] = {
313
323
  name: binding.workflowName,
@@ -329,11 +339,13 @@ export const buildWorkerOptions = async (
329
339
  );
330
340
  }
331
341
  if (isQueueEventSource(eventSource)) {
342
+ const dlq = eventSource.settings?.deadLetterQueue;
332
343
  (options.queueConsumers ??= {})[queue.name] = {
333
344
  maxBatchSize: eventSource.settings?.batchSize,
334
345
  maxBatchTimeout: eventSource.settings?.maxWaitTimeMs,
335
346
  maxRetries: eventSource.settings?.maxRetries,
336
347
  retryDelay: eventSource.settings?.retryDelay,
348
+ deadLetterQueue: typeof dlq === "string" ? dlq : dlq?.name,
337
349
  };
338
350
  } else {
339
351
  (options.queueConsumers ??= {})[eventSource.name] = {};
@@ -82,6 +82,8 @@ export async function createRemoteProxyWorker(input: {
82
82
  headers,
83
83
  body: req.body,
84
84
  redirect: "manual",
85
+ // @ts-expect-error - caused by @cloudflare/workers-types
86
+ duplex: req.body ? "half" : undefined,
85
87
  });
86
88
 
87
89
  const responseHeaders = new Headers(res.headers);
@@ -318,7 +318,7 @@ export async function createQueueConsumer(
318
318
  typeof props.settings.deadLetterQueue === "string"
319
319
  ? props.settings.deadLetterQueue
320
320
  : props.settings.deadLetterQueue.name;
321
- createPayload.settings.dead_letter_queue = dlqName;
321
+ createPayload.dead_letter_queue = dlqName;
322
322
  }
323
323
  }
324
324
 
@@ -405,7 +405,7 @@ async function updateQueueConsumer(
405
405
  typeof props.settings.deadLetterQueue === "string"
406
406
  ? props.settings.deadLetterQueue
407
407
  : props.settings.deadLetterQueue.name;
408
- updatePayload.settings.dead_letter_queue = dlqName;
408
+ updatePayload.dead_letter_queue = dlqName;
409
409
  }
410
410
  }
411
411
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * A binding for dynamic worker loaders.
3
+ *
4
+ */
5
+ export type WorkerLoader = {
6
+ type: "worker_loader";
7
+ };
8
+ export function WorkerLoader(): WorkerLoader {
9
+ return {
10
+ type: "worker_loader",
11
+ };
12
+ }
@@ -568,6 +568,11 @@ export async function prepareWorkerMetadata(
568
568
  name: bindingName,
569
569
  namespace: binding.namespaceName,
570
570
  });
571
+ } else if (binding.type === "worker_loader") {
572
+ meta.bindings.push({
573
+ type: "worker_loader",
574
+ name: bindingName,
575
+ });
571
576
  } else if (binding.type === "secret_key") {
572
577
  meta.bindings.push({
573
578
  type: "secret_key",
@@ -111,7 +111,7 @@ export async function enableWorkerSubdomain(
111
111
  ),
112
112
  ),
113
113
  (error) => error instanceof CloudflareApiError && error.status === 404,
114
- 10,
114
+ 20,
115
115
  1000,
116
116
  );
117
117
  }
@@ -982,9 +982,11 @@ const _Worker = Resource(
982
982
  bundle,
983
983
  port: props.dev?.port,
984
984
  tunnel: props.dev?.tunnel ?? this.scope.tunnel,
985
+ cwd: props.cwd ?? process.cwd(),
985
986
  });
986
987
  this.onCleanup(() => controller.dispose());
987
988
  }
989
+
988
990
  await provisionResources(
989
991
  {
990
992
  ...props,
@@ -1240,12 +1242,18 @@ async function provisionResources<B extends Bindings>(
1240
1242
  containers: options.containers,
1241
1243
  domains: props.domains?.map((domain) => {
1242
1244
  if (typeof domain === "string") {
1245
+ if (domain === "") {
1246
+ throw new Error("Domain names cannot be empty strings");
1247
+ }
1243
1248
  return {
1244
1249
  name: domain,
1245
1250
  zoneId: undefined,
1246
1251
  adopt: props.adopt,
1247
1252
  };
1248
1253
  }
1254
+ if (domain.domainName === "") {
1255
+ throw new Error("Domain names cannot be empty strings");
1256
+ }
1249
1257
  return {
1250
1258
  name: domain.domainName,
1251
1259
  zoneId: domain.zoneId,
@@ -4,7 +4,6 @@ import type { Context } from "../context.ts";
4
4
  import { Resource } from "../resource.ts";
5
5
  import { Scope } from "../scope.ts";
6
6
  import { isSecret } from "../secret.ts";
7
- import { unencryptSecrets } from "./util/filter-env-bindings.ts";
8
7
  import { assertNever } from "../util/assert-never.ts";
9
8
  import type { Bindings, WorkerBindingRateLimit } from "./bindings.ts";
10
9
  import type { R2BucketJurisdiction } from "./bucket.ts";
@@ -12,6 +11,7 @@ import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
12
11
  import type { EventSource } from "./event-source.ts";
13
12
  import { isQueueEventSource } from "./event-source.ts";
14
13
  import { isQueue } from "./queue.ts";
14
+ import { unencryptSecrets } from "./util/filter-env-bindings.ts";
15
15
  import { isWorker, type Worker, type WorkerProps } from "./worker.ts";
16
16
 
17
17
  type WranglerJsonRateLimit = Omit<WorkerBindingRateLimit, "type"> & {
@@ -301,7 +301,7 @@ export interface WranglerJsonSpec {
301
301
  */
302
302
  ai?: {
303
303
  binding: string;
304
- experimental_remote?: boolean;
304
+ remote?: boolean;
305
305
  };
306
306
 
307
307
  /**
@@ -309,7 +309,7 @@ export interface WranglerJsonSpec {
309
309
  */
310
310
  browser?: {
311
311
  binding: string;
312
- experimental_remote?: boolean;
312
+ remote?: boolean;
313
313
  };
314
314
 
315
315
  /**
@@ -317,7 +317,7 @@ export interface WranglerJsonSpec {
317
317
  */
318
318
  images?: {
319
319
  binding: string;
320
- experimental_remote?: boolean;
320
+ remote?: boolean;
321
321
  };
322
322
 
323
323
  /**
@@ -330,7 +330,7 @@ export interface WranglerJsonSpec {
330
330
  * The ID of the KV namespace used during `wrangler dev`
331
331
  */
332
332
  preview_id?: string;
333
- experimental_remote?: boolean;
333
+ remote?: boolean;
334
334
  }[];
335
335
 
336
336
  /**
@@ -355,7 +355,7 @@ export interface WranglerJsonSpec {
355
355
  * The preview name of this R2 bucket at the edge.
356
356
  */
357
357
  preview_bucket_name?: string;
358
- experimental_remote?: boolean;
358
+ remote?: boolean;
359
359
  }[];
360
360
 
361
361
  /**
@@ -376,6 +376,10 @@ export interface WranglerJsonSpec {
376
376
  entrypoint?: string;
377
377
  }[];
378
378
 
379
+ worker_loaders?: {
380
+ binding: string;
381
+ }[];
382
+
379
383
  /**
380
384
  * Workflow bindings
381
385
  */
@@ -392,7 +396,7 @@ export interface WranglerJsonSpec {
392
396
  vectorize?: {
393
397
  binding: string;
394
398
  index_name: string;
395
- experimental_remote?: boolean;
399
+ remote?: boolean;
396
400
  }[];
397
401
 
398
402
  /**
@@ -412,7 +416,7 @@ export interface WranglerJsonSpec {
412
416
  * The ID of the D1 database used during `wrangler dev`
413
417
  */
414
418
  preview_database_id?: string;
415
- experimental_remote?: boolean;
419
+ remote?: boolean;
416
420
  }[];
417
421
 
418
422
  /**
@@ -495,7 +499,7 @@ export interface WranglerJsonSpec {
495
499
  dispatch_namespaces?: {
496
500
  binding: string;
497
501
  namespace: string;
498
- experimental_remote?: boolean;
502
+ remote?: boolean;
499
503
  }[];
500
504
 
501
505
  /**
@@ -555,7 +559,7 @@ function processBindings(
555
559
  binding: string;
556
560
  id: string;
557
561
  preview_id: string;
558
- experimental_remote?: boolean;
562
+ remote?: boolean;
559
563
  }[] = [];
560
564
  const durableObjects: {
561
565
  name: string;
@@ -588,7 +592,7 @@ function processBindings(
588
592
  database_name: string;
589
593
  migrations_dir?: string;
590
594
  preview_database_id: string;
591
- experimental_remote?: boolean;
595
+ remote?: boolean;
592
596
  }[] = [];
593
597
  const queues: {
594
598
  producers: { queue: string; binding: string }[];
@@ -604,7 +608,7 @@ function processBindings(
604
608
  const vectorizeIndexes: {
605
609
  binding: string;
606
610
  index_name: string;
607
- experimental_remote?: boolean;
611
+ remote?: boolean;
608
612
  }[] = [];
609
613
  const analyticsEngineDatasets: { binding: string; dataset: string }[] = [];
610
614
  const hyperdrive: {
@@ -621,12 +625,15 @@ function processBindings(
621
625
  const dispatchNamespaces: {
622
626
  binding: string;
623
627
  namespace: string;
624
- experimental_remote?: boolean;
628
+ remote?: boolean;
625
629
  }[] = [];
626
630
  const unsafeBindings: WranglerJsonRateLimit[] = [];
627
631
  const containers: {
628
632
  class_name: string;
629
633
  }[] = [];
634
+ const workerLoaders: {
635
+ binding: string;
636
+ }[] = [];
630
637
 
631
638
  for (const eventSource of eventSources ?? []) {
632
639
  if (isQueueEventSource(eventSource)) {
@@ -686,9 +693,7 @@ function processBindings(
686
693
  binding: bindingName,
687
694
  id: id,
688
695
  preview_id: id,
689
- ...("dev" in binding && binding.dev?.remote
690
- ? { experimental_remote: true }
691
- : {}),
696
+ ...("dev" in binding && binding.dev?.remote ? { remote: true } : {}),
692
697
  });
693
698
  } else if (
694
699
  typeof binding === "object" &&
@@ -718,7 +723,7 @@ function processBindings(
718
723
  preview_bucket_name: name,
719
724
  jurisdiction:
720
725
  binding.jurisdiction === "default" ? undefined : binding.jurisdiction,
721
- ...(binding.dev?.remote ? { experimental_remote: true } : {}),
726
+ ...(binding.dev?.remote ? { remote: true } : {}),
722
727
  });
723
728
  } else if (binding.type === "secret") {
724
729
  // Secret binding
@@ -746,7 +751,7 @@ function processBindings(
746
751
  database_name: binding.name,
747
752
  migrations_dir: binding.migrationsDir,
748
753
  preview_database_id: id,
749
- ...(binding.dev?.remote ? { experimental_remote: true } : {}),
754
+ ...(binding.dev?.remote ? { remote: true } : {}),
750
755
  });
751
756
  } else if (binding.type === "queue") {
752
757
  const id =
@@ -762,7 +767,7 @@ function processBindings(
762
767
  binding: bindingName,
763
768
  index_name: binding.name,
764
769
  // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
765
- experimental_remote: true,
770
+ remote: true,
766
771
  });
767
772
  } else if (binding.type === "browser") {
768
773
  if (spec.browser) {
@@ -771,7 +776,7 @@ function processBindings(
771
776
  spec.browser = {
772
777
  binding: bindingName,
773
778
  // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
774
- experimental_remote: true,
779
+ remote: true,
775
780
  };
776
781
  } else if (binding.type === "ai") {
777
782
  if (spec.ai) {
@@ -780,7 +785,7 @@ function processBindings(
780
785
  spec.ai = {
781
786
  binding: bindingName,
782
787
  // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
783
- experimental_remote: true,
788
+ remote: true,
784
789
  };
785
790
  } else if (binding.type === "images") {
786
791
  if (spec.images) {
@@ -789,7 +794,7 @@ function processBindings(
789
794
  spec.images = {
790
795
  binding: bindingName,
791
796
  // https://developers.cloudflare.com/workers/development-testing/#recommended-remote-bindings
792
- experimental_remote: true,
797
+ remote: true,
793
798
  };
794
799
  } else if (binding.type === "analytics_engine") {
795
800
  analyticsEngineDatasets.push({
@@ -830,7 +835,7 @@ function processBindings(
830
835
  dispatchNamespaces.push({
831
836
  binding: bindingName,
832
837
  namespace: binding.namespaceName,
833
- experimental_remote: true,
838
+ remote: true,
834
839
  });
835
840
  } else if (binding.type === "ratelimit") {
836
841
  unsafeBindings.push({
@@ -850,6 +855,10 @@ function processBindings(
850
855
  containers.push({
851
856
  class_name: binding.className,
852
857
  });
858
+ } else if (binding.type === "worker_loader") {
859
+ workerLoaders.push({
860
+ binding: bindingName,
861
+ });
853
862
  } else {
854
863
  console.log("binding", binding);
855
864
  // biome-ignore lint/correctness/noVoidTypeReturn: it returns never
@@ -930,4 +939,8 @@ function processBindings(
930
939
  bindings: unsafeBindings,
931
940
  };
932
941
  }
942
+
943
+ if (workerLoaders.length > 0) {
944
+ spec.worker_loaders = workerLoaders;
945
+ }
933
946
  }
@@ -414,7 +414,7 @@ export const Zone = Resource(
414
414
 
415
415
  // Add a small delay to ensure settings are propagated
416
416
  // TODO(michael): do we need this?
417
- // https://github.com/sam-goodwin/alchemy/issues/681
417
+ // https://github.com/alchemy-run/alchemy/issues/681
418
418
  await new Promise((resolve) => setTimeout(resolve, 2000));
419
419
 
420
420
  // Update Bot Management configuration if provided
package/src/destroy.ts CHANGED
@@ -14,6 +14,7 @@ import { isScope, type PendingDeletions, Scope } from "./scope.ts";
14
14
  import type { State } from "./state.ts";
15
15
  import { formatFQN } from "./util/cli.ts";
16
16
  import { logger } from "./util/logger.ts";
17
+ import { createAndSendEvent } from "./util/telemetry.ts";
17
18
 
18
19
  export function isDestroyedSignal(error: any): error is DestroyedSignal {
19
20
  return error instanceof Error && (error as any).kind === "DestroyedSignal";
@@ -109,6 +110,7 @@ export async function destroy(
109
110
  logger.warn(`Resource "${instance[ResourceFQN]}" has no scope`);
110
111
  }
111
112
  const quiet = options?.quiet ?? scope.quiet;
113
+ const start = performance.now();
112
114
 
113
115
  try {
114
116
  if (!quiet && !options?.noop) {
@@ -122,6 +124,15 @@ export async function destroy(
122
124
  });
123
125
  }
124
126
 
127
+ await createAndSendEvent({
128
+ event: "resource.start",
129
+ resource: instance[ResourceKind],
130
+ status: "deleting",
131
+ phase: "destroy",
132
+ duration: performance.now() - start,
133
+ replaced: !!options?.replace,
134
+ });
135
+
125
136
  let state: State;
126
137
  let props: ResourceProps | undefined;
127
138
  if (options?.replace) {
@@ -224,7 +235,28 @@ export async function destroy(
224
235
  status: "success",
225
236
  });
226
237
  }
238
+
239
+ await createAndSendEvent({
240
+ event: "resource.success",
241
+ resource: instance[ResourceKind],
242
+ status: "deleted",
243
+ phase: "destroy",
244
+ duration: performance.now() - start,
245
+ replaced: !!options?.replace,
246
+ });
227
247
  } catch (error) {
248
+ let errorToSend = error instanceof Error ? error : new Error(String(error));
249
+ await createAndSendEvent(
250
+ {
251
+ event: "resource.error",
252
+ resource: instance[ResourceKind],
253
+ duration: performance.now() - start,
254
+ phase: "destroy",
255
+ status: "deleting",
256
+ replaced: !!options?.replace,
257
+ },
258
+ errorToSend,
259
+ );
228
260
  logger.error(error);
229
261
  throw error;
230
262
  }
@@ -190,7 +190,7 @@ export const Image = Resource(
190
190
 
191
191
  // Add build arguments
192
192
  for (const [key, value] of Object.entries(buildOptions)) {
193
- buildArgs.push("--build-arg", `${key}="${value}"`);
193
+ buildArgs.push("--build-arg", `${key}=${value}`);
194
194
  }
195
195
 
196
196
  buildArgs.push("-f", dockerfile);
@@ -173,17 +173,17 @@ export const GitHubComment = Resource(
173
173
  props: GitHubCommentProps,
174
174
  ): Promise<GitHubComment> {
175
175
  // Create authenticated Octokit client - will automatically handle token resolution
176
- const octokit = await createGitHubClient({
177
- token: props.token?.unencrypted,
178
- });
179
-
180
- // Verify authentication and permissions
181
- if (!this.quiet) {
182
- await verifyGitHubAuth(octokit, props.owner, props.repository);
183
- }
184
176
 
185
177
  if (this.phase === "delete") {
186
178
  if (this.output?.commentId && props.allowDelete) {
179
+ const octokit = await createGitHubClient({
180
+ token: props.token?.unencrypted,
181
+ });
182
+
183
+ // Verify authentication and permissions
184
+ if (!this.quiet) {
185
+ await verifyGitHubAuth(octokit, props.owner, props.repository);
186
+ }
187
187
  try {
188
188
  // Delete the comment
189
189
  await octokit.rest.issues.deleteComment({
@@ -209,6 +209,15 @@ export const GitHubComment = Resource(
209
209
  return this.destroy();
210
210
  }
211
211
 
212
+ const octokit = await createGitHubClient({
213
+ token: props.token?.unencrypted,
214
+ });
215
+
216
+ // Verify authentication and permissions
217
+ if (!this.quiet) {
218
+ await verifyGitHubAuth(octokit, props.owner, props.repository);
219
+ }
220
+
212
221
  try {
213
222
  if (this.phase === "update" && this.output?.commentId) {
214
223
  // Update existing comment
package/src/resource.ts CHANGED
@@ -179,7 +179,7 @@ export function Resource<
179
179
  const error = new Error(
180
180
  `Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.[ResourceKind]}' !== '${type}'`,
181
181
  );
182
- createAndSendEvent(
182
+ await createAndSendEvent(
183
183
  {
184
184
  event: "resource.error",
185
185
  resource: type,
package/src/scope.ts CHANGED
@@ -30,6 +30,7 @@ import { logger } from "./util/logger.ts";
30
30
  import { AsyncMutex } from "./util/mutex.ts";
31
31
  import { ALCHEMY_ROOT } from "./util/root-dir.ts";
32
32
  import { createAndSendEvent } from "./util/telemetry.ts";
33
+ import { validateResourceID } from "./util/validate-resource-id.ts";
33
34
 
34
35
  export class RootScopeStateAttemptError extends Error {
35
36
  constructor() {
@@ -115,6 +116,11 @@ export interface ScopeOptions extends ProviderCredentials {
115
116
  * `undefined` if the program was not run with `--app`
116
117
  */
117
118
  isSelected?: boolean;
119
+ /**
120
+ * @internal
121
+ * The timestamp when the scope was started.
122
+ */
123
+ startedAt?: DOMHighResTimeStamp;
118
124
  }
119
125
 
120
126
  /**
@@ -208,6 +214,7 @@ export class Scope {
208
214
  public readonly dotAlchemy: string;
209
215
  public readonly isSelected: boolean | undefined;
210
216
  public readonly profile: string | undefined;
217
+ public readonly startedAt: DOMHighResTimeStamp;
211
218
 
212
219
  // Provider credentials for scope-level credential overrides
213
220
  public readonly providerCredentials: ProviderCredentials;
@@ -215,7 +222,6 @@ export class Scope {
215
222
  private isErrored = false;
216
223
  private isSkipped = false;
217
224
  private finalized = false;
218
- private startedAt = performance.now();
219
225
  private deferred: (() => Promise<any>)[] = [];
220
226
  private cleanups: (() => Promise<void>)[] = [];
221
227
 
@@ -248,6 +254,7 @@ export class Scope {
248
254
  isSelected,
249
255
  noTrack,
250
256
  profile,
257
+ startedAt,
251
258
  ...providerCredentials
252
259
  } = options;
253
260
 
@@ -256,7 +263,7 @@ export class Scope {
256
263
  this.parent = parent ?? Scope.getScope();
257
264
  this.rootDir = rootDir ?? this.parent?.rootDir ?? ALCHEMY_ROOT;
258
265
  this.isSelected = isSelected ?? this.parent?.isSelected;
259
-
266
+ this.startedAt = startedAt ?? this.parent?.startedAt ?? performance.now();
260
267
  this.dotAlchemy =
261
268
  dotAlchemy ??
262
269
  this.parent?.dotAlchemy ??
@@ -265,21 +272,17 @@ export class Scope {
265
272
  // Store provider credentials (TypeScript ensures no conflicts with core options)
266
273
  this.providerCredentials = providerCredentials as ProviderCredentials;
267
274
 
268
- const isChild = this.parent !== undefined;
269
- if (this.scopeName?.includes(":") && isChild) {
270
- // TODO(sam): relax this constraint once we move to SQLite3 store
271
- throw new Error(
272
- `Scope name "${this.scopeName}" cannot contain double colons`,
273
- );
274
- }
275
-
276
275
  this.stage = stage ?? this.parent?.stage ?? DEFAULT_STAGE;
277
276
  this.profile = profile ?? this.parent?.profile;
278
277
  this.parent?.children.set(this.scopeName!, this);
279
278
  this.quiet = quiet ?? this.parent?.quiet ?? false;
280
- if (this.parent && !this.scopeName) {
281
- throw new Error("Scope name is required when creating a child scope");
279
+ if (this.parent) {
280
+ if (!this.scopeName) {
281
+ throw new Error("Scope name is required when creating a child scope");
282
+ }
283
+ validateResourceID(this.scopeName, "Scope");
282
284
  }
285
+
283
286
  this.password = password ?? this.parent?.password;
284
287
  this.noTrack = noTrack ?? this.parent?.noTrack ?? false;
285
288
  const resolvedPhase = phase ?? this.parent?.phase;
@@ -308,7 +311,7 @@ export class Scope {
308
311
  destroyStrategy ?? this.parent?.destroyStrategy ?? "sequential";
309
312
  if (this.local) {
310
313
  this.logger.warnOnce(
311
- "Development mode is in beta. Please report any issues to https://github.com/sam-goodwin/alchemy/issues.",
314
+ "Development mode is in beta. Please report any issues to https://github.com/alchemy-run/alchemy/issues.",
312
315
  );
313
316
  }
314
317
 
@@ -518,10 +521,15 @@ export class Scope {
518
521
  this.parent === undefined ||
519
522
  this?.parent?.scopeName === this.root.scopeName;
520
523
  if (this.phase === "read") {
521
- createAndSendEvent({
522
- event: "alchemy.success",
523
- duration: performance.now() - this.startedAt,
524
- });
524
+ if (this.parent == null) {
525
+ await createAndSendEvent(
526
+ {
527
+ event: this.isErrored ? "alchemy.error" : "alchemy.success",
528
+ duration: performance.now() - this.startedAt,
529
+ },
530
+ this.isErrored ? new Error("Scope failed") : undefined,
531
+ );
532
+ }
525
533
  return;
526
534
  }
527
535
  if (this.finalized && !shouldForce) {
@@ -569,18 +577,17 @@ export class Scope {
569
577
  force: shouldForce,
570
578
  noop: options?.noop,
571
579
  });
572
- createAndSendEvent({
573
- event: "alchemy.success",
574
- duration: performance.now() - this.startedAt,
575
- });
576
580
  } else if (this.isErrored) {
577
581
  this.logger.warn("Scope is in error, skipping finalize");
578
- createAndSendEvent(
582
+ }
583
+
584
+ if (this.parent == null) {
585
+ await createAndSendEvent(
579
586
  {
580
- event: "alchemy.error",
587
+ event: this.isErrored ? "alchemy.error" : "alchemy.success",
581
588
  duration: performance.now() - this.startedAt,
582
589
  },
583
- new Error("Scope failed"),
590
+ this.isErrored ? new Error("Scope failed") : undefined,
584
591
  );
585
592
  }
586
593
 
@@ -599,7 +606,7 @@ export class Scope {
599
606
  throw e;
600
607
  })) ?? [];
601
608
 
602
- //todo(michael): remove once we deprecate doss; see: https://github.com/sam-goodwin/alchemy/issues/585
609
+ //todo(michael): remove once we deprecate doss; see: https://github.com/alchemy-run/alchemy/issues/585
603
610
  let hasCorruptedResources = false;
604
611
  if (pendingDeletions) {
605
612
  for (const { resource, oldProps } of pendingDeletions) {
@@ -30,7 +30,7 @@ export class InstrumentedStateStore<T extends StateStore>
30
30
  error = err;
31
31
  throw err;
32
32
  } finally {
33
- createAndSendEvent(
33
+ await createAndSendEvent(
34
34
  {
35
35
  event,
36
36
  stateStore: this.stateStoreClass,