alchemy 2.0.0-cli-shim.1 → 2.0.0-fix-redacted

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 (62) hide show
  1. package/bin/alchemy.js +970 -815
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +4 -0
  4. package/bin/commands/deploy.ts +0 -1
  5. package/bin/commands/state.ts +230 -0
  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/StateStore/State.d.ts +1 -1
  28. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  29. package/lib/Deploy.d.ts +1 -1
  30. package/lib/Deploy.d.ts.map +1 -1
  31. package/lib/Destroy.d.ts +1 -1
  32. package/lib/Destroy.d.ts.map +1 -1
  33. package/lib/Output.d.ts.map +1 -1
  34. package/lib/Output.js +4 -0
  35. package/lib/Output.js.map +1 -1
  36. package/lib/Stack.d.ts +5 -4
  37. package/lib/Stack.d.ts.map +1 -1
  38. package/lib/Stack.js +27 -8
  39. package/lib/Stack.js.map +1 -1
  40. package/lib/State/HttpStateStore.js +32 -6
  41. package/lib/State/HttpStateStore.js.map +1 -1
  42. package/lib/State/Sync.d.ts +1 -0
  43. package/lib/State/Sync.d.ts.map +1 -1
  44. package/lib/State/Sync.js +1 -1
  45. package/lib/State/Sync.js.map +1 -1
  46. package/lib/Test/Bun.d.ts +2 -2
  47. package/lib/Test/Bun.d.ts.map +1 -1
  48. package/lib/Test/Bun.js +2 -4
  49. package/lib/Test/Bun.js.map +1 -1
  50. package/lib/Test/Vitest.d.ts +1 -1
  51. package/lib/cli/index.d.ts +2 -6
  52. package/lib/cli/index.d.ts.map +1 -1
  53. package/lib/cli/index.js +2 -43050
  54. package/lib/cli/index.js.map +1 -1
  55. package/package.json +2 -2
  56. package/src/Cli/InkCLI.tsx +0 -10
  57. package/src/Cli/index.ts +4 -1
  58. package/src/Output.ts +3 -0
  59. package/src/Stack.ts +33 -7
  60. package/src/State/HttpStateStore.ts +30 -7
  61. package/src/State/Sync.ts +4 -1
  62. package/src/Test/Bun.ts +2 -2
package/bin/alchemy.js CHANGED
@@ -8,12 +8,21 @@ 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";
14
23
  import * as Option from "effect/Option";
15
24
  import * as Schema from "effect/Schema";
16
- import * as Argument from "effect/unstable/cli/Argument";
25
+ import * as Argument$1 from "effect/unstable/cli/Argument";
17
26
  import * as CliError from "effect/unstable/cli/CliError";
18
27
  import * as Flag$1 from "effect/unstable/cli/Flag";
19
28
  import "@clack/prompts";
@@ -28,19 +37,10 @@ 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
- import { Command, Flag } from "effect/unstable/cli";
43
+ import { Argument, Command, Flag } from "effect/unstable/cli";
44
44
  import * as Stream$1 from "effect/Stream";
45
45
 
46
46
  //#region rolldown:runtime
@@ -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-cli-shim.1";
77
+ var version = "2.0.0-fix-redacted";
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.file("main", { mustExist: true }).pipe(Argument.withDescription("Main file to deploy, defaults to alchemy.run.ts"), Argument.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;
@@ -42867,15 +42291,86 @@ const useInput = (inputHandler, options = {}) => {
42867
42291
  inputHandler
42868
42292
  ]);
42869
42293
  };
42870
- var use_input_default = useInput;
42871
-
42872
- //#endregion
42873
- //#region ../../node_modules/.bun/ink@6.8.0+4d40c05051311df7/node_modules/ink/build/hooks/use-app.js
42294
+ var use_input_default = useInput;
42295
+
42296
+ //#endregion
42297
+ //#region ../../node_modules/.bun/ink@6.8.0+4d40c05051311df7/node_modules/ink/build/hooks/use-app.js
42298
+ /**
42299
+ `useApp` is a React hook that exposes a method to manually exit the app (unmount).
42300
+ */
42301
+ const useApp = () => (0, import_react.useContext)(AppContext_default);
42302
+ var use_app_default = useApp;
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
+ };
42874
42342
  /**
42875
- `useApp` is a React hook that exposes a method to manually exit the app (unmount).
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
+ * ```
42876
42352
  */
