alchemy 2.0.0-beta.14 → 2.0.0-beta.15

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 (127) hide show
  1. package/bin/alchemy.js +602 -596
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +2 -0
  4. package/bin/commands/deploy.ts +0 -1
  5. package/bin/commands/state.ts +8 -5
  6. package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
  7. package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
  8. package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
  9. package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
  10. package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
  11. package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
  12. package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
  13. package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
  14. package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
  15. package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
  16. package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
  17. package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
  18. package/lib/Agent/lsp/client.d.ts +2 -2
  19. package/lib/Cli/InkCLI.d.ts +0 -1
  20. package/lib/Cli/InkCLI.d.ts.map +1 -1
  21. package/lib/Cli/InkCLI.js +0 -5
  22. package/lib/Cli/InkCLI.js.map +1 -1
  23. package/lib/Cli/index.d.ts +0 -1
  24. package/lib/Cli/index.d.ts.map +1 -1
  25. package/lib/Cli/index.js +4 -1
  26. package/lib/Cli/index.js.map +1 -1
  27. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +58 -0
  28. package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -0
  29. package/lib/Cloudflare/ApiToken/AccountApiToken.js +137 -0
  30. package/lib/Cloudflare/ApiToken/AccountApiToken.js.map +1 -0
  31. package/lib/Cloudflare/ApiToken/Common.d.ts +91 -0
  32. package/lib/Cloudflare/ApiToken/Common.d.ts.map +1 -0
  33. package/lib/Cloudflare/ApiToken/Common.js +61 -0
  34. package/lib/Cloudflare/ApiToken/Common.js.map +1 -0
  35. package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts +1549 -0
  36. package/lib/Cloudflare/ApiToken/PermissionGroups.d.ts.map +1 -0
  37. package/lib/Cloudflare/ApiToken/PermissionGroups.js +1856 -0
  38. package/lib/Cloudflare/ApiToken/PermissionGroups.js.map +1 -0
  39. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +49 -0
  40. package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -0
  41. package/lib/Cloudflare/ApiToken/UserApiToken.js +113 -0
  42. package/lib/Cloudflare/ApiToken/UserApiToken.js.map +1 -0
  43. package/lib/Cloudflare/ApiToken/index.d.ts +5 -0
  44. package/lib/Cloudflare/ApiToken/index.d.ts.map +1 -0
  45. package/lib/Cloudflare/ApiToken/index.js +5 -0
  46. package/lib/Cloudflare/ApiToken/index.js.map +1 -0
  47. package/lib/Cloudflare/Auth/AuthProvider.js +2 -2
  48. package/lib/Cloudflare/Auth/AuthProvider.js.map +1 -1
  49. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  50. package/lib/Cloudflare/Providers.js +4 -1
  51. package/lib/Cloudflare/Providers.js.map +1 -1
  52. package/lib/Cloudflare/StateStore/Api.d.ts +7 -8
  53. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
  54. package/lib/Cloudflare/StateStore/Api.js +6 -4
  55. package/lib/Cloudflare/StateStore/Api.js.map +1 -1
  56. package/lib/Cloudflare/StateStore/State.d.ts +1 -1
  57. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  58. package/lib/Cloudflare/StateStore/Store.d.ts +2 -2
  59. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
  60. package/lib/Cloudflare/StateStore/Store.js +6 -4
  61. package/lib/Cloudflare/StateStore/Store.js.map +1 -1
  62. package/lib/Cloudflare/StateStore/Token.d.ts +6 -4
  63. package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -1
  64. package/lib/Cloudflare/StateStore/Token.js +8 -4
  65. package/lib/Cloudflare/StateStore/Token.js.map +1 -1
  66. package/lib/Cloudflare/index.d.ts +1 -0
  67. package/lib/Cloudflare/index.d.ts.map +1 -1
  68. package/lib/Cloudflare/index.js +1 -0
  69. package/lib/Cloudflare/index.js.map +1 -1
  70. package/lib/Deploy.d.ts +1 -1
  71. package/lib/Deploy.d.ts.map +1 -1
  72. package/lib/Destroy.d.ts +1 -1
  73. package/lib/Destroy.d.ts.map +1 -1
  74. package/lib/GitHub/Providers.d.ts +3 -0
  75. package/lib/GitHub/Providers.d.ts.map +1 -0
  76. package/lib/GitHub/Providers.js +5 -0
  77. package/lib/GitHub/Providers.js.map +1 -0
  78. package/lib/GitHub/index.d.ts +1 -0
  79. package/lib/GitHub/index.d.ts.map +1 -1
  80. package/lib/GitHub/index.js +1 -0
  81. package/lib/GitHub/index.js.map +1 -1
  82. package/lib/Output.d.ts.map +1 -1
  83. package/lib/Output.js +4 -0
  84. package/lib/Output.js.map +1 -1
  85. package/lib/Plan.d.ts.map +1 -1
  86. package/lib/Plan.js +4 -0
  87. package/lib/Plan.js.map +1 -1
  88. package/lib/Stack.d.ts +5 -4
  89. package/lib/Stack.d.ts.map +1 -1
  90. package/lib/Stack.js +27 -8
  91. package/lib/Stack.js.map +1 -1
  92. package/lib/State/HttpStateStore.js +1 -1
  93. package/lib/State/HttpStateStore.js.map +1 -1
  94. package/lib/Test/Bun.d.ts +2 -2
  95. package/lib/Test/Bun.d.ts.map +1 -1
  96. package/lib/Test/Bun.js +2 -4
  97. package/lib/Test/Bun.js.map +1 -1
  98. package/lib/Test/Vitest.d.ts +1 -1
  99. package/lib/Test/Vitest.d.ts.map +1 -1
  100. package/lib/Test/Vitest.js +11 -7
  101. package/lib/Test/Vitest.js.map +1 -1
  102. package/lib/cli/index.d.ts +2 -6
  103. package/lib/cli/index.d.ts.map +1 -1
  104. package/lib/cli/index.js +2 -43050
  105. package/lib/cli/index.js.map +1 -1
  106. package/package.json +5 -5
  107. package/src/Cli/InkCLI.tsx +0 -10
  108. package/src/Cli/index.ts +4 -1
  109. package/src/Cloudflare/ApiToken/AccountApiToken.ts +197 -0
  110. package/src/Cloudflare/ApiToken/Common.ts +148 -0
  111. package/src/Cloudflare/ApiToken/PermissionGroups.ts +1910 -0
  112. package/src/Cloudflare/ApiToken/UserApiToken.ts +168 -0
  113. package/src/Cloudflare/ApiToken/index.ts +4 -0
  114. package/src/Cloudflare/Auth/AuthProvider.ts +2 -2
  115. package/src/Cloudflare/Providers.ts +5 -0
  116. package/src/Cloudflare/StateStore/Api.ts +6 -4
  117. package/src/Cloudflare/StateStore/Store.ts +6 -4
  118. package/src/Cloudflare/StateStore/Token.ts +10 -4
  119. package/src/Cloudflare/index.ts +1 -0
  120. package/src/GitHub/Providers.ts +6 -0
  121. package/src/GitHub/index.ts +1 -0
  122. package/src/Output.ts +3 -0
  123. package/src/Plan.ts +3 -0
  124. package/src/Stack.ts +33 -7
  125. package/src/State/HttpStateStore.ts +1 -0
  126. package/src/Test/Bun.ts +2 -2
  127. package/src/Test/Vitest.ts +11 -7
package/bin/alchemy.js CHANGED
@@ -8,6 +8,15 @@ import * as EffectContext from "effect/Context";
8
8
  import * as FileSystem from "effect/FileSystem";
9
9
  import * as Path$1 from "effect/Path";
10
10
  import { Path } from "effect/Path";
11
+ import { PassThrough, Stream } from "node:stream";
12
+ import process$1, { cwd, env } from "node:process";
13
+ import os from "node:os";
14
+ import { execFileSync } from "node:child_process";
15
+ import * as fs$1 from "node:fs";
16
+ import fs from "node:fs";
17
+ import tty from "node:tty";
18
+ import { EventEmitter } from "node:events";
19
+ import { Buffer as Buffer$1 } from "node:buffer";
11
20
  import * as Cause from "effect/Cause";
12
21
  import * as Config from "effect/Config";
13
22
  import * as Console from "effect/Console";
@@ -28,18 +37,9 @@ import * as Schedule from "effect/Schedule";
28
37
  import { Credentials, fromAwsCredentialIdentity } from "@distilled.cloud/aws/Credentials";
29
38
  import * as Deferred from "effect/Deferred";
30
39
  import { pipe } from "effect/Function";
40
+ import * as Redacted from "effect/Redacted";
31
41
  import { SingleShotGen } from "effect/Utils";
32
42
  import { pipeArguments } from "effect/Pipeable";
33
- import { PassThrough, Stream } from "node:stream";
34
- import process$1, { cwd, env } from "node:process";
35
- import os from "node:os";
36
- import { execFileSync } from "node:child_process";
37
- import * as fs$1 from "node:fs";
38
- import fs from "node:fs";
39
- import tty from "node:tty";
40
- import { EventEmitter } from "node:events";
41
- import { Buffer as Buffer$1 } from "node:buffer";
42
- import * as Redacted from "effect/Redacted";
43
43
  import { Argument, Command, Flag } from "effect/unstable/cli";
44
44
  import * as Stream$1 from "effect/Stream";
45
45
 
@@ -74,7 +74,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
74
74
 
75
75
  //#endregion
76
76
  //#region package.json
77
- var version = "2.0.0-beta.14";
77
+ var version = "2.0.0-beta.15";
78
78
 
79
79
  //#endregion
80
80
  //#region src/AlchemyContext.ts
@@ -89,582 +89,6 @@ const AlchemyContextLive = Layer.effect(AlchemyContext, Effect.gen(function* ()
89
89
  };
90
90
  }));
91
91
 
