alchemy 0.65.1 → 0.67.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 (185) hide show
  1. package/bin/alchemy.js +139 -79
  2. package/bin/commands/init.ts +6 -3
  3. package/bin/services/cdp-manager/server.ts +3 -2
  4. package/bin/services/execute-alchemy.ts +33 -3
  5. package/bin/services/find-workspace-root.ts +48 -0
  6. package/lib/alchemy.d.ts +20 -3
  7. package/lib/alchemy.d.ts.map +1 -1
  8. package/lib/alchemy.js +18 -5
  9. package/lib/alchemy.js.map +1 -1
  10. package/lib/apply.d.ts.map +1 -1
  11. package/lib/apply.js +51 -1
  12. package/lib/apply.js.map +1 -1
  13. package/lib/cloudflare/bindings.d.ts +17 -5
  14. package/lib/cloudflare/bindings.d.ts.map +1 -1
  15. package/lib/cloudflare/bindings.js +3 -1
  16. package/lib/cloudflare/bindings.js.map +1 -1
  17. package/lib/cloudflare/bound.d.ts +4 -3
  18. package/lib/cloudflare/bound.d.ts.map +1 -1
  19. package/lib/cloudflare/bucket.js +1 -1
  20. package/lib/cloudflare/bucket.js.map +1 -1
  21. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts +1 -1
  22. package/lib/cloudflare/bundle/internal-worker-bundle.d.ts.map +1 -1
  23. package/lib/cloudflare/bundle/internal-worker-bundle.js.map +1 -1
  24. package/lib/cloudflare/cloudflare-env-proxy.js +3 -3
  25. package/lib/cloudflare/cloudflare-env-proxy.js.map +1 -1
  26. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  27. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  28. package/lib/cloudflare/d1-database.d.ts.map +1 -1
  29. package/lib/cloudflare/d1-database.js +2 -1
  30. package/lib/cloudflare/d1-database.js.map +1 -1
  31. package/lib/cloudflare/d1-local-migrations.d.ts +1 -0
  32. package/lib/cloudflare/d1-local-migrations.d.ts.map +1 -1
  33. package/lib/cloudflare/d1-local-migrations.js +3 -2
  34. package/lib/cloudflare/d1-local-migrations.js.map +1 -1
  35. package/lib/cloudflare/hyperdrive-ref.d.ts +23 -0
  36. package/lib/cloudflare/hyperdrive-ref.d.ts.map +1 -0
  37. package/lib/cloudflare/hyperdrive-ref.js +52 -0
  38. package/lib/cloudflare/hyperdrive-ref.js.map +1 -0
  39. package/lib/cloudflare/index.d.ts +2 -0
  40. package/lib/cloudflare/index.d.ts.map +1 -1
  41. package/lib/cloudflare/index.js +2 -0
  42. package/lib/cloudflare/index.js.map +1 -1
  43. package/lib/cloudflare/kv-namespace.js +1 -1
  44. package/lib/cloudflare/kv-namespace.js.map +1 -1
  45. package/lib/cloudflare/miniflare/build-worker-options.d.ts +2 -1
  46. package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
  47. package/lib/cloudflare/miniflare/build-worker-options.js +11 -6
  48. package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
  49. package/lib/cloudflare/miniflare/delete.d.ts +3 -2
  50. package/lib/cloudflare/miniflare/delete.d.ts.map +1 -1
  51. package/lib/cloudflare/miniflare/delete.js +13 -12
  52. package/lib/cloudflare/miniflare/delete.js.map +1 -1
  53. package/lib/cloudflare/miniflare/miniflare-controller.d.ts +3 -1
  54. package/lib/cloudflare/miniflare/miniflare-controller.d.ts.map +1 -1
  55. package/lib/cloudflare/miniflare/miniflare-controller.js +29 -13
  56. package/lib/cloudflare/miniflare/miniflare-controller.js.map +1 -1
  57. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts +24 -13
  58. package/lib/cloudflare/miniflare/miniflare-worker-proxy.d.ts.map +1 -1
  59. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js +95 -79
  60. package/lib/cloudflare/miniflare/miniflare-worker-proxy.js.map +1 -1
  61. package/lib/cloudflare/miniflare/paths.d.ts +4 -4
  62. package/lib/cloudflare/miniflare/paths.d.ts.map +1 -1
  63. package/lib/cloudflare/miniflare/paths.js +10 -4
  64. package/lib/cloudflare/miniflare/paths.js.map +1 -1
  65. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts +2 -0
  66. package/lib/cloudflare/miniflare/symlink-miniflare-state.d.ts.map +1 -0
  67. package/lib/cloudflare/miniflare/symlink-miniflare-state.js +19 -0
  68. package/lib/cloudflare/miniflare/symlink-miniflare-state.js.map +1 -0
  69. package/lib/cloudflare/miniflare/tunnel.d.ts +18 -0
  70. package/lib/cloudflare/miniflare/tunnel.d.ts.map +1 -0
  71. package/lib/cloudflare/miniflare/tunnel.js +79 -0
  72. package/lib/cloudflare/miniflare/tunnel.js.map +1 -0
  73. package/lib/cloudflare/next.d.ts.map +1 -1
  74. package/lib/cloudflare/next.js +3 -0
  75. package/lib/cloudflare/next.js.map +1 -1
  76. package/lib/cloudflare/nuxt/plugin.d.ts.map +1 -1
  77. package/lib/cloudflare/nuxt/plugin.js +3 -3
  78. package/lib/cloudflare/nuxt/plugin.js.map +1 -1
  79. package/lib/cloudflare/quick-tunnel.d.ts +1 -1
  80. package/lib/cloudflare/redwood/plugin.js +2 -2
  81. package/lib/cloudflare/redwood/plugin.js.map +1 -1
  82. package/lib/cloudflare/secret-ref.d.ts +38 -0
  83. package/lib/cloudflare/secret-ref.d.ts.map +1 -0
  84. package/lib/cloudflare/secret-ref.js +38 -0
  85. package/lib/cloudflare/secret-ref.js.map +1 -0
  86. package/lib/cloudflare/vite/plugin.d.ts.map +1 -1
  87. package/lib/cloudflare/vite/plugin.js +6 -3
  88. package/lib/cloudflare/vite/plugin.js.map +1 -1
  89. package/lib/cloudflare/website.d.ts.map +1 -1
  90. package/lib/cloudflare/website.js +5 -16
  91. package/lib/cloudflare/website.js.map +1 -1
  92. package/lib/cloudflare/worker-metadata.d.ts +1 -1
  93. package/lib/cloudflare/worker-metadata.d.ts.map +1 -1
  94. package/lib/cloudflare/worker-metadata.js +7 -9
  95. package/lib/cloudflare/worker-metadata.js.map +1 -1
  96. package/lib/cloudflare/worker.d.ts +16 -1
  97. package/lib/cloudflare/worker.d.ts.map +1 -1
  98. package/lib/cloudflare/worker.js +16 -2
  99. package/lib/cloudflare/worker.js.map +1 -1
  100. package/lib/cloudflare/wrangler.json.d.ts +2 -1
  101. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  102. package/lib/cloudflare/wrangler.json.js +5 -6
  103. package/lib/cloudflare/wrangler.json.js.map +1 -1
  104. package/lib/llms.cloudflare.txt +1 -1
  105. package/lib/scope.d.ts +26 -1
  106. package/lib/scope.d.ts.map +1 -1
  107. package/lib/scope.js +12 -5
  108. package/lib/scope.js.map +1 -1
  109. package/lib/state/file-system-state-store.d.ts.map +1 -1
  110. package/lib/state/file-system-state-store.js +1 -2
  111. package/lib/state/file-system-state-store.js.map +1 -1
  112. package/lib/state/sqlite-state-store.d.ts +11 -0
  113. package/lib/state/sqlite-state-store.d.ts.map +1 -1
  114. package/lib/state/sqlite-state-store.js +22 -14
  115. package/lib/state/sqlite-state-store.js.map +1 -1
  116. package/lib/stripe/customer.d.ts +0 -8
  117. package/lib/stripe/customer.d.ts.map +1 -1
  118. package/lib/stripe/customer.js +0 -6
  119. package/lib/stripe/customer.js.map +1 -1
  120. package/lib/stripe/price.d.ts +23 -23
  121. package/lib/stripe/price.d.ts.map +1 -1
  122. package/lib/stripe/price.js +6 -22
  123. package/lib/stripe/price.js.map +1 -1
  124. package/lib/test/vitest.d.ts.map +1 -1
  125. package/lib/test/vitest.js +1 -1
  126. package/lib/test/vitest.js.map +1 -1
  127. package/lib/util/assert-never.js +1 -1
  128. package/lib/util/assert-never.js.map +1 -1
  129. package/lib/util/cli.d.ts.map +1 -1
  130. package/lib/util/cli.js +12 -13
  131. package/lib/util/cli.js.map +1 -1
  132. package/lib/util/idempotent-spawn.d.ts.map +1 -1
  133. package/lib/util/idempotent-spawn.js +5 -8
  134. package/lib/util/idempotent-spawn.js.map +1 -1
  135. package/lib/util/root-dir.d.ts +2 -0
  136. package/lib/util/root-dir.d.ts.map +1 -0
  137. package/lib/util/root-dir.js +2 -0
  138. package/lib/util/root-dir.js.map +1 -0
  139. package/package.json +4 -4
  140. package/src/alchemy.ts +43 -8
  141. package/src/apply.ts +58 -3
  142. package/src/cloudflare/bindings.ts +21 -4
  143. package/src/cloudflare/bound.ts +50 -52
  144. package/src/cloudflare/bucket.ts +1 -1
  145. package/src/cloudflare/bundle/internal-worker-bundle.ts +2 -1
  146. package/src/cloudflare/cloudflare-env-proxy.ts +5 -5
  147. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  148. package/src/cloudflare/d1-database.ts +2 -1
  149. package/src/cloudflare/d1-local-migrations.ts +10 -6
  150. package/src/cloudflare/hyperdrive-ref.ts +98 -0
  151. package/src/cloudflare/index.ts +2 -0
  152. package/src/cloudflare/kv-namespace.ts +1 -1
  153. package/src/cloudflare/miniflare/build-worker-options.ts +21 -12
  154. package/src/cloudflare/miniflare/delete.ts +19 -11
  155. package/src/cloudflare/miniflare/miniflare-controller.ts +33 -14
  156. package/src/cloudflare/miniflare/miniflare-worker-proxy.ts +122 -102
  157. package/src/cloudflare/miniflare/paths.ts +11 -7
  158. package/src/cloudflare/miniflare/symlink-miniflare-state.ts +19 -0
  159. package/src/cloudflare/miniflare/tunnel.ts +116 -0
  160. package/src/cloudflare/next.ts +3 -0
  161. package/src/cloudflare/nuxt/plugin.ts +12 -3
  162. package/src/cloudflare/redwood/plugin.ts +2 -2
  163. package/src/cloudflare/secret-ref.ts +56 -0
  164. package/src/cloudflare/vite/plugin.ts +8 -3
  165. package/src/cloudflare/website.ts +5 -19
  166. package/src/cloudflare/worker-metadata.ts +14 -12
  167. package/src/cloudflare/worker.ts +36 -3
  168. package/src/cloudflare/wrangler.json.ts +14 -11
  169. package/src/llms.cloudflare.txt +1 -1
  170. package/src/scope.ts +38 -6
  171. package/src/state/file-system-state-store.ts +1 -2
  172. package/src/state/sqlite-state-store.ts +41 -14
  173. package/src/stripe/customer.ts +0 -14
  174. package/src/stripe/price.ts +59 -56
  175. package/src/test/vitest.ts +4 -1
  176. package/src/util/assert-never.ts +1 -1
  177. package/src/util/cli.ts +15 -13
  178. package/src/util/idempotent-spawn.ts +5 -6
  179. package/src/util/root-dir.ts +1 -0
  180. package/templates/nextjs/alchemy.run.ts +2 -2
  181. package/templates/rwsdk/.cursor/rules/alchemy_cloudflare.mdc +1 -1
  182. package/templates/rwsdk/package.json +3 -3
  183. package/workers/cloudflare-state-store.js +1 -1
  184. package/workers/tunnel-proxy.js +110 -0
  185. package/workers/tunnel-proxy.ts +121 -0