42877
- const useApp = () => (0, import_react.useContext)(AppContext_default);
42878
- var use_app_default = useApp;
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
+ };
42879
42374
 
42880
42375
  //#endregion
42881
42376
  //#region src/Cli/NamespaceTree.ts
@@ -43422,227 +42917,732 @@ const buildLogicalIdIndex = (rows) => {
43422
42917
  if (keys) keys.push(row.key);
43423
42918
  else index.set(row.id, [row.key]);
43424
42919
  }
43425
- return index;
43426
- };
43427
- function toPlanTask(rowOrId, planItem) {
43428
- if (typeof rowOrId === "string") return {
43429
- key: rowOrId,
43430
- id: rowOrId,
43431
- type: planItem.resource.Type,
43432
- status: planItem.action === "noop" ? planItem.state.status : "pending",
43433
- updatedAt: Date.now()
42920
+ return index;
42921
+ };
42922
+ function toPlanTask(rowOrId, planItem) {
42923
+ if (typeof rowOrId === "string") return {
42924
+ key: rowOrId,
42925
+ id: rowOrId,
42926
+ type: planItem.resource.Type,
42927
+ status: planItem.action === "noop" ? planItem.state.status : "pending",
42928
+ updatedAt: Date.now()
42929
+ };
42930
+ return {
42931
+ key: rowOrId.key,
42932
+ id: rowOrId.id,
42933
+ type: rowOrId.resourceType,
42934
+ status: rowOrId.action === "noop" ? rowOrId.persistedApplyStatus ?? "created" : "pending",
42935
+ updatedAt: Date.now()
42936
+ };
42937
+ }
42938
+ const buildInitialTasks = (rows) => new Map(rows.flatMap((row) => row.type === "resource" ? [[row.key, toPlanTask(row)]] : []));
42939
+ function PlanProgress(props) {
42940
+ const { source, plan } = props;
42941
+ const spinner = useGlobalSpinner();
42942
+ const rows = (0, import_react.useMemo)(() => buildProgressRows(plan), [plan]);
42943
+ const logicalIdIndex = (0, import_react.useMemo)(() => buildLogicalIdIndex(rows), [rows]);
42944
+ const [tasks, setTasks] = (0, import_react.useState)(() => buildInitialTasks(rows));
42945
+ const unsubscribeRef = (0, import_react.useRef)(null);
42946
+ (0, import_react.useEffect)(() => {
42947
+ unsubscribeRef.current?.();
42948
+ unsubscribeRef.current = source.subscribe((event) => {
42949
+ setTasks((prev) => {
42950
+ const next = new Map(prev);
42951
+ const keys = logicalIdIndex.get(event.id) ?? [];
42952
+ if (event.kind === "status-change") {
42953
+ if (!event.bindingId) for (const key of keys) {
42954
+ const current = next.get(key);
42955
+ next.set(key, {
42956
+ key,
42957
+ id: event.id,
42958
+ type: event.type,
42959
+ status: event.status,
42960
+ message: event.message ?? current?.message,
42961
+ updatedAt: Date.now()
42962
+ });
42963
+ }
42964
+ } else for (const key of keys) {
42965
+ const current = next.get(key);
42966
+ if (!current) continue;
42967
+ next.set(key, {
42968
+ ...current,
42969
+ message: event.message,
42970
+ updatedAt: Date.now()
42971
+ });
42972
+ }
42973
+ return next;
42974
+ });
42975
+ });
42976
+ return () => {
42977
+ unsubscribeRef.current?.();
42978
+ unsubscribeRef.current = null;
42979
+ };
42980
+ }, [logicalIdIndex, source]);
42981
+ (0, import_react.useEffect)(() => {
42982
+ setTasks(buildInitialTasks(rows));
42983
+ }, [rows]);
42984
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
42985
+ flexDirection: "column",
42986
+ children: rows.map((row) => {
42987
+ const indent = " ".repeat(row.depth);
42988
+ if (row.type === "namespace") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, {
42989
+ flexDirection: "row",
42990
+ children: [
42991
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: indent }),
42992
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
42993
+ width: 2,
42994
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
42995
+ color: "blueBright",
42996
+ children: "↳ "
42997
+ })
42998
+ }),
42999
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
43000
+ color: "blueBright",
43001
+ children: row.id
43002
+ })
43003
+ ]
43004
+ }, row.key);
43005
+ const task = tasks.get(row.key) ?? toPlanTask(row);
43006
+ const displayStatus = getDisplayStatus(row, task.status);
43007
+ const color = statusColor(displayStatus);
43008
+ const icon = statusIcon(task.status, spinner);
43009
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, {
43010
+ flexDirection: "column",
43011
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, {
43012
+ flexDirection: "row",
43013
+ children: [
43014
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: indent }),
43015
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
43016
+ width: 2,
43017
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43018
+ color,
43019
+ children: [icon, " "]
43020
+ })
43021
+ }),
43022
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
43023
+ bold: true,
43024
+ children: task.id
43025
+ }),
43026
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43027
+ dimColor: true,
43028
+ children: [
43029
+ " (",
43030
+ task.type,
43031
+ ")"
43032
+ ]
43033
+ }),
43034
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43035
+ color,
43036
+ children: [" ", displayStatus]
43037
+ })
43038
+ ]
43039
+ }), task.message ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
43040
+ paddingLeft: row.depth * 2 + 2,
43041
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43042
+ dimColor: true,
43043
+ children: ["• ", task.message]
43044
+ })
43045
+ }) : null]
43046
+ }, row.key);
43047
+ })
43048
+ });
43049
+ }
43050
+ function getDisplayStatus(row, status) {
43051
+ if (row.action === "noop" && (status === "created" || status === "updated")) return "no change";
43052
+ return status;
43053
+ }
43054
+ function statusColor(status) {
43055
+ switch (status) {
43056
+ case "no change": return "gray";
43057
+ case "pending": return "gray";
43058
+ case "creating":
43059
+ case "created": return "green";
43060
+ case "updating":
43061
+ case "updated": return "yellow";
43062
+ case "deleting":
43063
+ case "deleted": return "red";
43064
+ case "fail": return "redBright";
43065
+ default: return;
43066
+ }
43067
+ }
43068
+ function statusIcon(status, spinnerChar) {
43069
+ if (isInProgress(status)) return spinnerChar;
43070
+ if (status === "fail") return "✗";
43071
+ return "✓";
43072
+ }
43073
+ function isInProgress(status) {
43074
+ return status === "pending" || status === "creating" || status === "updating" || status === "deleting";
43075
+ }
43076
+ const spinnerFrames = [
43077
+ "⠋",
43078
+ "⠙",
43079
+ "⠹",
43080
+ "⠸",
43081
+ "⠼",
43082
+ "⠴",
43083
+ "⠦",
43084
+ "⠧",
43085
+ "⠇",
43086
+ "⠏"
43087
+ ];
43088
+ function useGlobalSpinner(intervalMs = 80) {
43089
+ const [index, setIndex] = (0, import_react.useState)(0);
43090
+ (0, import_react.useEffect)(() => {
43091
+ const timer = setInterval(() => {
43092
+ setIndex((i) => (i + 1) % spinnerFrames.length);
43093
+ }, intervalMs);
43094
+ return () => clearInterval(timer);
43095
+ }, [intervalMs]);
43096
+ return spinnerFrames[index];
43097
+ }
43098
+ const findCrudByLogicalId = (plan, logicalId) => {
43099
+ for (const node of Object.values(plan.resources)) if (node.resource.LogicalId === logicalId) return node;
43100
+ for (const node of Object.values(plan.deletions)) if (node?.resource.LogicalId === logicalId) return node;
43101
+ };
43102
+
43103
+ //#endregion
43104
+ //#region src/Cli/InkCLI.tsx
43105
+ const inkCLI = () => Layer.succeed(Cli, Cli.of({
43106
+ approvePlan,
43107
+ displayPlan,
43108
+ startApplySession
43109
+ }));
43110
+ const approvePlan = Effect.fn(function* (plan) {
43111
+ let approved = false;
43112
+ const { waitUntilExit } = render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ApprovePlan, {
43113
+ plan,
43114
+ approve: (a) => approved = a
43115
+ }));
43116
+ yield* Effect.promise(waitUntilExit);
43117
+ return approved;
43118
+ });
43119
+ const displayPlan = (plan) => Effect.sync(() => {
43120
+ const { unmount } = render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plan, { plan }));
43121
+ unmount();
43122
+ });
43123
+ const startApplySession = Effect.fn(function* (plan) {
43124
+ const listeners = /* @__PURE__ */ new Set();
43125
+ const { unmount } = render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlanProgress, {
43126
+ plan,
43127
+ source: { subscribe(listener) {
43128
+ listeners.add(listener);
43129
+ return () => listeners.delete(listener);
43130
+ } }
43131
+ }));
43132
+ return {
43133
+ done: Effect.fn(function* () {
43134
+ yield* Effect.sleep(10);
43135
+ yield* Effect.sync(() => unmount());
43136
+ }),
43137
+ emit: (event) => Effect.sync(() => {
43138
+ for (const listener of listeners) listener(event);
43139
+ })
43140
+ };
43141
+ });
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
43434
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
+ }
43435
43458
  return {
43436
- key: rowOrId.key,
43437
- id: rowOrId.id,
43438
- type: rowOrId.resourceType,
43439
- status: rowOrId.action === "noop" ? rowOrId.persistedApplyStatus ?? "created" : "pending",
43440
- updatedAt: Date.now()
43459
+ bucketNames,
43460
+ destroyed: bucketNames.length
43441
43461
  };
