alchemy 2.0.0-cli-shim.1 → 2.0.0-cli-bundle-fix.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/alchemy.js +968 -814
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +4 -0
- package/bin/commands/deploy.ts +0 -1
- package/bin/commands/state.ts +230 -0
- package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
- package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
- package/lib/Agent/lsp/client.d.ts +2 -2
- package/lib/Cli/InkCLI.d.ts +0 -1
- package/lib/Cli/InkCLI.d.ts.map +1 -1
- package/lib/Cli/InkCLI.js +0 -5
- package/lib/Cli/InkCLI.js.map +1 -1
- package/lib/Cli/index.d.ts +0 -1
- package/lib/Cli/index.d.ts.map +1 -1
- package/lib/Cli/index.js +4 -1
- package/lib/Cli/index.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Deploy.d.ts +1 -1
- package/lib/Deploy.d.ts.map +1 -1
- package/lib/Destroy.d.ts +1 -1
- package/lib/Destroy.d.ts.map +1 -1
- package/lib/Stack.d.ts +5 -4
- package/lib/Stack.d.ts.map +1 -1
- package/lib/Stack.js +27 -8
- package/lib/Stack.js.map +1 -1
- package/lib/State/HttpStateStore.js +32 -6
- package/lib/State/HttpStateStore.js.map +1 -1
- package/lib/State/Sync.d.ts +1 -0
- package/lib/State/Sync.d.ts.map +1 -1
- package/lib/State/Sync.js +1 -1
- package/lib/State/Sync.js.map +1 -1
- package/lib/Test/Bun.d.ts +2 -2
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +2 -4
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/Vitest.d.ts +1 -1
- package/lib/cli/index.d.ts +2 -6
- package/lib/cli/index.d.ts.map +1 -1
- package/lib/cli/index.js +2 -43050
- package/lib/cli/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Cli/InkCLI.tsx +0 -10
- package/src/Cli/index.ts +4 -1
- package/src/Stack.ts +33 -7
- package/src/State/HttpStateStore.ts +30 -7
- package/src/State/Sync.ts +4 -1
- 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";
|
|
@@ -30,17 +39,8 @@ import * as Deferred from "effect/Deferred";
|
|
|
30
39
|
import { pipe } from "effect/Function";
|
|
31
40
|
import { SingleShotGen } from "effect/Utils";
|
|
32
41
|
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
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-
|
|
77
|
+
var version = "2.0.0-cli-bundle-fix.1";
|
|
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
|
-
|
|
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
|
|
42878
|
-
|
|
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
|
-
|
|
43437
|
-
|
|
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
|
-
|
|
43444
|
-
|
|
43445
|
-
|
|
43446
|
-
|
|
43447
|
-
|
|
43448
|
-
|
|
43449
|
-
|
|
43450
|
-
|
|
43451
|
-
|
|
43452
|
-
|
|
43453
|
-
|
|
43454
|
-
|
|
43455
|
-
|
|
43456
|
-
|
|
43457
|
-
|
|
43458
|
-
|
|
43459
|
-
|
|
43460
|
-
|
|
43461
|
-
|
|
43462
|
-
|
|
43463
|
-
|
|
43464
|
-
|
|
43465
|
-
|
|
43466
|
-
|
|
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/
|
|
43610
|
-
const
|
|
43611
|
-
|
|
43612
|
-
|
|
43613
|
-
|
|
43614
|
-
|
|
43615
|
-
|
|
43616
|
-
|
|
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
|
-
|
|
43629
|
-
|
|
43630
|
-
|
|
43631
|
-
|
|
43632
|
-
|
|
43633
|
-
|
|
43634
|
-
|
|
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
|
-
|
|
43639
|
-
|
|
43640
|
-
|
|
43581
|
+
get: (key) => Effect.sync(() => bag.get(key)),
|
|
43582
|
+
set: (key, value) => Effect.sync(() => {
|
|
43583
|
+
bag.set(key, value);
|
|
43641
43584
|
}),
|
|
43642
|
-
|
|
43643
|
-
|
|
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) =>
|
|
43679
|
-
name
|
|
43680
|
-
|
|
43681
|
-
|
|
43682
|
-
|
|
43683
|
-
|
|
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()
|
|
@@ -45471,7 +45475,7 @@ var UnsatisfiedResourceCycle = class extends Data.TaggedError("UnsatisfiedResour
|
|
|
45471
45475
|
//#region bin/commands/deploy.ts
|
|
45472
45476
|
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
45477
|
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)),
|
|
45478
|
+
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
45479
|
yield* Effect.gen(function* () {
|
|
45476
45480
|
const cli$1 = yield* Cli;
|
|
45477
45481
|
const stack = yield* stackEffect;
|
|
@@ -45631,6 +45635,155 @@ const logsCommand = Command.make("logs", {
|
|
|
45631
45635
|
}).pipe(Effect.provide(services$1));
|
|
45632
45636
|
}));
|
|
45633
45637
|
|
|
45638
|
+
//#endregion
|
|
45639
|
+
//#region bin/commands/state.ts
|
|
45640
|
+
/**
|
|
45641
|
+
* When set, the State service is replaced with the on-disk
|
|
45642
|
+
* `LocalState` (`.alchemy/state` under the cwd) instead of whatever
|
|
45643
|
+
* the stack file configures (e.g. the Cloudflare HTTP state store).
|
|
45644
|
+
* Useful for inspecting orphaned local state after a partially-failed
|
|
45645
|
+
* bootstrap.
|
|
45646
|
+
*/
|
|
45647
|
+
const localFlag = Flag.boolean("local").pipe(Flag.withDescription("Read from local .alchemy/state instead of the stack's configured state store"), Flag.withDefault(false));
|
|
45648
|
+
const stackArg = Argument.string("stack").pipe(Argument.withDescription("Stack name (e.g. AlchemyEffectWebsite)"));
|
|
45649
|
+
Argument.string("stage").pipe(Argument.withDescription("Stage name (e.g. dev_samgoodwin, prod)"));
|
|
45650
|
+
const fqnArg = Argument.string("fqn").pipe(Argument.withDescription("Fully-qualified resource name"));
|
|
45651
|
+
/**
|
|
45652
|
+
* Build the layer stack used by every `alchemy state ...` subcommand.
|
|
45653
|
+
*
|
|
45654
|
+
* The stack file is imported and evaluated so that its `state` layer
|
|
45655
|
+
* (Cloudflare HTTP store, in-memory, etc.) is in scope. Pass `local`
|
|
45656
|
+
* to swap the configured State for an on-disk LocalState instead.
|
|
45657
|
+
*/
|
|
45658
|
+
const withStateService = (args, body) => Effect.gen(function* () {
|
|
45659
|
+
const stackEffect = yield* importStack(args.main);
|
|
45660
|
+
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);
|
|
45661
|
+
return yield* Effect.gen(function* () {
|
|
45662
|
+
const stack = yield* stackEffect;
|
|
45663
|
+
return yield* Effect.gen(function* () {
|
|
45664
|
+
return yield* body(yield* State);
|
|
45665
|
+
}).pipe(Effect.provide(stack.services));
|
|
45666
|
+
}).pipe(Effect.provide(services$1));
|
|
45667
|
+
});
|
|
45668
|
+
const stacksCommand = Command.make("stacks", {
|
|
45669
|
+
main,
|
|
45670
|
+
envFile,
|
|
45671
|
+
stage,
|
|
45672
|
+
profile,
|
|
45673
|
+
local: localFlag
|
|
45674
|
+
}, Effect.fnUntraced(function* (args) {
|
|
45675
|
+
yield* withStateService(args, (state) => Effect.gen(function* () {
|
|
45676
|
+
const stacks = yield* state.listStacks();
|
|
45677
|
+
if (stacks.length === 0) {
|
|
45678
|
+
yield* Console.log("(no stacks)");
|
|
45679
|
+
return;
|
|
45680
|
+
}
|
|
45681
|
+
for (const s of [...stacks].sort()) yield* Console.log(s);
|
|
45682
|
+
}));
|
|
45683
|
+
}));
|
|
45684
|
+
const stagesCommand = Command.make("stages", {
|
|
45685
|
+
stack: stackArg,
|
|
45686
|
+
main,
|
|
45687
|
+
envFile,
|
|
45688
|
+
stage,
|
|
45689
|
+
profile,
|
|
45690
|
+
local: localFlag
|
|
45691
|
+
}, Effect.fnUntraced(function* ({ stack: stackName,...rest }) {
|
|
45692
|
+
yield* withStateService(rest, (state) => Effect.gen(function* () {
|
|
45693
|
+
const stages = yield* state.listStages(stackName);
|
|
45694
|
+
if (stages.length === 0) {
|
|
45695
|
+
yield* Console.log(`(no stages in ${stackName})`);
|
|
45696
|
+
return;
|
|
45697
|
+
}
|
|
45698
|
+
for (const s of [...stages].sort()) yield* Console.log(s);
|
|
45699
|
+
}));
|
|
45700
|
+
}));
|
|
45701
|
+
const resourcesCommand = Command.make("resources", {
|
|
45702
|
+
stack: stackArg,
|
|
45703
|
+
stageName: Argument.string("stage").pipe(Argument.withDescription("Stage to list resources from")),
|
|
45704
|
+
main,
|
|
45705
|
+
envFile,
|
|
45706
|
+
stage,
|
|
45707
|
+
profile,
|
|
45708
|
+
local: localFlag
|
|
45709
|
+
}, Effect.fnUntraced(function* ({ stack: stackName, stageName,...rest }) {
|
|
45710
|
+
yield* withStateService(rest, (state) => Effect.gen(function* () {
|
|
45711
|
+
const fqns = yield* state.list({
|
|
45712
|
+
stack: stackName,
|
|
45713
|
+
stage: stageName
|
|
45714
|
+
});
|
|
45715
|
+
if (fqns.length === 0) {
|
|
45716
|
+
yield* Console.log(`(no resources in ${stackName}/${stageName})`);
|
|
45717
|
+
return;
|
|
45718
|
+
}
|
|
45719
|
+
for (const f of [...fqns].sort()) yield* Console.log(f);
|
|
45720
|
+
}));
|
|
45721
|
+
}));
|
|
45722
|
+
const getCommand = Command.make("get", {
|
|
45723
|
+
stack: stackArg,
|
|
45724
|
+
stageName: Argument.string("stage").pipe(Argument.withDescription("Stage the resource lives in")),
|
|
45725
|
+
fqn: fqnArg,
|
|
45726
|
+
main,
|
|
45727
|
+
envFile,
|
|
45728
|
+
stage,
|
|
45729
|
+
profile,
|
|
45730
|
+
local: localFlag
|
|
45731
|
+
}, Effect.fnUntraced(function* ({ stack: stackName, stageName, fqn,...rest }) {
|
|
45732
|
+
yield* withStateService(rest, (state) => Effect.gen(function* () {
|
|
45733
|
+
const value = yield* state.get({
|
|
45734
|
+
stack: stackName,
|
|
45735
|
+
stage: stageName,
|
|
45736
|
+
fqn
|
|
45737
|
+
});
|
|
45738
|
+
if (value === void 0) {
|
|
45739
|
+
yield* Console.log(`(not found: ${stackName}/${stageName}/${fqn})`);
|
|
45740
|
+
return;
|
|
45741
|
+
}
|
|
45742
|
+
yield* Console.log(JSON.stringify(encodeState(value), null, 2));
|
|
45743
|
+
}));
|
|
45744
|
+
}));
|
|
45745
|
+
const treeCommand = Command.make("tree", {
|
|
45746
|
+
main,
|
|
45747
|
+
envFile,
|
|
45748
|
+
stage,
|
|
45749
|
+
profile,
|
|
45750
|
+
local: localFlag
|
|
45751
|
+
}, Effect.fnUntraced(function* (args) {
|
|
45752
|
+
yield* withStateService(args, (state) => Effect.gen(function* () {
|
|
45753
|
+
const stacks = [...yield* state.listStacks()].sort();
|
|
45754
|
+
if (stacks.length === 0) {
|
|
45755
|
+
yield* Console.log("(empty state store)");
|
|
45756
|
+
return;
|
|
45757
|
+
}
|
|
45758
|
+
for (const stk of stacks) {
|
|
45759
|
+
yield* Console.log(stk);
|
|
45760
|
+
const stages = [...yield* state.listStages(stk)].sort();
|
|
45761
|
+
for (let i = 0; i < stages.length; i++) {
|
|
45762
|
+
const stg = stages[i];
|
|
45763
|
+
const stageBranch = i === stages.length - 1 ? "└─" : "├─";
|
|
45764
|
+
yield* Console.log(`${stageBranch} ${stg}`);
|
|
45765
|
+
const indent = i === stages.length - 1 ? " " : "│ ";
|
|
45766
|
+
const fqns = [...yield* state.list({
|
|
45767
|
+
stack: stk,
|
|
45768
|
+
stage: stg
|
|
45769
|
+
})].sort();
|
|
45770
|
+
for (let j = 0; j < fqns.length; j++) {
|
|
45771
|
+
const fqn = fqns[j];
|
|
45772
|
+
const leaf = j === fqns.length - 1 ? "└─" : "├─";
|
|
45773
|
+
yield* Console.log(`${indent}${leaf} ${fqn}`);
|
|
45774
|
+
}
|
|
45775
|
+
}
|
|
45776
|
+
}
|
|
45777
|
+
}));
|
|
45778
|
+
}));
|
|
45779
|
+
const stateCommand = Command.make("state", {}).pipe(Command.withSubcommands([
|
|
45780
|
+
stacksCommand,
|
|
45781
|
+
stagesCommand,
|
|
45782
|
+
resourcesCommand,
|
|
45783
|
+
getCommand,
|
|
45784
|
+
treeCommand
|
|
45785
|
+
]));
|
|
45786
|
+
|
|
45634
45787
|
//#endregion
|
|
45635
45788
|
//#region bin/commands/tail.ts
|
|
45636
45789
|
const tailCommand = Command$1.make("tail", {
|
|
@@ -45700,10 +45853,11 @@ const root = Command$1.make("alchemy", {}).pipe(Command$1.withSubcommands([
|
|
|
45700
45853
|
planCommand,
|
|
45701
45854
|
tailCommand,
|
|
45702
45855
|
logsCommand,
|
|
45703
|
-
loginCommand
|
|
45856
|
+
loginCommand,
|
|
45857
|
+
stateCommand
|
|
45704
45858
|
]));
|
|
45705
45859
|
const cli = Command$1.run(root, { version });
|
|
45706
|
-
const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()));
|
|
45860
|
+
const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()), inkCLI());
|
|
45707
45861
|
cli.pipe(Effect.provide(services), Effect.scoped, handleCancellation, runMain);
|
|
45708
45862
|
|
|
45709
45863
|
//#endregion
|