92
- //#endregion
93
- //#region src/Util/PlatformServices.ts
94
- const isBun$1 = typeof Bun !== "undefined";
95
- const PlatformServices = Effect.promise(async () => {
96
- if (isBun$1) {
97
- const [BunServices, BunSocket] = await Promise.all([import("@effect/platform-bun/BunServices"), import("@effect/platform-bun/BunSocket")]);
98
- return Layer.merge(BunServices.layer, BunSocket.layerWebSocketConstructor);
99
- } else {
100
- const [NodeServices, NodeSocket] = await Promise.all([import("@effect/platform-node/NodeServices"), import("@effect/platform-node/NodeSocket")]);
101
- return Layer.merge(NodeServices.layer, NodeSocket.layerWebSocketConstructor);
102
- }
103
- }).pipe(Layer.unwrap);
104
- const runMain = (effect, options) => {
105
- if (isBun$1) import("@effect/platform-bun/BunRuntime").then((BunRuntime) => BunRuntime.runMain(effect, options));
106
- else import("@effect/platform-node/NodeRuntime").then((NodeRuntime) => NodeRuntime.runMain(effect, options));
107
- };
108
-
109
- //#endregion
110
- //#region src/Auth/AuthProvider.ts
111
- var AuthError = class extends Schema.TaggedErrorClass()("AuthError", {
112
- message: Schema.String,
113
- cause: Schema.optional(Schema.Defect)
114
- }) {};
115
- var AuthProviders = class extends EffectContext.Service()("AuthProviders") {};
116
-
117
- //#endregion
118
- //#region src/Util/Clank.ts
119
- var PromptCancelled = class extends Data.TaggedError("PromptCancelled") {};
120
- const retryIfCancelled = Effect.retry({ while: (e) => e instanceof PromptCancelled });
121
-
122
- //#endregion
123
- //#region bin/commands/_shared.ts
124
- const USER = Config.string("USER").pipe(Config.orElse(() => Config.string("USERNAME")), Config.withDefault("unknown"));
125
- const STAGE = Config.string("stage").pipe(Config.option, (a) => a.asEffect(), Effect.map(Option.getOrUndefined));
126
- /**
127
- * `true` if `e` is a {@link PromptCancelled}, or an {@link AuthError} whose
128
- * `cause` chain bottoms out in one. Schema-tagged errors don't always
129
- * survive `instanceof` across module boundaries, so we also accept any
130
- * object whose `_tag` matches.
131
- */
132
- const isPromptCancellation = (e) => {
133
- for (let cur = e, i = 0; cur != null && i < 16; i++) {
134
- if (cur instanceof PromptCancelled) return true;
135
- if (typeof cur === "object" && cur._tag === "PromptCancelled") return true;
136
- if (cur instanceof AuthError || typeof cur === "object" && cur._tag === "AuthError") {
137
- cur = cur.cause;
138
- continue;
139
- }
140
- return false;
141
- }
142
- return false;
143
- };
144
- /**
145
- * Catches user cancellations (Ctrl+C inside a prompt, surfaced as
146
- * {@link PromptCancelled} or wrapped in an {@link AuthError}) and exits
147
- * the CLI cleanly with a friendly message instead of dumping a stack
148
- * trace.
149
- */
150
- const handleCancellation = (self$1) => self$1.pipe(Effect.catchCause((cause) => {
151
- return cause.reasons.some((r) => {
152
- if (Cause.isFailReason(r)) return isPromptCancellation(r.error);
153
- if (Cause.isDieReason(r)) return isPromptCancellation(r.defect);
154
- return false;
155
- }) ? Console.log("\nCancelled.") : Effect.failCause(cause);
156
- }), Effect.onInterrupt(() => Console.log("\nInterrupted.")));
157
- const stage = Flag$1.string("stage").pipe(Flag$1.withSchema(Schema.String.check(Schema.isPattern(/^[a-z0-9]+([-_a-z0-9]+)*$/gi))), Flag$1.withDescription("Stage to deploy to, defaults to dev_${USER}"), Flag$1.optional, Flag$1.map(Option.getOrUndefined), Flag$1.mapEffect(Effect.fn(function* (stage$1) {
158
- if (stage$1) return stage$1;
159
- return yield* STAGE.pipe(Effect.catch(() => Effect.fail(new CliError.MissingOption({ option: "stage" }))), Effect.flatMap((s) => s === void 0 ? USER.asEffect().pipe(Effect.map((user) => `dev_${user}`), Effect.catch(() => Effect.succeed("unknown"))) : Effect.succeed(s)));
160
- })));
161
- const envFile = Flag$1.file("env-file").pipe(Flag$1.optional, Flag$1.withDescription("File to load environment variables from, defaults to .env"));
162
- const dryRun = Flag$1.boolean("dry-run").pipe(Flag$1.withDescription("Dry run the deployment, do not actually deploy"), Flag$1.withDefault(false));
163
- const yes = Flag$1.boolean("yes").pipe(Flag$1.withDescription("Yes to all prompts"), Flag$1.withDefault(false));
164
- const force = Flag$1.boolean("force").pipe(Flag$1.withDescription("Force updates for resources that would otherwise no-op"), Flag$1.withDefault(false));
165
- const main = Argument$1.file("main", { mustExist: true }).pipe(Argument$1.withDescription("Main file to deploy, defaults to alchemy.run.ts"), Argument$1.withDefault("alchemy.run.ts"));
166
- const profile = Flag$1.string("profile").pipe(Flag$1.withDescription("Auth profile to use (~/.alchemy/profiles.json). Defaults to 'default' or $ALCHEMY_PROFILE."), Flag$1.optional, Flag$1.map(Option.getOrElse(() => "default")));
167
- const resourceFilter = Flag$1.string("filter").pipe(Flag$1.withDescription("Comma-separated logical resource IDs (e.g. Api,Sandbox). Only those resources are included."), Flag$1.optional, Flag$1.map(Option.getOrUndefined));
168
- const TAIL_COLORS = [
169
- "\x1B[36m",
170
- "\x1B[35m",
171
- "\x1B[33m",
172
- "\x1B[32m",
173
- "\x1B[34m",
174
- "\x1B[91m",
175
- "\x1B[96m",
176
- "\x1B[95m",
177
- "\x1B[93m",
178
- "\x1B[92m"
179
- ];
180
- const TAIL_RESET = "\x1B[0m";
181
- const formatLocalTimestamp = (date) => {
182
- return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}.${String(date.getMilliseconds()).padStart(3, "0")} ${new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value ?? ""}`;
183
- };
184
- const parseResourceFilter = (filter) => {
185
- if (filter === void 0) return void 0;
186
- const ids = filter.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
187
- if (ids.length === 0) return void 0;
188
- return new Set(ids);
189
- };
190
- const parseSince = (value) => {
191
- const match = value.match(/^(\d+)([smhd])$/);
192
- if (match) {
193
- const num = parseInt(match[1], 10);
194
- const unit = match[2];
195
- const ms = unit === "s" ? num * 1e3 : unit === "m" ? num * 6e4 : unit === "h" ? num * 36e5 : num * 864e5;
196
- return new Date(Date.now() - ms);
197
- }
198
- const parsed = new Date(value);
199
- if (isNaN(parsed.getTime())) throw new Error(`Invalid --since value: '${value}'. Use a duration (e.g. '1h', '30m') or ISO date.`);
200
- return parsed;
201
- };
202
- const importStack = Effect.fn(function* (main$1) {
203
- const path = yield* Path$1.Path;
204
- const stackEffect = (yield* Effect.promise(() => import(path.resolve(process.cwd(), main$1)))).default;
205
- if (!Effect.isEffect(stackEffect)) return yield* Effect.die(/* @__PURE__ */ new Error(`Main file '${main$1}' must export a default stack definition (export default Alchemy.Stack({...}))`));
206
- return stackEffect;
207
- });
208
-
209
- //#endregion
210
- //#region src/AWS/Environment.ts
211
- const AWS_PROFILE = Config.string("AWS_PROFILE").pipe(Config.withDefault("default"));
212
- const AWS_REGION = Config.string("AWS_REGION");
213
- var AWSEnvironment = class extends EffectContext.Service()("AWS::Environment") {};
214
- /**
215
- * Build an `AWSEnvironment` from an SSO profile (`AWS_PROFILE` env var,
216
- * defaults to `"default"`). Uses the profile's `sso_account_id` and `region`,
217
- * and resolves credentials lazily via `aws sso login`.
218
- */
219
- const Default = Layer.effect(AWSEnvironment, Effect.suspend(() => loadDefault())).pipe(Layer.orDie);
220
- const loadDefault = () => Effect.gen(function* () {
221
- const profileName = yield* AWS_PROFILE;
222
- const auth = yield* Auth.Default;
223
- const profile$1 = yield* auth.loadProfile(profileName);
224
- if (!profile$1.sso_account_id) return yield* Effect.die(`AWS SSO profile '${profileName}' is missing sso_account_id`);
225
- const region = profile$1.region ?? (yield* AWS_REGION.pipe(Config.option, Config.map(Option.getOrElse(() => "us-east-1"))));
226
- return {
227
- profile: profileName,
228
- accountId: profile$1.sso_account_id,
229
- region,
230
- credentials: auth.loadProfileCredentials(profileName)
231
- };
232
- });
233
- const isStatic = (shape) => shape.credentials != null && typeof shape.credentials.accessKeyId === "string";
234
- /**
235
- * Build an `AWSEnvironment` Layer directly from values — useful for
236
- * static credentials in CI or tests.
237
- *
238
- * Named `makeEnvironment` rather than `of` because `Context.Service.of`
239
- * already exists with different semantics (builds the service value, not
240
- * a Layer); putting both on `AWSEnvironment` would be confusing.
241
- */
242
- const makeEnvironment = (shape) => Layer.succeed(AWSEnvironment, isStatic(shape) ? {
243
- ...shape,
244
- credentials: Effect.succeed(fromAwsCredentialIdentity(shape.credentials))
245
- } : shape);
246
-
247
- //#endregion
248
- //#region src/AWS/Bootstrap.ts
249
- /**
250
- * Tag key used to identify the alchemy assets bucket.
251
- */
252
- const ASSETS_BUCKET_TAG = "alchemy::assets-bucket";
253
- /**
254
- * Tag key used to scope the alchemy assets bucket to an AWS region.
255
- */
256
- const ASSETS_BUCKET_REGION_TAG = "alchemy::assets-bucket-region";
257
- /**
258
- * Build an account-regional namespace bucket name.
259
- *
260
- * Account-regional buckets must follow the naming convention:
261
- * `<prefix>-<accountId>-<region>-an`
262
- *
263
- * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html#account-regional-gp-buckets
264
- */
265
- const createAssetsBucketName = (accountId, region) => `alchemy-assets-${accountId}-${region}-an`.toLowerCase();
266
- const createAssetsBucketTags = (region) => [{
267
- Key: ASSETS_BUCKET_TAG,
268
- Value: "true"
269
- }, {
270
- Key: ASSETS_BUCKET_REGION_TAG,
271
- Value: region
272
- }];
273
- const getBucketTags = (bucketName) => s3.getBucketTagging({ Bucket: bucketName }).pipe(Effect.map((response) => response.TagSet ?? []), Effect.catchTag("NoSuchTagSet", () => Effect.succeed([])));
274
- const hasAssetsBucketTag = (tags) => tags.some((tag) => tag.Key === ASSETS_BUCKET_TAG && tag.Value === "true");
275
- const normalizeBucketRegion = (location) => {
276
- if (!location) return "us-east-1";
277
- if (location === "EU") return "eu-west-1";
278
- return location;
279
- };
280
- const bucketMatchesRegion = (tags, region) => {
281
- const taggedRegion = tags.find((tag) => tag.Key === ASSETS_BUCKET_REGION_TAG)?.Value;
282
- return taggedRegion === void 0 ? void 0 : taggedRegion === region;
283
- };
284
- const lookupAssetsBuckets = Effect.gen(function* () {
285
- const region = yield* Region;
286
- const buckets = (yield* s3.listBuckets({})).Buckets ?? [];
287
- const matchingBuckets = [];
288
- for (const bucket of buckets) {
289
- const bucketName = bucket.Name;
290
- if (!bucketName) continue;
291
- const tags = yield* getBucketTags(bucketName).pipe(Effect.catch(() => Effect.succeed([])));
292
- if (!hasAssetsBucketTag(tags)) continue;
293
- const taggedRegionMatch = bucketMatchesRegion(tags, region);
294
- if (taggedRegionMatch === true) {
295
- matchingBuckets.push(bucketName);
296
- continue;
297
- }
298
- if (taggedRegionMatch === false) continue;
299
- if ((yield* s3.getBucketLocation({ Bucket: bucketName }).pipe(Effect.map((response) => normalizeBucketRegion(response.LocationConstraint)), Effect.catch(() => Effect.succeed(void 0)))) === region) matchingBuckets.push(bucketName);
300
- }
301
- return matchingBuckets;
302
- });
303
- const lookupAssetsBucket = Effect.gen(function* () {
304
- const matchingBuckets = yield* lookupAssetsBuckets;
305
- for (const bucketName of matchingBuckets) return Option.some(bucketName);
306
- return Option.none();
307
- });
308
- const ensureAssetsBucketTags = Effect.fn(function* (bucketName, region) {
309
- const tagSet = [...(yield* getBucketTags(bucketName)).filter((tag) => tag.Key !== ASSETS_BUCKET_TAG && tag.Key !== ASSETS_BUCKET_REGION_TAG), ...createAssetsBucketTags(region)];
310
- yield* s3.putBucketTagging({
311
- Bucket: bucketName,
312
- Tagging: { TagSet: tagSet.map((tag) => ({
313
- Key: tag.Key,
314
- Value: tag.Value
315
- })) }
316
- });
317
- });
318
- const deleteAllObjects = Effect.fn(function* (bucketName) {
319
- let continuationToken;
320
- do {
321
- const listResponse = yield* s3.listObjectsV2({
322
- Bucket: bucketName,
323
- ContinuationToken: continuationToken
324
- });
325
- if (listResponse.Contents && listResponse.Contents.length > 0) yield* s3.deleteObjects({
326
- Bucket: bucketName,
327
- Delete: {
328
- Objects: listResponse.Contents.map((obj) => ({ Key: obj.Key })),
329
- Quiet: true
330
- }
331
- });
332
- continuationToken = listResponse.NextContinuationToken;
333
- } while (continuationToken);
334
- let keyMarker;
335
- let versionIdMarker;
336
- do {
337
- const listVersionsResponse = yield* s3.listObjectVersions({
338
- Bucket: bucketName,
339
- KeyMarker: keyMarker,
340
- VersionIdMarker: versionIdMarker
341
- });
342
- const objectsToDelete = [...(listVersionsResponse.Versions ?? []).map((v) => ({
343
- Key: v.Key,
344
- VersionId: v.VersionId
345
- })), ...(listVersionsResponse.DeleteMarkers ?? []).map((dm) => ({
346
- Key: dm.Key,
347
- VersionId: dm.VersionId
348
- }))];
349
- if (objectsToDelete.length > 0) yield* s3.deleteObjects({
350
- Bucket: bucketName,
351
- Delete: {
352
- Objects: objectsToDelete,
353
- Quiet: true
354
- }
355
- });
356
- keyMarker = listVersionsResponse.NextKeyMarker;
357
- versionIdMarker = listVersionsResponse.NextVersionIdMarker;
358
- } while (keyMarker);
359
- });
360
- /**
361
- * Bootstrap the AWS environment by creating the assets bucket.
362
- *
363
- * This is idempotent - running it multiple times is safe.
364
- * The bucket is tagged and later discovered by tag lookup instead of by name.
365
- */
366
- const bootstrap = Effect.fn(function* () {
367
- const region = yield* Region;
368
- const existingBucket = yield* lookupAssetsBucket;
369
- if (Option.isSome(existingBucket)) {
370
- yield* ensureAssetsBucketTags(existingBucket.value, region);
371
- yield* Effect.logInfo(`Assets bucket already exists: ${existingBucket.value}`);
372
- return {
373
- bucketName: existingBucket.value,
374
- created: false
375
- };
376
- }
377
- const { accountId } = yield* AWSEnvironment;
378
- const bucketName = createAssetsBucketName(accountId, region);
379
- yield* s3.createBucket({
380
- Bucket: bucketName,
381
- BucketNamespace: "account-regional",
382
- CreateBucketConfiguration: {
383
- Tags: createAssetsBucketTags(region),
384
- ...region === "us-east-1" ? {} : { LocationConstraint: region }
385
- }
386
- }).pipe(Effect.catchTag("BucketAlreadyOwnedByYou", () => Effect.void), Effect.retry({
387
- while: (e) => e._tag === "OperationAborted" || e._tag === "ServiceUnavailable",
388
- schedule: Schedule.exponential(100)
389
- }));
390
- yield* s3.headBucket({ Bucket: bucketName }).pipe(Effect.retry({ schedule: Schedule.exponential(100).pipe(Schedule.both(Schedule.recurs(10))) }));
391
- yield* Effect.logInfo(`Created assets bucket: ${bucketName}`);
392
- return {
393
- bucketName,
394
- created: true
395
- };
396
- });
397
- const destroyBootstrap = Effect.fn(function* () {
398
- const bucketNames = yield* lookupAssetsBuckets;
399
- for (const bucketName of bucketNames) {
400
- yield* Effect.logInfo(`Destroying assets bucket: ${bucketName}`);
401
- yield* deleteAllObjects(bucketName);
402
- yield* s3.deleteBucket({ Bucket: bucketName }).pipe(Effect.retry({
403
- while: (e) => e._tag === "OperationAborted" || e._tag === "ServiceUnavailable",
404
- schedule: Schedule.exponential(100).pipe(Schedule.both(Schedule.recurs(10)))
405
- }));
406
- }
407
- return {
408
- bucketNames,
409
- destroyed: bucketNames.length
410
- };
411
- });
412
-
413
- //#endregion
414
- //#region src/AWS/Credentials.ts
415
- /**
416
- * Lazy `Credentials` layer derived from the surrounding {@link AWSEnvironment}.
417
- * Credentials are resolved on first access (not during layer construction),
418
- * matching the existing @distilled.cloud/aws semantics.
419
- */
420
- const fromEnvironment$1 = Layer.effect(Credentials, Effect.map(AWSEnvironment.asEffect(), (env$2) => env$2.credentials));
421
-
422
- //#endregion
423
- //#region src/AWS/Region.ts
424
- /**
425
- * Derive the AWS region from the surrounding {@link AWSEnvironment}.
426
- */
427
- const fromEnvironment = Layer.effect(Region$1.Region, Effect.map(AWSEnvironment.asEffect(), (env$2) => env$2.region));
428
-
429
- //#endregion
430
- //#region src/Util/ConfigProvider.ts
431
- const loadConfigProvider = (envFile$1) => {
432
- if (Option.isSome(envFile$1)) return ConfigProvider.fromDotEnv({ path: envFile$1.value }).pipe(Effect.map((dotEnv) => ConfigProvider.orElse(dotEnv, ConfigProvider.fromEnv())));
433
- return Effect.gen(function* () {
434
- if (!(yield* (yield* FileSystem.FileSystem).exists(".env"))) return ConfigProvider.fromEnv();
435
- return ConfigProvider.orElse(yield* ConfigProvider.fromDotEnv({ path: ".env" }), ConfigProvider.fromEnv());
436
- });
437
- };
438
-
439
- //#endregion
440
- //#region src/Util/FileLogger.ts
441
- const fileLogger = Effect.fnUntraced(function* (...segments) {
442
- const { dotAlchemy } = yield* AlchemyContext;
443
- const fs$2 = yield* FileSystem.FileSystem;
444
- const path = yield* Path;
445
- const logFile = path.join(dotAlchemy, "log", ...segments);
446
- yield* fs$2.makeDirectory(path.dirname(logFile), { recursive: true });
447
- return yield* Logger.formatLogFmt.pipe(Logger.toFile(logFile, { flag: "a" }));
448
- });
449
-
450
- //#endregion
451
- //#region bin/commands/bootstrap.ts
452
- const awsProfile = Flag$1.string("profile").pipe(Flag$1.withDescription("AWS profile to use for credentials"), Flag$1.optional, Flag$1.map(Option.getOrElse(() => "default")));
453
- const awsRegion = Flag$1.string("region").pipe(Flag$1.withDescription("AWS region to bootstrap (defaults to AWS_REGION env var)"), Flag$1.optional, Flag$1.map(Option.getOrUndefined));
454
- const bootstrapDestroy = Flag$1.boolean("destroy").pipe(Flag$1.withDescription("Destroy all bootstrap buckets in the selected region"), Flag$1.withDefault(false));
455
- const bootstrapCommand = Command$1.make("bootstrap", {
456
- envFile,
457
- profile: awsProfile,
458
- region: awsRegion,
459
- destroy: bootstrapDestroy
460
- }, Effect.fnUntraced(function* ({ envFile: envFile$1, profile: profile$1, region, destroy }) {
461
- const logger = Logger.layer([fileLogger("bootstrap.txt")]);
462
- return yield* Effect.gen(function* () {
463
- const ssoProfile = yield* Auth.loadProfile(profile$1);
464
- if (!ssoProfile.sso_account_id) return yield* Effect.die(`AWS SSO profile '${profile$1}' is missing sso_account_id`);
465
- const ambient = yield* Effect.context();
466
- const environment = makeEnvironment({
467
- accountId: ssoProfile.sso_account_id,
468
- region: region ?? ssoProfile.region ?? "us-east-1",
469
- credentials: Auth.loadProfileCredentials(profile$1).pipe(Effect.provide(ambient)),
470
- profile: profile$1
471
- });
472
- const awsLayers = Layer.provideMerge(Layer.mergeAll(fromEnvironment, fromEnvironment$1), environment);
473
- return yield* Effect.gen(function* () {
474
- const provider = yield* loadConfigProvider(envFile$1);
475
- const bootstrapLayer = Layer.provide(awsLayers, Layer.succeed(ConfigProvider.ConfigProvider, provider));
476
- if (destroy) {
477
- yield* destroyBootstrap().pipe(Effect.tap((result) => result.destroyed === 0 ? Console.log("✓ No bootstrap buckets found to destroy") : Console.log(`✓ Destroyed ${result.destroyed} bootstrap bucket(s): ${result.bucketNames.join(", ")}`)), Effect.provide(bootstrapLayer));
478
- return;
479
- }
480
- yield* bootstrap().pipe(Effect.tap(({ bucketName, created }) => created ? Console.log(`✓ Created assets bucket: ${bucketName}`) : Console.log(`✓ Assets bucket already exists: ${bucketName}`)), Effect.provide(bootstrapLayer));
481
- });
482
- }).pipe(Effect.provide(logger));
483
- }));
484
-
485
- //#endregion
486
- //#region src/Artifacts.ts
487
- /**
488
- * Per-resource in-memory artifacts shared across a single `Plan.make -> apply`
489
- * execution.
490
- *
491
- * The engine scopes this service by resource `FQN` before invoking lifecycle
492
- * handlers, so providers should treat it as a resource-local bag for expensive,
493
- * deterministic intermediate results that can be reused across phases.
494
- *
495
- * Expected usage:
496
- *
497
- * - `diff` computes an expensive artifact once and stores it
498
- * - `create` / `update` reads the same artifact and skips recomputing it
499
- * - artifacts are ephemeral and must never be required for correctness on a
500
- * later deploy because the bag is reset between runs
501
- *
502
- * Example:
503
- *
504
- * ```ts
505
- * const artifacts = yield* Artifacts;
506
- * const cached = yield* artifacts.get<PreparedBundle>("bundle");
507
- * if (cached) return cached;
508
- *
509
- * const bundle = yield* prepareBundle();
510
- * yield* artifacts.set("bundle", bundle);
511
- * return bundle;
512
- * ```
513
- */
514
- var Artifacts = class extends EffectContext.Service()("Artifacts") {};
515
- var ArtifactStore = class extends EffectContext.Service()("Artifacts/Store") {};
516
- /**
517
- * Create a fresh root store for one deploy/test run.
518
- */
519
- const createArtifactStore = () => /* @__PURE__ */ new Map();
520
- const getOrCreateBag = (store, fqn) => {
521
- const existing = store.get(fqn);
522
- if (existing) return existing;
523
- const bag = /* @__PURE__ */ new Map();
524
- store.set(fqn, bag);
525
- return bag;
526
- };
527
- const makeScopedArtifacts = (store, fqn) => {
528
- const bag = getOrCreateBag(store, fqn);
529
- return {
530
- get: (key) => Effect.sync(() => bag.get(key)),
531
- set: (key, value) => Effect.sync(() => {
532
- bag.set(key, value);
533
- }),
534
- delete: (key) => Effect.sync(() => {
535
- bag.delete(key);
536
- })
537
- };
538
- };
539
- /**
540
- * Ensure an artifact root exists, reusing the ambient store when one is already
541
- * present. This lets nested helpers participate in the same run-scoped cache.
542
- */
543
- const ensureArtifactStore = (effect) => Effect.serviceOption(ArtifactStore).pipe(Effect.map(Option.getOrUndefined), Effect.flatMap((existing) => effect.pipe(Effect.provideService(ArtifactStore, existing ?? createArtifactStore()))));
544
-
545
- //#endregion
546
- //#region src/Cli/Cli.ts
547
- var Cli = class extends EffectContext.Service()("CLI") {};
548
-
549
- //#endregion
550
- //#region src/Util/service.ts
551
- const GenericService = () => (Kind) => {
552
- const service = EffectContext.Service(Kind);
553
- const make$2 = (Type) => EffectContext.Service(`${Kind}<${Type}>`);
554
- return Object.assign(Object.setPrototypeOf(make$2, service), service);
555
- };
556
-
557
- //#endregion
558
- //#region src/ExecutionContext.ts
559
- const ExecutionContext = GenericService()("Alchemy::ExecutionContext");
560
- const CurrentExecutionContext = Effect.serviceOption(ExecutionContext).pipe(Effect.map(Option.getOrUndefined));
561
-
562
- //#endregion
563
- //#region src/Ref.ts
564
- const RefMetadata = Symbol.for("alchemy/RefMetadata");
565
- const isRef = (s) => s && s[RefMetadata] !== void 0;
566
- const getRefMetadata = (ref$1) => ref$1[RefMetadata];
567
- const ref = ({ stack, id, stage: stage$1 }) => {
568
- const ref$1 = new Proxy({}, { get: (_, prop) => {
569
- if (prop === RefMetadata) return {
570
- stack,
571
- stage: stage$1,
572
- id
573
- };
574
- return of(ref$1)[prop];
575
- } });
576
- return ref$1;
577
- };
578
-
579
- //#endregion
580
- //#region src/FQN.ts
581
- /**
582
- * Separator used in FQN strings.
583
- */
584
- const FQN_SEPARATOR = "/";
585
- /**
586
- * Encode an FQN for safe filesystem storage.
587
- * Replaces `/` (FQN separator) with `__` to avoid subdirectory creation.
588
- */
589
- const encodeFqn = (fqn) => fqn.replaceAll(FQN_SEPARATOR, "__");
590
- /**
591
- * Decode a filename back to FQN.
592
- */
593
- const decodeFqn = (filename) => filename.replaceAll("__", FQN_SEPARATOR);
594
- /**
595
- * Convert a NamespaceNode chain to an array of namespace IDs, from root to leaf.
596
- *
597
- * @example
598
- * ```ts
599
- * const ns = { Id: "Child", Parent: { Id: "Parent", Parent: { Id: "Root" } } };
600
- * toPath(ns); // ["Root", "Parent", "Child"]
601
- * ```
602
- */
603
- const toPath = (ns) => {
604
- if (!ns) return [];
605
- const path = [];
606
- let current = ns;
607
- while (current) {
608
- path.unshift(current.Id);
609
- current = current.Parent;
610
- }
611
- return path;
612
- };
613
- /**
614
- * Create a fully-qualified name (FQN) from a namespace and logical ID.
615
- * The FQN is a flat key suitable for state storage.
616
- *
617
- * @example
618
- * ```ts
619
- * const ns = { Id: "Child", Parent: { Id: "Parent" } };
620
- * toFqn(ns, "MyResource"); // "Parent/Child/MyResource"
621
- * toFqn(undefined, "MyResource"); // "MyResource"
622
- * ```
623
- */
624
- const toFqn = (ns, logicalId) => {
625
- const path = toPath(ns);
626
- path.push(logicalId);
627
- return path.join(FQN_SEPARATOR);
628
- };
629
- /**
630
- * Parse an FQN back into namespace path segments and the logical ID.
631
- *
632
- * @example
633
- * ```ts
634
- * parseFqn("Parent/Child/MyResource"); // { path: ["Parent", "Child"], logicalId: "MyResource" }
635
- * parseFqn("MyResource"); // { path: [], logicalId: "MyResource" }
636
- * ```
637
- */
638
- const parseFqn = (fqn) => {
639
- const parts = fqn.split(FQN_SEPARATOR);
640
- return {
641
- path: parts,
642
- logicalId: parts.pop()
643
- };
644
- };
645
-
646
- //#endregion
647
- //#region src/Provider.ts
648
- const Provider = (type) => EffectContext.Service()(type);
649
- const isProviderCollectionService = (value) => {
650
- return typeof value === "object" && value !== null && "kind" in value && value.kind === "ProviderCollection";
651
- };
652
- const findProviderByType = (type) => tryFindProviderByType(type).pipe(Effect.flatMap(Option.match({
653
- onNone: () => Effect.die(`Provider not found for ${type}`),
654
- onSome: (provider) => Effect.succeed(provider)
655
- })));
656
- const tryFindProviderByType = Effect.fnUntraced(function* (resourceType) {
657
- const Tag = Provider(resourceType);
658
- const direct = yield* Effect.serviceOption(Tag);
659
- if (Option.isSome(direct)) return direct;
660
- const context = yield* Effect.context();
661
- for (const value of context.mapUnsafe.values()) if (isProviderCollectionService(value)) {
662
- const provider = value.get(resourceType);
663
- if (provider) return Option.some(provider);
664
- }
665
- return Option.none();
666
- });
667
-
668
92
  //#endregion
669
93
  //#region ../../node_modules/.bun/react@19.2.5/node_modules/react/cjs/react.production.js
670
94
  var require_react_production = /* @__PURE__ */ __commonJS({ "../../node_modules/.bun/react@19.2.5/node_modules/react/cjs/react.production.js": ((exports) => {
@@ -1882,7 +1306,7 @@ function throttle(func, throttleMs = 0, options = {}) {
1882
1306
  //#region ../../node_modules/.bun/environment@1.1.0/node_modules/environment/index.js
1883
1307
  const isBrowser = globalThis.window?.document !== void 0;
1884
1308
  const isNode = globalThis.process?.versions?.node !== void 0;
1885
- const isBun = globalThis.process?.versions?.bun !== void 0;
1309
+ const isBun$1 = globalThis.process?.versions?.bun !== void 0;
1886
1310
  const isDeno = globalThis.Deno?.version?.deno !== void 0;
1887
1311
  const isElectron = globalThis.process?.versions?.electron !== void 0;
1888
1312
  const isJsDom = globalThis.navigator?.userAgent?.includes("jsdom") === true;
@@ -42877,6 +42301,77 @@ var use_input_default = useInput;
42877
42301
  const useApp = () => (0, import_react.useContext)(AppContext_default);
42878
42302
  var use_app_default = useApp;
42879
42303
 
42304
+ //#endregion
42305
+ //#region src/Cli/Cli.ts
42306
+ var Cli = class extends EffectContext.Service()("CLI") {};
42307
+
42308
+ //#endregion
42309
+ //#region src/FQN.ts
42310
+ /**
42311
+ * Separator used in FQN strings.
42312
+ */
42313
+ const FQN_SEPARATOR = "/";
42314
+ /**
42315
+ * Encode an FQN for safe filesystem storage.
42316
+ * Replaces `/` (FQN separator) with `__` to avoid subdirectory creation.
42317
+ */
42318
+ const encodeFqn = (fqn) => fqn.replaceAll(FQN_SEPARATOR, "__");
42319
+ /**
42320
+ * Decode a filename back to FQN.
42321
+ */
42322
+ const decodeFqn = (filename) => filename.replaceAll("__", FQN_SEPARATOR);
42323
+ /**
42324
+ * Convert a NamespaceNode chain to an array of namespace IDs, from root to leaf.
42325
+ *
42326
+ * @example
42327
+ * ```ts
42328
+ * const ns = { Id: "Child", Parent: { Id: "Parent", Parent: { Id: "Root" } } };
42329
+ * toPath(ns); // ["Root", "Parent", "Child"]
42330
+ * ```
42331
+ */
42332
+ const toPath = (ns) => {
42333
+ if (!ns) return [];
42334
+ const path = [];
42335
+ let current = ns;
42336
+ while (current) {
42337
+ path.unshift(current.Id);
42338
+ current = current.Parent;
42339
+ }
42340
+ return path;
42341
+ };
42342
+ /**
42343
+ * Create a fully-qualified name (FQN) from a namespace and logical ID.
42344
+ * The FQN is a flat key suitable for state storage.
42345
+ *
42346
+ * @example
42347
+ * ```ts
42348
+ * const ns = { Id: "Child", Parent: { Id: "Parent" } };
42349
+ * toFqn(ns, "MyResource"); // "Parent/Child/MyResource"
42350
+ * toFqn(undefined, "MyResource"); // "MyResource"
42351
+ * ```
42352
+ */
42353
+ const toFqn = (ns, logicalId) => {
42354
+ const path = toPath(ns);
42355
+ path.push(logicalId);
42356
+ return path.join(FQN_SEPARATOR);
42357
+ };
42358
+ /**
42359
+ * Parse an FQN back into namespace path segments and the logical ID.
42360
+ *
42361
+ * @example
42362
+ * ```ts
42363
+ * parseFqn("Parent/Child/MyResource"); // { path: ["Parent", "Child"], logicalId: "MyResource" }
42364
+ * parseFqn("MyResource"); // { path: [], logicalId: "MyResource" }
42365
+ * ```
42366
+ */
42367
+ const parseFqn = (fqn) => {
42368
+ const parts = fqn.split(FQN_SEPARATOR);
42369
+ return {
42370
+ path: parts,
42371
+ logicalId: parts.pop()
42372
+ };
42373
+ };
42374
+
42880
42375
  //#endregion
42881
42376
  //#region src/Cli/NamespaceTree.ts
42882
42377
  function buildNamespaceTree(items) {
@@ -43645,6 +43140,511 @@ const startApplySession = Effect.fn(function* (plan) {
43645
43140
  };
43646
43141
  });
43647
43142
 
43143
+ //#endregion
43144
+ //#region src/Util/PlatformServices.ts
43145
+ const isBun = typeof Bun !== "undefined";
43146
+ const PlatformServices = Effect.promise(async () => {
43147
+ if (isBun) {
43148
+ const [BunServices, BunSocket] = await Promise.all([import("@effect/platform-bun/BunServices"), import("@effect/platform-bun/BunSocket")]);
43149
+ return Layer.merge(BunServices.layer, BunSocket.layerWebSocketConstructor);
43150
+ } else {
43151
+ const [NodeServices, NodeSocket] = await Promise.all([import("@effect/platform-node/NodeServices"), import("@effect/platform-node/NodeSocket")]);
43152
+ return Layer.merge(NodeServices.layer, NodeSocket.layerWebSocketConstructor);
43153
+ }
43154
+ }).pipe(Layer.unwrap);
43155
+ const runMain = (effect, options) => {
43156
+ if (isBun) import("@effect/platform-bun/BunRuntime").then((BunRuntime) => BunRuntime.runMain(effect, options));
43157
+ else import("@effect/platform-node/NodeRuntime").then((NodeRuntime) => NodeRuntime.runMain(effect, options));
43158
+ };
43159
+
43160
+ //#endregion
43161
+ //#region src/Auth/AuthProvider.ts
43162
+ var AuthError = class extends Schema.TaggedErrorClass()("AuthError", {
43163
+ message: Schema.String,
43164
+ cause: Schema.optional(Schema.Defect)
43165
+ }) {};
43166
+ var AuthProviders = class extends EffectContext.Service()("AuthProviders") {};
43167
+
43168
+ //#endregion
43169
+ //#region src/Util/Clank.ts
43170
+ var PromptCancelled = class extends Data.TaggedError("PromptCancelled") {};
43171
+ const retryIfCancelled = Effect.retry({ while: (e) => e instanceof PromptCancelled });
43172
+
43173
+ //#endregion
43174
+ //#region bin/commands/_shared.ts
43175
+ const USER = Config.string("USER").pipe(Config.orElse(() => Config.string("USERNAME")), Config.withDefault("unknown"));
43176
+ const STAGE = Config.string("stage").pipe(Config.option, (a) => a.asEffect(), Effect.map(Option.getOrUndefined));
43177
+ /**
43178
+ * `true` if `e` is a {@link PromptCancelled}, or an {@link AuthError} whose
43179
+ * `cause` chain bottoms out in one. Schema-tagged errors don't always
43180
+ * survive `instanceof` across module boundaries, so we also accept any
43181
+ * object whose `_tag` matches.
43182
+ */
43183
+ const isPromptCancellation = (e) => {
43184
+ for (let cur = e, i = 0; cur != null && i < 16; i++) {
43185
+ if (cur instanceof PromptCancelled) return true;
43186
+ if (typeof cur === "object" && cur._tag === "PromptCancelled") return true;
43187
+ if (cur instanceof AuthError || typeof cur === "object" && cur._tag === "AuthError") {
43188
+ cur = cur.cause;
43189
+ continue;
43190
+ }
43191
+ return false;
43192
+ }
43193
+ return false;
43194
+ };
43195
+ /**
43196
+ * Catches user cancellations (Ctrl+C inside a prompt, surfaced as
43197
+ * {@link PromptCancelled} or wrapped in an {@link AuthError}) and exits
43198
+ * the CLI cleanly with a friendly message instead of dumping a stack
43199
+ * trace.
43200
+ */
43201
+ const handleCancellation = (self$1) => self$1.pipe(Effect.catchCause((cause) => {
43202
+ return cause.reasons.some((r) => {
43203
+ if (Cause.isFailReason(r)) return isPromptCancellation(r.error);
43204
+ if (Cause.isDieReason(r)) return isPromptCancellation(r.defect);
43205
+ return false;
43206
+ }) ? Console.log("\nCancelled.") : Effect.failCause(cause);
43207
+ }), Effect.onInterrupt(() => Console.log("\nInterrupted.")));
43208
+ const stage = Flag$1.string("stage").pipe(Flag$1.withSchema(Schema.String.check(Schema.isPattern(/^[a-z0-9]+([-_a-z0-9]+)*$/gi))), Flag$1.withDescription("Stage to deploy to, defaults to dev_${USER}"), Flag$1.optional, Flag$1.map(Option.getOrUndefined), Flag$1.mapEffect(Effect.fn(function* (stage$1) {
43209
+ if (stage$1) return stage$1;
43210
+ return yield* STAGE.pipe(Effect.catch(() => Effect.fail(new CliError.MissingOption({ option: "stage" }))), Effect.flatMap((s) => s === void 0 ? USER.asEffect().pipe(Effect.map((user) => `dev_${user}`), Effect.catch(() => Effect.succeed("unknown"))) : Effect.succeed(s)));
43211
+ })));
43212
+ const envFile = Flag$1.file("env-file").pipe(Flag$1.optional, Flag$1.withDescription("File to load environment variables from, defaults to .env"));
43213
+ const dryRun = Flag$1.boolean("dry-run").pipe(Flag$1.withDescription("Dry run the deployment, do not actually deploy"), Flag$1.withDefault(false));
43214
+ const yes = Flag$1.boolean("yes").pipe(Flag$1.withDescription("Yes to all prompts"), Flag$1.withDefault(false));
43215
+ const force = Flag$1.boolean("force").pipe(Flag$1.withDescription("Force updates for resources that would otherwise no-op"), Flag$1.withDefault(false));
43216
+ const main = Argument$1.file("main", { mustExist: true }).pipe(Argument$1.withDescription("Main file to deploy, defaults to alchemy.run.ts"), Argument$1.withDefault("alchemy.run.ts"));
43217
+ const profile = Flag$1.string("profile").pipe(Flag$1.withDescription("Auth profile to use (~/.alchemy/profiles.json). Defaults to 'default' or $ALCHEMY_PROFILE."), Flag$1.optional, Flag$1.map(Option.getOrElse(() => "default")));
43218
+ const resourceFilter = Flag$1.string("filter").pipe(Flag$1.withDescription("Comma-separated logical resource IDs (e.g. Api,Sandbox). Only those resources are included."), Flag$1.optional, Flag$1.map(Option.getOrUndefined));
43219
+ const TAIL_COLORS = [
43220
+ "\x1B[36m",
43221
+ "\x1B[35m",
43222
+ "\x1B[33m",
43223
+ "\x1B[32m",
43224
+ "\x1B[34m",
43225
+ "\x1B[91m",
43226
+ "\x1B[96m",
43227
+ "\x1B[95m",
43228
+ "\x1B[93m",
43229
+ "\x1B[92m"
43230
+ ];
43231
+ const TAIL_RESET = "\x1B[0m";
43232
+ const formatLocalTimestamp = (date) => {
43233
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}.${String(date.getMilliseconds()).padStart(3, "0")} ${new Intl.DateTimeFormat("en-US", { timeZoneName: "short" }).formatToParts(date).find((p) => p.type === "timeZoneName")?.value ?? ""}`;
43234
+ };
43235
+ const parseResourceFilter = (filter) => {
43236
+ if (filter === void 0) return void 0;
43237
+ const ids = filter.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
43238
+ if (ids.length === 0) return void 0;
43239
+ return new Set(ids);
43240
+ };
43241
+ const parseSince = (value) => {
43242
+ const match = value.match(/^(\d+)([smhd])$/);
43243
+ if (match) {
43244
+ const num = parseInt(match[1], 10);
43245
+ const unit = match[2];
43246
+ const ms = unit === "s" ? num * 1e3 : unit === "m" ? num * 6e4 : unit === "h" ? num * 36e5 : num * 864e5;
43247
+ return new Date(Date.now() - ms);
43248
+ }
43249
+ const parsed = new Date(value);
43250
+ if (isNaN(parsed.getTime())) throw new Error(`Invalid --since value: '${value}'. Use a duration (e.g. '1h', '30m') or ISO date.`);
43251
+ return parsed;
43252
+ };
43253
+ const importStack = Effect.fn(function* (main$1) {
43254
+ const path = yield* Path$1.Path;
43255
+ const stackEffect = (yield* Effect.promise(() => import(path.resolve(process.cwd(), main$1)))).default;
43256
+ if (!Effect.isEffect(stackEffect)) return yield* Effect.die(/* @__PURE__ */ new Error(`Main file '${main$1}' must export a default stack definition (export default Alchemy.Stack({...}))`));
43257
+ return stackEffect;
43258
+ });
43259
+
43260
+ //#endregion
43261
+ //#region src/AWS/Environment.ts
43262
+ const AWS_PROFILE = Config.string("AWS_PROFILE").pipe(Config.withDefault("default"));
43263
+ const AWS_REGION = Config.string("AWS_REGION");
43264
+ var AWSEnvironment = class extends EffectContext.Service()("AWS::Environment") {};
43265
+ /**
43266
+ * Build an `AWSEnvironment` from an SSO profile (`AWS_PROFILE` env var,
43267
+ * defaults to `"default"`). Uses the profile's `sso_account_id` and `region`,
43268
+ * and resolves credentials lazily via `aws sso login`.
43269
+ */
43270
+ const Default = Layer.effect(AWSEnvironment, Effect.suspend(() => loadDefault())).pipe(Layer.orDie);
43271
+ const loadDefault = () => Effect.gen(function* () {
43272
+ const profileName = yield* AWS_PROFILE;
43273
+ const auth = yield* Auth.Default;
43274
+ const profile$1 = yield* auth.loadProfile(profileName);
43275
+ if (!profile$1.sso_account_id) return yield* Effect.die(`AWS SSO profile '${profileName}' is missing sso_account_id`);
43276
+ const region = profile$1.region ?? (yield* AWS_REGION.pipe(Config.option, Config.map(Option.getOrElse(() => "us-east-1"))));
43277
+ return {
43278
+ profile: profileName,
43279
+ accountId: profile$1.sso_account_id,
43280
+ region,
43281
+ credentials: auth.loadProfileCredentials(profileName)
43282
+ };
43283
+ });
43284
+ const isStatic = (shape) => shape.credentials != null && typeof shape.credentials.accessKeyId === "string";
43285
+ /**
43286
+ * Build an `AWSEnvironment` Layer directly from values — useful for
43287
+ * static credentials in CI or tests.
43288
+ *
43289
+ * Named `makeEnvironment` rather than `of` because `Context.Service.of`
43290
+ * already exists with different semantics (builds the service value, not
43291
+ * a Layer); putting both on `AWSEnvironment` would be confusing.
43292
+ */
43293
+ const makeEnvironment = (shape) => Layer.succeed(AWSEnvironment, isStatic(shape) ? {
43294
+ ...shape,
43295
+ credentials: Effect.succeed(fromAwsCredentialIdentity(shape.credentials))
43296
+ } : shape);
43297
+
43298
+ //#endregion
43299
+ //#region src/AWS/Bootstrap.ts
43300
+ /**
43301
+ * Tag key used to identify the alchemy assets bucket.
43302
+ */
43303
+ const ASSETS_BUCKET_TAG = "alchemy::assets-bucket";
43304
+ /**
43305
+ * Tag key used to scope the alchemy assets bucket to an AWS region.
43306
+ */
43307
+ const ASSETS_BUCKET_REGION_TAG = "alchemy::assets-bucket-region";
43308
+ /**
43309
+ * Build an account-regional namespace bucket name.
43310
+ *
43311
+ * Account-regional buckets must follow the naming convention:
43312
+ * `<prefix>-<accountId>-<region>-an`
43313
+ *
43314
+ * @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/gpbucketnamespaces.html#account-regional-gp-buckets
43315
+ */
43316
+ const createAssetsBucketName = (accountId, region) => `alchemy-assets-${accountId}-${region}-an`.toLowerCase();
43317
+ const createAssetsBucketTags = (region) => [{
43318
+ Key: ASSETS_BUCKET_TAG,
43319
+ Value: "true"
43320
+ }, {
43321
+ Key: ASSETS_BUCKET_REGION_TAG,
43322
+ Value: region
43323
+ }];
43324
+ const getBucketTags = (bucketName) => s3.getBucketTagging({ Bucket: bucketName }).pipe(Effect.map((response) => response.TagSet ?? []), Effect.catchTag("NoSuchTagSet", () => Effect.succeed([])));
43325
+ const hasAssetsBucketTag = (tags) => tags.some((tag) => tag.Key === ASSETS_BUCKET_TAG && tag.Value === "true");
43326
+ const normalizeBucketRegion = (location) => {
43327
+ if (!location) return "us-east-1";
43328
+ if (location === "EU") return "eu-west-1";
43329
+ return location;
43330
+ };
43331
+ const bucketMatchesRegion = (tags, region) => {
43332
+ const taggedRegion = tags.find((tag) => tag.Key === ASSETS_BUCKET_REGION_TAG)?.Value;
43333
+ return taggedRegion === void 0 ? void 0 : taggedRegion === region;
43334
+ };
43335
+ const lookupAssetsBuckets = Effect.gen(function* () {
43336
+ const region = yield* Region;
43337
+ const buckets = (yield* s3.listBuckets({})).Buckets ?? [];
43338
+ const matchingBuckets = [];
43339
+ for (const bucket of buckets) {
43340
+ const bucketName = bucket.Name;
43341
+ if (!bucketName) continue;
43342
+ const tags = yield* getBucketTags(bucketName).pipe(Effect.catch(() => Effect.succeed([])));
43343
+ if (!hasAssetsBucketTag(tags)) continue;
43344
+ const taggedRegionMatch = bucketMatchesRegion(tags, region);
43345
+ if (taggedRegionMatch === true) {
43346
+ matchingBuckets.push(bucketName);
43347
+ continue;
43348
+ }
43349
+ if (taggedRegionMatch === false) continue;
43350
+ if ((yield* s3.getBucketLocation({ Bucket: bucketName }).pipe(Effect.map((response) => normalizeBucketRegion(response.LocationConstraint)), Effect.catch(() => Effect.succeed(void 0)))) === region) matchingBuckets.push(bucketName);
43351
+ }
43352
+ return matchingBuckets;
43353
+ });
43354
+ const lookupAssetsBucket = Effect.gen(function* () {
43355
+ const matchingBuckets = yield* lookupAssetsBuckets;
43356
+ for (const bucketName of matchingBuckets) return Option.some(bucketName);
43357
+ return Option.none();
43358
+ });
43359
+ const ensureAssetsBucketTags = Effect.fn(function* (bucketName, region) {
43360
+ const tagSet = [...(yield* getBucketTags(bucketName)).filter((tag) => tag.Key !== ASSETS_BUCKET_TAG && tag.Key !== ASSETS_BUCKET_REGION_TAG), ...createAssetsBucketTags(region)];
43361
+ yield* s3.putBucketTagging({
43362
+ Bucket: bucketName,
43363
+ Tagging: { TagSet: tagSet.map((tag) => ({
43364
+ Key: tag.Key,
43365
+ Value: tag.Value
43366
+ })) }
43367
+ });
43368
+ });
43369
+ const deleteAllObjects = Effect.fn(function* (bucketName) {
43370
+ let continuationToken;
43371
+ do {
43372
+ const listResponse = yield* s3.listObjectsV2({
43373
+ Bucket: bucketName,
43374
+ ContinuationToken: continuationToken
43375
+ });
43376
+ if (listResponse.Contents && listResponse.Contents.length > 0) yield* s3.deleteObjects({
43377
+ Bucket: bucketName,
43378
+ Delete: {
43379
+ Objects: listResponse.Contents.map((obj) => ({ Key: obj.Key })),
43380
+ Quiet: true
43381
+ }
43382
+ });
43383
+ continuationToken = listResponse.NextContinuationToken;
43384
+ } while (continuationToken);
43385
+ let keyMarker;
43386
+ let versionIdMarker;
43387
+ do {
43388
+ const listVersionsResponse = yield* s3.listObjectVersions({
43389
+ Bucket: bucketName,
43390
+ KeyMarker: keyMarker,
43391
+ VersionIdMarker: versionIdMarker
43392
+ });
43393
+ const objectsToDelete = [...(listVersionsResponse.Versions ?? []).map((v) => ({
43394
+ Key: v.Key,
43395
+ VersionId: v.VersionId
43396
+ })), ...(listVersionsResponse.DeleteMarkers ?? []).map((dm) => ({
43397
+ Key: dm.Key,
43398
+ VersionId: dm.VersionId
43399
+ }))];
43400
+ if (objectsToDelete.length > 0) yield* s3.deleteObjects({
43401
+ Bucket: bucketName,
43402
+ Delete: {
43403
+ Objects: objectsToDelete,
43404
+ Quiet: true
43405
+ }
43406
+ });
43407
+ keyMarker = listVersionsResponse.NextKeyMarker;
43408
+ versionIdMarker = listVersionsResponse.NextVersionIdMarker;
43409
+ } while (keyMarker);
43410
+ });
43411
+ /**
43412
+ * Bootstrap the AWS environment by creating the assets bucket.
43413
+ *
43414
+ * This is idempotent - running it multiple times is safe.
43415
+ * The bucket is tagged and later discovered by tag lookup instead of by name.
43416
+ */
43417
+ const bootstrap = Effect.fn(function* () {
43418
+ const region = yield* Region;
43419
+ const existingBucket = yield* lookupAssetsBucket;
43420
+ if (Option.isSome(existingBucket)) {
43421
+ yield* ensureAssetsBucketTags(existingBucket.value, region);
43422
+ yield* Effect.logInfo(`Assets bucket already exists: ${existingBucket.value}`);
43423
+ return {
43424
+ bucketName: existingBucket.value,
43425
+ created: false
43426
+ };
43427
+ }
43428
+ const { accountId } = yield* AWSEnvironment;
43429
+ const bucketName = createAssetsBucketName(accountId, region);
43430
+ yield* s3.createBucket({
43431
+ Bucket: bucketName,
43432
+ BucketNamespace: "account-regional",
43433
+ CreateBucketConfiguration: {
43434
+ Tags: createAssetsBucketTags(region),
43435
+ ...region === "us-east-1" ? {} : { LocationConstraint: region }
43436
+ }
43437
+ }).pipe(Effect.catchTag("BucketAlreadyOwnedByYou", () => Effect.void), Effect.retry({
43438
+ while: (e) => e._tag === "OperationAborted" || e._tag === "ServiceUnavailable",
43439
+ schedule: Schedule.exponential(100)
43440
+ }));
43441
+ yield* s3.headBucket({ Bucket: bucketName }).pipe(Effect.retry({ schedule: Schedule.exponential(100).pipe(Schedule.both(Schedule.recurs(10))) }));
43442
+ yield* Effect.logInfo(`Created assets bucket: ${bucketName}`);
43443
+ return {
43444
+ bucketName,
43445
+ created: true
43446
+ };
43447
+ });
43448
+ const destroyBootstrap = Effect.fn(function* () {
43449
+ const bucketNames = yield* lookupAssetsBuckets;
43450
+ for (const bucketName of bucketNames) {
43451
+ yield* Effect.logInfo(`Destroying assets bucket: ${bucketName}`);
43452
+ yield* deleteAllObjects(bucketName);
43453
+ yield* s3.deleteBucket({ Bucket: bucketName }).pipe(Effect.retry({
43454
+ while: (e) => e._tag === "OperationAborted" || e._tag === "ServiceUnavailable",
43455
+ schedule: Schedule.exponential(100).pipe(Schedule.both(Schedule.recurs(10)))
43456
+ }));
43457
+ }
43458
+ return {
43459
+ bucketNames,
43460
+ destroyed: bucketNames.length
43461
+ };
43462
+ });
43463
+
43464
+ //#endregion
43465
+ //#region src/AWS/Credentials.ts
43466
+ /**
43467
+ * Lazy `Credentials` layer derived from the surrounding {@link AWSEnvironment}.
43468
+ * Credentials are resolved on first access (not during layer construction),
43469
+ * matching the existing @distilled.cloud/aws semantics.
43470
+ */
43471
+ const fromEnvironment$1 = Layer.effect(Credentials, Effect.map(AWSEnvironment.asEffect(), (env$2) => env$2.credentials));
43472
+
43473
+ //#endregion
43474
+ //#region src/AWS/Region.ts
43475
+ /**
43476
+ * Derive the AWS region from the surrounding {@link AWSEnvironment}.
43477
+ */
43478
+ const fromEnvironment = Layer.effect(Region$1.Region, Effect.map(AWSEnvironment.asEffect(), (env$2) => env$2.region));
43479
+
43480
+ //#endregion
43481
+ //#region src/Util/ConfigProvider.ts
43482
+ const loadConfigProvider = (envFile$1) => {
43483
+ if (Option.isSome(envFile$1)) return ConfigProvider.fromDotEnv({ path: envFile$1.value }).pipe(Effect.map((dotEnv) => ConfigProvider.orElse(dotEnv, ConfigProvider.fromEnv())));
43484
+ return Effect.gen(function* () {
43485
+ if (!(yield* (yield* FileSystem.FileSystem).exists(".env"))) return ConfigProvider.fromEnv();
43486
+ return ConfigProvider.orElse(yield* ConfigProvider.fromDotEnv({ path: ".env" }), ConfigProvider.fromEnv());
43487
+ });
43488
+ };
43489
+
43490
+ //#endregion
43491
+ //#region src/Util/FileLogger.ts
43492
+ const fileLogger = Effect.fnUntraced(function* (...segments) {
43493
+ const { dotAlchemy } = yield* AlchemyContext;
43494
+ const fs$2 = yield* FileSystem.FileSystem;
43495
+ const path = yield* Path;
43496
+ const logFile = path.join(dotAlchemy, "log", ...segments);
43497
+ yield* fs$2.makeDirectory(path.dirname(logFile), { recursive: true });
43498
+ return yield* Logger.formatLogFmt.pipe(Logger.toFile(logFile, { flag: "a" }));
43499
+ });
43500
+
43501
+ //#endregion
43502
+ //#region bin/commands/bootstrap.ts
43503
+ const awsProfile = Flag$1.string("profile").pipe(Flag$1.withDescription("AWS profile to use for credentials"), Flag$1.optional, Flag$1.map(Option.getOrElse(() => "default")));
43504
+ const awsRegion = Flag$1.string("region").pipe(Flag$1.withDescription("AWS region to bootstrap (defaults to AWS_REGION env var)"), Flag$1.optional, Flag$1.map(Option.getOrUndefined));
43505
+ const bootstrapDestroy = Flag$1.boolean("destroy").pipe(Flag$1.withDescription("Destroy all bootstrap buckets in the selected region"), Flag$1.withDefault(false));
43506
+ const bootstrapCommand = Command$1.make("bootstrap", {
43507
+ envFile,
43508
+ profile: awsProfile,
43509
+ region: awsRegion,
43510
+ destroy: bootstrapDestroy
43511
+ }, Effect.fnUntraced(function* ({ envFile: envFile$1, profile: profile$1, region, destroy }) {
43512
+ const logger = Logger.layer([fileLogger("bootstrap.txt")]);
43513
+ return yield* Effect.gen(function* () {
43514
+ const ssoProfile = yield* Auth.loadProfile(profile$1);
43515
+ if (!ssoProfile.sso_account_id) return yield* Effect.die(`AWS SSO profile '${profile$1}' is missing sso_account_id`);
43516
+ const ambient = yield* Effect.context();
43517
+ const environment = makeEnvironment({
43518
+ accountId: ssoProfile.sso_account_id,
43519
+ region: region ?? ssoProfile.region ?? "us-east-1",
43520
+ credentials: Auth.loadProfileCredentials(profile$1).pipe(Effect.provide(ambient)),
43521
+ profile: profile$1
43522
+ });
43523
+ const awsLayers = Layer.provideMerge(Layer.mergeAll(fromEnvironment, fromEnvironment$1), environment);
43524
+ return yield* Effect.gen(function* () {
43525
+ const provider = yield* loadConfigProvider(envFile$1);
43526
+ const bootstrapLayer = Layer.provide(awsLayers, Layer.succeed(ConfigProvider.ConfigProvider, provider));
43527
+ if (destroy) {
43528
+ yield* destroyBootstrap().pipe(Effect.tap((result) => result.destroyed === 0 ? Console.log("✓ No bootstrap buckets found to destroy") : Console.log(`✓ Destroyed ${result.destroyed} bootstrap bucket(s): ${result.bucketNames.join(", ")}`)), Effect.provide(bootstrapLayer));
43529
+ return;
43530
+ }
43531
+ yield* bootstrap().pipe(Effect.tap(({ bucketName, created }) => created ? Console.log(`✓ Created assets bucket: ${bucketName}`) : Console.log(`✓ Assets bucket already exists: ${bucketName}`)), Effect.provide(bootstrapLayer));
43532
+ });
43533
+ }).pipe(Effect.provide(logger));
43534
+ }));
43535
+
43536
+ //#endregion
43537
+ //#region src/Artifacts.ts
43538
+ /**
43539
+ * Per-resource in-memory artifacts shared across a single `Plan.make -> apply`
43540
+ * execution.
43541
+ *
43542
+ * The engine scopes this service by resource `FQN` before invoking lifecycle
43543
+ * handlers, so providers should treat it as a resource-local bag for expensive,
43544
+ * deterministic intermediate results that can be reused across phases.
43545
+ *
43546
+ * Expected usage:
43547
+ *
43548
+ * - `diff` computes an expensive artifact once and stores it
43549
+ * - `create` / `update` reads the same artifact and skips recomputing it
43550
+ * - artifacts are ephemeral and must never be required for correctness on a
43551
+ * later deploy because the bag is reset between runs
43552
+ *
43553
+ * Example:
43554
+ *
43555
+ * ```ts
43556
+ * const artifacts = yield* Artifacts;
43557
+ * const cached = yield* artifacts.get<PreparedBundle>("bundle");
43558
+ * if (cached) return cached;
43559
+ *
43560
+ * const bundle = yield* prepareBundle();
43561
+ * yield* artifacts.set("bundle", bundle);
43562
+ * return bundle;
43563
+ * ```
43564
+ */
43565
+ var Artifacts = class extends EffectContext.Service()("Artifacts") {};
43566
+ var ArtifactStore = class extends EffectContext.Service()("Artifacts/Store") {};
43567
+ /**
43568
+ * Create a fresh root store for one deploy/test run.
43569
+ */
43570
+ const createArtifactStore = () => /* @__PURE__ */ new Map();
43571
+ const getOrCreateBag = (store, fqn) => {
43572
+ const existing = store.get(fqn);
43573
+ if (existing) return existing;
43574
+ const bag = /* @__PURE__ */ new Map();
43575
+ store.set(fqn, bag);
43576
+ return bag;
43577
+ };
43578
+ const makeScopedArtifacts = (store, fqn) => {
43579
+ const bag = getOrCreateBag(store, fqn);
43580
+ return {
43581
+ get: (key) => Effect.sync(() => bag.get(key)),
43582
+ set: (key, value) => Effect.sync(() => {
43583
+ bag.set(key, value);
43584
+ }),
43585
+ delete: (key) => Effect.sync(() => {
43586
+ bag.delete(key);
43587
+ })
43588
+ };
43589
+ };
43590
+ /**
43591
+ * Ensure an artifact root exists, reusing the ambient store when one is already
43592
+ * present. This lets nested helpers participate in the same run-scoped cache.
43593
+ */
43594
+ const ensureArtifactStore = (effect) => Effect.serviceOption(ArtifactStore).pipe(Effect.map(Option.getOrUndefined), Effect.flatMap((existing) => effect.pipe(Effect.provideService(ArtifactStore, existing ?? createArtifactStore()))));
43595
+
43596
+ //#endregion
43597
+ //#region src/Util/service.ts
43598
+ const GenericService = () => (Kind) => {
43599
+ const service = EffectContext.Service(Kind);
43600
+ const make$2 = (Type) => EffectContext.Service(`${Kind}<${Type}>`);
43601
+ return Object.assign(Object.setPrototypeOf(make$2, service), service);
43602
+ };
43603
+
43604
+ //#endregion
43605
+ //#region src/ExecutionContext.ts
43606
+ const ExecutionContext = GenericService()("Alchemy::ExecutionContext");
43607
+ const CurrentExecutionContext = Effect.serviceOption(ExecutionContext).pipe(Effect.map(Option.getOrUndefined));
43608
+
43609
+ //#endregion
43610
+ //#region src/Ref.ts
43611
+ const RefMetadata = Symbol.for("alchemy/RefMetadata");
43612
+ const isRef = (s) => s && s[RefMetadata] !== void 0;
43613
+ const getRefMetadata = (ref$1) => ref$1[RefMetadata];
43614
+ const ref = ({ stack, id, stage: stage$1 }) => {
43615
+ const ref$1 = new Proxy({}, { get: (_, prop) => {
43616
+ if (prop === RefMetadata) return {
43617
+ stack,
43618
+ stage: stage$1,
43619
+ id
43620
+ };
43621
+ return of(ref$1)[prop];
43622
+ } });
43623
+ return ref$1;
43624
+ };
43625
+
43626
+ //#endregion
43627
+ //#region src/Provider.ts
43628
+ const Provider = (type) => EffectContext.Service()(type);
43629
+ const isProviderCollectionService = (value) => {
43630
+ return typeof value === "object" && value !== null && "kind" in value && value.kind === "ProviderCollection";
43631
+ };
43632
+ const findProviderByType = (type) => tryFindProviderByType(type).pipe(Effect.flatMap(Option.match({
43633
+ onNone: () => Effect.die(`Provider not found for ${type}`),
43634
+ onSome: (provider) => Effect.succeed(provider)
43635
+ })));
43636
+ const tryFindProviderByType = Effect.fnUntraced(function* (resourceType) {
43637
+ const Tag = Provider(resourceType);
43638
+ const direct = yield* Effect.serviceOption(Tag);
43639
+ if (Option.isSome(direct)) return direct;
43640
+ const context = yield* Effect.context();
43641
+ for (const value of context.mapUnsafe.values()) if (isProviderCollectionService(value)) {
43642
+ const provider = value.get(resourceType);
43643
+ if (provider) return Option.some(provider);
43644
+ }
43645
+ return Option.none();
43646
+ });
43647
+
43648
43648
  //#endregion