@@ -3,11 +3,10 @@ import { logger } from "../util/logger.ts";
3
3
  import { memoize } from "../util/memoize.ts";
4
4
  import { extractCloudflareResult } from "./api-response.ts";
5
5
  import type { CloudflareApi } from "./api.ts";
6
- import {
7
- Self,
8
- type Bindings,
9
- type WorkerBindingDurableObjectNamespace,
10
- type WorkerBindingSpec,
6
+ import type {
7
+ Bindings,
8
+ WorkerBindingDurableObjectNamespace,
9
+ WorkerBindingSpec,
11
10
  } from "./bindings.ts";
12
11
  import { isContainer, type Container } from "./container.ts";
13
12
  import {
@@ -392,11 +391,12 @@ export async function prepareWorkerMetadata(
392
391
  name: bindingName,
393
392
  text: binding,
394
393
  });
395
- } else if (binding === Self) {
394
+ } else if (binding.type === "cloudflare::Worker::Self") {
396
395
  meta.bindings.push({
397
396
  type: "service",
398
397
  name: bindingName,
399
398
  service: props.workerName,
399
+ entrypoint: binding.__entrypoint__,
400
400
  });
401
401
  } else if (binding.type === "d1") {
402
402
  meta.bindings.push({
@@ -416,6 +416,8 @@ export async function prepareWorkerMetadata(
416
416
  type: "service",
417
417
  name: bindingName,
418
418
  service: "service" in binding ? binding.service : binding.name,
419
+ entrypoint:
420
+ "__entrypoint__" in binding ? binding.__entrypoint__ : undefined,
419
421
  });
420
422
  } else if (binding.type === "durable_object_namespace") {
421
423
  meta.bindings.push({
@@ -491,12 +493,6 @@ export async function prepareWorkerMetadata(
491
493
  name: bindingName,
492
494
  index_name: binding.name,
493
495
  });
494
- } else if (binding.type === "ai_gateway") {
495
- // AI Gateway binding - just needs the name property
496
- meta.bindings.push({
497
- type: "ai",
498
- name: bindingName,
499
- });
500
496
  } else if (binding.type === "hyperdrive") {
501
497
  // Hyperdrive binding
502
498
  meta.bindings.push({
@@ -510,6 +506,12 @@ export async function prepareWorkerMetadata(
510
506
  name: bindingName,
511
507
  });
512
508
  } else if (binding.type === "ai") {
509
+ const existing = meta.bindings.find((b) => b.type === "ai");
510
+ if (existing) {
511
+ throw new Error(
512
+ `Workers cannot have multiple AI bindings. Binding "${bindingName}" conflicts with "${existing.name}".`,
513
+ );
514
+ }
513
515
  meta.bindings.push({
514
516
  type: "ai",
515
517
  name: bindingName,
@@ -15,6 +15,7 @@ import {
15
15
  import type { Assets } from "./assets.ts";
16
16
  import type {
17
17
  Bindings,
18
+ Self,
18
19
  WorkerBindingDurableObjectNamespace,
19
20
  WorkerBindingSpec,
20
21
  } from "./bindings.ts";
@@ -58,6 +59,8 @@ import { Workflow, isWorkflow, upsertWorkflow } from "./workflow.ts";
58
59
  // Previous versions of `Worker` used the `Bundle` resource.
59
60
  // This import is here to avoid errors when destroying the `Bundle` resource.
60
61
  import "../esbuild/bundle.ts";
62
+ import { Scope } from "../scope.ts";
63
+ import type { WorkerRef } from "./worker-ref.ts";
61
64
 
62
65
  /**
63
66
  * Configuration options for static assets
@@ -330,11 +333,19 @@ export interface BaseWorkerProps<
330
333
  * @default false
331
334
  */
332
335
  remote?: boolean;
336
+ /**
337
+ * Whether to expose the worker via a Cloudflare Tunnel.
338
+ *
339
+ * @default false
340
+ */
341
+ tunnel?: boolean;
333
342
  url?: undefined;
334
343
  }
335
344
  | {
336
345
  url: string;
337
346
  remote?: undefined;
347
+ tunnel?: undefined;
348
+ port?: undefined;
338
349
  };
339
350
 
340
351
  /**
@@ -699,6 +710,25 @@ export function Worker<const B extends Bindings>(
699
710
  return _Worker(id, props as WorkerProps<B>);
700
711
  }
701
712
 
713
+ Worker.experimentalEntrypoint = <RPC extends Rpc.WorkerEntrypointBranded>(
714
+ worker: Worker | WorkerRef | Self,
715
+ entrypoint: string,
716
+ ) => {
717
+ if (Scope.getScope()?.local) {
718
+ logger.warn(
719
+ "Worker.experimentalEntrypoint is not supported in local development. See: https://github.com/cloudflare/workers-sdk/issues/10681",
720
+ );
721
+ }
722
+ return {
723
+ ...worker,
724
+ // we rename the entrypoint in order to prevent collisions with entrypoint on Worker
725
+ __entrypoint__: entrypoint,
726
+ } as (Worker | WorkerRef) & {
727
+ __entrypoint__?: string;
728
+ __rpc__?: RPC;
729
+ };
730
+ };
731
+
702
732
  const _Worker = Resource(
703
733
  "cloudflare::Worker",
704
734
  {
@@ -755,7 +785,9 @@ const _Worker = Resource(
755
785
  compatibilityDate,
756
786
  compatibilityFlags,
757
787
  outdir:
758
- props.bundle?.outdir ?? path.join(cwd, ".alchemy", "out", workerName),
788
+ props.bundle?.outdir ??
789
+ // the out folder can't be moved to the root of a monorepo, it must be ${cwd} or else miniflare throws a fit
790
+ path.join(process.cwd(), ".alchemy", "out", workerName),
759
791
  sourceMap: "sourceMap" in props ? props.sourceMap : undefined,
760
792
  });
761
793
 
@@ -812,7 +844,7 @@ const _Worker = Resource(
812
844
  if (options.bundle.isOk()) {
813
845
  await options.bundle.value.delete?.();
814
846
  }
815
- await deleteMiniflareWorkerData(options.name, {
847
+ await deleteMiniflareWorkerData(this.scope, options.name, {
816
848
  durableObjects: options.durableObjects,
817
849
  workflows: options.workflows,
818
850
  });
@@ -852,7 +884,8 @@ const _Worker = Resource(
852
884
  eventSources: props.eventSources,
853
885
  assets: props.assets,
854
886
  bundle,
855
- port: (props.dev as { port?: number } | undefined)?.port,
887
+ port: props.dev?.port,
888
+ tunnel: props.dev?.tunnel ?? this.scope.tunnel,
856
889
  });
857
890
  this.onCleanup(() => controller.dispose());
858
891
  }
@@ -5,11 +5,7 @@ import { Resource } from "../resource.ts";
5
5
  import { Scope } from "../scope.ts";
6
6
  import { isSecret } from "../secret.ts";
7
7
  import { assertNever } from "../util/assert-never.ts";
8
- import {
9
- Self,
10
- type Bindings,
11
- type WorkerBindingRateLimit,
12
- } from "./bindings.ts";
8
+ import type { Bindings, WorkerBindingRateLimit } from "./bindings.ts";
13
9
  import type { R2BucketJurisdiction } from "./bucket.ts";
14
10
  import type { DurableObjectNamespace } from "./durable-object-namespace.ts";
15
11
  import type { EventSource } from "./event-source.ts";
@@ -371,6 +367,7 @@ export interface WranglerJsonSpec {
371
367
  binding: string;
372
368
  service: string;
373
369
  environment?: string;
370
+ entrypoint?: string;
374
371
  }[];
375
372
 
376
373
  /**
@@ -534,8 +531,12 @@ function processBindings(
534
531
  preview_bucket_name: string;
535
532
  jurisdiction?: R2BucketJurisdiction;
536
533
  }[] = [];
537
- const services: { binding: string; service: string; environment?: string }[] =
538
- [];
534
+ const services: {
535
+ binding: string;
536
+ service: string;
537
+ environment?: string;
538
+ entrypoint?: string;
539
+ }[] = [];
539
540
  const secrets: string[] = [];
540
541
  const workflows: {
541
542
  name: string;
@@ -625,17 +626,20 @@ function processBindings(
625
626
  } else if (writeSecrets && isSecret(binding)) {
626
627
  spec.vars ??= {};
627
628
  spec.vars[bindingName] = binding as any;
628
- } else if (binding === Self) {
629
+ } else if (binding.type === "cloudflare::Worker::Self") {
629
630
  // Self(service) binding
630
631
  services.push({
631
632
  binding: bindingName,
632
633
  service: workerName,
634
+ entrypoint: binding.__entrypoint__,
633
635
  });
634
636
  } else if (binding.type === "service") {
635
637
  // Service binding
636
638
  services.push({
637
639
  binding: bindingName,
638
640
  service: "name" in binding ? binding.name : binding.service,
641
+ entrypoint:
642
+ "__entrypoint__" in binding ? binding.__entrypoint__ : undefined,
639
643
  });
640
644
  } else if (binding.type === "kv_namespace") {
641
645
  // KV Namespace binding
@@ -759,8 +763,6 @@ function processBindings(
759
763
  binding: bindingName,
760
764
  dataset: binding.dataset,
761
765
  });
762
- } else if (binding.type === "ai_gateway") {
763
- // no-op
764
766
  } else if (binding.type === "version_metadata") {
765
767
  if (spec.version_metadata) {
766
768
  throw new Error(
@@ -775,7 +777,7 @@ function processBindings(
775
777
  binding: bindingName,
776
778
  id: binding.hyperdriveId,
777
779
  localConnectionString: writeSecrets
778
- ? binding.dev.origin.unencrypted
780
+ ? binding.dev?.origin.unencrypted
779
781
  : undefined,
780
782
  });
781
783
  } else if (binding.type === "pipeline") {
@@ -816,6 +818,7 @@ function processBindings(
816
818
  class_name: binding.className,
817
819
  });
818
820
  } else {
821
+ console.log("binding", binding);
819
822
  // biome-ignore lint/correctness/noVoidTypeReturn: it returns never
820
823
  return assertNever(binding);
821
824
  }
@@ -269,7 +269,7 @@ const worker = await Worker("my-worker", {
269
269
  ### 2. Enable Development Compatibility
270
270
  ```typescript
271
271
  // Generate wrangler.json for local development
272
- await WranglerJson("wrangler.json", {
272
+ await WranglerJson({
273
273
  worker,
274
274
  });
275
275
  ```
package/src/scope.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  } from "./util/idempotent-spawn.ts";
29
29
  import { logger } from "./util/logger.ts";
30
30
  import { AsyncMutex } from "./util/mutex.ts";
31
+ import { ALCHEMY_ROOT } from "./util/root-dir.ts";
31
32
  import type { ITelemetryClient } from "./util/telemetry/client.ts";
32
33
 
33
34
  export class RootScopeStateAttemptError extends Error {
@@ -56,6 +57,12 @@ export interface ScopeOptions extends ProviderCredentials {
56
57
  * @default - `true` if ran with `alchemy dev`, `alchemy watch`, `bun --watch ./alchemy.run.ts`
57
58
  */
58
59
  watch?: boolean;
60
+ /**
61
+ * Whether to create a tunnel for supported resources.
62
+ *
63
+ * @default false
64
+ */
65
+ tunnel?: boolean;
59
66
  /**
60
67
  * Apply updates to resources even if there are no changes.
61
68
  *
@@ -89,6 +96,20 @@ export interface ScopeOptions extends ProviderCredentials {
89
96
  * @default false
90
97
  */
91
98
  adopt?: boolean;
99
+ /**
100
+ * The path to the root directory of the project.
101
+ *
102
+ * @default process.cwd()
103
+ */
104
+ rootDir?: string;
105
+ /**
106
+ * Whether this is the application that was selected with `--app`
107
+ *
108
+ * `true` if the application was selected with `--app`
109
+ * `false` if the application was not selected with `--app`
110
+ * `undefined` if the program was not run with `--app`
111
+ */
112
+ isSelected?: boolean;
92
113
  }
93
114
 
94
115
  /**
@@ -171,13 +192,16 @@ export class Scope {
171
192
  public readonly phase: Phase;
172
193
  public readonly local: boolean;
173
194
  public readonly watch: boolean;
195
+ public readonly tunnel: boolean;
174
196
  public readonly force: boolean;
175
197
  public readonly adopt: boolean;
176
198
  public readonly destroyStrategy: DestroyStrategy;
177
199
  public readonly logger: LoggerApi;
178
200
  public readonly telemetryClient: ITelemetryClient;
179
201
  public readonly dataMutex: AsyncMutex;
202
+ public readonly rootDir: string;
180
203
  public readonly dotAlchemy: string;
204
+ public readonly isSelected: boolean | undefined;
181
205
 
182
206
  // Provider credentials for scope-level credential overrides
183
207
  public readonly providerCredentials: ProviderCredentials;
@@ -208,23 +232,28 @@ export class Scope {
208
232
  phase,
209
233
  local,
210
234
  watch,
235
+ tunnel,
211
236
  force,
212
237
  destroyStrategy,
213
238
  telemetryClient,
214
239
  logger,
215
240
  adopt,
216
241
  dotAlchemy,
242
+ rootDir,
243
+ isSelected,
217
244
  ...providerCredentials
218
245
  } = options;
219
246
 
220
- this.dotAlchemy =
221
- dotAlchemy ??
222
- this.parent?.dotAlchemy ??
223
- path.join(process.cwd(), ".alchemy");
224
-
225
247
  this.scopeName = scopeName;
226
248
  this.name = this.scopeName;
227
249
  this.parent = parent ?? Scope.getScope();
250
+ this.rootDir = rootDir ?? this.parent?.rootDir ?? ALCHEMY_ROOT;
251
+ this.isSelected = isSelected ?? this.parent?.isSelected;
252
+
253
+ this.dotAlchemy =
254
+ dotAlchemy ??
255
+ this.parent?.dotAlchemy ??
256
+ path.resolve(this.rootDir, ".alchemy");
228
257
 
229
258
  // Store provider credentials (TypeScript ensures no conflicts with core options)
230
259
  this.providerCredentials = providerCredentials as ProviderCredentials;
@@ -263,6 +292,7 @@ export class Scope {
263
292
 
264
293
  this.local = local ?? this.parent?.local ?? false;
265
294
  this.watch = watch ?? this.parent?.watch ?? false;
295
+ this.tunnel = tunnel ?? this.parent?.tunnel ?? false;
266
296
  this.force = force ?? this.parent?.force ?? false;
267
297
  this.adopt = adopt ?? this.parent?.adopt ?? false;
268
298
  this.destroyStrategy =
@@ -306,7 +336,9 @@ export class Scope {
306
336
  >(
307
337
  // TODO(sam): validate uniqueness? Ensure a flat .logs/${id}.log dir? Or nest in scope dirs?
308
338
  id: string,
309
- options: Omit<IdempotentSpawnOptions, "log" | "stateFile">,
339
+ options: Omit<IdempotentSpawnOptions, "log" | "stateFile"> & {
340
+ extract?: E;
341
+ },
310
342
  ): Promise<E extends undefined ? undefined : string> {
311
343
  const logsDir = path.join(this.dotAlchemy, "logs");
312
344
  const pidsDir = path.join(this.dotAlchemy, "pids");
@@ -6,7 +6,6 @@ import { deserialize, serialize } from "../serde.ts";
6
6
  import type { State, StateStore } from "../state.ts";
7
7
  import { ignore } from "../util/ignore.ts";
8
8
 
9
- const stateRootDir = path.join(process.cwd(), ".alchemy");
10
9
  const ALCHEMY_SEPERATOR_CHAR = process.platform === "win32" ? "-" : ":";
11
10
 
12
11
  export class FileSystemStateStore implements StateStore {
@@ -18,7 +17,7 @@ export class FileSystemStateStore implements StateStore {
18
17
  rootDir?: string;
19
18
  },
20
19
  ) {
21
- this.dir = path.join(options?.rootDir ?? stateRootDir, ...scope.chain);
20
+ this.dir = path.join(options?.rootDir ?? scope.dotAlchemy, ...scope.chain);
22
21
  }
23
22
 
24
23
  async init(): Promise<void> {
@@ -1,7 +1,8 @@
1
1
  import type { Config as LibSQLConfig } from "@libsql/client";
2
- import fs from "node:fs";
2
+ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import type { Scope } from "../scope.ts";
5
+ import { exists } from "../util/exists.ts";
5
6
  import { memoize } from "../util/memoize.ts";
6
7
  import { importPeer } from "../util/peer.ts";
7
8
  import { MIGRATIONS_DIRECTORY } from "./migrations.ts";
@@ -27,6 +28,7 @@ interface BunSQLiteStateStoreOptions {
27
28
  readwrite?: boolean;
28
29
  safeIntegers?: boolean;
29
30
  strict?: boolean;
31
+ dotAlchemy?: string;
30
32
  }
31
33
 
32
34
  interface LibSQLStateStoreOptions extends Omit<LibSQLConfig, "url"> {
@@ -50,6 +52,11 @@ interface LibSQLStateStoreOptions extends Omit<LibSQLConfig, "url"> {
50
52
  * https://github.com/libsql/libsql-client-ts#supported-urls
51
53
  */
52
54
  url?: string;
55
+ /**
56
+ * The path to the .alchemy directory.
57
+ * @default "./.alchemy"
58
+ */
59
+ dotAlchemy?: string;
53
60
  }
54
61
 
55
62
  interface AutoSQLiteStateStoreOptions {
@@ -64,6 +71,12 @@ interface AutoSQLiteStateStoreOptions {
64
71
  * @default ".alchemy/state.sqlite"
65
72
  */
66
73
  filename?: string;
74
+
75
+ /**
76
+ * The path to the .alchemy directory.
77
+ * @default "./.alchemy"
78
+ */
79
+ dotAlchemy?: string;
67
80
  }
68
81
 
69
82
  type SQLiteStateStoreOptions =
@@ -80,7 +93,10 @@ export class SQLiteStateStore extends StateStoreProxy {
80
93
  }
81
94
 
82
95
  async provision(): Promise<StateStoreProxy.Dispatch> {
83
- const db = await createDatabase(this.options);
96
+ const db = await createDatabase({
97
+ ...(this.options ?? {}),
98
+ dotAlchemy: this.scope.dotAlchemy,
99
+ });
84
100
  const { SQLiteStateStoreOperations } = await import("./operations.js");
85
101
  const operations = new SQLiteStateStoreOperations(db, {
86
102
  chain: this.scope.chain,
@@ -97,17 +113,23 @@ const createDatabase = memoize(
97
113
  case "libsql":
98
114
  return await createLibSQLDatabase(options);
99
115
  default: {
100
- return await createDefaultDatabase(options?.filename);
116
+ return await createDefaultDatabase(
117
+ options?.filename,
118
+ options?.dotAlchemy,
119
+ );
101
120
  }
102
121
  }
103
122
  },
104
123
  );
105
124
 
106
- async function createDefaultDatabase(filename: string | undefined) {
125
+ async function createDefaultDatabase(
126
+ filename: string | undefined,
127
+ dotAlchemy: string | undefined,
128
+ ) {
107
129
  if ("Bun" in globalThis) {
108
- return createBunSQLiteDatabase({ engine: "bun", filename });
130
+ return createBunSQLiteDatabase({ engine: "bun", filename, dotAlchemy });
109
131
  }
110
- return createLibSQLDatabase({ engine: "libsql", filename });
132
+ return createLibSQLDatabase({ engine: "libsql", filename, dotAlchemy });
111
133
  }
112
134
 
113
135
  async function createBunSQLiteDatabase(
@@ -122,8 +144,8 @@ async function createBunSQLiteDatabase(
122
144
  const filename =
123
145
  options?.filename ??
124
146
  process.env.ALCHEMY_STATE_FILE ??
125
- ".alchemy/state.sqlite";
126
- ensureDirectory(filename);
147
+ path.join(options?.dotAlchemy ?? ".alchemy", "state.sqlite");
148
+ await ensureDirectory(filename);
127
149
  const { Database } = await import("bun:sqlite");
128
150
  const { drizzle } = await importPeer(
129
151
  import("drizzle-orm/bun-sqlite"),
@@ -135,7 +157,11 @@ async function createBunSQLiteDatabase(
135
157
  // options are passed in, so here's some ugly destructuring!
136
158
  const { engine: _engine, filename: _filename, ...rest } = options ?? {};
137
159
  const bunOptions = Object.keys(rest).length > 0 ? rest : undefined;
138
- const client = new Database(filename, bunOptions);
160
+ const client = new Database(filename, {
161
+ readwrite: true,
162
+ create: true,
163
+ ...bunOptions,
164
+ });
139
165
  client.exec("PRAGMA journal_mode = WAL;");
140
166
  const db = drizzle(client, {
141
167
  schema,
@@ -149,10 +175,11 @@ async function createLibSQLDatabase(
149
175
  ) {
150
176
  const url =
151
177
  options?.url ??
152
- `file:${options?.filename ?? process.env.ALCHEMY_STATE_FILE ?? ".alchemy/state.sqlite"}`;
178
+ `file:${options?.filename ?? process.env.ALCHEMY_STATE_FILE ?? path.join(options?.dotAlchemy ?? ".alchemy", "state.sqlite")}`;
179
+
153
180
  const filename = url.startsWith("file:") ? url.slice(5) : undefined;
154
181
  if (filename) {
155
- ensureDirectory(filename);
182
+ await ensureDirectory(filename);
156
183
  }
157
184
  const { drizzle } = await importPeer(
158
185
  import("drizzle-orm/libsql"),
@@ -173,9 +200,9 @@ async function createLibSQLDatabase(
173
200
  return db;
174
201
  }
175
202
 
176
- const ensureDirectory = (filename: string) => {
203
+ const ensureDirectory = async (filename: string) => {
177
204
  const dir = path.dirname(filename);
178
- if (!fs.existsSync(dir)) {
179
- fs.mkdirSync(dir, { recursive: true });
205
+ if (!(await exists(dir))) {
206
+ await fs.mkdir(dir, { recursive: true });
180
207
  }
181
208
  };
@@ -92,10 +92,6 @@ export interface CustomerProps {
92
92
  * An integer amount in cents that represents the customer's current balance
93
93
  */
94
94
  balance?: number;
95
- /**
96
- * If you provide a coupon code, the customer will have a discount applied on all recurring charges
97
- */
98
- coupon?: string;
99
95
  /**
100
96
  * An arbitrary string attached to the object
101
97
  */
@@ -136,10 +132,6 @@ export interface CustomerProps {
136
132
  * Customer's preferred languages, ordered by preference
137
133
  */
138
134
  preferredLocales?: string[];
139
- /**
140
- * The ID of a promotion code to apply to the customer
141
- */
142
- promotionCode?: string;
143
135
  /**
144
136
  * The customer's shipping information
145
137
  */
@@ -311,7 +303,6 @@ export const Customer = Resource(
311
303
  }
312
304
  : undefined,
313
305
  balance: props.balance,
314
- coupon: props.coupon,
315
306
  description: props.description,
316
307
  email: props.email,
317
308
  invoice_prefix: props.invoicePrefix,
@@ -320,7 +311,6 @@ export const Customer = Resource(
320
311
  next_invoice_sequence: props.nextInvoiceSequence,
321
312
  phone: props.phone,
322
313
  preferred_locales: props.preferredLocales,
323
- promotion_code: props.promotionCode,
324
314
  shipping: props.shipping as any,
325
315
  source: props.source,
326
316
  tax_exempt: props.taxExempt,
@@ -354,7 +344,6 @@ export const Customer = Resource(
354
344
  }
355
345
  : undefined,
356
346
  balance: props.balance,
357
- coupon: props.coupon,
358
347
  description: props.description,
359
348
  email: props.email,
360
349
  invoice_prefix: props.invoicePrefix,
@@ -364,7 +353,6 @@ export const Customer = Resource(
364
353
  payment_method: props.paymentMethod,
365
354
  phone: props.phone,
366
355
  preferred_locales: props.preferredLocales,
367
- promotion_code: props.promotionCode,
368
356
  shipping: props.shipping as any,
369
357
  source: props.source,
370
358
  tax_exempt: props.taxExempt,
@@ -404,7 +392,6 @@ export const Customer = Resource(
404
392
  }
405
393
  : undefined,
406
394
  balance: props.balance,
407
- coupon: props.coupon,
408
395
  description: props.description,
409
396
  email: props.email,
410
397
  invoice_prefix: props.invoicePrefix,
@@ -413,7 +400,6 @@ export const Customer = Resource(
413
400
  next_invoice_sequence: props.nextInvoiceSequence,
414
401
  phone: props.phone,
415
402
  preferred_locales: props.preferredLocales,
416
- promotion_code: props.promotionCode,
417
403
  shipping: props.shipping as any,
418
404
  source: props.source,
419
405
  tax_exempt: props.taxExempt,