43442
- }
43443
- const buildInitialTasks = (rows) => new Map(rows.flatMap((row) => row.type === "resource" ? [[row.key, toPlanTask(row)]] : []));
43444
- function PlanProgress(props) {
43445
- const { source, plan } = props;
43446
- const spinner = useGlobalSpinner();
43447
- const rows = (0, import_react.useMemo)(() => buildProgressRows(plan), [plan]);
43448
- const logicalIdIndex = (0, import_react.useMemo)(() => buildLogicalIdIndex(rows), [rows]);
43449
- const [tasks, setTasks] = (0, import_react.useState)(() => buildInitialTasks(rows));
43450
- const unsubscribeRef = (0, import_react.useRef)(null);
43451
- (0, import_react.useEffect)(() => {
43452
- unsubscribeRef.current?.();
43453
- unsubscribeRef.current = source.subscribe((event) => {
43454
- setTasks((prev) => {
43455
- const next = new Map(prev);
43456
- const keys = logicalIdIndex.get(event.id) ?? [];
43457
- if (event.kind === "status-change") {
43458
- if (!event.bindingId) for (const key of keys) {
43459
- const current = next.get(key);
43460
- next.set(key, {
43461
- key,
43462
- id: event.id,
43463
- type: event.type,
43464
- status: event.status,
43465
- message: event.message ?? current?.message,
43466
- updatedAt: Date.now()
43467
- });
43468
- }
43469
- } else for (const key of keys) {
43470
- const current = next.get(key);
43471
- if (!current) continue;
43472
- next.set(key, {
43473
- ...current,
43474
- message: event.message,
43475
- updatedAt: Date.now()
43476
- });
43477
- }
43478
- return next;
43479
- });
43480
- });
43481
- return () => {
43482
- unsubscribeRef.current?.();
43483
- unsubscribeRef.current = null;
43484
- };
43485
- }, [logicalIdIndex, source]);
43486
- (0, import_react.useEffect)(() => {
43487
- setTasks(buildInitialTasks(rows));
43488
- }, [rows]);
43489
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
43490
- flexDirection: "column",
43491
- children: rows.map((row) => {
43492
- const indent = " ".repeat(row.depth);
43493
- if (row.type === "namespace") return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, {
43494
- flexDirection: "row",
43495
- children: [
43496
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: indent }),
43497
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
43498
- width: 2,
43499
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
43500
- color: "blueBright",
43501
- children: "↳ "
43502
- })
43503
- }),
43504
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
43505
- color: "blueBright",
43506
- children: row.id
43507
- })
43508
- ]
43509
- }, row.key);
43510
- const task = tasks.get(row.key) ?? toPlanTask(row);
43511
- const displayStatus = getDisplayStatus(row, task.status);
43512
- const color = statusColor(displayStatus);
43513
- const icon = statusIcon(task.status, spinner);
43514
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, {
43515
- flexDirection: "column",
43516
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, {
43517
- flexDirection: "row",
43518
- children: [
43519
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { children: indent }),
43520
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
43521
- width: 2,
43522
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43523
- color,
43524
- children: [icon, " "]
43525
- })
43526
- }),
43527
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, {
43528
- bold: true,
43529
- children: task.id
43530
- }),
43531
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43532
- dimColor: true,
43533
- children: [
43534
- " (",
43535
- task.type,
43536
- ")"
43537
- ]
43538
- }),
43539
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43540
- color,
43541
- children: [" ", displayStatus]
43542
- })
43543
- ]
43544
- }), task.message ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, {
43545
- paddingLeft: row.depth * 2 + 2,
43546
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Text, {
43547
- dimColor: true,
43548
- children: ["• ", task.message]
43549
- })
43550
- }) : null]
43551
- }, row.key);
43552
- })
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());
43553
43487
  });