43649
43649
  //#region src/Stage.ts
43650
43650
  var Stage = class extends EffectContext.Service()("Stage") {};
@@ -43675,12 +43675,16 @@ const Stack = Object.assign(taggedFunction(EffectContext.Service()("Stack"), (st
43675
43675
  id: stackName
43676
43676
  }) }) }))));
43677
43677
  const StackName = Stack.use((stack) => Effect.succeed(stack.name));
43678
- const make$1 = (options) => (effect) => Effect.scope.pipe(Effect.flatMap((scope) => options.providers.pipe(Layer.provideMerge(options.state), Layer.provideMerge(Layer.effect(Stack, Stage.asEffect().pipe(Effect.map((stage$1) => options.stack ?? {
43679
- name: options.name,
43680
- stage: stage$1,
43681
- resources: {},
43682
- bindings: {}
43683
- })))), Layer.buildWithScope(scope))), Effect.flatMap((context) => Effect.all([
43678
+ const make$1 = (options) => (effect) => Effect.scope.pipe(Effect.flatMap((scope) => {
43679
+ if (options.state == null) return Effect.die(/* @__PURE__ */ new Error(`Stack "${options.name}" is missing a state store. Add a \`state\` layer to the stack options, e.g.:\n Alchemy.Stack("${options.name}", {\n providers: Cloudflare.providers(),\n state: Cloudflare.state(), // <-- required\n }, ...)\nSee https://alchemy.run/concepts/state-store for available state stores.`));
43680
+ if (options.providers == null) return Effect.die(/* @__PURE__ */ new Error(`Stack "${options.name}" is missing a providers layer. Add a \`providers\` layer to the stack options, e.g.:\n Alchemy.Stack("${options.name}", {\n providers: Cloudflare.providers(), // <-- required\n state: Cloudflare.state(),\n }, ...)`));
43681
+ return options.providers.pipe(Layer.provideMerge(options.state), Layer.provideMerge(Layer.effect(Stack, Stage.asEffect().pipe(Effect.map((stage$1) => options.stack ?? {
43682
+ name: options.name,
43683
+ stage: stage$1,
43684
+ resources: {},
43685
+ bindings: {}
43686
+ })))), Layer.buildWithScope(scope));
43687
+ }), Effect.flatMap((context) => Effect.all([
43684
43688
  effect,
43685
43689
  Stack.asEffect(),
43686
43690
  Effect.context()
@@ -43895,6 +43899,7 @@ const evaluate = (expr, upstream$1) => Effect.gen(function* () {
43895
43899
  }
43896
43900
  }
43897
43901
  if (Array.isArray(expr)) return yield* Effect.all(expr.map((item) => evaluate(item, upstream$1)));
43902
+ else if (Redacted.isRedacted(expr)) return expr;
43898
43903
  else if (typeof expr === "object" && expr !== null) return Object.fromEntries(yield* Effect.all(Object.entries(expr).map(([key, value]) => evaluate(value, upstream$1).pipe(Effect.map((value$1) => [key, value$1])))));
43899
43904
  return expr;
43900
43905
  });
@@ -45194,6 +45199,7 @@ const make = (stack, options = {}) => Effect.gen(function* () {
45194
45199
  const resolveInput = (input) => Effect.gen(function* () {
45195
45200
  if (!input) return input;
45196
45201
  else if (isExpr(input)) return yield* resolveOutput(input);
45202
+ else if (Redacted.isRedacted(input)) return input;
45197
45203
  else if (Array.isArray(input)) return yield* Effect.all(input.map(resolveInput), { concurrency: "unbounded" });
45198
45204
  else if (isResource(input)) return yield* resolveInput(yield* resolveOutput(of(input)));
45199
45205
  else if (typeof input === "object") return Object.fromEntries(yield* Effect.all(Object.entries(input).map(([key, value]) => resolveInput(value).pipe(Effect.map((value$1) => [key, value$1]))), { concurrency: "unbounded" }));
@@ -45471,7 +45477,7 @@ var UnsatisfiedResourceCycle = class extends Data.TaggedError("UnsatisfiedResour
45471
45477
  //#region bin/commands/deploy.ts
45472
45478
  const execStack = Effect.fn(function* ({ main: main$1, stage: stage$1, envFile: envFile$1, profile: profile$1, dryRun: dryRun$1 = false, force: force$1 = false, yes: yes$1 = false, destroy = false }) {
45473
45479
  const stackEffect = yield* importStack(main$1);
45474
- const services$1 = Layer.mergeAll(Layer.succeed(ArtifactStore, createArtifactStore()), Layer.succeed(AuthProviders, yield* Effect.serviceOption(AuthProviders).pipe(Effect.map(Option.getOrElse(() => ({}))))), ConfigProvider.layer(withProfileOverride(yield* loadConfigProvider(envFile$1), profile$1)), inkCLI(), Logger.layer([fileLogger("out")]), Layer.succeed(Stage, stage$1));
45480
+ const services$1 = Layer.mergeAll(Layer.succeed(ArtifactStore, createArtifactStore()), Layer.succeed(AuthProviders, yield* Effect.serviceOption(AuthProviders).pipe(Effect.map(Option.getOrElse(() => ({}))))), ConfigProvider.layer(withProfileOverride(yield* loadConfigProvider(envFile$1), profile$1)), Logger.layer([fileLogger("out")]), Layer.succeed(Stage, stage$1));
45475
45481
  yield* Effect.gen(function* () {
45476
45482
  const cli$1 = yield* Cli;
45477
45483
  const stack = yield* stackEffect;
@@ -45853,7 +45859,7 @@ const root = Command$1.make("alchemy", {}).pipe(Command$1.withSubcommands([
45853
45859
  stateCommand
45854
45860
  ]));
45855
45861
  const cli = Command$1.run(root, { version });
45856
- const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()));
45862
+ const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()), inkCLI());
45857
45863
  cli.pipe(Effect.provide(services), Effect.scoped, handleCancellation, runMain);
45858
45864
 
45859
45865
  //#endregion