alchemy 2.0.0-beta.25 → 2.0.0-beta.27
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.d.ts +2 -0
- package/bin/alchemy.d.ts.map +1 -0
- package/bin/alchemy.js +3 -57288
- package/bin/alchemy.js.map +1 -1
- package/bin/alchemy.ts +3 -59
- package/bin/exec.d.ts +2 -0
- package/bin/exec.d.ts.map +1 -0
- package/bin/exec.js +2360 -0
- package/bin/exec.js.map +1 -0
- package/bin/exec.ts +3 -21
- package/lib/AWS/AuthProvider.d.ts +1 -1
- package/lib/AWS/AuthProvider.d.ts.map +1 -1
- 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/Auth/AuthProvider.d.ts.map +1 -1
- package/lib/Auth/AuthProvider.js +20 -3
- package/lib/Auth/AuthProvider.js.map +1 -1
- package/lib/Auth/Lock.d.ts +12 -0
- package/lib/Auth/Lock.d.ts.map +1 -0
- package/lib/Auth/Lock.js +27 -0
- package/lib/Auth/Lock.js.map +1 -0
- package/lib/Axiom/AuthProvider.d.ts +1 -1
- package/lib/Axiom/AuthProvider.d.ts.map +1 -1
- package/lib/Axiom/Credentials.d.ts +1 -1
- package/lib/Axiom/Credentials.d.ts.map +1 -1
- package/lib/Bundle/Bundle.d.ts +3 -3
- package/lib/Cli/commands/_shared.d.ts +51 -0
- package/lib/Cli/commands/_shared.d.ts.map +1 -0
- package/lib/Cli/commands/_shared.js +176 -0
- package/lib/Cli/commands/_shared.js.map +1 -0
- package/lib/Cli/commands/bootstrap.d.ts +6 -0
- package/lib/Cli/commands/bootstrap.d.ts.map +1 -0
- package/lib/Cli/commands/bootstrap.js +106 -0
- package/lib/Cli/commands/bootstrap.js.map +1 -0
- package/lib/Cli/commands/deploy.d.ts +56 -0
- package/lib/Cli/commands/deploy.d.ts.map +1 -0
- package/lib/Cli/commands/deploy.js +128 -0
- package/lib/Cli/commands/deploy.js.map +1 -0
- package/lib/Cli/commands/dev.d.ts +10 -0
- package/lib/Cli/commands/dev.d.ts.map +1 -0
- package/lib/Cli/commands/dev.js +45 -0
- package/lib/Cli/commands/dev.js.map +1 -0
- package/lib/Cli/commands/login.d.ts +10 -0
- package/lib/Cli/commands/login.d.ts.map +1 -0
- package/lib/Cli/commands/login.js +59 -0
- package/lib/Cli/commands/login.js.map +1 -0
- package/lib/Cli/commands/logs.d.ts +12 -0
- package/lib/Cli/commands/logs.d.ts.map +1 -0
- package/lib/Cli/commands/logs.js +99 -0
- package/lib/Cli/commands/logs.js.map +1 -0
- package/lib/Cli/commands/profile.d.ts +3 -0
- package/lib/Cli/commands/profile.d.ts.map +1 -0
- package/lib/Cli/commands/profile.js +76 -0
- package/lib/Cli/commands/profile.js.map +1 -0
- package/lib/Cli/commands/state.d.ts +3 -0
- package/lib/Cli/commands/state.d.ts.map +1 -0
- package/lib/Cli/commands/state.js +221 -0
- package/lib/Cli/commands/state.js.map +1 -0
- package/lib/Cli/commands/tail.d.ts +9 -0
- package/lib/Cli/commands/tail.d.ts.map +1 -0
- package/lib/Cli/commands/tail.js +93 -0
- package/lib/Cli/commands/tail.js.map +1 -0
- package/lib/Cli/exec.d.ts +3 -0
- package/lib/Cli/exec.d.ts.map +1 -0
- package/lib/Cli/exec.js +12 -0
- package/lib/Cli/exec.js.map +1 -0
- package/lib/Cli/index.d.ts +2 -1
- package/lib/Cli/index.d.ts.map +1 -1
- package/lib/Cli/index.js +2 -5
- package/lib/Cli/index.js.map +1 -1
- package/lib/Cli/main.d.ts +3 -0
- package/lib/Cli/main.d.ts.map +1 -0
- package/lib/Cli/main.js +40 -0
- package/lib/Cli/main.js.map +1 -0
- package/lib/Cli/{InkCLI.d.ts → tui/InkCLI.d.ts} +1 -1
- package/lib/Cli/tui/InkCLI.d.ts.map +1 -0
- package/lib/Cli/{InkCLI.js → tui/InkCLI.js} +1 -1
- package/lib/Cli/tui/InkCLI.js.map +1 -0
- package/lib/Cli/{components → tui/components}/ApprovePlan.d.ts +1 -1
- package/lib/Cli/tui/components/ApprovePlan.d.ts.map +1 -0
- package/lib/Cli/tui/components/ApprovePlan.js.map +1 -0
- package/lib/Cli/{components → tui/components}/Plan.d.ts +1 -1
- package/lib/Cli/tui/components/Plan.d.ts.map +1 -0
- package/lib/Cli/{components → tui/components}/Plan.js +1 -1
- package/lib/Cli/tui/components/Plan.js.map +1 -0
- package/lib/Cli/{components → tui/components}/PlanProgress.d.ts +3 -3
- package/lib/Cli/tui/components/PlanProgress.d.ts.map +1 -0
- package/lib/Cli/{components → tui/components}/PlanProgress.js +1 -1
- package/lib/Cli/tui/components/PlanProgress.js.map +1 -0
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts +1 -1
- package/lib/Cloudflare/ApiToken/AccountApiToken.d.ts.map +1 -1
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts +1 -1
- package/lib/Cloudflare/ApiToken/UserApiToken.d.ts.map +1 -1
- package/lib/Cloudflare/Auth/AuthProvider.d.ts +1 -1
- package/lib/Cloudflare/Auth/AuthProvider.d.ts.map +1 -1
- package/lib/Cloudflare/Auth/AuthProvider.js +0 -4
- package/lib/Cloudflare/Auth/AuthProvider.js.map +1 -1
- package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
- package/lib/Cloudflare/CloudflareEnvironment.d.ts.map +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.d.ts +1 -1
- package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
- package/lib/Cloudflare/Credentials.d.ts +1 -1
- package/lib/Cloudflare/Credentials.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Clone.d.ts +1 -1
- package/lib/Cloudflare/D1/D1Clone.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Import.d.ts +1 -1
- package/lib/Cloudflare/D1/D1Import.d.ts.map +1 -1
- package/lib/Cloudflare/D1/D1Migrations.d.ts +1 -1
- package/lib/Cloudflare/D1/D1Migrations.d.ts.map +1 -1
- package/lib/Cloudflare/KV/KVNamespace.d.ts +1 -1
- package/lib/Cloudflare/KV/KVNamespace.d.ts.map +1 -1
- package/lib/Cloudflare/Logs.d.ts +1 -1
- package/lib/Cloudflare/Logs.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/Queue.d.ts +1 -1
- package/lib/Cloudflare/Queue/Queue.d.ts.map +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts +1 -1
- package/lib/Cloudflare/Queue/QueueConsumer.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.d.ts +1 -1
- package/lib/Cloudflare/SecretsStore/Secret.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.d.ts.map +1 -1
- package/lib/Cloudflare/SecretsStore/SecretsStore.js +5 -5
- package/lib/Cloudflare/SecretsStore/SecretsStore.js.map +1 -1
- package/lib/Cloudflare/StateStore/Api.d.ts +13 -0
- package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/Api.js +15 -2
- package/lib/Cloudflare/StateStore/Api.js.map +1 -1
- package/lib/Cloudflare/StateStore/State.d.ts +3 -3
- package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
- package/lib/Cloudflare/StateStore/State.js.map +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts +1 -1
- package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.d.ts +2 -2
- package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
- package/lib/Cloudflare/Workers/Worker.js +14 -12
- package/lib/Cloudflare/Workers/Worker.js.map +1 -1
- package/lib/Cloudflare/Workers/Workflow.d.ts +1 -1
- package/lib/Cloudflare/Workers/Workflow.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/GitHub/AuthProvider.d.ts +1 -1
- package/lib/GitHub/AuthProvider.d.ts.map +1 -1
- package/lib/GitHub/Credentials.d.ts +1 -1
- package/lib/GitHub/Credentials.d.ts.map +1 -1
- package/lib/GitHub/Providers.d.ts +1 -1
- package/lib/GitHub/Providers.d.ts.map +1 -1
- package/lib/Output.d.ts.map +1 -1
- package/lib/Output.js +13 -2
- package/lib/Output.js.map +1 -1
- package/lib/Provider.d.ts +1 -1
- package/lib/Provider.d.ts.map +1 -1
- package/lib/Stack.js +2 -2
- package/lib/Stack.js.map +1 -1
- package/lib/Telemetry/Layer.d.ts.map +1 -1
- package/lib/Telemetry/Layer.js +13 -1
- package/lib/Telemetry/Layer.js.map +1 -1
- package/lib/Test/Bun.d.ts +59 -28
- package/lib/Test/Bun.d.ts.map +1 -1
- package/lib/Test/Bun.js +77 -81
- package/lib/Test/Bun.js.map +1 -1
- package/lib/Test/Core.d.ts +73 -0
- package/lib/Test/Core.d.ts.map +1 -0
- package/lib/Test/Core.js +98 -0
- package/lib/Test/Core.js.map +1 -0
- package/lib/Test/Vitest.d.ts +53 -72
- package/lib/Test/Vitest.d.ts.map +1 -1
- package/lib/Test/Vitest.js +73 -232
- package/lib/Test/Vitest.js.map +1 -1
- package/lib/Test/index.d.ts +0 -1
- package/lib/Test/index.d.ts.map +1 -1
- package/lib/Test/index.js +0 -1
- package/lib/Test/index.js.map +1 -1
- package/lib/Util/index.d.ts +1 -0
- package/lib/Util/index.d.ts.map +1 -1
- package/lib/Util/index.js +1 -0
- package/lib/Util/index.js.map +1 -1
- package/lib/index.d.ts +7 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -0
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +68 -52
- package/src/Auth/AuthProvider.ts +22 -4
- package/src/Auth/Lock.ts +35 -0
- package/{bin → src/Cli}/commands/_shared.ts +5 -5
- package/{bin → src/Cli}/commands/bootstrap.ts +18 -14
- package/{bin → src/Cli}/commands/deploy.ts +16 -16
- package/{bin → src/Cli}/commands/dev.ts +13 -3
- package/{bin → src/Cli}/commands/login.ts +9 -9
- package/{bin → src/Cli}/commands/logs.ts +11 -11
- package/{bin → src/Cli}/commands/profile.ts +8 -8
- package/{bin → src/Cli}/commands/state.ts +47 -26
- package/{bin → src/Cli}/commands/tail.ts +11 -11
- package/src/Cli/exec.ts +23 -0
- package/src/Cli/index.ts +2 -5
- package/src/Cli/main.ts +60 -0
- package/src/Cli/{InkCLI.tsx → tui/InkCLI.tsx} +3 -3
- package/src/Cli/{components → tui/components}/ApprovePlan.tsx +1 -1
- package/src/Cli/{components → tui/components}/Plan.tsx +6 -2
- package/src/Cli/{components → tui/components}/PlanProgress.tsx +7 -3
- package/src/Cloudflare/Auth/AuthProvider.ts +0 -5
- package/src/Cloudflare/SecretsStore/SecretsStore.ts +5 -5
- package/src/Cloudflare/StateStore/Api.ts +15 -2
- package/src/Cloudflare/StateStore/State.ts +3 -9
- package/src/Cloudflare/Workers/Worker.ts +29 -44
- package/src/Output.ts +10 -2
- package/src/Provider.ts +1 -1
- package/src/Stack.ts +2 -2
- package/src/Telemetry/Layer.ts +13 -1
- package/src/Test/Bun.ts +164 -140
- package/src/Test/Core.ts +225 -0
- package/src/Test/Vitest.ts +157 -509
- package/src/Test/index.ts +0 -1
- package/src/Util/index.ts +1 -0
- package/src/index.ts +14 -0
- package/lib/Cli/InkCLI.d.ts.map +0 -1
- package/lib/Cli/InkCLI.js.map +0 -1
- package/lib/Cli/components/ApprovePlan.d.ts.map +0 -1
- package/lib/Cli/components/ApprovePlan.js.map +0 -1
- package/lib/Cli/components/Plan.d.ts.map +0 -1
- package/lib/Cli/components/Plan.js.map +0 -1
- package/lib/Cli/components/PlanProgress.d.ts.map +0 -1
- package/lib/Cli/components/PlanProgress.js.map +0 -1
- package/lib/cli/index.d.ts +0 -413
- package/lib/cli/index.d.ts.map +0 -1
- package/lib/cli/index.js +0 -10
- package/lib/cli/index.js.map +0 -1
- /package/lib/Cli/{components → tui/components}/ApprovePlan.js +0 -0
package/bin/exec.js
ADDED
|
@@ -0,0 +1,2360 @@
|
|
|
1
|
+
import * as EffectContext from "effect/Context";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as FileSystem from "effect/FileSystem";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Path$1 from "effect/Path";
|
|
6
|
+
import { Path } from "effect/Path";
|
|
7
|
+
import * as Data from "effect/Data";
|
|
8
|
+
import { pipe } from "effect/Function";
|
|
9
|
+
import * as Redacted from "effect/Redacted";
|
|
10
|
+
import { SingleShotGen } from "effect/Utils";
|
|
11
|
+
import * as Option from "effect/Option";
|
|
12
|
+
import { pipeArguments } from "effect/Pipeable";
|
|
13
|
+
import * as ConfigProvider from "effect/ConfigProvider";
|
|
14
|
+
import * as Logger from "effect/Logger";
|
|
15
|
+
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
|
|
16
|
+
import * as Deferred from "effect/Deferred";
|
|
17
|
+
import * as S from "effect/Schema";
|
|
18
|
+
import path from "pathe";
|
|
19
|
+
import * as Config from "effect/Config";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import * as Console from "effect/Console";
|
|
22
|
+
import * as Command from "effect/unstable/cli/Command";
|
|
23
|
+
import * as Flag from "effect/unstable/cli/Flag";
|
|
24
|
+
import * as Duration from "effect/Duration";
|
|
25
|
+
import * as Exit from "effect/Exit";
|
|
26
|
+
import * as Metric from "effect/Metric";
|
|
27
|
+
import "effect/Cause";
|
|
28
|
+
import * as Argument from "effect/unstable/cli/Argument";
|
|
29
|
+
import * as CliError from "effect/unstable/cli/CliError";
|
|
30
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
31
|
+
import { Box, Text, render, useApp, useInput } from "ink";
|
|
32
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
33
|
+
|
|
34
|
+
//#region lib/AlchemyContext.js
|
|
35
|
+
var AlchemyContext = class extends EffectContext.Service()("alchemy/Context") {};
|
|
36
|
+
const AlchemyContextLive = Layer.effect(AlchemyContext, Effect.gen(function* () {
|
|
37
|
+
const fs = yield* FileSystem.FileSystem;
|
|
38
|
+
const dir = (yield* Path$1.Path).join(process.cwd(), ".alchemy");
|
|
39
|
+
yield* fs.makeDirectory(dir, { recursive: true });
|
|
40
|
+
return {
|
|
41
|
+
dotAlchemy: dir,
|
|
42
|
+
updateStateStore: false,
|
|
43
|
+
dev: false
|
|
44
|
+
};
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region lib/Util/data.js
|
|
49
|
+
const isPrimitive = (value) => value === void 0 || value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string" || typeof value === "symbol" || typeof value === "bigint";
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region lib/Util/PlatformServices.js
|
|
53
|
+
const isBun = typeof Bun !== "undefined";
|
|
54
|
+
const PlatformServices = Effect.promise(async () => {
|
|
55
|
+
if (isBun) {
|
|
56
|
+
const [BunServices, BunSocket] = await Promise.all([import("@effect/platform-bun/BunServices"), import("@effect/platform-bun/BunSocket")]);
|
|
57
|
+
return Layer.merge(BunServices.layer, BunSocket.layerWebSocketConstructor);
|
|
58
|
+
} else {
|
|
59
|
+
const [NodeServices, NodeSocket] = await Promise.all([import("@effect/platform-node/NodeServices"), import("@effect/platform-node/NodeSocket")]);
|
|
60
|
+
return Layer.merge(NodeServices.layer, NodeSocket.layerWebSocketConstructor);
|
|
61
|
+
}
|
|
62
|
+
}).pipe(Layer.unwrap);
|
|
63
|
+
const runMain = (effect, options) => {
|
|
64
|
+
if (isBun) import("@effect/platform-bun/BunRuntime").then((BunRuntime) => BunRuntime.runMain(effect, options));
|
|
65
|
+
else import("@effect/platform-node/NodeRuntime").then((NodeRuntime) => NodeRuntime.runMain(effect, options));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region lib/Util/service.js
|
|
70
|
+
const GenericService = () => (Kind) => {
|
|
71
|
+
const service = EffectContext.Service(Kind);
|
|
72
|
+
const make$2 = (Type) => EffectContext.Service(`${Kind}<${Type}>`);
|
|
73
|
+
return Object.assign(Object.setPrototypeOf(make$2, service), service);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region lib/Util/types.js
|
|
78
|
+
const asEffect = (effect) => Effect.isEffect(effect) ? effect : Effect.succeed(effect);
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region lib/ExecutionContext.js
|
|
82
|
+
const ExecutionContext = GenericService()("Alchemy::ExecutionContext");
|
|
83
|
+
const CurrentExecutionContext = Effect.serviceOption(ExecutionContext).pipe(Effect.map(Option.getOrUndefined));
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region lib/Ref.js
|
|
87
|
+
const RefMetadata = Symbol.for("alchemy/RefMetadata");
|
|
88
|
+
const isRef = (s) => s && s[RefMetadata] !== void 0;
|
|
89
|
+
const getRefMetadata = (ref$1) => ref$1[RefMetadata];
|
|
90
|
+
const ref = ({ stack, id, stage: stage$1 }) => {
|
|
91
|
+
const ref$1 = new Proxy({}, { get: (_, prop) => {
|
|
92
|
+
if (prop === RefMetadata) return {
|
|
93
|
+
stack,
|
|
94
|
+
stage: stage$1,
|
|
95
|
+
id
|
|
96
|
+
};
|
|
97
|
+
return of(ref$1)[prop];
|
|
98
|
+
} });
|
|
99
|
+
return ref$1;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region lib/FQN.js
|
|
104
|
+
/**
|
|
105
|
+
* Separator used in FQN strings.
|
|
106
|
+
*/
|
|
107
|
+
const FQN_SEPARATOR = "/";
|
|
108
|
+
/**
|
|
109
|
+
* Convert a NamespaceNode chain to an array of namespace IDs, from root to leaf.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const ns = { Id: "Child", Parent: { Id: "Parent", Parent: { Id: "Root" } } };
|
|
114
|
+
* toPath(ns); // ["Root", "Parent", "Child"]
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
const toPath = (ns) => {
|
|
118
|
+
if (!ns) return [];
|
|
119
|
+
const path$1 = [];
|
|
120
|
+
let current = ns;
|
|
121
|
+
while (current) {
|
|
122
|
+
path$1.unshift(current.Id);
|
|
123
|
+
current = current.Parent;
|
|
124
|
+
}
|
|
125
|
+
return path$1;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Create a fully-qualified name (FQN) from a namespace and logical ID.
|
|
129
|
+
* The FQN is a flat key suitable for state storage.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* const ns = { Id: "Child", Parent: { Id: "Parent" } };
|
|
134
|
+
* toFqn(ns, "MyResource"); // "Parent/Child/MyResource"
|
|
135
|
+
* toFqn(undefined, "MyResource"); // "MyResource"
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
const toFqn = (ns, logicalId) => {
|
|
139
|
+
const path$1 = toPath(ns);
|
|
140
|
+
path$1.push(logicalId);
|
|
141
|
+
return path$1.join(FQN_SEPARATOR);
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Parse an FQN back into namespace path segments and the logical ID.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* parseFqn("Parent/Child/MyResource"); // { path: ["Parent", "Child"], logicalId: "MyResource" }
|
|
149
|
+
* parseFqn("MyResource"); // { path: [], logicalId: "MyResource" }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
const parseFqn = (fqn) => {
|
|
153
|
+
const parts = fqn.split(FQN_SEPARATOR);
|
|
154
|
+
return {
|
|
155
|
+
path: parts,
|
|
156
|
+
logicalId: parts.pop()
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region lib/Provider.js
|
|
162
|
+
const Provider = (type) => EffectContext.Service()(type);
|
|
163
|
+
const isProviderCollectionService = (value) => {
|
|
164
|
+
return typeof value === "object" && value !== null && "kind" in value && value.kind === "ProviderCollection";
|
|
165
|
+
};
|
|
166
|
+
const findProviderByType = (type) => tryFindProviderByType(type).pipe(Effect.flatMap(Option.match({
|
|
167
|
+
onNone: () => Effect.die(`Provider not found for ${type}`),
|
|
168
|
+
onSome: (provider) => Effect.succeed(provider)
|
|
169
|
+
})));
|
|
170
|
+
const tryFindProviderByType = Effect.fnUntraced(function* (resourceType) {
|
|
171
|
+
const Tag = Provider(resourceType);
|
|
172
|
+
const direct = yield* Effect.serviceOption(Tag);
|
|
173
|
+
if (Option.isSome(direct)) return direct;
|
|
174
|
+
const context = yield* Effect.context();
|
|
175
|
+
for (const value of context.mapUnsafe.values()) if (isProviderCollectionService(value)) {
|
|
176
|
+
const provider = value.get(resourceType);
|
|
177
|
+
if (provider) return Option.some(provider);
|
|
178
|
+
}
|
|
179
|
+
return Option.none();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
//#endregion
|
|
183
|
+
//#region lib/Artifacts.js
|
|
184
|
+
/**
|
|
185
|
+
* Per-resource in-memory artifacts shared across a single `Plan.make -> apply`
|
|
186
|
+
* execution.
|
|
187
|
+
*
|
|
188
|
+
* The engine scopes this service by resource `FQN` before invoking lifecycle
|
|
189
|
+
* handlers, so providers should treat it as a resource-local bag for expensive,
|
|
190
|
+
* deterministic intermediate results that can be reused across phases.
|
|
191
|
+
*
|
|
192
|
+
* Expected usage:
|
|
193
|
+
*
|
|
194
|
+
* - `diff` computes an expensive artifact once and stores it
|
|
195
|
+
* - `create` / `update` reads the same artifact and skips recomputing it
|
|
196
|
+
* - artifacts are ephemeral and must never be required for correctness on a
|
|
197
|
+
* later deploy because the bag is reset between runs
|
|
198
|
+
*
|
|
199
|
+
* Example:
|
|
200
|
+
*
|
|
201
|
+
* ```ts
|
|
202
|
+
* const artifacts = yield* Artifacts;
|
|
203
|
+
* const cached = yield* artifacts.get<PreparedBundle>("bundle");
|
|
204
|
+
* if (cached) return cached;
|
|
205
|
+
*
|
|
206
|
+
* const bundle = yield* prepareBundle();
|
|
207
|
+
* yield* artifacts.set("bundle", bundle);
|
|
208
|
+
* return bundle;
|
|
209
|
+
* ```
|
|
210
|
+
*/
|
|
211
|
+
var Artifacts = class extends EffectContext.Service()("Artifacts") {};
|
|
212
|
+
var ArtifactStore = class extends EffectContext.Service()("Artifacts/Store") {};
|
|
213
|
+
/**
|
|
214
|
+
* Create a fresh root store for one deploy/test run.
|
|
215
|
+
*/
|
|
216
|
+
const createArtifactStore = () => /* @__PURE__ */ new Map();
|
|
217
|
+
const getOrCreateBag = (store, fqn) => {
|
|
218
|
+
const existing = store.get(fqn);
|
|
219
|
+
if (existing) return existing;
|
|
220
|
+
const bag = /* @__PURE__ */ new Map();
|
|
221
|
+
store.set(fqn, bag);
|
|
222
|
+
return bag;
|
|
223
|
+
};
|
|
224
|
+
const makeScopedArtifacts = (store, fqn) => {
|
|
225
|
+
const bag = getOrCreateBag(store, fqn);
|
|
226
|
+
return {
|
|
227
|
+
get: (key) => Effect.sync(() => bag.get(key)),
|
|
228
|
+
set: (key, value) => Effect.sync(() => {
|
|
229
|
+
bag.set(key, value);
|
|
230
|
+
}),
|
|
231
|
+
delete: (key) => Effect.sync(() => {
|
|
232
|
+
bag.delete(key);
|
|
233
|
+
})
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* Ensure an artifact root exists, reusing the ambient store when one is already
|
|
238
|
+
* present. This lets nested helpers participate in the same run-scoped cache.
|
|
239
|
+
*/
|
|
240
|
+
const ensureArtifactStore = (effect) => Effect.serviceOption(ArtifactStore).pipe(Effect.map(Option.getOrUndefined), Effect.flatMap((existing) => effect.pipe(Effect.provideService(ArtifactStore, existing ?? createArtifactStore()))));
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region lib/Auth/Profile.js
|
|
244
|
+
const rootDir = path.join(os.homedir(), ".alchemy");
|
|
245
|
+
const configFilePath = path.join(rootDir, "profiles.json");
|
|
246
|
+
/**
|
|
247
|
+
* Config key consulted by the various `fromAuthProvider` /
|
|
248
|
+
* `fromEnvironment` layers to pick which named profile in
|
|
249
|
+
* `~/.alchemy/profiles.json` to use. Defaults to `"default"`.
|
|
250
|
+
*/
|
|
251
|
+
const ALCHEMY_PROFILE = Config.string("ALCHEMY_PROFILE").pipe(Config.withDefault("default"));
|
|
252
|
+
const CONFIG_VERSION = 0;
|
|
253
|
+
const emptyConfig = () => ({
|
|
254
|
+
version: CONFIG_VERSION,
|
|
255
|
+
profiles: {}
|
|
256
|
+
});
|
|
257
|
+
const readConfig = FileSystem.FileSystem.asEffect().pipe(Effect.flatMap((fs) => fs.readFileString(configFilePath)), Effect.flatMap((data) => Effect.try({
|
|
258
|
+
try: () => {
|
|
259
|
+
const parsed = JSON.parse(data);
|
|
260
|
+
if (parsed?.version !== CONFIG_VERSION) return emptyConfig();
|
|
261
|
+
return parsed;
|
|
262
|
+
},
|
|
263
|
+
catch: emptyConfig
|
|
264
|
+
})), Effect.orElseSucceed(emptyConfig));
|
|
265
|
+
/**
|
|
266
|
+
* Returns a `ConfigProvider` that overrides `ALCHEMY_PROFILE` with the
|
|
267
|
+
* given `profile` (when explicitly passed via the CLI `--profile` flag),
|
|
268
|
+
* falling through to `base` for everything else.
|
|
269
|
+
*
|
|
270
|
+
* Use this to let the CLI's `--profile <name>` win over `$ALCHEMY_PROFILE`
|
|
271
|
+
* without disturbing other config lookups.
|
|
272
|
+
*/
|
|
273
|
+
const withProfileOverride = (base, profile$1) => {
|
|
274
|
+
if (profile$1 === void 0) return base;
|
|
275
|
+
const overrides = { ALCHEMY_PROFILE: profile$1 };
|
|
276
|
+
const overrideProvider = ConfigProvider.make((path$1) => Effect.succeed(path$1.length === 1 && typeof path$1[0] === "string" && path$1[0] in overrides ? ConfigProvider.makeValue(overrides[path$1[0]]) : void 0));
|
|
277
|
+
return ConfigProvider.orElse(base)(overrideProvider);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region lib/Auth/AuthProvider.js
|
|
282
|
+
var AuthProviders = class extends EffectContext.Service()("AuthProviders") {};
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region lib/Cli/Cli.js
|
|
286
|
+
var Cli = class extends EffectContext.Service()("CLI") {};
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region lib/Stage.js
|
|
290
|
+
var Stage = class extends EffectContext.Service()("Stage") {};
|
|
291
|
+
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region lib/Util/ConfigProvider.js
|
|
294
|
+
const loadConfigProvider = (envFile$1) => {
|
|
295
|
+
if (Option.isSome(envFile$1)) return ConfigProvider.fromDotEnv({ path: envFile$1.value }).pipe(Effect.map((dotEnv) => ConfigProvider.orElse(dotEnv, ConfigProvider.fromEnv())));
|
|
296
|
+
return Effect.gen(function* () {
|
|
297
|
+
if (!(yield* (yield* FileSystem.FileSystem).exists(".env"))) return ConfigProvider.fromEnv();
|
|
298
|
+
return ConfigProvider.orElse(yield* ConfigProvider.fromDotEnv({ path: ".env" }), ConfigProvider.fromEnv());
|
|
299
|
+
});
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region lib/Util/effect.js
|
|
304
|
+
const taggedFunction = (tag, fn) => {
|
|
305
|
+
const overrides = {
|
|
306
|
+
asEffect: () => tag.asEffect(),
|
|
307
|
+
[Symbol.iterator]: () => tag[Symbol.iterator](),
|
|
308
|
+
pipe: (...fns) => pipeArguments(tag.asEffect(), fns),
|
|
309
|
+
toString: () => `${tag.toString()}.${fn.name}`
|
|
310
|
+
};
|
|
311
|
+
return new Proxy(fn, {
|
|
312
|
+
get: (target, prop, receiver) => Reflect.has(overrides, prop) ? Reflect.get(overrides, prop, receiver) : Reflect.has(target, prop) ? Reflect.get(target, prop, receiver) : Reflect.get(tag, prop, tag),
|
|
313
|
+
has: (target, prop) => Reflect.has(overrides, prop) || Reflect.has(target, prop) || Reflect.has(tag, prop)
|
|
314
|
+
});
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region lib/Util/FileLogger.js
|
|
319
|
+
const fileLogger = Effect.fnUntraced(function* (...segments) {
|
|
320
|
+
const { dotAlchemy } = yield* AlchemyContext;
|
|
321
|
+
const fs = yield* FileSystem.FileSystem;
|
|
322
|
+
const path$1 = yield* Path;
|
|
323
|
+
const logFile = path$1.join(dotAlchemy, "log", ...segments);
|
|
324
|
+
yield* fs.makeDirectory(path$1.dirname(logFile), { recursive: true });
|
|
325
|
+
return yield* Logger.formatLogFmt.pipe(Logger.toFile(logFile, { flag: "a" }));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region lib/Stack.js
|
|
330
|
+
const Stack = Object.assign(taggedFunction(EffectContext.Service()("Stack"), (stackName, options, eff) => eff.pipe(make$1({
|
|
331
|
+
name: stackName,
|
|
332
|
+
...options
|
|
333
|
+
}), (eff$1) => Object.assign(eff$1, { stage: new Proxy({}, { get: (_, stage$1) => ref({
|
|
334
|
+
stack: stackName,
|
|
335
|
+
stage: stage$1,
|
|
336
|
+
id: stackName
|
|
337
|
+
}) }) }))));
|
|
338
|
+
const StackName = Stack.use((stack) => Effect.succeed(stack.name));
|
|
339
|
+
const make$1 = (options) => (effect) => Effect.scope.pipe(Effect.flatMap((scope) => {
|
|
340
|
+
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://v2.alchemy.run/concepts/state-store for available state stores.`));
|
|
341
|
+
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 }, ...)`));
|
|
342
|
+
return options.providers.pipe(Layer.provideMerge(options.state), Layer.provideMerge(Layer.effect(Stack, Stage.asEffect().pipe(Effect.map((stage$1) => options.stack ?? {
|
|
343
|
+
name: options.name,
|
|
344
|
+
stage: stage$1,
|
|
345
|
+
resources: {},
|
|
346
|
+
bindings: {}
|
|
347
|
+
})))), Layer.buildWithScope(scope));
|
|
348
|
+
}), Effect.flatMap((context) => Effect.all([
|
|
349
|
+
effect,
|
|
350
|
+
Stack.asEffect(),
|
|
351
|
+
Effect.context()
|
|
352
|
+
]).pipe(Effect.map(([output, stack, services$1]) => ({
|
|
353
|
+
...stack,
|
|
354
|
+
output,
|
|
355
|
+
services: EffectContext.merge(services$1, context)
|
|
356
|
+
})), Effect.provideContext(context))));
|
|
357
|
+
const CurrentStack = Effect.serviceOption(Stack).asEffect().pipe(Effect.map(Option.getOrUndefined));
|
|
358
|
+
const platform = Layer.mergeAll(PlatformServices, FetchHttpClient.layer, Logger.layer([fileLogger("out")], { mergeWithExisting: true }));
|
|
359
|
+
const alchemy = Layer.mergeAll(AlchemyContextLive);
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
//#region lib/Resource.js
|
|
363
|
+
const isResource = (value) => {
|
|
364
|
+
return typeof value === "object" && value !== null && "Type" in value;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region lib/State/State.js
|
|
369
|
+
var State = class extends EffectContext.Service()("alchemy/State") {};
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region lib/Output.js
|
|
373
|
+
const inspect = Symbol.for("nodejs.util.inspect.custom");
|
|
374
|
+
const of = (resource) => {
|
|
375
|
+
if (isRef(resource)) {
|
|
376
|
+
const metadata = getRefMetadata(resource);
|
|
377
|
+
return new RefExpr(metadata.stack, metadata.stage, metadata.id);
|
|
378
|
+
}
|
|
379
|
+
return new ResourceExpr(resource);
|
|
380
|
+
};
|
|
381
|
+
const isOutput = (value) => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value;
|
|
382
|
+
const ExprSymbol = Symbol.for("alchemy/Expr");
|
|
383
|
+
const isExpr = (value) => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value;
|
|
384
|
+
var BaseExpr = class {
|
|
385
|
+
constructor() {}
|
|
386
|
+
as() {
|
|
387
|
+
return this;
|
|
388
|
+
}
|
|
389
|
+
[Symbol.iterator]() {
|
|
390
|
+
return new SingleShotGen(this);
|
|
391
|
+
}
|
|
392
|
+
asEffect() {
|
|
393
|
+
return this.bind(this.toString());
|
|
394
|
+
}
|
|
395
|
+
bind(id) {
|
|
396
|
+
return ExecutionContext.asEffect().pipe(Effect.flatMap((ctx) => Effect.map(ctx.set(id, this), (key) => ctx.get(key))));
|
|
397
|
+
}
|
|
398
|
+
pipe(...fns) {
|
|
399
|
+
return pipe(this, ...fns);
|
|
400
|
+
}
|
|
401
|
+
toString() {
|
|
402
|
+
return this[inspect]();
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const isResourceExpr = (node) => node?.kind === "ResourceExpr";
|
|
406
|
+
var ResourceExpr = class extends BaseExpr {
|
|
407
|
+
src;
|
|
408
|
+
stables;
|
|
409
|
+
kind = "ResourceExpr";
|
|
410
|
+
constructor(src, stables) {
|
|
411
|
+
super();
|
|
412
|
+
this.src = src;
|
|
413
|
+
this.stables = stables;
|
|
414
|
+
return proxy(this);
|
|
415
|
+
}
|
|
416
|
+
[inspect]() {
|
|
417
|
+
return this.src.LogicalId;
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
const isPropExpr = (node) => node?.kind === "PropExpr";
|
|
421
|
+
var PropExpr = class extends BaseExpr {
|
|
422
|
+
expr;
|
|
423
|
+
identifier;
|
|
424
|
+
kind = "PropExpr";
|
|
425
|
+
constructor(expr, identifier) {
|
|
426
|
+
super();
|
|
427
|
+
this.expr = expr;
|
|
428
|
+
this.identifier = identifier;
|
|
429
|
+
return proxy(this);
|
|
430
|
+
}
|
|
431
|
+
[inspect]() {
|
|
432
|
+
return `${this.expr[inspect]()}.${this.identifier.toString()}`;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const isLiteralExpr = (node) => node?.kind === "LiteralExpr";
|
|
436
|
+
var LiteralExpr = class extends BaseExpr {
|
|
437
|
+
value;
|
|
438
|
+
kind = "LiteralExpr";
|
|
439
|
+
constructor(value) {
|
|
440
|
+
super();
|
|
441
|
+
this.value = value;
|
|
442
|
+
return proxy(this);
|
|
443
|
+
}
|
|
444
|
+
[inspect]() {
|
|
445
|
+
return String(this.value);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
const VoidExpr = new LiteralExpr(void 0);
|
|
449
|
+
const isApplyExpr = (node) => node?.kind === "ApplyExpr";
|
|
450
|
+
var ApplyExpr = class extends BaseExpr {
|
|
451
|
+
expr;
|
|
452
|
+
f;
|
|
453
|
+
kind = "ApplyExpr";
|
|
454
|
+
constructor(expr, f) {
|
|
455
|
+
super();
|
|
456
|
+
this.expr = expr;
|
|
457
|
+
this.f = f;
|
|
458
|
+
return proxy(this);
|
|
459
|
+
}
|
|
460
|
+
[inspect]() {
|
|
461
|
+
return `${this.expr[inspect]()}.map(${this.f.toString()})`;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
const isEffectExpr = (node) => node?.kind === "EffectExpr";
|
|
465
|
+
var EffectExpr = class extends BaseExpr {
|
|
466
|
+
expr;
|
|
467
|
+
f;
|
|
468
|
+
kind = "EffectExpr";
|
|
469
|
+
constructor(expr, f) {
|
|
470
|
+
super();
|
|
471
|
+
this.expr = expr;
|
|
472
|
+
this.f = f;
|
|
473
|
+
return proxy(this);
|
|
474
|
+
}
|
|
475
|
+
[inspect]() {
|
|
476
|
+
return `${this.expr[inspect]()}.mapEffect(${this.f.toString()})`;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
const isAllExpr = (node) => node?.kind === "AllExpr";
|
|
480
|
+
var AllExpr = class extends BaseExpr {
|
|
481
|
+
outs;
|
|
482
|
+
kind = "AllExpr";
|
|
483
|
+
constructor(outs) {
|
|
484
|
+
super();
|
|
485
|
+
this.outs = outs;
|
|
486
|
+
return proxy(this);
|
|
487
|
+
}
|
|
488
|
+
[inspect]() {
|
|
489
|
+
return `all(${this.outs.map((out) => out[inspect]()).join(", ")})`;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const isRefExpr = (node) => node?.kind === "RefExpr";
|
|
493
|
+
var RefExpr = class extends BaseExpr {
|
|
494
|
+
stack;
|
|
495
|
+
stage;
|
|
496
|
+
resourceId;
|
|
497
|
+
kind = "RefExpr";
|
|
498
|
+
constructor(stack, stage$1, resourceId) {
|
|
499
|
+
super();
|
|
500
|
+
this.stack = stack;
|
|
501
|
+
this.stage = stage$1;
|
|
502
|
+
this.resourceId = resourceId;
|
|
503
|
+
return proxy(this);
|
|
504
|
+
}
|
|
505
|
+
[inspect]() {
|
|
506
|
+
return `ref(${this.resourceId}, { stack: ${this.stack}, stage: ${this.stage} })`;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function proxy(self) {
|
|
510
|
+
const target = Object.assign(() => {}, self);
|
|
511
|
+
if (inspect in self) Object.defineProperty(target, inspect, {
|
|
512
|
+
value: self[inspect].bind(self),
|
|
513
|
+
configurable: true
|
|
514
|
+
});
|
|
515
|
+
const proxy$1 = new Proxy(target, {
|
|
516
|
+
has: (_, prop) => prop === ExprSymbol || prop === inspect ? true : prop in self,
|
|
517
|
+
get: (target$1, prop) => prop === Symbol.toPrimitive ? (hint) => hint === "number" ? NaN : self.toString() : prop === ExprSymbol ? self : prop === inspect ? target$1[inspect] : isResourceExpr(self) && self.stables && prop in self.stables ? self.stables[prop] : prop === "apply" ? self[prop] : prop in self ? typeof self[prop] === "function" && !("kind" in self) ? new PropExpr(proxy$1, prop) : self[prop] : new PropExpr(proxy$1, prop),
|
|
518
|
+
apply: (_, thisArg, args) => {
|
|
519
|
+
if (isPropExpr(self)) {
|
|
520
|
+
if (self.identifier === "apply") return new ApplyExpr(self.expr, args[0]);
|
|
521
|
+
else if (self.identifier === "effect") return new EffectExpr(self.expr, args[0]);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
return proxy$1;
|
|
526
|
+
}
|
|
527
|
+
var MissingSourceError = class extends Data.TaggedError("MissingSourceError") {};
|
|
528
|
+
var InvalidReferenceError = class extends Data.TaggedError("InvalidReferenceError") {};
|
|
529
|
+
const evaluate = (expr, upstream$1) => Effect.gen(function* () {
|
|
530
|
+
if (isResource(expr)) {
|
|
531
|
+
const srcId = expr.FQN;
|
|
532
|
+
const src = upstream$1[srcId];
|
|
533
|
+
if (!src) return yield* new MissingSourceError({
|
|
534
|
+
message: `Source ${srcId} not found`,
|
|
535
|
+
srcId
|
|
536
|
+
});
|
|
537
|
+
return src;
|
|
538
|
+
} else if (isOutput(expr)) {
|
|
539
|
+
if (isResourceExpr(expr)) {
|
|
540
|
+
const srcId = expr.src.FQN;
|
|
541
|
+
const src = upstream$1[srcId];
|
|
542
|
+
if (!src) return yield* new MissingSourceError({
|
|
543
|
+
message: `Source ${srcId} not found`,
|
|
544
|
+
srcId
|
|
545
|
+
});
|
|
546
|
+
return src;
|
|
547
|
+
} else if (isLiteralExpr(expr)) return expr.value;
|
|
548
|
+
else if (isApplyExpr(expr)) return expr.f(yield* evaluate(expr.expr, upstream$1));
|
|
549
|
+
else if (isEffectExpr(expr)) return yield* expr.f(yield* evaluate(expr.expr, upstream$1));
|
|
550
|
+
else if (isAllExpr(expr)) return yield* Effect.all(expr.outs.map((out) => evaluate(out, upstream$1)));
|
|
551
|
+
else if (isPropExpr(expr)) return (yield* evaluate(expr.expr, upstream$1))?.[expr.identifier];
|
|
552
|
+
else if (isRefExpr(expr)) {
|
|
553
|
+
const state = yield* State;
|
|
554
|
+
const stack = expr.stack ?? (yield* Stack).name;
|
|
555
|
+
const stage$1 = expr.stage ?? (yield* Stage);
|
|
556
|
+
const resource = yield* state.get({
|
|
557
|
+
stack,
|
|
558
|
+
stage: stage$1,
|
|
559
|
+
fqn: expr.resourceId
|
|
560
|
+
});
|
|
561
|
+
if (!resource) return yield* Effect.fail(new InvalidReferenceError({
|
|
562
|
+
message: `Reference to '${expr.resourceId}' in stack '${stack}' and stage '${stage$1}' not found. Have you deployed '${stage$1}' or '${stack}'?`,
|
|
563
|
+
stack,
|
|
564
|
+
stage: stage$1,
|
|
565
|
+
resourceId: expr.resourceId
|
|
566
|
+
}));
|
|
567
|
+
return resource.attr;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (Array.isArray(expr)) return yield* Effect.all(expr.map((item) => evaluate(item, upstream$1)));
|
|
571
|
+
else if (Redacted.isRedacted(expr)) return expr;
|
|
572
|
+
else if (typeof expr === "object" && expr !== null) return Object.fromEntries(yield* Effect.all(Object.entries(expr).map(([key, value]) => evaluate(value, upstream$1).pipe(Effect.map((value$1) => [key, value$1])))));
|
|
573
|
+
return expr;
|
|
574
|
+
});
|
|
575
|
+
const hasOutputs = (value) => Object.keys(upstreamAny(value)).length > 0;
|
|
576
|
+
const upstreamAny = (value) => {
|
|
577
|
+
if (isResource(value)) return { [value.FQN]: value };
|
|
578
|
+
else if (isExpr(value)) return upstream(value);
|
|
579
|
+
else if (Array.isArray(value)) return Object.assign({}, ...value.map(resolveUpstream));
|
|
580
|
+
else if (value && (typeof value === "object" || typeof value === "function")) return Object.assign({}, ...Object.values(value).map((value$1) => resolveUpstream(value$1)));
|
|
581
|
+
return {};
|
|
582
|
+
};
|
|
583
|
+
const upstream = (expr) => {
|
|
584
|
+
if (isResource(expr)) return { [expr.FQN]: expr };
|
|
585
|
+
else if (isResourceExpr(expr)) return { [expr.src.FQN]: expr.src };
|
|
586
|
+
else if (isPropExpr(expr)) return upstream(expr.expr);
|
|
587
|
+
else if (isAllExpr(expr)) return Object.assign({}, ...expr.outs.map((out) => upstream(out)));
|
|
588
|
+
else if (isEffectExpr(expr) || isApplyExpr(expr)) return upstream(expr.expr);
|
|
589
|
+
else if (Array.isArray(expr)) return expr.map(upstream).reduce(toObject, {});
|
|
590
|
+
else if (typeof expr === "object" && expr !== null) return Object.values(expr).map((v) => upstream(v)).reduce(toObject, {});
|
|
591
|
+
return {};
|
|
592
|
+
};
|
|
593
|
+
const resolveUpstream = (value) => {
|
|
594
|
+
if (isPrimitive(value)) return {};
|
|
595
|
+
else if (isResource(value)) return { [value.FQN]: value };
|
|
596
|
+
else if (isOutput(value)) return upstream(value);
|
|
597
|
+
else if (Array.isArray(value)) return Object.fromEntries(value.map((v) => resolveUpstream(v)).flatMap(Object.entries));
|
|
598
|
+
else if (typeof value === "object" || typeof value === "function") return Object.fromEntries(Object.values(value).map(resolveUpstream).flatMap(Object.entries));
|
|
599
|
+
return {};
|
|
600
|
+
};
|
|
601
|
+
const toObject = (acc, v) => ({
|
|
602
|
+
...acc,
|
|
603
|
+
...v
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
//#endregion
|
|
607
|
+
//#region lib/AdoptPolicy.js
|
|
608
|
+
var AdoptPolicy = class extends EffectContext.Service()("AdoptPolicy") {};
|
|
609
|
+
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region lib/Diff.js
|
|
612
|
+
const havePropsChanged = (oldProps, newProps) => hasOutputs(newProps) || JSON.stringify(canonicalize(oldProps ?? {})) !== JSON.stringify(canonicalize(newProps ?? {}));
|
|
613
|
+
const canonicalize = (value) => {
|
|
614
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
615
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, nested]) => [key, canonicalize(nested)]));
|
|
616
|
+
return value;
|
|
617
|
+
};
|
|
618
|
+
const diffBindings = (oldBindings, newBindings) => {
|
|
619
|
+
const oldMap = new Map(oldBindings.map((b) => [b.sid, b]));
|
|
620
|
+
const newMap = new Map(newBindings.map((b) => [b.sid, b]));
|
|
621
|
+
return [...Array.from(oldMap).filter(([sid]) => !newMap.has(sid)).map(([sid, old]) => ({
|
|
622
|
+
sid,
|
|
623
|
+
action: "delete",
|
|
624
|
+
data: old.data
|
|
625
|
+
})), ...Array.from(newMap).map(([sid, binding]) => {
|
|
626
|
+
const old = oldMap.get(sid);
|
|
627
|
+
return {
|
|
628
|
+
sid,
|
|
629
|
+
action: !old ? "create" : havePropsChanged(old.data, binding.data) ? "update" : "noop",
|
|
630
|
+
data: binding.data
|
|
631
|
+
};
|
|
632
|
+
})];
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region lib/InstanceId.js
|
|
637
|
+
/** A 16-byte (128-bit) random hex-encoded string representing an physical instance of a logical resource */
|
|
638
|
+
var InstanceId = class extends EffectContext.Service()("instance-id") {};
|
|
639
|
+
/**
|
|
640
|
+
* @returns Hex-encoded instance ID (16 random bytes)
|
|
641
|
+
*/
|
|
642
|
+
const generateInstanceId = () => Effect.sync(() => {
|
|
643
|
+
const bytes = new Uint8Array(16);
|
|
644
|
+
crypto.getRandomValues(bytes);
|
|
645
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region lib/Plan.js
|
|
650
|
+
const make = (stack, options = {}) => Effect.gen(function* () {
|
|
651
|
+
const state = yield* State;
|
|
652
|
+
const resources = Object.values(stack.resources);
|
|
653
|
+
const stackName = stack.name;
|
|
654
|
+
const stage$1 = stack.stage;
|
|
655
|
+
const resourceFqns = yield* state.list({
|
|
656
|
+
stack: stackName,
|
|
657
|
+
stage: stage$1
|
|
658
|
+
});
|
|
659
|
+
const oldResources = yield* Effect.all(resourceFqns.map((fqn) => state.get({
|
|
660
|
+
stack: stackName,
|
|
661
|
+
stage: stage$1,
|
|
662
|
+
fqn
|
|
663
|
+
})), { concurrency: "unbounded" });
|
|
664
|
+
const resolvedResources = {};
|
|
665
|
+
const resolveResource = (resourceExpr) => Effect.gen(function* () {
|
|
666
|
+
return yield* resolvedResources[resourceExpr.src.FQN] ??= yield* Effect.cached(Effect.gen(function* () {
|
|
667
|
+
const resource = resourceExpr.src;
|
|
668
|
+
const provider = yield* findProviderByType(resource.Type);
|
|
669
|
+
const props = yield* resolveInput(resource.Props);
|
|
670
|
+
const oldState = yield* state.get({
|
|
671
|
+
stack: stackName,
|
|
672
|
+
stage: stage$1,
|
|
673
|
+
fqn: resource.FQN
|
|
674
|
+
});
|
|
675
|
+
if (!oldState || oldState.status === "creating") return resourceExpr;
|
|
676
|
+
const oldProps = oldState.status === "updating" ? oldState.old.props : oldState.props;
|
|
677
|
+
const oldBindings = oldState.bindings ?? [];
|
|
678
|
+
const newBindings = stack.bindings[resource.FQN] ?? [];
|
|
679
|
+
const diff = yield* provider.diff ? provider.diff({
|
|
680
|
+
id: resource.LogicalId,
|
|
681
|
+
olds: oldProps,
|
|
682
|
+
instanceId: oldState.instanceId,
|
|
683
|
+
news: props,
|
|
684
|
+
output: oldState.attr,
|
|
685
|
+
oldBindings,
|
|
686
|
+
newBindings
|
|
687
|
+
}).pipe(providePlanScope(resource.FQN, oldState.instanceId)) : Effect.succeed(void 0);
|
|
688
|
+
const stables = [...provider.stables ?? [], ...diff?.stables ?? []];
|
|
689
|
+
const withStables = (output) => stables.length > 0 ? new ResourceExpr(resourceExpr.src, Object.fromEntries(stables.map((stable) => [stable, output?.[stable]]))) : resourceExpr;
|
|
690
|
+
if (diff == null) {
|
|
691
|
+
if (havePropsChanged(oldProps, props)) return withStables(oldState?.attr);
|
|
692
|
+
} else if (diff.action === "update") return withStables(oldState?.attr);
|
|
693
|
+
else if (diff.action === "replace") return resourceExpr;
|
|
694
|
+
if (oldState.status === "created" || oldState.status === "updated" || oldState.status === "replaced") return oldState?.attr;
|
|
695
|
+
else return resourceExpr;
|
|
696
|
+
}));
|
|
697
|
+
});
|
|
698
|
+
const resolveInput = (input) => Effect.gen(function* () {
|
|
699
|
+
if (!input) return input;
|
|
700
|
+
else if (isExpr(input)) return yield* resolveOutput(input);
|
|
701
|
+
else if (Redacted.isRedacted(input)) return input;
|
|
702
|
+
else if (Array.isArray(input)) return yield* Effect.all(input.map(resolveInput), { concurrency: "unbounded" });
|
|
703
|
+
else if (isResource(input)) return yield* resolveInput(yield* resolveOutput(of(input)));
|
|
704
|
+
else if (typeof input === "object") return Object.fromEntries(yield* Effect.all(Object.entries(input).map(([key, value]) => resolveInput(value).pipe(Effect.map((value$1) => [key, value$1]))), { concurrency: "unbounded" }));
|
|
705
|
+
return input;
|
|
706
|
+
});
|
|
707
|
+
const resolveOutput = (expr) => Effect.gen(function* () {
|
|
708
|
+
if (isResourceExpr(expr)) return yield* resolveResource(expr);
|
|
709
|
+
else if (isPropExpr(expr)) return (yield* resolveOutput(expr.expr))?.[expr.identifier];
|
|
710
|
+
else if (isApplyExpr(expr)) {
|
|
711
|
+
const upstream$1 = yield* resolveOutput(expr.expr);
|
|
712
|
+
return hasOutputs(upstream$1) ? expr : expr.f(upstream$1);
|
|
713
|
+
} else if (isEffectExpr(expr)) {
|
|
714
|
+
const upstream$1 = yield* resolveOutput(expr.expr);
|
|
715
|
+
return hasOutputs(upstream$1) ? expr : yield* expr.f(upstream$1);
|
|
716
|
+
} else if (isAllExpr(expr)) return yield* Effect.all(expr.outs.map(resolveOutput), { concurrency: "unbounded" });
|
|
717
|
+
else if (isLiteralExpr(expr)) return expr.value;
|
|
718
|
+
return yield* Effect.die(/* @__PURE__ */ new Error("Not implemented yet"));
|
|
719
|
+
});
|
|
720
|
+
const oldDownstreamDependencies = Object.fromEntries(oldResources.filter((resource) => !!resource).map((resource) => [resource.fqn, resource.downstream]));
|
|
721
|
+
const newResourceFqns = new Set(resources.map((r) => r.FQN));
|
|
722
|
+
const newUpstreamDependencies = Object.fromEntries(resources.map((resource) => [resource.FQN, Object.values(upstreamAny(resource.Props)).map((r) => r.FQN)]));
|
|
723
|
+
const bindingUpstreamDependencies = Object.fromEntries(resources.map((resource) => [resource.FQN, Object.values(upstreamAny(stack.bindings[resource.FQN] ?? [])).map((r) => r.FQN)]));
|
|
724
|
+
const rawUpstreamDependencies = Object.fromEntries(resources.map((resource) => {
|
|
725
|
+
const fqn = resource.FQN;
|
|
726
|
+
const propDeps = newUpstreamDependencies[fqn] ?? [];
|
|
727
|
+
const bindDeps = bindingUpstreamDependencies[fqn] ?? [];
|
|
728
|
+
return [fqn, [...new Set([...propDeps, ...bindDeps])]];
|
|
729
|
+
}));
|
|
730
|
+
const allUpstreamDependencies = Object.fromEntries(resources.map((resource) => {
|
|
731
|
+
const fqn = resource.FQN;
|
|
732
|
+
return [fqn, (rawUpstreamDependencies[fqn] ?? []).filter((dep) => newResourceFqns.has(dep))];
|
|
733
|
+
}));
|
|
734
|
+
const newDownstreamDependencies = Object.fromEntries(resources.map((resource) => [resource.FQN, Object.entries(newUpstreamDependencies).filter(([_, upstream$1]) => upstream$1.includes(resource.FQN)).map(([depFqn]) => depFqn)]));
|
|
735
|
+
const resourceGraph = Object.fromEntries((yield* Effect.all(resources.map(Effect.fn("plan.diff.resource")(function* (resource) {
|
|
736
|
+
const provider = yield* findProviderByType(resource.Type);
|
|
737
|
+
const id = resource.LogicalId;
|
|
738
|
+
const fqn = resource.FQN;
|
|
739
|
+
const news = yield* resolveInput(resource.Props);
|
|
740
|
+
const downstream = newDownstreamDependencies[fqn] ?? [];
|
|
741
|
+
const newBindings = yield* resolveInput(stack.bindings[fqn] ?? []);
|
|
742
|
+
const oldState = yield* state.get({
|
|
743
|
+
stack: stackName,
|
|
744
|
+
stage: stage$1,
|
|
745
|
+
fqn
|
|
746
|
+
});
|
|
747
|
+
const oldBindings = oldState?.bindings ?? [];
|
|
748
|
+
const bindingDiffs = diffBindings(oldBindings, newBindings);
|
|
749
|
+
const Node = (node) => ({
|
|
750
|
+
...node,
|
|
751
|
+
provider,
|
|
752
|
+
resource,
|
|
753
|
+
bindings: bindingDiffs,
|
|
754
|
+
downstream
|
|
755
|
+
});
|
|
756
|
+
if (oldState === void 0) return Node({
|
|
757
|
+
action: "create",
|
|
758
|
+
props: news,
|
|
759
|
+
state: oldState
|
|
760
|
+
});
|
|
761
|
+
else if (oldState.status === "creating" && oldState.attr === void 0) {
|
|
762
|
+
if (provider.read) {
|
|
763
|
+
const attr = yield* provider.read({
|
|
764
|
+
id,
|
|
765
|
+
instanceId: oldState.instanceId,
|
|
766
|
+
olds: oldState.props,
|
|
767
|
+
output: oldState.attr
|
|
768
|
+
}).pipe(providePlanScope(fqn, oldState.instanceId));
|
|
769
|
+
if (attr) return Node({
|
|
770
|
+
action: "create",
|
|
771
|
+
props: news,
|
|
772
|
+
state: {
|
|
773
|
+
...oldState,
|
|
774
|
+
attr
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const oldProps = oldState.props;
|
|
780
|
+
const diff = yield* asEffect(provider?.diff?.({
|
|
781
|
+
id,
|
|
782
|
+
olds: oldProps,
|
|
783
|
+
instanceId: oldState.instanceId,
|
|
784
|
+
output: oldState.attr,
|
|
785
|
+
news,
|
|
786
|
+
oldBindings,
|
|
787
|
+
newBindings
|
|
788
|
+
}).pipe(providePlanScope(fqn, oldState.instanceId))).pipe(Effect.map((diff$1) => diff$1 ?? { action: havePropsChanged(oldProps, news) || bindingDiffs.some((b) => b.action !== "noop") ? "update" : "noop" }), Effect.map((diff$1) => options.force && diff$1.action === "noop" ? { action: "update" } : diff$1));
|
|
789
|
+
if (oldState.status === "creating") if (diff.action === "noop") return Node({
|
|
790
|
+
action: "create",
|
|
791
|
+
props: news,
|
|
792
|
+
state: oldState
|
|
793
|
+
});
|
|
794
|
+
else if (diff.action === "update") return Node({
|
|
795
|
+
action: "create",
|
|
796
|
+
props: news,
|
|
797
|
+
state: oldState
|
|
798
|
+
});
|
|
799
|
+
else return Node({
|
|
800
|
+
action: "replace",
|
|
801
|
+
props: news,
|
|
802
|
+
deleteFirst: diff.deleteFirst ?? false,
|
|
803
|
+
state: oldState
|
|
804
|
+
});
|
|
805
|
+
else if (oldState.status === "updating") if (diff.action === "update" || diff.action === "noop") return Node({
|
|
806
|
+
action: "update",
|
|
807
|
+
props: news,
|
|
808
|
+
state: oldState
|
|
809
|
+
});
|
|
810
|
+
else return Node({
|
|
811
|
+
action: "replace",
|
|
812
|
+
deleteFirst: diff.deleteFirst ?? false,
|
|
813
|
+
props: news,
|
|
814
|
+
state: oldState
|
|
815
|
+
});
|
|
816
|
+
else if (oldState.status === "replacing") if (diff.action === "noop") return Node({
|
|
817
|
+
action: "replace",
|
|
818
|
+
deleteFirst: oldState.deleteFirst,
|
|
819
|
+
props: news,
|
|
820
|
+
state: oldState
|
|
821
|
+
});
|
|
822
|
+
else if (diff.action === "update") return Node({
|
|
823
|
+
action: "replace",
|
|
824
|
+
deleteFirst: oldState.deleteFirst,
|
|
825
|
+
props: news,
|
|
826
|
+
state: oldState
|
|
827
|
+
});
|
|
828
|
+
else return Node({
|
|
829
|
+
restart: true,
|
|
830
|
+
action: "replace",
|
|
831
|
+
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
832
|
+
props: news,
|
|
833
|
+
state: oldState
|
|
834
|
+
});
|
|
835
|
+
else if (oldState.status === "replaced") if (diff.action === "noop") return Node({
|
|
836
|
+
action: "replace",
|
|
837
|
+
deleteFirst: oldState.deleteFirst,
|
|
838
|
+
props: news,
|
|
839
|
+
state: oldState
|
|
840
|
+
});
|
|
841
|
+
else if (diff.action === "update") return Node({
|
|
842
|
+
action: "update",
|
|
843
|
+
props: news,
|
|
844
|
+
state: oldState
|
|
845
|
+
});
|
|
846
|
+
else return Node({
|
|
847
|
+
restart: true,
|
|
848
|
+
action: "replace",
|
|
849
|
+
deleteFirst: diff.deleteFirst ?? oldState.deleteFirst,
|
|
850
|
+
props: news,
|
|
851
|
+
state: oldState
|
|
852
|
+
});
|
|
853
|
+
else if (oldState.status === "deleting") return Node({
|
|
854
|
+
action: "create",
|
|
855
|
+
props: news,
|
|
856
|
+
state: {
|
|
857
|
+
...oldState,
|
|
858
|
+
status: "creating",
|
|
859
|
+
props: news
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
else if (diff.action === "update") return Node({
|
|
863
|
+
action: "update",
|
|
864
|
+
props: news,
|
|
865
|
+
state: oldState
|
|
866
|
+
});
|
|
867
|
+
else if (diff.action === "replace") return Node({
|
|
868
|
+
action: "replace",
|
|
869
|
+
props: news,
|
|
870
|
+
state: oldState,
|
|
871
|
+
deleteFirst: diff?.deleteFirst ?? false
|
|
872
|
+
});
|
|
873
|
+
else return Node({
|
|
874
|
+
action: "noop",
|
|
875
|
+
state: oldState
|
|
876
|
+
});
|
|
877
|
+
})), { concurrency: "unbounded" })).map((update) => [update.resource.FQN, update]));
|
|
878
|
+
{
|
|
879
|
+
const createReplaceNodes = new Set(Object.entries(resourceGraph).filter(([_, node]) => node.action === "create" || node.action === "replace").map(([fqn]) => fqn));
|
|
880
|
+
if (createReplaceNodes.size > 0) {
|
|
881
|
+
const hasPrecreate = new Set([...createReplaceNodes].filter((fqn) => !!resourceGraph[fqn]?.provider?.precreate));
|
|
882
|
+
const resolved = new Set(hasPrecreate);
|
|
883
|
+
let changed = true;
|
|
884
|
+
while (changed) {
|
|
885
|
+
changed = false;
|
|
886
|
+
for (const fqn of createReplaceNodes) {
|
|
887
|
+
if (resolved.has(fqn)) continue;
|
|
888
|
+
if ((allUpstreamDependencies[fqn] ?? []).filter((dep) => createReplaceNodes.has(dep)).every((dep) => resolved.has(dep))) {
|
|
889
|
+
resolved.add(fqn);
|
|
890
|
+
changed = true;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const deadlocked = [...createReplaceNodes].filter((fqn) => !resolved.has(fqn));
|
|
895
|
+
if (deadlocked.length > 0) {
|
|
896
|
+
const missingPrecreate = deadlocked.filter((fqn) => !hasPrecreate.has(fqn));
|
|
897
|
+
return yield* Effect.die(new UnsatisfiedResourceCycle({
|
|
898
|
+
message: `Circular dependency detected that cannot be resolved: [${deadlocked.join(", ")}]. Resources lacking a precreate handler: [${missingPrecreate.join(", ")}]. All resources in a dependency cycle must implement precreate to allow early signaling.`,
|
|
899
|
+
cycle: deadlocked,
|
|
900
|
+
missingPrecreate
|
|
901
|
+
}));
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
const deletions = Object.fromEntries((yield* Effect.all((yield* state.list({
|
|
906
|
+
stack: stackName,
|
|
907
|
+
stage: stage$1
|
|
908
|
+
})).map(Effect.fn("plan.diff.deletion")(function* (fqn) {
|
|
909
|
+
if (newResourceFqns.has(fqn)) return;
|
|
910
|
+
const oldState = yield* state.get({
|
|
911
|
+
stack: stackName,
|
|
912
|
+
stage: stage$1,
|
|
913
|
+
fqn
|
|
914
|
+
});
|
|
915
|
+
let attr = oldState?.attr;
|
|
916
|
+
if (oldState) {
|
|
917
|
+
const { logicalId } = parseFqn(fqn);
|
|
918
|
+
const resourceType = oldState.resourceType;
|
|
919
|
+
const provider = yield* findProviderByType(resourceType);
|
|
920
|
+
if (oldState.attr === void 0) {
|
|
921
|
+
if (provider.read) attr = yield* provider.read({
|
|
922
|
+
id: logicalId,
|
|
923
|
+
instanceId: oldState.instanceId,
|
|
924
|
+
olds: oldState.props,
|
|
925
|
+
output: oldState.attr
|
|
926
|
+
}).pipe(providePlanScope(fqn, oldState.instanceId));
|
|
927
|
+
}
|
|
928
|
+
return [fqn, {
|
|
929
|
+
action: "delete",
|
|
930
|
+
state: {
|
|
931
|
+
...oldState,
|
|
932
|
+
attr
|
|
933
|
+
},
|
|
934
|
+
provider,
|
|
935
|
+
resource: {
|
|
936
|
+
Namespace: oldState.namespace,
|
|
937
|
+
FQN: fqn,
|
|
938
|
+
LogicalId: logicalId,
|
|
939
|
+
Type: oldState.resourceType,
|
|
940
|
+
Attributes: attr,
|
|
941
|
+
Props: oldState.props,
|
|
942
|
+
Binding: void 0,
|
|
943
|
+
Provider: Provider(resourceType),
|
|
944
|
+
RemovalPolicy: oldState.removalPolicy,
|
|
945
|
+
ExecutionContext: void 0,
|
|
946
|
+
Providers: void 0
|
|
947
|
+
},
|
|
948
|
+
downstream: oldDownstreamDependencies[fqn] ?? [],
|
|
949
|
+
bindings: oldState.bindings.map((binding) => ({
|
|
950
|
+
sid: binding.sid,
|
|
951
|
+
action: "delete",
|
|
952
|
+
data: binding.data
|
|
953
|
+
}))
|
|
954
|
+
}];
|
|
955
|
+
}
|
|
956
|
+
})), { concurrency: "unbounded" })).filter((v) => !!v));
|
|
957
|
+
for (const resourceFqn of Object.keys(deletions)) {
|
|
958
|
+
const dependencies = Object.entries(rawUpstreamDependencies).filter(([survivorFqn, upstream$1]) => survivorFqn in resourceGraph && upstream$1.includes(resourceFqn)).map(([survivorFqn]) => survivorFqn);
|
|
959
|
+
if (dependencies.length > 0) return yield* new DeleteResourceHasDownstreamDependencies({
|
|
960
|
+
message: `Resource ${resourceFqn} has downstream dependencies`,
|
|
961
|
+
resourceId: resourceFqn,
|
|
962
|
+
dependencies
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
return {
|
|
966
|
+
resources: resourceGraph,
|
|
967
|
+
deletions,
|
|
968
|
+
output: stack.output
|
|
969
|
+
};
|
|
970
|
+
}).pipe(ensureArtifactStore, Effect.withSpan("plan.make", { attributes: {
|
|
971
|
+
"alchemy.stack": stack.name,
|
|
972
|
+
"alchemy.stage": stack.stage,
|
|
973
|
+
"alchemy.resources.count": Object.keys(stack.resources).length,
|
|
974
|
+
"alchemy.force": !!options.force
|
|
975
|
+
} }));
|
|
976
|
+
const providePlanScope = (fqn, instanceId) => (effect) => Effect.serviceOption(ArtifactStore).pipe(Effect.map(Option.getOrElse(createArtifactStore)), Effect.flatMap((store) => effect.pipe(Effect.provideService(Artifacts, makeScopedArtifacts(store, fqn)), Effect.provideService(InstanceId, instanceId))));
|
|
977
|
+
var DeleteResourceHasDownstreamDependencies = class extends Data.TaggedError("DeleteResourceHasDownstreamDependencies") {};
|
|
978
|
+
var UnsatisfiedResourceCycle = class extends Data.TaggedError("UnsatisfiedResourceCycle") {};
|
|
979
|
+
|
|
980
|
+
//#endregion
|
|
981
|
+
//#region lib/Telemetry/Metrics.js
|
|
982
|
+
/**
|
|
983
|
+
* Counter for resource lifecycle operations. Tagged per call with
|
|
984
|
+
* `resource_type`, `op` (`precreate`/`create`/`update`/`delete`/`read`),
|
|
985
|
+
* and `status` (`success`/`error`).
|
|
986
|
+
*/
|
|
987
|
+
const resourceCounter = Metric.counter("alchemy.resource.operations", {
|
|
988
|
+
description: "Number of resource lifecycle operations dispatched by alchemy apply.",
|
|
989
|
+
incremental: true
|
|
990
|
+
});
|
|
991
|
+
/**
|
|
992
|
+
* Histogram of how long each lifecycle operation takes.
|
|
993
|
+
*/
|
|
994
|
+
const resourceDuration = Metric.timer("alchemy.resource.duration", { description: "Wall-clock duration of resource lifecycle operations dispatched by alchemy apply." });
|
|
995
|
+
/**
|
|
996
|
+
* Counter for CLI command invocations.
|
|
997
|
+
*/
|
|
998
|
+
const cliCounter = Metric.counter("alchemy.cli.invocations", {
|
|
999
|
+
description: "Number of alchemy CLI command invocations.",
|
|
1000
|
+
incremental: true
|
|
1001
|
+
});
|
|
1002
|
+
/**
|
|
1003
|
+
* Counter for Cloudflare State Store bootstrap/deploy operations.
|
|
1004
|
+
* Tagged per call with `op` (e.g. `deploy`) and `status`
|
|
1005
|
+
* (`success`/`error`) so that the deploy success rate of the
|
|
1006
|
+
* state store itself can be tracked separately from regular
|
|
1007
|
+
* resource lifecycle ops.
|
|
1008
|
+
*/
|
|
1009
|
+
const stateStoreCounter = Metric.counter("alchemy.state_store.operations", {
|
|
1010
|
+
description: "Number of Cloudflare State Store deploy/bootstrap operations dispatched by alchemy.",
|
|
1011
|
+
incremental: true
|
|
1012
|
+
});
|
|
1013
|
+
/**
|
|
1014
|
+
* Wraps a resource lifecycle Effect to record a counter + timer entry,
|
|
1015
|
+
* tagged with `resource_type`, `op`, and `status` (`success`/`error`).
|
|
1016
|
+
*
|
|
1017
|
+
* Usage:
|
|
1018
|
+
* ```ts
|
|
1019
|
+
* provider.create(input).pipe(recordResourceOp(node.resource.Type, "create"))
|
|
1020
|
+
* ```
|
|
1021
|
+
*/
|
|
1022
|
+
const recordResourceOp = (resourceType, op) => (self) => Effect.suspend(() => {
|
|
1023
|
+
const startNs = process.hrtime.bigint();
|
|
1024
|
+
const baseAttrs = {
|
|
1025
|
+
resource_type: resourceType,
|
|
1026
|
+
op
|
|
1027
|
+
};
|
|
1028
|
+
return self.pipe(Effect.onExit((exit) => recordOutcome(baseAttrs, Exit.isSuccess(exit) ? "success" : "error", elapsed(startNs))));
|
|
1029
|
+
});
|
|
1030
|
+
/**
|
|
1031
|
+
* Wraps a CLI command Effect to bump {@link cliCounter} with the
|
|
1032
|
+
* outcome (`success`/`error`).
|
|
1033
|
+
*/
|
|
1034
|
+
const recordCli = (command) => (self) => self.pipe(Effect.onExit((exit) => Metric.update(Metric.withAttributes(cliCounter, {
|
|
1035
|
+
command,
|
|
1036
|
+
status: Exit.isSuccess(exit) ? "success" : "error"
|
|
1037
|
+
}), 1)));
|
|
1038
|
+
const elapsed = (startNs) => Duration.nanos(process.hrtime.bigint() - startNs);
|
|
1039
|
+
const recordOutcome = (baseAttrs, status, duration) => {
|
|
1040
|
+
const attrs = {
|
|
1041
|
+
...baseAttrs,
|
|
1042
|
+
status
|
|
1043
|
+
};
|
|
1044
|
+
return Effect.all([Metric.update(Metric.withAttributes(resourceCounter, attrs), 1), Metric.update(Metric.withAttributes(resourceDuration, attrs), duration)], { discard: true });
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
//#endregion
|
|
1048
|
+
//#region lib/Apply.js
|
|
1049
|
+
const provideLifecycleScope = (fqn, instanceId) => (effect) => Effect.serviceOption(ArtifactStore).pipe(Effect.map(Option.getOrElse(createArtifactStore)), Effect.flatMap((store) => effect.pipe(Effect.provideService(Artifacts, makeScopedArtifacts(store, fqn)), Effect.provideService(InstanceId, instanceId))));
|
|
1050
|
+
/**
|
|
1051
|
+
* Instruments a single provider lifecycle call with an OTel span
|
|
1052
|
+
* (`provider.<op>`), the resource counter / duration histogram, and the
|
|
1053
|
+
* scoped artifacts/instance services normally supplied by
|
|
1054
|
+
* {@link provideLifecycleScope}.
|
|
1055
|
+
*
|
|
1056
|
+
* This is the only call site through which provider lifecycle methods
|
|
1057
|
+
* are dispatched, so wrapping it here gives us a fully-instrumented
|
|
1058
|
+
* toolchain without touching any individual provider implementation.
|
|
1059
|
+
*/
|
|
1060
|
+
const instrumentLifecycle = (op, fqn, resourceType, logicalId, instanceId) => (effect) => effect.pipe(provideLifecycleScope(fqn, instanceId), recordResourceOp(resourceType, op), Effect.withSpan(`provider.${op}`, { attributes: {
|
|
1061
|
+
"alchemy.resource.fqn": fqn,
|
|
1062
|
+
"alchemy.resource.type": resourceType,
|
|
1063
|
+
"alchemy.resource.logical_id": logicalId,
|
|
1064
|
+
"alchemy.resource.instance_id": instanceId,
|
|
1065
|
+
"alchemy.resource.op": op
|
|
1066
|
+
} }));
|
|
1067
|
+
const apply = (plan) => Effect.gen(function* () {
|
|
1068
|
+
const session = yield* (yield* Cli).startApplySession(plan);
|
|
1069
|
+
const state = yield* State;
|
|
1070
|
+
const stack = yield* Stack;
|
|
1071
|
+
const stage$1 = yield* Stage;
|
|
1072
|
+
const stackName = stack.name;
|
|
1073
|
+
const tracker = {};
|
|
1074
|
+
const terminalStatuses = /* @__PURE__ */ new Map();
|
|
1075
|
+
yield* executePlan(plan, tracker, terminalStatuses, session, state, stackName, stage$1);
|
|
1076
|
+
yield* collectGarbage(plan, session);
|
|
1077
|
+
yield* converge(plan, tracker, terminalStatuses, session, state, stackName, stage$1);
|
|
1078
|
+
yield* Effect.forEach(Array.from(terminalStatuses.values()), ({ id, type, status }) => session.emit({
|
|
1079
|
+
kind: "status-change",
|
|
1080
|
+
id,
|
|
1081
|
+
type,
|
|
1082
|
+
status
|
|
1083
|
+
}), { concurrency: "unbounded" });
|
|
1084
|
+
yield* session.done();
|
|
1085
|
+
if (!plan.output) return;
|
|
1086
|
+
const outputs = Object.fromEntries(Object.entries(tracker).map(([fqn, t]) => [fqn, t.output]));
|
|
1087
|
+
return yield* evaluate(plan.output, outputs);
|
|
1088
|
+
}).pipe(ensureArtifactStore, Effect.withSpan("apply", { attributes: {
|
|
1089
|
+
"alchemy.resources.count": Object.keys(plan.resources).length,
|
|
1090
|
+
"alchemy.deletions.count": Object.keys(plan.deletions).length
|
|
1091
|
+
} }));
|
|
1092
|
+
const executePlan = Effect.fnUntraced(function* (plan, tracker, terminalStatuses, session, state, stackName, stage$1) {
|
|
1093
|
+
const ready = Object.fromEntries(yield* Effect.all(Object.keys(plan.resources).map((fqn) => Effect.map(Deferred.make(), (d) => [fqn, d]))));
|
|
1094
|
+
const getOutputs = () => Object.fromEntries(Object.entries(tracker).map(([fqn, t]) => [fqn, t.output]));
|
|
1095
|
+
const waitForDeps = (fqns) => Effect.all(fqns.filter((fqn) => fqn in ready).map((fqn) => Deferred.await(ready[fqn])), { concurrency: "unbounded" });
|
|
1096
|
+
yield* Effect.all(Object.entries(plan.resources).map(([fqn, node]) => executeNode(fqn, node, tracker, ready, terminalStatuses, session, state, stackName, stage$1, getOutputs, waitForDeps)), { concurrency: "unbounded" });
|
|
1097
|
+
});
|
|
1098
|
+
const executeNode = (fqn, node, tracker, ready, terminalStatuses, session, state, stackName, stage$1, getOutputs, waitForDeps) => Effect.gen(function* () {
|
|
1099
|
+
const logicalId = node.resource.LogicalId;
|
|
1100
|
+
const namespace = node.resource.Namespace;
|
|
1101
|
+
const commit = (value) => state.set({
|
|
1102
|
+
stack: stackName,
|
|
1103
|
+
stage: stage$1,
|
|
1104
|
+
fqn,
|
|
1105
|
+
value: {
|
|
1106
|
+
...value,
|
|
1107
|
+
namespace
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
const scopedSession = {
|
|
1111
|
+
...session,
|
|
1112
|
+
note: (note) => session.emit({
|
|
1113
|
+
id: logicalId,
|
|
1114
|
+
kind: "annotate",
|
|
1115
|
+
message: note
|
|
1116
|
+
})
|
|
1117
|
+
};
|
|
1118
|
+
const report = (status) => session.emit({
|
|
1119
|
+
kind: "status-change",
|
|
1120
|
+
id: logicalId,
|
|
1121
|
+
type: node.resource.Type,
|
|
1122
|
+
status
|
|
1123
|
+
});
|
|
1124
|
+
const markTerminal = (status) => Effect.sync(() => {
|
|
1125
|
+
terminalStatuses.set(fqn, {
|
|
1126
|
+
id: logicalId,
|
|
1127
|
+
type: node.resource.Type,
|
|
1128
|
+
status
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
const signalReady = Deferred.succeed(ready[fqn], void 0);
|
|
1132
|
+
const storeAndSignal = (t) => Effect.gen(function* () {
|
|
1133
|
+
tracker[fqn] = t;
|
|
1134
|
+
yield* signalReady;
|
|
1135
|
+
});
|
|
1136
|
+
if (node.action === "noop") {
|
|
1137
|
+
yield* storeAndSignal({
|
|
1138
|
+
output: node.state.attr,
|
|
1139
|
+
props: node.state.props,
|
|
1140
|
+
bindings: node.state.bindings ?? [],
|
|
1141
|
+
instanceId: node.state.instanceId
|
|
1142
|
+
});
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
const allUpstreamFqns = () => {
|
|
1146
|
+
const propDeps = Object.keys(resolveUpstream(node.props));
|
|
1147
|
+
const bindingDeps = Object.keys(resolveUpstream(node.bindings));
|
|
1148
|
+
return [...new Set([...propDeps, ...bindingDeps])];
|
|
1149
|
+
};
|
|
1150
|
+
const instanceId = yield* Effect.gen(function* () {
|
|
1151
|
+
if (node.action === "create" && !node.state?.instanceId) {
|
|
1152
|
+
const id = yield* generateInstanceId();
|
|
1153
|
+
yield* commit({
|
|
1154
|
+
status: "creating",
|
|
1155
|
+
fqn,
|
|
1156
|
+
logicalId,
|
|
1157
|
+
instanceId: id,
|
|
1158
|
+
downstream: node.downstream,
|
|
1159
|
+
props: node.props,
|
|
1160
|
+
providerVersion: node.provider.version ?? 0,
|
|
1161
|
+
resourceType: node.resource.Type,
|
|
1162
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1163
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1164
|
+
});
|
|
1165
|
+
return id;
|
|
1166
|
+
} else if (node.action === "replace") {
|
|
1167
|
+
if ((node.state.status === "replaced" || node.state.status === "replacing") && !node.restart) return node.state.instanceId;
|
|
1168
|
+
const id = yield* generateInstanceId();
|
|
1169
|
+
yield* commit({
|
|
1170
|
+
status: "replacing",
|
|
1171
|
+
fqn,
|
|
1172
|
+
logicalId,
|
|
1173
|
+
instanceId: id,
|
|
1174
|
+
downstream: node.downstream,
|
|
1175
|
+
props: node.props,
|
|
1176
|
+
providerVersion: node.provider.version ?? 0,
|
|
1177
|
+
resourceType: node.resource.Type,
|
|
1178
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1179
|
+
old: node.state,
|
|
1180
|
+
deleteFirst: node.deleteFirst,
|
|
1181
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1182
|
+
});
|
|
1183
|
+
return id;
|
|
1184
|
+
} else if (node.state?.instanceId) return node.state.instanceId;
|
|
1185
|
+
return yield* Effect.die(`Instance ID not found for resource '${logicalId}' and action is '${node.action}'`);
|
|
1186
|
+
});
|
|
1187
|
+
yield* Effect.gen(function* () {
|
|
1188
|
+
if (node.action === "create") {
|
|
1189
|
+
if (!node.state) yield* commit({
|
|
1190
|
+
status: "creating",
|
|
1191
|
+
fqn,
|
|
1192
|
+
logicalId,
|
|
1193
|
+
instanceId,
|
|
1194
|
+
resourceType: node.resource.Type,
|
|
1195
|
+
props: node.props,
|
|
1196
|
+
attr: void 0,
|
|
1197
|
+
providerVersion: node.provider.version ?? 0,
|
|
1198
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1199
|
+
downstream: node.downstream,
|
|
1200
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1201
|
+
});
|
|
1202
|
+
let attr = node.state?.attr;
|
|
1203
|
+
if (attr !== void 0) yield* storeAndSignal({
|
|
1204
|
+
output: attr,
|
|
1205
|
+
props: {},
|
|
1206
|
+
bindings: [],
|
|
1207
|
+
instanceId
|
|
1208
|
+
});
|
|
1209
|
+
if (node.provider.precreate && attr === void 0) {
|
|
1210
|
+
yield* report("pre-creating");
|
|
1211
|
+
attr = yield* node.provider.precreate({
|
|
1212
|
+
id: logicalId,
|
|
1213
|
+
news: node.props,
|
|
1214
|
+
session: scopedSession,
|
|
1215
|
+
instanceId,
|
|
1216
|
+
bindings: excludeDeletedBindings(node.bindings)
|
|
1217
|
+
}).pipe(instrumentLifecycle("precreate", fqn, node.resource.Type, logicalId, instanceId));
|
|
1218
|
+
yield* commit({
|
|
1219
|
+
status: "creating",
|
|
1220
|
+
fqn,
|
|
1221
|
+
logicalId,
|
|
1222
|
+
instanceId,
|
|
1223
|
+
resourceType: node.resource.Type,
|
|
1224
|
+
props: node.props,
|
|
1225
|
+
attr,
|
|
1226
|
+
providerVersion: node.provider.version ?? 0,
|
|
1227
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1228
|
+
downstream: node.downstream,
|
|
1229
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1230
|
+
});
|
|
1231
|
+
yield* storeAndSignal({
|
|
1232
|
+
output: attr,
|
|
1233
|
+
props: {},
|
|
1234
|
+
bindings: [],
|
|
1235
|
+
instanceId
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
yield* report("creating");
|
|
1239
|
+
yield* waitForDeps(allUpstreamFqns());
|
|
1240
|
+
const outputs = getOutputs();
|
|
1241
|
+
const news = yield* evaluate(node.props, outputs);
|
|
1242
|
+
const bindingOutputs = excludeDeletedBindings(yield* evaluate(node.bindings, outputs));
|
|
1243
|
+
attr = yield* node.provider.create({
|
|
1244
|
+
id: logicalId,
|
|
1245
|
+
news,
|
|
1246
|
+
instanceId,
|
|
1247
|
+
bindings: bindingOutputs,
|
|
1248
|
+
session: scopedSession,
|
|
1249
|
+
output: attr
|
|
1250
|
+
}).pipe(instrumentLifecycle("create", fqn, node.resource.Type, logicalId, instanceId));
|
|
1251
|
+
yield* commit({
|
|
1252
|
+
status: "created",
|
|
1253
|
+
fqn,
|
|
1254
|
+
logicalId,
|
|
1255
|
+
instanceId,
|
|
1256
|
+
resourceType: node.resource.Type,
|
|
1257
|
+
props: news,
|
|
1258
|
+
attr,
|
|
1259
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1260
|
+
providerVersion: node.provider.version ?? 0,
|
|
1261
|
+
downstream: node.downstream,
|
|
1262
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1263
|
+
});
|
|
1264
|
+
tracker[fqn] = {
|
|
1265
|
+
output: attr,
|
|
1266
|
+
props: news,
|
|
1267
|
+
bindings: bindingOutputs,
|
|
1268
|
+
instanceId
|
|
1269
|
+
};
|
|
1270
|
+
yield* signalReady;
|
|
1271
|
+
yield* markTerminal("created");
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (node.action === "update") {
|
|
1275
|
+
yield* storeAndSignal({
|
|
1276
|
+
output: node.state.attr,
|
|
1277
|
+
props: node.state.props,
|
|
1278
|
+
bindings: node.state.bindings ?? [],
|
|
1279
|
+
instanceId
|
|
1280
|
+
});
|
|
1281
|
+
yield* waitForDeps(allUpstreamFqns());
|
|
1282
|
+
const outputs = getOutputs();
|
|
1283
|
+
const news = yield* evaluate(node.props, outputs);
|
|
1284
|
+
yield* node.state.status === "replaced" ? commit({
|
|
1285
|
+
...node.state,
|
|
1286
|
+
attr: node.state.attr,
|
|
1287
|
+
props: news
|
|
1288
|
+
}) : commit({
|
|
1289
|
+
status: "updating",
|
|
1290
|
+
fqn,
|
|
1291
|
+
logicalId,
|
|
1292
|
+
instanceId,
|
|
1293
|
+
resourceType: node.resource.Type,
|
|
1294
|
+
props: news,
|
|
1295
|
+
attr: node.state.attr,
|
|
1296
|
+
providerVersion: node.provider.version ?? 0,
|
|
1297
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1298
|
+
downstream: node.downstream,
|
|
1299
|
+
old: node.state.status === "updating" ? node.state.old : node.state,
|
|
1300
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1301
|
+
});
|
|
1302
|
+
yield* report("updating");
|
|
1303
|
+
const previousProps = node.state.status === "created" || node.state.status === "updated" || node.state.status === "replaced" ? node.state.props : node.state.old.props;
|
|
1304
|
+
const bindingOutputs = excludeDeletedBindings(yield* evaluate(node.bindings, outputs));
|
|
1305
|
+
const attr = yield* node.provider.update({
|
|
1306
|
+
id: logicalId,
|
|
1307
|
+
news,
|
|
1308
|
+
instanceId,
|
|
1309
|
+
bindings: bindingOutputs,
|
|
1310
|
+
session: scopedSession,
|
|
1311
|
+
olds: previousProps,
|
|
1312
|
+
output: node.state.attr
|
|
1313
|
+
}).pipe(instrumentLifecycle("update", fqn, node.resource.Type, logicalId, instanceId));
|
|
1314
|
+
if (node.state.status === "replaced") yield* commit({
|
|
1315
|
+
...node.state,
|
|
1316
|
+
attr,
|
|
1317
|
+
props: news
|
|
1318
|
+
});
|
|
1319
|
+
else yield* commit({
|
|
1320
|
+
status: "updated",
|
|
1321
|
+
fqn,
|
|
1322
|
+
logicalId,
|
|
1323
|
+
instanceId,
|
|
1324
|
+
resourceType: node.resource.Type,
|
|
1325
|
+
props: news,
|
|
1326
|
+
attr,
|
|
1327
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1328
|
+
providerVersion: node.provider.version ?? 0,
|
|
1329
|
+
downstream: node.downstream,
|
|
1330
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1331
|
+
});
|
|
1332
|
+
tracker[fqn] = {
|
|
1333
|
+
output: attr,
|
|
1334
|
+
props: news,
|
|
1335
|
+
bindings: bindingOutputs,
|
|
1336
|
+
instanceId
|
|
1337
|
+
};
|
|
1338
|
+
yield* markTerminal("updated");
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (node.action === "replace") {
|
|
1342
|
+
if (node.state.status === "replaced" && !node.restart) {
|
|
1343
|
+
tracker[fqn] = {
|
|
1344
|
+
output: node.state.attr,
|
|
1345
|
+
props: node.state.props,
|
|
1346
|
+
bindings: node.state.bindings ?? [],
|
|
1347
|
+
instanceId
|
|
1348
|
+
};
|
|
1349
|
+
yield* signalReady;
|
|
1350
|
+
yield* markTerminal("created");
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
let replState;
|
|
1354
|
+
if (node.state.status !== "replacing" || node.restart) replState = yield* commit({
|
|
1355
|
+
status: "replacing",
|
|
1356
|
+
fqn,
|
|
1357
|
+
logicalId,
|
|
1358
|
+
instanceId,
|
|
1359
|
+
resourceType: node.resource.Type,
|
|
1360
|
+
props: node.props,
|
|
1361
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1362
|
+
attr: void 0,
|
|
1363
|
+
providerVersion: node.provider.version ?? 0,
|
|
1364
|
+
deleteFirst: node.deleteFirst,
|
|
1365
|
+
old: node.state,
|
|
1366
|
+
downstream: node.downstream,
|
|
1367
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1368
|
+
});
|
|
1369
|
+
else replState = node.state;
|
|
1370
|
+
let attr = replState.attr;
|
|
1371
|
+
if (attr !== void 0) yield* storeAndSignal({
|
|
1372
|
+
output: attr,
|
|
1373
|
+
props: {},
|
|
1374
|
+
bindings: [],
|
|
1375
|
+
instanceId
|
|
1376
|
+
});
|
|
1377
|
+
if (node.provider.precreate && attr === void 0) {
|
|
1378
|
+
yield* report("pre-creating");
|
|
1379
|
+
attr = yield* node.provider.precreate({
|
|
1380
|
+
id: logicalId,
|
|
1381
|
+
news: node.props,
|
|
1382
|
+
session: scopedSession,
|
|
1383
|
+
instanceId,
|
|
1384
|
+
bindings: excludeDeletedBindings(node.bindings)
|
|
1385
|
+
}).pipe(instrumentLifecycle("precreate", fqn, node.resource.Type, logicalId, instanceId));
|
|
1386
|
+
yield* commit({
|
|
1387
|
+
status: "replacing",
|
|
1388
|
+
fqn,
|
|
1389
|
+
logicalId,
|
|
1390
|
+
instanceId,
|
|
1391
|
+
resourceType: node.resource.Type,
|
|
1392
|
+
props: node.props,
|
|
1393
|
+
attr,
|
|
1394
|
+
providerVersion: node.provider.version ?? 0,
|
|
1395
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1396
|
+
downstream: node.downstream,
|
|
1397
|
+
old: replState.old,
|
|
1398
|
+
deleteFirst: node.deleteFirst,
|
|
1399
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1400
|
+
});
|
|
1401
|
+
yield* storeAndSignal({
|
|
1402
|
+
output: attr,
|
|
1403
|
+
props: {},
|
|
1404
|
+
bindings: [],
|
|
1405
|
+
instanceId
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
yield* report("creating replacement");
|
|
1409
|
+
yield* waitForDeps(allUpstreamFqns());
|
|
1410
|
+
const outputs = getOutputs();
|
|
1411
|
+
const news = yield* evaluate(node.props, outputs);
|
|
1412
|
+
const bindingOutputs = excludeDeletedBindings(yield* evaluate(node.bindings, outputs));
|
|
1413
|
+
attr = yield* node.provider.create({
|
|
1414
|
+
id: logicalId,
|
|
1415
|
+
news,
|
|
1416
|
+
instanceId,
|
|
1417
|
+
bindings: bindingOutputs,
|
|
1418
|
+
session: scopedSession,
|
|
1419
|
+
output: attr
|
|
1420
|
+
}).pipe(instrumentLifecycle("create", fqn, node.resource.Type, logicalId, instanceId));
|
|
1421
|
+
yield* commit({
|
|
1422
|
+
status: "replaced",
|
|
1423
|
+
fqn,
|
|
1424
|
+
logicalId,
|
|
1425
|
+
instanceId,
|
|
1426
|
+
resourceType: node.resource.Type,
|
|
1427
|
+
props: news,
|
|
1428
|
+
attr,
|
|
1429
|
+
providerVersion: node.provider.version ?? 0,
|
|
1430
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1431
|
+
downstream: node.downstream,
|
|
1432
|
+
old: replState.old,
|
|
1433
|
+
deleteFirst: node.deleteFirst,
|
|
1434
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1435
|
+
});
|
|
1436
|
+
tracker[fqn] = {
|
|
1437
|
+
output: attr,
|
|
1438
|
+
props: news,
|
|
1439
|
+
bindings: bindingOutputs,
|
|
1440
|
+
instanceId
|
|
1441
|
+
};
|
|
1442
|
+
yield* signalReady;
|
|
1443
|
+
yield* markTerminal("created");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
return yield* Effect.die(`Unknown action: ${node.action}`);
|
|
1447
|
+
});
|
|
1448
|
+
}).pipe(Effect.withSpan("apply.resource", { attributes: {
|
|
1449
|
+
"alchemy.resource.fqn": fqn,
|
|
1450
|
+
"alchemy.resource.type": node.resource.Type,
|
|
1451
|
+
"alchemy.resource.logical_id": node.resource.LogicalId,
|
|
1452
|
+
"alchemy.resource.action": node.action
|
|
1453
|
+
} }));
|
|
1454
|
+
const converge = Effect.fnUntraced(function* (plan, tracker, terminalStatuses, session, state, stackName, stage$1) {
|
|
1455
|
+
for (;;) {
|
|
1456
|
+
let anyUpdated = false;
|
|
1457
|
+
for (const [fqn, node] of Object.entries(plan.resources)) {
|
|
1458
|
+
if (node.action === "noop") continue;
|
|
1459
|
+
if (!tracker[fqn]) continue;
|
|
1460
|
+
const outputs = Object.fromEntries(Object.entries(tracker).map(([k, t]) => [k, t.output]));
|
|
1461
|
+
const newProps = yield* evaluate(node.props, outputs);
|
|
1462
|
+
const newBindings = excludeDeletedBindings(yield* evaluate(node.bindings, outputs));
|
|
1463
|
+
const oldProps = tracker[fqn].props;
|
|
1464
|
+
const oldBindings = tracker[fqn].bindings;
|
|
1465
|
+
const propsChanged = havePropsChanged(oldProps, newProps);
|
|
1466
|
+
const bindingsChanged = JSON.stringify(oldBindings) !== JSON.stringify(newBindings);
|
|
1467
|
+
if (!propsChanged && !bindingsChanged) continue;
|
|
1468
|
+
anyUpdated = true;
|
|
1469
|
+
const logicalId = node.resource.LogicalId;
|
|
1470
|
+
const namespace = node.resource.Namespace;
|
|
1471
|
+
const instanceId = tracker[fqn].instanceId;
|
|
1472
|
+
const scopedSession = {
|
|
1473
|
+
...session,
|
|
1474
|
+
note: (note) => session.emit({
|
|
1475
|
+
id: logicalId,
|
|
1476
|
+
kind: "annotate",
|
|
1477
|
+
message: note
|
|
1478
|
+
})
|
|
1479
|
+
};
|
|
1480
|
+
const attr = yield* node.provider.update({
|
|
1481
|
+
id: logicalId,
|
|
1482
|
+
news: newProps,
|
|
1483
|
+
instanceId,
|
|
1484
|
+
bindings: newBindings,
|
|
1485
|
+
session: scopedSession,
|
|
1486
|
+
olds: oldProps,
|
|
1487
|
+
output: tracker[fqn].output
|
|
1488
|
+
}).pipe(instrumentLifecycle("update", fqn, node.resource.Type, logicalId, instanceId));
|
|
1489
|
+
tracker[fqn] = {
|
|
1490
|
+
output: attr,
|
|
1491
|
+
props: newProps,
|
|
1492
|
+
bindings: newBindings,
|
|
1493
|
+
instanceId
|
|
1494
|
+
};
|
|
1495
|
+
yield* state.set({
|
|
1496
|
+
stack: stackName,
|
|
1497
|
+
stage: stage$1,
|
|
1498
|
+
fqn,
|
|
1499
|
+
value: {
|
|
1500
|
+
status: "updated",
|
|
1501
|
+
fqn,
|
|
1502
|
+
logicalId,
|
|
1503
|
+
instanceId,
|
|
1504
|
+
resourceType: node.resource.Type,
|
|
1505
|
+
props: newProps,
|
|
1506
|
+
attr,
|
|
1507
|
+
providerVersion: node.provider.version ?? 0,
|
|
1508
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1509
|
+
downstream: node.downstream,
|
|
1510
|
+
namespace,
|
|
1511
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
terminalStatuses.set(fqn, {
|
|
1515
|
+
id: logicalId,
|
|
1516
|
+
type: node.resource.Type,
|
|
1517
|
+
status: "updated"
|
|
1518
|
+
});
|
|
1519
|
+
}
|
|
1520
|
+
if (!anyUpdated) break;
|
|
1521
|
+
}
|
|
1522
|
+
});
|
|
1523
|
+
const collectGarbage = Effect.fnUntraced(function* (plan, session) {
|
|
1524
|
+
const state = yield* State;
|
|
1525
|
+
const stackName = (yield* Stack).name;
|
|
1526
|
+
const stage$1 = yield* Stage;
|
|
1527
|
+
const deleteGraph = Effect.fnUntraced(function* (deletionGraph) {
|
|
1528
|
+
const deletions = {};
|
|
1529
|
+
const deleteResource = (node, ancestors = /* @__PURE__ */ new Set()) => Effect.gen(function* () {
|
|
1530
|
+
const isDeleteNode = (node$1) => "action" in node$1;
|
|
1531
|
+
const { logicalId, namespace, resourceType, instanceId, downstream, props, attr, provider } = isDeleteNode(node) ? {
|
|
1532
|
+
logicalId: node.resource.LogicalId,
|
|
1533
|
+
namespace: node.resource.Namespace,
|
|
1534
|
+
resourceType: node.resource.Type,
|
|
1535
|
+
instanceId: node.state.instanceId,
|
|
1536
|
+
downstream: node.downstream,
|
|
1537
|
+
props: node.state.props,
|
|
1538
|
+
attr: node.state.attr,
|
|
1539
|
+
provider: node.provider
|
|
1540
|
+
} : {
|
|
1541
|
+
logicalId: node.logicalId,
|
|
1542
|
+
namespace: node.namespace,
|
|
1543
|
+
resourceType: node.old.resourceType,
|
|
1544
|
+
instanceId: node.old.instanceId,
|
|
1545
|
+
downstream: node.old.downstream,
|
|
1546
|
+
props: node.old.props,
|
|
1547
|
+
attr: node.old.attr,
|
|
1548
|
+
provider: yield* findProviderByType(node.old.resourceType)
|
|
1549
|
+
};
|
|
1550
|
+
const fqn = toFqn(namespace, logicalId);
|
|
1551
|
+
const nextAncestors = new Set(ancestors).add(fqn);
|
|
1552
|
+
const commit = (value) => state.set({
|
|
1553
|
+
stack: stackName,
|
|
1554
|
+
stage: stage$1,
|
|
1555
|
+
fqn,
|
|
1556
|
+
value: {
|
|
1557
|
+
...value,
|
|
1558
|
+
namespace
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
const report = (status) => session.emit({
|
|
1562
|
+
kind: "status-change",
|
|
1563
|
+
id: logicalId,
|
|
1564
|
+
type: resourceType,
|
|
1565
|
+
status
|
|
1566
|
+
});
|
|
1567
|
+
const scopedSession = {
|
|
1568
|
+
...session,
|
|
1569
|
+
note: (note) => session.emit({
|
|
1570
|
+
id: logicalId,
|
|
1571
|
+
kind: "annotate",
|
|
1572
|
+
message: note
|
|
1573
|
+
})
|
|
1574
|
+
};
|
|
1575
|
+
return yield* deletions[fqn] ??= yield* Effect.cached(Effect.gen(function* () {
|
|
1576
|
+
yield* Effect.all(downstream.map((dep) => dep !== fqn && dep in deletionGraph && !ancestors.has(dep) ? deleteResource(deletionGraph[dep], nextAncestors) : Effect.void), { concurrency: "unbounded" });
|
|
1577
|
+
if (isDeleteNode(node)) {
|
|
1578
|
+
yield* report("deleting");
|
|
1579
|
+
if (node.resource.RemovalPolicy === "retain") {
|
|
1580
|
+
yield* state.delete({
|
|
1581
|
+
stack: stackName,
|
|
1582
|
+
stage: stage$1,
|
|
1583
|
+
fqn
|
|
1584
|
+
});
|
|
1585
|
+
yield* report("deleted");
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
yield* commit({
|
|
1589
|
+
status: "deleting",
|
|
1590
|
+
fqn,
|
|
1591
|
+
logicalId,
|
|
1592
|
+
instanceId,
|
|
1593
|
+
resourceType,
|
|
1594
|
+
props,
|
|
1595
|
+
attr,
|
|
1596
|
+
downstream,
|
|
1597
|
+
providerVersion: provider.version ?? 0,
|
|
1598
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1599
|
+
removalPolicy: node.resource.RemovalPolicy
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
if (attr !== void 0) yield* provider.delete({
|
|
1603
|
+
id: logicalId,
|
|
1604
|
+
instanceId,
|
|
1605
|
+
olds: props,
|
|
1606
|
+
output: attr,
|
|
1607
|
+
session: scopedSession,
|
|
1608
|
+
bindings: []
|
|
1609
|
+
}).pipe(instrumentLifecycle("delete", fqn, resourceType, logicalId, instanceId));
|
|
1610
|
+
if (isDeleteNode(node)) {
|
|
1611
|
+
yield* state.delete({
|
|
1612
|
+
stack: stackName,
|
|
1613
|
+
stage: stage$1,
|
|
1614
|
+
fqn
|
|
1615
|
+
});
|
|
1616
|
+
yield* report("deleted");
|
|
1617
|
+
} else {
|
|
1618
|
+
yield* scopedSession.note("Cleaning up replaced resource...");
|
|
1619
|
+
if (node.old.status === "replacing" || node.old.status === "replaced") yield* commit({
|
|
1620
|
+
status: "replaced",
|
|
1621
|
+
fqn,
|
|
1622
|
+
logicalId: node.logicalId,
|
|
1623
|
+
instanceId: node.instanceId,
|
|
1624
|
+
resourceType: node.resourceType,
|
|
1625
|
+
props: node.props,
|
|
1626
|
+
attr: node.attr,
|
|
1627
|
+
providerVersion: node.providerVersion,
|
|
1628
|
+
downstream: node.downstream,
|
|
1629
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1630
|
+
old: node.old.old,
|
|
1631
|
+
deleteFirst: node.deleteFirst,
|
|
1632
|
+
removalPolicy: node.removalPolicy
|
|
1633
|
+
});
|
|
1634
|
+
else yield* commit({
|
|
1635
|
+
status: "created",
|
|
1636
|
+
fqn,
|
|
1637
|
+
logicalId: node.logicalId,
|
|
1638
|
+
instanceId: node.instanceId,
|
|
1639
|
+
resourceType: node.resourceType,
|
|
1640
|
+
props: node.props,
|
|
1641
|
+
attr: node.attr,
|
|
1642
|
+
providerVersion: node.providerVersion,
|
|
1643
|
+
downstream: node.downstream,
|
|
1644
|
+
bindings: excludeDeletedBindings(node.bindings),
|
|
1645
|
+
removalPolicy: node.removalPolicy
|
|
1646
|
+
});
|
|
1647
|
+
yield* scopedSession.note("Replaced resource cleanup complete.");
|
|
1648
|
+
}
|
|
1649
|
+
}));
|
|
1650
|
+
});
|
|
1651
|
+
yield* Effect.all(Object.values(deletionGraph).filter((node) => node !== void 0).map((node) => deleteResource(node)), { concurrency: "unbounded" });
|
|
1652
|
+
});
|
|
1653
|
+
let first = true;
|
|
1654
|
+
while (true) {
|
|
1655
|
+
const remainingReplacedResources = yield* state.getReplacedResources({
|
|
1656
|
+
stack: stackName,
|
|
1657
|
+
stage: stage$1
|
|
1658
|
+
});
|
|
1659
|
+
if (!first && remainingReplacedResources.length === 0) break;
|
|
1660
|
+
yield* deleteGraph({
|
|
1661
|
+
...first ? plan.deletions : {},
|
|
1662
|
+
...Object.fromEntries(remainingReplacedResources.map((replaced) => [toFqn(replaced.namespace, replaced.logicalId), replaced]))
|
|
1663
|
+
});
|
|
1664
|
+
first = false;
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
const excludeDeletedBindings = (bindings) => bindings.flatMap(({ action, sid, data }) => action === "delete" ? [] : [{
|
|
1668
|
+
sid,
|
|
1669
|
+
data
|
|
1670
|
+
}]);
|
|
1671
|
+
|
|
1672
|
+
//#endregion
|
|
1673
|
+
//#region lib/Cli/commands/_shared.js
|
|
1674
|
+
var __rewriteRelativeImportExtension = void 0 && (void 0).__rewriteRelativeImportExtension || function(path$1, preserveJsx) {
|
|
1675
|
+
if (typeof path$1 === "string" && /^\.\.?\//.test(path$1)) return path$1.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
|
|
1676
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
|
|
1677
|
+
});
|
|
1678
|
+
return path$1;
|
|
1679
|
+
};
|
|
1680
|
+
const USER = Config.string("USER").pipe(Config.orElse(() => Config.string("USERNAME")), Config.withDefault("unknown"));
|
|
1681
|
+
const STAGE = Config.string("stage").pipe(Config.option, (a) => a.asEffect(), Effect.map(Option.getOrUndefined));
|
|
1682
|
+
const stage = Flag.string("stage").pipe(Flag.withSchema(S.String.check(S.isPattern(/^[a-z0-9]+([-_a-z0-9]+)*$/gi))), Flag.withDescription("Stage to deploy to, defaults to dev_${USER}"), Flag.optional, Flag.map(Option.getOrUndefined), Flag.mapEffect(Effect.fn(function* (stage$1) {
|
|
1683
|
+
if (stage$1) return stage$1;
|
|
1684
|
+
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)));
|
|
1685
|
+
})));
|
|
1686
|
+
const envFile = Flag.file("env-file").pipe(Flag.optional, Flag.withDescription("File to load environment variables from, defaults to .env"));
|
|
1687
|
+
const dryRun = Flag.boolean("dry-run").pipe(Flag.withDescription("Dry run the deployment, do not actually deploy"), Flag.withDefault(false));
|
|
1688
|
+
const yes = Flag.boolean("yes").pipe(Flag.withDescription("Yes to all prompts"), Flag.withDefault(false));
|
|
1689
|
+
const force = Flag.boolean("force").pipe(Flag.withDescription("Force updates for resources that would otherwise no-op"), Flag.withDefault(false));
|
|
1690
|
+
const script = Argument.file("main", { mustExist: true }).pipe(Argument.withDescription("Main file to deploy, defaults to alchemy.run.ts"), Argument.withDefault("alchemy.run.ts"));
|
|
1691
|
+
const profile = Flag.string("profile").pipe(Flag.withDescription("Auth profile to use (~/.alchemy/profiles.json). Defaults to 'default' or $ALCHEMY_PROFILE."), Flag.optional, Flag.map(Option.getOrElse(() => "default")));
|
|
1692
|
+
const resourceFilter = Flag.string("filter").pipe(Flag.withDescription("Comma-separated logical resource IDs (e.g. Api,Sandbox). Only those resources are included."), Flag.optional, Flag.map(Option.getOrUndefined));
|
|
1693
|
+
/**
|
|
1694
|
+
* Wraps a CLI command handler with a top-level OpenTelemetry span
|
|
1695
|
+
* (`cli.<command>`) and bumps the `alchemy.cli.invocations` counter.
|
|
1696
|
+
*
|
|
1697
|
+
* `attrs` runs against the parsed command args and contributes
|
|
1698
|
+
* additional attributes to the span (e.g. stage, profile, dry-run flag).
|
|
1699
|
+
*
|
|
1700
|
+
* Usage:
|
|
1701
|
+
* ```ts
|
|
1702
|
+
* Command.make(
|
|
1703
|
+
* "deploy",
|
|
1704
|
+
* { ...flags },
|
|
1705
|
+
* instrumentCommand("deploy", (a) => ({
|
|
1706
|
+
* "alchemy.stage": a.stage,
|
|
1707
|
+
* "alchemy.profile": a.profile,
|
|
1708
|
+
* }))(execStack),
|
|
1709
|
+
* );
|
|
1710
|
+
* ```
|
|
1711
|
+
*/
|
|
1712
|
+
const instrumentCommand = (command, attrs) => (handler) => (args) => handler(args).pipe(Effect.withSpan(`cli.${command}`, { attributes: attrs ? attrs(args) : {} }), recordCli(command));
|
|
1713
|
+
const importStack = Effect.fn(function* (main) {
|
|
1714
|
+
const path$1 = yield* Path$1.Path;
|
|
1715
|
+
const stackEffect = (yield* Effect.promise(() => import(__rewriteRelativeImportExtension(path$1.resolve(process.cwd(), main))))).default;
|
|
1716
|
+
if (!Effect.isEffect(stackEffect)) return yield* Effect.die(/* @__PURE__ */ new Error(`Main file '${main}' must export a default stack definition (export default Alchemy.Stack({...}))`));
|
|
1717
|
+
return stackEffect;
|
|
1718
|
+
});
|
|
1719
|
+
|
|
1720
|
+
//#endregion
|
|
1721
|
+
//#region lib/Cli/commands/deploy.js
|
|
1722
|
+
const ExecStackOptions = S.Struct({
|
|
1723
|
+
main: S.String,
|
|
1724
|
+
stage: S.String,
|
|
1725
|
+
envFile: S.OptionFromOptional(S.String),
|
|
1726
|
+
profile: S.optional(S.String),
|
|
1727
|
+
dryRun: S.optional(S.Boolean),
|
|
1728
|
+
force: S.optional(S.Boolean),
|
|
1729
|
+
yes: S.optional(S.Boolean),
|
|
1730
|
+
destroy: S.optional(S.Boolean),
|
|
1731
|
+
dev: S.optional(S.Boolean),
|
|
1732
|
+
adopt: S.optional(S.Boolean)
|
|
1733
|
+
});
|
|
1734
|
+
const stackSpanAttrs = (args) => ({
|
|
1735
|
+
"alchemy.stage": args.stage,
|
|
1736
|
+
"alchemy.profile": args.profile,
|
|
1737
|
+
"alchemy.main": args.main,
|
|
1738
|
+
"alchemy.dry_run": !!args.dryRun,
|
|
1739
|
+
"alchemy.force": !!args.force,
|
|
1740
|
+
"alchemy.destroy": !!args.destroy,
|
|
1741
|
+
"alchemy.dev": !!args.dev,
|
|
1742
|
+
"alchemy.adopt": !!args.adopt
|
|
1743
|
+
});
|
|
1744
|
+
const adopt = Flag.boolean("adopt").pipe(Flag.withDescription("Adopt pre-existing cloud resources that conflict with this stack instead of failing. Useful for re-importing infrastructure into a fresh state store."), Flag.withDefault(false));
|
|
1745
|
+
const execStack = Effect.fn(function* ({ main, stage: stage$1, envFile: envFile$1, profile: profile$1, dryRun: dryRun$1 = false, force: force$1 = false, yes: yes$1 = false, destroy = false, dev = false, adopt: adopt$1 = false }) {
|
|
1746
|
+
const stackEffect = yield* importStack(main);
|
|
1747
|
+
const services$1 = Layer.mergeAll(Layer.effect(AlchemyContext, AlchemyContext.asEffect().pipe(Effect.map((ctx) => ({
|
|
1748
|
+
...ctx,
|
|
1749
|
+
dev
|
|
1750
|
+
})))), Layer.succeed(AdoptPolicy, adopt$1), 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")], { mergeWithExisting: true }), Layer.succeed(Stage, stage$1));
|
|
1751
|
+
yield* Effect.gen(function* () {
|
|
1752
|
+
const cli = yield* Cli;
|
|
1753
|
+
const stack = yield* stackEffect;
|
|
1754
|
+
yield* Effect.gen(function* () {
|
|
1755
|
+
const updatePlan = yield* make(destroy ? {
|
|
1756
|
+
...stack,
|
|
1757
|
+
resources: {},
|
|
1758
|
+
bindings: {},
|
|
1759
|
+
output: {}
|
|
1760
|
+
} : stack, { force: force$1 });
|
|
1761
|
+
if (dryRun$1) yield* cli.displayPlan(updatePlan);
|
|
1762
|
+
else {
|
|
1763
|
+
const hasChanges = Object.keys(updatePlan.deletions).length > 0 || Object.values(updatePlan.resources).some((node) => node.action !== "noop" || node.bindings.some((b) => b.action !== "noop"));
|
|
1764
|
+
if (!yes$1 && hasChanges) {
|
|
1765
|
+
if (!(yield* cli.approvePlan(updatePlan))) return;
|
|
1766
|
+
}
|
|
1767
|
+
const outputs = yield* apply(updatePlan);
|
|
1768
|
+
if (outputs !== void 0) yield* Console.log(outputs);
|
|
1769
|
+
if (dev) yield* Effect.never;
|
|
1770
|
+
}
|
|
1771
|
+
}).pipe(Effect.provide(stack.services));
|
|
1772
|
+
}).pipe(Effect.provide(services$1));
|
|
1773
|
+
});
|
|
1774
|
+
const deployCommand = Command.make("deploy", {
|
|
1775
|
+
dryRun,
|
|
1776
|
+
force,
|
|
1777
|
+
main: script,
|
|
1778
|
+
envFile,
|
|
1779
|
+
stage,
|
|
1780
|
+
yes,
|
|
1781
|
+
profile,
|
|
1782
|
+
adopt
|
|
1783
|
+
}, instrumentCommand("deploy", stackSpanAttrs)(execStack));
|
|
1784
|
+
const destroyCommand = Command.make("destroy", {
|
|
1785
|
+
dryRun,
|
|
1786
|
+
main: script,
|
|
1787
|
+
envFile,
|
|
1788
|
+
stage,
|
|
1789
|
+
yes,
|
|
1790
|
+
profile
|
|
1791
|
+
}, instrumentCommand("destroy", stackSpanAttrs)((args) => execStack({
|
|
1792
|
+
...args,
|
|
1793
|
+
destroy: true
|
|
1794
|
+
})));
|
|
1795
|
+
const planCommand = Command.make("plan", {
|
|
1796
|
+
main: script,
|
|
1797
|
+
envFile,
|
|
1798
|
+
stage,
|
|
1799
|
+
profile
|
|
1800
|
+
}, instrumentCommand("plan", stackSpanAttrs)((args) => execStack({
|
|
1801
|
+
...args,
|
|
1802
|
+
dryRun: true
|
|
1803
|
+
})));
|
|
1804
|
+
|
|
1805
|
+
//#endregion
|
|
1806
|
+
//#region lib/Cli/NamespaceTree.js
|
|
1807
|
+
function buildNamespaceTree(items) {
|
|
1808
|
+
const root = {
|
|
1809
|
+
id: "",
|
|
1810
|
+
path: [],
|
|
1811
|
+
children: /* @__PURE__ */ new Map(),
|
|
1812
|
+
resources: []
|
|
1813
|
+
};
|
|
1814
|
+
const getNode = (path$1) => {
|
|
1815
|
+
let current = root;
|
|
1816
|
+
for (let i = 0; i < path$1.length; i++) {
|
|
1817
|
+
const segment = path$1[i];
|
|
1818
|
+
let child = current.children.get(segment);
|
|
1819
|
+
if (!child) {
|
|
1820
|
+
child = {
|
|
1821
|
+
id: segment,
|
|
1822
|
+
path: path$1.slice(0, i + 1),
|
|
1823
|
+
children: /* @__PURE__ */ new Map(),
|
|
1824
|
+
resources: []
|
|
1825
|
+
};
|
|
1826
|
+
current.children.set(segment, child);
|
|
1827
|
+
}
|
|
1828
|
+
current = child;
|
|
1829
|
+
}
|
|
1830
|
+
return current;
|
|
1831
|
+
};
|
|
1832
|
+
for (const item of items) getNode(toPath(item.resource.Namespace)).resources.push(item);
|
|
1833
|
+
return root;
|
|
1834
|
+
}
|
|
1835
|
+
function deriveNamespaceAction(node) {
|
|
1836
|
+
const actions = /* @__PURE__ */ new Set();
|
|
1837
|
+
for (const resource of node.resources) actions.add(deriveResourceChildrenAction(resource, node));
|
|
1838
|
+
for (const child of node.children.values()) {
|
|
1839
|
+
const childAction = deriveNamespaceAction(child);
|
|
1840
|
+
if (childAction === "mixed") return "mixed";
|
|
1841
|
+
actions.add(childAction);
|
|
1842
|
+
}
|
|
1843
|
+
return deriveAction(actions);
|
|
1844
|
+
}
|
|
1845
|
+
function flattenTree(node, depth = 0, result = []) {
|
|
1846
|
+
flattenNamespace(node, depth, result);
|
|
1847
|
+
return result;
|
|
1848
|
+
}
|
|
1849
|
+
const flattenNamespace = (node, depth, result) => {
|
|
1850
|
+
const sortedResources = [...node.resources].sort((a, b) => a.resource.LogicalId.localeCompare(b.resource.LogicalId));
|
|
1851
|
+
const sortedChildren = Array.from(node.children.entries()).sort(([a], [b]) => a.localeCompare(b));
|
|
1852
|
+
const resourceIds = new Set(sortedResources.map((resource) => resource.resource.LogicalId));
|
|
1853
|
+
for (const [id, child] of sortedChildren) {
|
|
1854
|
+
if (resourceIds.has(id) || isEmpty(child)) continue;
|
|
1855
|
+
result.push({
|
|
1856
|
+
type: "namespace",
|
|
1857
|
+
depth,
|
|
1858
|
+
id,
|
|
1859
|
+
path: child.path,
|
|
1860
|
+
action: deriveNamespaceAction(child),
|
|
1861
|
+
hasChildren: true
|
|
1862
|
+
});
|
|
1863
|
+
flattenNamespace(child, depth + 1, result);
|
|
1864
|
+
}
|
|
1865
|
+
for (const resource of sortedResources) {
|
|
1866
|
+
const childNamespace = node.children.get(resource.resource.LogicalId);
|
|
1867
|
+
result.push({
|
|
1868
|
+
type: "resource",
|
|
1869
|
+
depth,
|
|
1870
|
+
id: resource.resource.LogicalId,
|
|
1871
|
+
path: [...node.path, resource.resource.LogicalId],
|
|
1872
|
+
action: resource.action,
|
|
1873
|
+
resourceType: resource.resource.Type,
|
|
1874
|
+
bindingCount: resource.bindings.length
|
|
1875
|
+
});
|
|
1876
|
+
for (const binding of [...resource.bindings].sort((a, b) => a.sid.localeCompare(b.sid))) result.push({
|
|
1877
|
+
type: "binding",
|
|
1878
|
+
depth: depth + 1,
|
|
1879
|
+
id: binding.sid,
|
|
1880
|
+
path: [
|
|
1881
|
+
...node.path,
|
|
1882
|
+
resource.resource.LogicalId,
|
|
1883
|
+
binding.sid
|
|
1884
|
+
],
|
|
1885
|
+
action: binding.action,
|
|
1886
|
+
bindingSid: binding.sid
|
|
1887
|
+
});
|
|
1888
|
+
if (childNamespace) flattenNamespace(childNamespace, depth + 1, result);
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
const isEmpty = (node) => node.resources.length === 0 && Array.from(node.children.values()).every(isEmpty);
|
|
1892
|
+
const deriveResourceChildrenAction = (resource, node) => {
|
|
1893
|
+
const actions = new Set([resource.action]);
|
|
1894
|
+
for (const binding of resource.bindings) actions.add(binding.action);
|
|
1895
|
+
const childNamespace = node.children.get(resource.resource.LogicalId);
|
|
1896
|
+
if (childNamespace) actions.add(deriveNamespaceAction(childNamespace));
|
|
1897
|
+
return deriveAction(actions);
|
|
1898
|
+
};
|
|
1899
|
+
const deriveAction = (actions) => {
|
|
1900
|
+
if (actions.size === 0) return "noop";
|
|
1901
|
+
if (actions.has("replace")) return actions.size === 1 ? "replace" : "mixed";
|
|
1902
|
+
if (actions.has("delete")) return actions.size === 1 ? "delete" : "mixed";
|
|
1903
|
+
if (actions.has("create")) return actions.size === 1 ? "create" : "mixed";
|
|
1904
|
+
if (actions.has("update")) return actions.size === 1 ? "update" : "mixed";
|
|
1905
|
+
return "noop";
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
//#endregion
|
|
1909
|
+
//#region lib/Cli/tui/components/Plan.js
|
|
1910
|
+
function Plan({ plan }) {
|
|
1911
|
+
const items = useMemo(() => [...Object.values(plan.resources), ...Object.values(plan.deletions)], [plan]);
|
|
1912
|
+
const flatItems = useMemo(() => {
|
|
1913
|
+
return flattenTree(buildNamespaceTree(items));
|
|
1914
|
+
}, [items]);
|
|
1915
|
+
if (items.length === 0) return jsx(Text, {
|
|
1916
|
+
color: "gray",
|
|
1917
|
+
children: "No changes planned"
|
|
1918
|
+
});
|
|
1919
|
+
const counts = items.reduce((acc, item) => (acc[item.action]++, acc), {
|
|
1920
|
+
create: 0,
|
|
1921
|
+
update: 0,
|
|
1922
|
+
delete: 0,
|
|
1923
|
+
noop: 0,
|
|
1924
|
+
replace: 0
|
|
1925
|
+
});
|
|
1926
|
+
const actions = [
|
|
1927
|
+
"create",
|
|
1928
|
+
"update",
|
|
1929
|
+
"delete",
|
|
1930
|
+
"replace"
|
|
1931
|
+
].filter((action) => counts[action] > 0);
|
|
1932
|
+
return jsxs(Box, {
|
|
1933
|
+
flexDirection: "column",
|
|
1934
|
+
children: [jsxs(Box, {
|
|
1935
|
+
marginTop: 1,
|
|
1936
|
+
children: [
|
|
1937
|
+
jsx(Text, {
|
|
1938
|
+
underline: true,
|
|
1939
|
+
children: "Plan"
|
|
1940
|
+
}),
|
|
1941
|
+
jsx(Text, { children: ": " }),
|
|
1942
|
+
actions.flatMap((action, i) => {
|
|
1943
|
+
const count = counts[action];
|
|
1944
|
+
const color = actionColor(action);
|
|
1945
|
+
if (count === 0) return [];
|
|
1946
|
+
const box = jsx(Box, { children: jsxs(Text, {
|
|
1947
|
+
color,
|
|
1948
|
+
children: [
|
|
1949
|
+
count,
|
|
1950
|
+
" to ",
|
|
1951
|
+
action
|
|
1952
|
+
]
|
|
1953
|
+
}) }, action);
|
|
1954
|
+
return i === actions.length - 1 ? [box] : [box, jsx(Text, { children: " | " }, `${action}-separator`)];
|
|
1955
|
+
})
|
|
1956
|
+
]
|
|
1957
|
+
}), jsx(Box, {
|
|
1958
|
+
flexDirection: "column",
|
|
1959
|
+
marginTop: 1,
|
|
1960
|
+
children: flatItems.map((item) => {
|
|
1961
|
+
const indent = " ".repeat(item.depth);
|
|
1962
|
+
const color = getActionColor(item.action);
|
|
1963
|
+
const icon = getActionIcon(item.action);
|
|
1964
|
+
const key = item.path.join("/");
|
|
1965
|
+
if (item.type === "namespace") return jsxs(Box, {
|
|
1966
|
+
flexDirection: "row",
|
|
1967
|
+
children: [
|
|
1968
|
+
jsx(Text, { children: indent }),
|
|
1969
|
+
jsx(Box, {
|
|
1970
|
+
width: 2,
|
|
1971
|
+
children: jsxs(Text, {
|
|
1972
|
+
color,
|
|
1973
|
+
children: [icon, " "]
|
|
1974
|
+
})
|
|
1975
|
+
}),
|
|
1976
|
+
jsx(Text, {
|
|
1977
|
+
color: "blueBright",
|
|
1978
|
+
children: item.id
|
|
1979
|
+
})
|
|
1980
|
+
]
|
|
1981
|
+
}, key);
|
|
1982
|
+
if (item.type === "binding") return jsxs(Box, {
|
|
1983
|
+
flexDirection: "row",
|
|
1984
|
+
children: [
|
|
1985
|
+
jsx(Text, { children: indent }),
|
|
1986
|
+
jsx(Box, {
|
|
1987
|
+
width: 2,
|
|
1988
|
+
children: jsxs(Text, {
|
|
1989
|
+
color,
|
|
1990
|
+
children: [icon, " "]
|
|
1991
|
+
})
|
|
1992
|
+
}),
|
|
1993
|
+
jsx(Text, {
|
|
1994
|
+
color: "cyan",
|
|
1995
|
+
children: item.bindingSid
|
|
1996
|
+
})
|
|
1997
|
+
]
|
|
1998
|
+
}, key);
|
|
1999
|
+
return jsxs(Box, {
|
|
2000
|
+
flexDirection: "row",
|
|
2001
|
+
children: [
|
|
2002
|
+
jsx(Text, { children: indent }),
|
|
2003
|
+
jsx(Box, {
|
|
2004
|
+
width: 2,
|
|
2005
|
+
children: jsxs(Text, {
|
|
2006
|
+
color,
|
|
2007
|
+
children: [icon, " "]
|
|
2008
|
+
})
|
|
2009
|
+
}),
|
|
2010
|
+
jsx(Box, { children: jsx(Text, {
|
|
2011
|
+
bold: true,
|
|
2012
|
+
children: item.id
|
|
2013
|
+
}) }),
|
|
2014
|
+
jsx(Box, {
|
|
2015
|
+
marginLeft: 1,
|
|
2016
|
+
children: jsxs(Text, {
|
|
2017
|
+
color: "blackBright",
|
|
2018
|
+
children: [
|
|
2019
|
+
"(",
|
|
2020
|
+
item.resourceType,
|
|
2021
|
+
")"
|
|
2022
|
+
]
|
|
2023
|
+
})
|
|
2024
|
+
}),
|
|
2025
|
+
item.bindingCount !== void 0 && item.bindingCount > 0 && jsx(Box, {
|
|
2026
|
+
marginLeft: 1,
|
|
2027
|
+
children: jsxs(Text, {
|
|
2028
|
+
color: "cyan",
|
|
2029
|
+
children: [
|
|
2030
|
+
"(",
|
|
2031
|
+
item.bindingCount,
|
|
2032
|
+
" bindings)"
|
|
2033
|
+
]
|
|
2034
|
+
})
|
|
2035
|
+
})
|
|
2036
|
+
]
|
|
2037
|
+
}, key);
|
|
2038
|
+
})
|
|
2039
|
+
})]
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
const getActionColor = (action) => ({
|
|
2043
|
+
noop: "gray",
|
|
2044
|
+
create: "green",
|
|
2045
|
+
update: "yellow",
|
|
2046
|
+
delete: "red",
|
|
2047
|
+
replace: "magenta",
|
|
2048
|
+
mixed: "cyan"
|
|
2049
|
+
})[action] ?? "gray";
|
|
2050
|
+
const getActionIcon = (action) => ({
|
|
2051
|
+
create: "+",
|
|
2052
|
+
update: "~",
|
|
2053
|
+
delete: "-",
|
|
2054
|
+
noop: "•",
|
|
2055
|
+
replace: "!",
|
|
2056
|
+
mixed: "*"
|
|
2057
|
+
})[action] ?? "?";
|
|
2058
|
+
const actionColor = (action) => getActionColor(action);
|
|
2059
|
+
|
|
2060
|
+
//#endregion
|
|
2061
|
+
//#region lib/Cli/tui/components/ApprovePlan.js
|
|
2062
|
+
function ApprovePlan(props) {
|
|
2063
|
+
const { plan, approve } = props;
|
|
2064
|
+
const [selected, setSelected] = useState(0);
|
|
2065
|
+
const { exit } = useApp();
|
|
2066
|
+
useInput((_input, key) => {
|
|
2067
|
+
if (key.leftArrow || key.rightArrow) setSelected((prev) => prev === 0 ? 1 : 0);
|
|
2068
|
+
else if (key.return) {
|
|
2069
|
+
approve(selected === 0);
|
|
2070
|
+
exit();
|
|
2071
|
+
}
|
|
2072
|
+
});
|
|
2073
|
+
return jsxs(Box, {
|
|
2074
|
+
flexDirection: "column",
|
|
2075
|
+
children: [
|
|
2076
|
+
jsx(Plan, { plan }),
|
|
2077
|
+
jsx(Box, {
|
|
2078
|
+
marginTop: 1,
|
|
2079
|
+
children: jsx(Text, { children: "Proceed?" })
|
|
2080
|
+
}),
|
|
2081
|
+
jsxs(Box, {
|
|
2082
|
+
gap: 1,
|
|
2083
|
+
children: [jsxs(Text, {
|
|
2084
|
+
color: selected === 0 ? "green" : "gray",
|
|
2085
|
+
children: [selected === 0 ? "◉" : "○", " Yes"]
|
|
2086
|
+
}), jsxs(Text, {
|
|
2087
|
+
color: selected === 1 ? "red" : "gray",
|
|
2088
|
+
children: [selected === 1 ? "◉" : "○", " No"]
|
|
2089
|
+
})]
|
|
2090
|
+
})
|
|
2091
|
+
]
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
//#endregion
|
|
2096
|
+
//#region lib/Cli/tui/components/PlanProgress.js
|
|
2097
|
+
const getTaskKey = (item) => item.path.join("/");
|
|
2098
|
+
const buildProgressRows = (plan) => {
|
|
2099
|
+
return flattenTree(buildNamespaceTree([...Object.values(plan.resources), ...Object.values(plan.deletions).filter((item) => item !== void 0)])).filter((item) => item.type !== "binding").map((item) => item.type === "namespace" ? {
|
|
2100
|
+
key: getTaskKey(item),
|
|
2101
|
+
type: "namespace",
|
|
2102
|
+
id: item.id,
|
|
2103
|
+
depth: item.depth,
|
|
2104
|
+
action: item.action
|
|
2105
|
+
} : {
|
|
2106
|
+
key: getTaskKey(item),
|
|
2107
|
+
type: "resource",
|
|
2108
|
+
id: item.id,
|
|
2109
|
+
depth: item.depth,
|
|
2110
|
+
resourceType: item.resourceType ?? "unknown",
|
|
2111
|
+
action: item.action,
|
|
2112
|
+
persistedApplyStatus: item.action === "noop" ? (() => {
|
|
2113
|
+
const crud = findCrudByLogicalId(plan, item.id);
|
|
2114
|
+
return crud?.action === "noop" ? crud.state.status : void 0;
|
|
2115
|
+
})() : void 0
|
|
2116
|
+
});
|
|
2117
|
+
};
|
|
2118
|
+
const buildLogicalIdIndex = (rows) => {
|
|
2119
|
+
const index = /* @__PURE__ */ new Map();
|
|
2120
|
+
for (const row of rows) {
|
|
2121
|
+
if (row.type !== "resource") continue;
|
|
2122
|
+
const keys = index.get(row.id);
|
|
2123
|
+
if (keys) keys.push(row.key);
|
|
2124
|
+
else index.set(row.id, [row.key]);
|
|
2125
|
+
}
|
|
2126
|
+
return index;
|
|
2127
|
+
};
|
|
2128
|
+
function toPlanTask(rowOrId, planItem) {
|
|
2129
|
+
if (typeof rowOrId === "string") return {
|
|
2130
|
+
key: rowOrId,
|
|
2131
|
+
id: rowOrId,
|
|
2132
|
+
type: planItem.resource.Type,
|
|
2133
|
+
status: planItem.action === "noop" ? planItem.state.status : "pending",
|
|
2134
|
+
updatedAt: Date.now()
|
|
2135
|
+
};
|
|
2136
|
+
return {
|
|
2137
|
+
key: rowOrId.key,
|
|
2138
|
+
id: rowOrId.id,
|
|
2139
|
+
type: rowOrId.resourceType,
|
|
2140
|
+
status: rowOrId.action === "noop" ? rowOrId.persistedApplyStatus ?? "created" : "pending",
|
|
2141
|
+
updatedAt: Date.now()
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
const buildInitialTasks = (rows) => new Map(rows.flatMap((row) => row.type === "resource" ? [[row.key, toPlanTask(row)]] : []));
|
|
2145
|
+
function PlanProgress(props) {
|
|
2146
|
+
const { source, plan } = props;
|
|
2147
|
+
const spinner = useGlobalSpinner();
|
|
2148
|
+
const rows = useMemo(() => buildProgressRows(plan), [plan]);
|
|
2149
|
+
const logicalIdIndex = useMemo(() => buildLogicalIdIndex(rows), [rows]);
|
|
2150
|
+
const [tasks, setTasks] = useState(() => buildInitialTasks(rows));
|
|
2151
|
+
const unsubscribeRef = useRef(null);
|
|
2152
|
+
useEffect(() => {
|
|
2153
|
+
unsubscribeRef.current?.();
|
|
2154
|
+
unsubscribeRef.current = source.subscribe((event) => {
|
|
2155
|
+
setTasks((prev) => {
|
|
2156
|
+
const next = new Map(prev);
|
|
2157
|
+
const keys = logicalIdIndex.get(event.id) ?? [];
|
|
2158
|
+
if (event.kind === "status-change") {
|
|
2159
|
+
if (!event.bindingId) for (const key of keys) {
|
|
2160
|
+
const current = next.get(key);
|
|
2161
|
+
next.set(key, {
|
|
2162
|
+
key,
|
|
2163
|
+
id: event.id,
|
|
2164
|
+
type: event.type,
|
|
2165
|
+
status: event.status,
|
|
2166
|
+
message: event.message ?? current?.message,
|
|
2167
|
+
updatedAt: Date.now()
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
} else for (const key of keys) {
|
|
2171
|
+
const current = next.get(key);
|
|
2172
|
+
if (!current) continue;
|
|
2173
|
+
next.set(key, {
|
|
2174
|
+
...current,
|
|
2175
|
+
message: event.message,
|
|
2176
|
+
updatedAt: Date.now()
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
return next;
|
|
2180
|
+
});
|
|
2181
|
+
});
|
|
2182
|
+
return () => {
|
|
2183
|
+
unsubscribeRef.current?.();
|
|
2184
|
+
unsubscribeRef.current = null;
|
|
2185
|
+
};
|
|
2186
|
+
}, [logicalIdIndex, source]);
|
|
2187
|
+
useEffect(() => {
|
|
2188
|
+
setTasks(buildInitialTasks(rows));
|
|
2189
|
+
}, [rows]);
|
|
2190
|
+
return jsx(Box, {
|
|
2191
|
+
flexDirection: "column",
|
|
2192
|
+
children: rows.map((row) => {
|
|
2193
|
+
const indent = " ".repeat(row.depth);
|
|
2194
|
+
if (row.type === "namespace") return jsxs(Box, {
|
|
2195
|
+
flexDirection: "row",
|
|
2196
|
+
children: [
|
|
2197
|
+
jsx(Text, { children: indent }),
|
|
2198
|
+
jsx(Box, {
|
|
2199
|
+
width: 2,
|
|
2200
|
+
children: jsx(Text, {
|
|
2201
|
+
color: "blueBright",
|
|
2202
|
+
children: "↳ "
|
|
2203
|
+
})
|
|
2204
|
+
}),
|
|
2205
|
+
jsx(Text, {
|
|
2206
|
+
color: "blueBright",
|
|
2207
|
+
children: row.id
|
|
2208
|
+
})
|
|
2209
|
+
]
|
|
2210
|
+
}, row.key);
|
|
2211
|
+
const task = tasks.get(row.key) ?? toPlanTask(row);
|
|
2212
|
+
const displayStatus = getDisplayStatus(row, task.status);
|
|
2213
|
+
const color = statusColor(displayStatus);
|
|
2214
|
+
const icon = statusIcon(task.status, spinner);
|
|
2215
|
+
return jsxs(Box, {
|
|
2216
|
+
flexDirection: "column",
|
|
2217
|
+
children: [jsxs(Box, {
|
|
2218
|
+
flexDirection: "row",
|
|
2219
|
+
children: [
|
|
2220
|
+
jsx(Text, { children: indent }),
|
|
2221
|
+
jsx(Box, {
|
|
2222
|
+
width: 2,
|
|
2223
|
+
children: jsxs(Text, {
|
|
2224
|
+
color,
|
|
2225
|
+
children: [icon, " "]
|
|
2226
|
+
})
|
|
2227
|
+
}),
|
|
2228
|
+
jsx(Text, {
|
|
2229
|
+
bold: true,
|
|
2230
|
+
children: task.id
|
|
2231
|
+
}),
|
|
2232
|
+
jsxs(Text, {
|
|
2233
|
+
dimColor: true,
|
|
2234
|
+
children: [
|
|
2235
|
+
" (",
|
|
2236
|
+
task.type,
|
|
2237
|
+
")"
|
|
2238
|
+
]
|
|
2239
|
+
}),
|
|
2240
|
+
jsxs(Text, {
|
|
2241
|
+
color,
|
|
2242
|
+
children: [" ", displayStatus]
|
|
2243
|
+
})
|
|
2244
|
+
]
|
|
2245
|
+
}), task.message ? jsx(Box, {
|
|
2246
|
+
paddingLeft: row.depth * 2 + 2,
|
|
2247
|
+
children: jsxs(Text, {
|
|
2248
|
+
dimColor: true,
|
|
2249
|
+
children: ["• ", task.message]
|
|
2250
|
+
})
|
|
2251
|
+
}) : null]
|
|
2252
|
+
}, row.key);
|
|
2253
|
+
})
|
|
2254
|
+
});
|
|
2255
|
+
}
|
|
2256
|
+
function getDisplayStatus(row, status) {
|
|
2257
|
+
if (row.action === "noop" && (status === "created" || status === "updated")) return "no change";
|
|
2258
|
+
return status;
|
|
2259
|
+
}
|
|
2260
|
+
function statusColor(status) {
|
|
2261
|
+
switch (status) {
|
|
2262
|
+
case "no change": return "gray";
|
|
2263
|
+
case "pending": return "gray";
|
|
2264
|
+
case "creating":
|
|
2265
|
+
case "created": return "green";
|
|
2266
|
+
case "updating":
|
|
2267
|
+
case "updated": return "yellow";
|
|
2268
|
+
case "deleting":
|
|
2269
|
+
case "deleted": return "red";
|
|
2270
|
+
case "fail": return "redBright";
|
|
2271
|
+
default: return;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
function statusIcon(status, spinnerChar) {
|
|
2275
|
+
if (isInProgress(status)) return spinnerChar;
|
|
2276
|
+
if (status === "fail") return "✗";
|
|
2277
|
+
return "✓";
|
|
2278
|
+
}
|
|
2279
|
+
function isInProgress(status) {
|
|
2280
|
+
return status === "pending" || status === "creating" || status === "updating" || status === "deleting";
|
|
2281
|
+
}
|
|
2282
|
+
const spinnerFrames = [
|
|
2283
|
+
"⠋",
|
|
2284
|
+
"⠙",
|
|
2285
|
+
"⠹",
|
|
2286
|
+
"⠸",
|
|
2287
|
+
"⠼",
|
|
2288
|
+
"⠴",
|
|
2289
|
+
"⠦",
|
|
2290
|
+
"⠧",
|
|
2291
|
+
"⠇",
|
|
2292
|
+
"⠏"
|
|
2293
|
+
];
|
|
2294
|
+
function useGlobalSpinner(intervalMs = 80) {
|
|
2295
|
+
const [index, setIndex] = useState(0);
|
|
2296
|
+
useEffect(() => {
|
|
2297
|
+
const timer = setInterval(() => {
|
|
2298
|
+
setIndex((i) => (i + 1) % spinnerFrames.length);
|
|
2299
|
+
}, intervalMs);
|
|
2300
|
+
return () => clearInterval(timer);
|
|
2301
|
+
}, [intervalMs]);
|
|
2302
|
+
return spinnerFrames[index];
|
|
2303
|
+
}
|
|
2304
|
+
const findCrudByLogicalId = (plan, logicalId) => {
|
|
2305
|
+
for (const node of Object.values(plan.resources)) if (node.resource.LogicalId === logicalId) return node;
|
|
2306
|
+
for (const node of Object.values(plan.deletions)) if (node?.resource.LogicalId === logicalId) return node;
|
|
2307
|
+
};
|
|
2308
|
+
|
|
2309
|
+
//#endregion
|
|
2310
|
+
//#region lib/Cli/tui/InkCLI.js
|
|
2311
|
+
const inkCLI = () => Layer.succeed(Cli, Cli.of({
|
|
2312
|
+
approvePlan,
|
|
2313
|
+
displayPlan,
|
|
2314
|
+
startApplySession
|
|
2315
|
+
}));
|
|
2316
|
+
const approvePlan = Effect.fn(function* (plan) {
|
|
2317
|
+
let approved = false;
|
|
2318
|
+
const { waitUntilExit } = render(jsx(ApprovePlan, {
|
|
2319
|
+
plan,
|
|
2320
|
+
approve: (a) => approved = a
|
|
2321
|
+
}));
|
|
2322
|
+
yield* Effect.promise(waitUntilExit);
|
|
2323
|
+
return approved;
|
|
2324
|
+
});
|
|
2325
|
+
const displayPlan = (plan) => Effect.sync(() => {
|
|
2326
|
+
const { unmount } = render(jsx(Plan, { plan }));
|
|
2327
|
+
unmount();
|
|
2328
|
+
});
|
|
2329
|
+
const startApplySession = Effect.fn(function* (plan) {
|
|
2330
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2331
|
+
const { unmount } = render(jsx(PlanProgress, {
|
|
2332
|
+
plan,
|
|
2333
|
+
source: { subscribe(listener) {
|
|
2334
|
+
listeners.add(listener);
|
|
2335
|
+
return () => listeners.delete(listener);
|
|
2336
|
+
} }
|
|
2337
|
+
}));
|
|
2338
|
+
return {
|
|
2339
|
+
done: Effect.fn(function* () {
|
|
2340
|
+
yield* Effect.sleep(10);
|
|
2341
|
+
yield* Effect.sync(() => unmount());
|
|
2342
|
+
}),
|
|
2343
|
+
emit: (event) => Effect.sync(() => {
|
|
2344
|
+
for (const listener of listeners) listener(event);
|
|
2345
|
+
})
|
|
2346
|
+
};
|
|
2347
|
+
});
|
|
2348
|
+
|
|
2349
|
+
//#endregion
|
|
2350
|
+
//#region lib/Cli/exec.js
|
|
2351
|
+
const services = Layer.mergeAll(Layer.provideMerge(AlchemyContextLive, PlatformServices), FetchHttpClient.layer, ConfigProvider.layer(ConfigProvider.fromEnv()), inkCLI());
|
|
2352
|
+
const exec = () => execStack(S.decodeSync(ExecStackOptions)(JSON.parse(process.env.ALCHEMY_EXEC_OPTIONS))).pipe(Effect.provide(services), Effect.scoped);
|
|
2353
|
+
|
|
2354
|
+
//#endregion
|
|
2355
|
+
//#region bin/exec.ts
|
|
2356
|
+
exec().pipe(runMain);
|
|
2357
|
+
|
|
2358
|
+
//#endregion
|
|
2359
|
+
export { };
|
|
2360
|
+
//# sourceMappingURL=exec.js.map
|