43554
- }
43555
- function getDisplayStatus(row, status) {
43556
- if (row.action === "noop" && (status === "created" || status === "updated")) return "no change";
43557
- return status;
43558
- }
43559
- function statusColor(status) {
43560
- switch (status) {
43561
- case "no change": return "gray";
43562
- case "pending": return "gray";
43563
- case "creating":
43564
- case "created": return "green";
43565
- case "updating":
43566
- case "updated": return "yellow";
43567
- case "deleting":
43568
- case "deleted": return "red";
43569
- case "fail": return "redBright";
43570
- default: return;
43571
- }
43572
- }
43573
- function statusIcon(status, spinnerChar) {
43574
- if (isInProgress(status)) return spinnerChar;
43575
- if (status === "fail") return "✗";
43576
- return "✓";
43577
- }
43578
- function isInProgress(status) {
43579
- return status === "pending" || status === "creating" || status === "updating" || status === "deleting";
43580
- }
43581
- const spinnerFrames = [
43582
- "⠋",
43583
- "⠙",
43584
- "⠹",
43585
- "⠸",
43586
- "⠼",
43587
- "⠴",
43588
- "⠦",
43589
- "⠧",
43590
- "⠇",
43591
- "⠏"
43592
- ];
43593
- function useGlobalSpinner(intervalMs = 80) {
43594
- const [index, setIndex] = (0, import_react.useState)(0);
43595
- (0, import_react.useEffect)(() => {
43596
- const timer = setInterval(() => {
43597
- setIndex((i) => (i + 1) % spinnerFrames.length);
43598
- }, intervalMs);
43599
- return () => clearInterval(timer);
43600
- }, [intervalMs]);
43601
- return spinnerFrames[index];
43602
- }
43603
- const findCrudByLogicalId = (plan, logicalId) => {
43604
- for (const node of Object.values(plan.resources)) if (node.resource.LogicalId === logicalId) return node;
43605
- for (const node of Object.values(plan.deletions)) if (node?.resource.LogicalId === logicalId) return node;
43606
43488
  };
43607
43489
 
43608
43490
  //#endregion
43609
- //#region src/Cli/InkCLI.tsx
43610
- const inkCLI = () => Layer.succeed(Cli, Cli.of({
43611
- approvePlan,
43612
- displayPlan,
43613
- startApplySession
43614
- }));
43615
- const approvePlan = Effect.fn(function* (plan) {
43616
- let approved = false;
43617
- const { waitUntilExit } = render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ApprovePlan, {
43618
- plan,
43619
- approve: (a) => approved = a
43620
- }));
43621
- yield* Effect.promise(waitUntilExit);
43622
- return approved;
43623
- });
43624
- const displayPlan = (plan) => Effect.sync(() => {
43625
- const { unmount } = render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Plan, { plan }));
43626
- unmount();
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" }));
43627
43499
  });
43628
- const startApplySession = Effect.fn(function* (plan) {
43629
- const listeners = /* @__PURE__ */ new Set();
43630
- const { unmount } = render_default(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlanProgress, {
43631
- plan,
43632
- source: { subscribe(listener) {
43633
- listeners.add(listener);
43634
- return () => listeners.delete(listener);
43635
- } }
43636
- }));
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);
43637
43580
  return {
43638
- done: Effect.fn(function* () {
43639
- yield* Effect.sleep(10);
43640
- yield* Effect.sync(() => unmount());
43581
+ get: (key) => Effect.sync(() => bag.get(key)),
43582
+ set: (key, value) => Effect.sync(() => {
43583
+ bag.set(key, value);
43641
43584
  }),
43642
- emit: (event) => Effect.sync(() => {
43643
- for (const listener of listeners) listener(event);
43585
+ delete: (key) => Effect.sync(() => {
43586
+ bag.delete(key);
43644
43587
  })
43645
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
43646
  });
43647
43647
 
43648
43648
  //#endregion
@@ -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
  });
@@ -45471,7 +45476,7 @@ var UnsatisfiedResourceCycle = class extends Data.TaggedError("UnsatisfiedResour
45471
45476
  //#region bin/commands/deploy.ts
45472
45477
  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
45478
  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));
45479
+ 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
45480
  yield* Effect.gen(function* () {
45476
45481
  const cli$1 = yield* Cli;
45477
45482
  const stack = yield* stackEffect;
@@ -45631,6 +45636,155 @@ const logsCommand = Command.make("logs", {
45631
45636
  }).pipe(Effect.provide(services$1));
45632
45637
  }));
45633
45638
 
45639
+ //#endregion
45640
+ //#region bin/commands/state.ts
45641
+ /**
45642
+ * When set, the State service is replaced with the on-disk
45643
+ * `LocalState` (`.alchemy/state` under the cwd) instead of whatever
45644
+ * the stack file configures (e.g. the Cloudflare HTTP state store).
45645
+ * Useful for inspecting orphaned local state after a partially-failed
45646
+ * bootstrap.
45647
+ */
45648
+ const localFlag = Flag.boolean("local").pipe(Flag.withDescription("Read from local .alchemy/state instead of the stack's configured state store"), Flag.withDefault(false));
45649
+ const stackArg = Argument.string("stack").pipe(Argument.withDescription("Stack name (e.g. AlchemyEffectWebsite)"));
45650
+ Argument.string("stage").pipe(Argument.withDescription("Stage name (e.g. dev_samgoodwin, prod)"));
45651
+ const fqnArg = Argument.string("fqn").pipe(Argument.withDescription("Fully-qualified resource name"));
45652
+ /**
45653
+ * Build the layer stack used by every `alchemy state ...` subcommand.
45654
+ *
45655
+ * The stack file is imported and evaluated so that its `state` layer
45656
+ * (Cloudflare HTTP store, in-memory, etc.) is in scope. Pass `local`
45657
+ * to swap the configured State for an on-disk LocalState instead.
45658
+ */
45659
+ const withStateService = (args, body) => Effect.gen(function* () {
45660
+ const stackEffect = yield* importStack(args.main);
45661
+ const services$1 = Layer.mergeAll(Layer.succeed(AuthProviders, {}), ConfigProvider.layer(withProfileOverride(yield* loadConfigProvider(args.envFile), args.profile)), Logger.layer([fileLogger("out")]), Layer.succeed(Stage, args.stage), args.local ? localState() : Layer.empty);
45662
+ return yield* Effect.gen(function* () {
45663
+ const stack = yield* stackEffect;
45664
+ return yield* Effect.gen(function* () {
45665
+ return yield* body(yield* State);
45666
+ }).pipe(Effect.provide(stack.services));
45667
+ }).pipe(Effect.provide(services$1));
45668
+ });
45669
+ const stacksCommand = Command.make("stacks", {
45670
+ main,
45671
+ envFile,
45672
+ stage,
45673
+ profile,
45674
+ local: localFlag
45675
+ }, Effect.fnUntraced(function* (args) {
45676
+ yield* withStateService(args, (state) => Effect.gen(function* () {
45677
+ const stacks = yield* state.listStacks();
45678
+ if (stacks.length === 0) {
45679
+ yield* Console.log("(no stacks)");
45680
+ return;
45681
+ }
45682
+ for (const s of [...stacks].sort()) yield* Console.log(s);
45683
+ }));
45684
+ }));
45685
+ const stagesCommand = Command.make("stages", {
45686
+ stack: stackArg,
45687
+ main,
45688
+ envFile,
45689
+ stage,
45690
+ profile,
45691
+ local: localFlag
45692
+ }, Effect.fnUntraced(function* ({ stack: stackName,...rest }) {
45693
+ yield* withStateService(rest, (state) => Effect.gen(function* () {
45694
+ const stages = yield* state.listStages(stackName);
45695
+ if (stages.length === 0) {
45696
+ yield* Console.log(`(no stages in ${stackName})`);
45697
+ return;
45698
+ }
45699
+ for (const s of [...stages].sort()) yield* Console.log(s);
45700
+ }));
45701
+ }));
45702
+ const resourcesCommand = Command.make("resources", {
45703
+ stack: stackArg,
45704
+ stageName: Argument.string("stage").pipe(Argument.withDescription("Stage to list resources from")),
45705
+ main,
45706
+ envFile,
45707
+ stage,
45708
+ profile,
45709
+ local: localFlag
45710
+ }, Effect.fnUntraced(function* ({ stack: stackName, stageName,...rest }) {
45711
+ yield* withStateService(rest, (state) => Effect.gen(function* () {
45712
+ const fqns = yield* state.list({
45713
+ stack: stackName,
45714
+ stage: stageName
45715
+ });
45716
+ if (fqns.length === 0) {
45717
+ yield* Console.log(`(no resources in ${stackName}/${stageName})`);
45718
+ return;
45719
+ }
45720
+ for (const f of [...fqns].sort()) yield* Console.log(f);
45721
+ }));
45722
+ }));
45723
+ const getCommand = Command.make("get", {
45724
+ stack: stackArg,
45725
+ stageName: Argument.string("stage").pipe(Argument.withDescription("Stage the resource lives in")),
45726
+ fqn: fqnArg,
45727
+ main,
45728
+ envFile,
45729
+ stage,
45730
+ profile,
45731
+ local: localFlag
45732
+ }, Effect.fnUntraced(function* ({ stack: stackName, stageName, fqn,...rest }) {
45733
+ yield* withStateService(rest, (state) => Effect.gen(function* () {
45734
+ const value = yield* state.get({
45735
+ stack: stackName,
45736
+ stage: stageName,
45737
+ fqn
45738
+ });
45739
+ if (value === void 0) {
45740
+ yield* Console.log(`(not found: ${stackName}/${stageName}/${fqn})`);
45741
+ return;
45742
+ }
45743
+ yield* Console.log(JSON.stringify(encodeState(value), null, 2));
45744
+ }));
45745
+ }));
45746
+ const treeCommand = Command.make("tree", {
45747
+ main,
45748
+ envFile,
45749
+ stage,
45750
+ profile,
45751
+ local: localFlag
45752
+ }, Effect.fnUntraced(function* (args) {
45753
+ yield* withStateService(args, (state) => Effect.gen(function* () {
45754
+ const stacks = [...yield* state.listStacks()].sort();
45755
+ if (stacks.length === 0) {
45756
+ yield* Console.log("(empty state store)");
45757
+ return;
45758
+ }
45759
+ for (const stk of stacks) {
45760
+ yield* Console.log(stk);
45761
+ const stages = [...yield* state.listStages(stk)].sort();
45762
+ for (let i = 0; i < stages.length; i++) {
45763
+ const stg = stages[i];
45764
+ const stageBranch = i === stages.length - 1 ? "└─" : "├─";
45765
+ yield* Console.log(`${stageBranch} ${stg}`);
45766
+ const indent = i === stages.length - 1 ? " " : "│ ";
45767
+ const fqns = [...yield* state.list({
45768
+ stack: stk,
45769
+ stage: stg
45770
+ })].sort();
45771
+ for (let j = 0; j < fqns.length; j++) {
45772
+ const fqn = fqns[j];
45773
+ const leaf = j === fqns.length - 1 ? "└─" : "├─";
45774
+ yield* Console.log(`${indent}${leaf} ${fqn}`);
45775
+ }
45776
+ }
45777
+ }
45778
+ }));
45779
+ }));
45780
+ const stateCommand = Command.make("state", {}).pipe(Command.withSubcommands([
45781
+ stacksCommand,
45782
+ stagesCommand,
45783
+ resourcesCommand,
45784
+ getCommand,
45785
+ treeCommand
45786
+ ]));
45787
+
45634
45788
  //#endregion
45635
45789
  //#region bin/commands/tail.ts
45636
45790
  const tailCommand = Command$1.make("tail", {
@@ -45700,10 +45854,11 @@ const root = Command$1.make("alchemy", {}).pipe(Command$1.withSubcommands([
45700
45854
  planCommand,
45701
45855
  tailCommand,
45702
45856
  logsCommand,
45703
- loginCommand
45857
+ loginCommand,
45858
+ stateCommand
45704
45859
  ]));
45705
45860
  const cli = Command$1.run(root, { version });
45706
- const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()));
45861
+ const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()), inkCLI());
45707
45862
  cli.pipe(Effect.provide(services), Effect.scoped, handleCancellation, runMain);
45708
45863
 
45709
45864
  //#endregion