alchemy 2.0.0-beta.13 → 2.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/bin/alchemy.js +8386 -8205
  2. package/bin/alchemy.js.map +1 -1
  3. package/bin/alchemy.ts +4 -2
  4. package/bin/cli.js +61 -0
  5. package/bin/commands/deploy.ts +6 -3
  6. package/bin/commands/login.ts +1 -1
  7. package/bin/commands/logs.ts +1 -1
  8. package/bin/commands/state.ts +230 -0
  9. package/bin/commands/tail.ts +1 -1
  10. package/lib/AWS/AutoScaling/LaunchTemplate.d.ts +1 -1
  11. package/lib/AWS/EC2/Instance.d.ts +1 -1
  12. package/lib/AWS/EC2/hosted.d.ts +2 -2
  13. package/lib/AWS/ECS/Task.d.ts +4 -4
  14. package/lib/AWS/ECS/Task.d.ts.map +1 -1
  15. package/lib/AWS/ECS/Task.js +2 -2
  16. package/lib/AWS/ECS/Task.js.map +1 -1
  17. package/lib/AWS/Environment.d.ts +2 -2
  18. package/lib/AWS/Lambda/BucketEventSource.d.ts +1 -1
  19. package/lib/AWS/Lambda/BucketEventSource.d.ts.map +1 -1
  20. package/lib/AWS/Lambda/EventBridgeEventSource.d.ts +1 -1
  21. package/lib/AWS/Lambda/EventBridgeEventSource.d.ts.map +1 -1
  22. package/lib/AWS/Lambda/Function.d.ts +2 -2
  23. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  24. package/lib/AWS/Lambda/Function.js +1 -2
  25. package/lib/AWS/Lambda/Function.js.map +1 -1
  26. package/lib/AWS/Lambda/QueueEventSource.d.ts +1 -1
  27. package/lib/AWS/Lambda/QueueEventSource.d.ts.map +1 -1
  28. package/lib/AWS/Lambda/StreamEventSource.d.ts +1 -1
  29. package/lib/AWS/Lambda/StreamEventSource.d.ts.map +1 -1
  30. package/lib/AWS/Lambda/TableEventSource.d.ts +1 -1
  31. package/lib/AWS/Lambda/TableEventSource.d.ts.map +1 -1
  32. package/lib/AWS/Lambda/TopicEventSource.d.ts +1 -1
  33. package/lib/AWS/Lambda/TopicEventSource.d.ts.map +1 -1
  34. package/lib/AWS/SQS/QueueSink.d.ts.map +1 -1
  35. package/lib/AWS/SQS/QueueSink.js +1 -2
  36. package/lib/AWS/SQS/QueueSink.js.map +1 -1
  37. package/lib/Agent/coding/bash.d.ts +2 -6
  38. package/lib/Agent/coding/bash.d.ts.map +1 -1
  39. package/lib/Agent/coding/bash.js +2 -3
  40. package/lib/Agent/coding/bash.js.map +1 -1
  41. package/lib/Agent/coding/edit.d.ts +1 -1
  42. package/lib/Agent/coding/glob.d.ts +2 -4
  43. package/lib/Agent/coding/glob.d.ts.map +1 -1
  44. package/lib/Agent/coding/glob.js +1 -2
  45. package/lib/Agent/coding/glob.js.map +1 -1
  46. package/lib/Agent/coding/grep.d.ts +2 -4
  47. package/lib/Agent/coding/grep.d.ts.map +1 -1
  48. package/lib/Agent/coding/grep.js +1 -2
  49. package/lib/Agent/coding/grep.js.map +1 -1
  50. package/lib/Agent/coding/read.d.ts +1 -1
  51. package/lib/Agent/coding/write.d.ts +1 -1
  52. package/lib/Agent/util/ripgrep.d.ts +2 -2
  53. package/lib/AlchemyContext.d.ts +13 -0
  54. package/lib/AlchemyContext.d.ts.map +1 -0
  55. package/lib/AlchemyContext.js +18 -0
  56. package/lib/AlchemyContext.js.map +1 -0
  57. package/lib/Auth/AuthProvider.js +1 -1
  58. package/lib/Auth/AuthProvider.js.map +1 -1
  59. package/lib/Auth/Profile.d.ts +1 -1
  60. package/lib/Auth/index.d.ts +3 -1
  61. package/lib/Auth/index.d.ts.map +1 -1
  62. package/lib/Auth/index.js +3 -0
  63. package/lib/Auth/index.js.map +1 -1
  64. package/lib/Build/Command.d.ts +1 -1
  65. package/lib/Build/Memo.d.ts +1 -1
  66. package/lib/Bundle/Docker.d.ts +3 -3
  67. package/lib/Bundle/TempRoot.d.ts +3 -3
  68. package/lib/Cli/InkCLI.d.ts +1 -0
  69. package/lib/Cli/InkCLI.d.ts.map +1 -1
  70. package/lib/Cli/InkCLI.js +5 -0
  71. package/lib/Cli/InkCLI.js.map +1 -1
  72. package/lib/Cli/LoggingCli.d.ts +4 -0
  73. package/lib/Cli/LoggingCli.d.ts.map +1 -0
  74. package/lib/{Test/TestCli.js → Cli/LoggingCli.js} +3 -3
  75. package/lib/Cli/LoggingCli.js.map +1 -0
  76. package/lib/Cloudflare/CloudflareEnvironment.d.ts +1 -1
  77. package/lib/Cloudflare/Container/ContainerApplication.d.ts +2 -2
  78. package/lib/Cloudflare/Container/ContainerApplication.d.ts.map +1 -1
  79. package/lib/Cloudflare/Container/ContainerApplication.js +7 -2
  80. package/lib/Cloudflare/Container/ContainerApplication.js.map +1 -1
  81. package/lib/Cloudflare/EdgeSession.d.ts +53 -0
  82. package/lib/Cloudflare/EdgeSession.d.ts.map +1 -0
  83. package/lib/Cloudflare/EdgeSession.js +78 -0
  84. package/lib/Cloudflare/EdgeSession.js.map +1 -0
  85. package/lib/Cloudflare/Providers.d.ts +1 -1
  86. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  87. package/lib/Cloudflare/StateStore/Api.d.ts +62 -0
  88. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -0
  89. package/lib/Cloudflare/StateStore/Api.js +101 -0
  90. package/lib/Cloudflare/StateStore/Api.js.map +1 -0
  91. package/lib/Cloudflare/StateStore/State.d.ts +39 -0
  92. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -0
  93. package/lib/Cloudflare/StateStore/State.js +217 -0
  94. package/lib/Cloudflare/StateStore/State.js.map +1 -0
  95. package/lib/Cloudflare/StateStore/Store.d.ts +82 -0
  96. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -0
  97. package/lib/Cloudflare/StateStore/Store.js +189 -0
  98. package/lib/Cloudflare/StateStore/Store.js.map +1 -0
  99. package/lib/Cloudflare/StateStore/Token.d.ts +41 -0
  100. package/lib/Cloudflare/StateStore/Token.d.ts.map +1 -0
  101. package/lib/Cloudflare/StateStore/Token.js +57 -0
  102. package/lib/Cloudflare/StateStore/Token.js.map +1 -0
  103. package/lib/Cloudflare/StateStore/index.d.ts +2 -0
  104. package/lib/Cloudflare/StateStore/index.d.ts.map +1 -0
  105. package/lib/Cloudflare/StateStore/index.js +2 -0
  106. package/lib/Cloudflare/StateStore/index.js.map +1 -0
  107. package/lib/Cloudflare/Workers/Assets.d.ts +2 -2
  108. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts +2 -2
  109. package/lib/Cloudflare/Workers/DurableObjectNamespace.d.ts.map +1 -1
  110. package/lib/Cloudflare/Workers/DurableObjectNamespace.js +0 -1
  111. package/lib/Cloudflare/Workers/DurableObjectNamespace.js.map +1 -1
  112. package/lib/Cloudflare/Workers/HttpServer.d.ts +1 -1
  113. package/lib/Cloudflare/Workers/HttpServer.d.ts.map +1 -1
  114. package/lib/Cloudflare/Workers/HttpServer.js.map +1 -1
  115. package/lib/Cloudflare/Workers/Worker.d.ts +5 -5
  116. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  117. package/lib/Cloudflare/Workers/Worker.js +0 -1
  118. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  119. package/lib/Cloudflare/index.d.ts +3 -1
  120. package/lib/Cloudflare/index.d.ts.map +1 -1
  121. package/lib/Cloudflare/index.js +3 -1
  122. package/lib/Cloudflare/index.js.map +1 -1
  123. package/lib/Deploy.d.ts +10 -0
  124. package/lib/Deploy.d.ts.map +1 -0
  125. package/lib/Deploy.js +12 -0
  126. package/lib/Deploy.js.map +1 -0
  127. package/lib/Destroy.d.ts +8 -3
  128. package/lib/Destroy.d.ts.map +1 -1
  129. package/lib/Destroy.js +8 -8
  130. package/lib/Destroy.js.map +1 -1
  131. package/lib/Http.d.ts +1 -1
  132. package/lib/Http.d.ts.map +1 -1
  133. package/lib/Http.js +7 -3
  134. package/lib/Http.js.map +1 -1
  135. package/lib/Kubernetes/client.d.ts +10 -10
  136. package/lib/Output.d.ts.map +1 -1
  137. package/lib/Output.js.map +1 -1
  138. package/lib/Platform.d.ts +2 -2
  139. package/lib/Platform.d.ts.map +1 -1
  140. package/lib/Platform.js.map +1 -1
  141. package/lib/Provider.d.ts.map +1 -1
  142. package/lib/Provider.js +0 -1
  143. package/lib/Provider.js.map +1 -1
  144. package/lib/Stack.d.ts +23 -12
  145. package/lib/Stack.d.ts.map +1 -1
  146. package/lib/Stack.js +32 -14
  147. package/lib/Stack.js.map +1 -1
  148. package/lib/State/HttpStateApi.d.ts +96 -0
  149. package/lib/State/HttpStateApi.d.ts.map +1 -0
  150. package/lib/State/HttpStateApi.js +96 -0
  151. package/lib/State/HttpStateApi.js.map +1 -0
  152. package/lib/State/HttpStateStore.d.ts +10 -0
  153. package/lib/State/HttpStateStore.d.ts.map +1 -0
  154. package/lib/State/HttpStateStore.js +102 -0
  155. package/lib/State/HttpStateStore.js.map +1 -0
  156. package/lib/State/InMemoryState.d.ts +2 -30
  157. package/lib/State/InMemoryState.d.ts.map +1 -1
  158. package/lib/State/InMemoryState.js +23 -18
  159. package/lib/State/InMemoryState.js.map +1 -1
  160. package/lib/State/LocalState.d.ts +4 -2
  161. package/lib/State/LocalState.d.ts.map +1 -1
  162. package/lib/State/LocalState.js +9 -41
  163. package/lib/State/LocalState.js.map +1 -1
  164. package/lib/State/State.d.ts +11 -4
  165. package/lib/State/State.d.ts.map +1 -1
  166. package/lib/State/State.js +1 -1
  167. package/lib/State/State.js.map +1 -1
  168. package/lib/State/StateEncoding.d.ts +31 -0
  169. package/lib/State/StateEncoding.d.ts.map +1 -0
  170. package/lib/State/StateEncoding.js +82 -0
  171. package/lib/State/StateEncoding.js.map +1 -0
  172. package/lib/State/Sync.d.ts +23 -0
  173. package/lib/State/Sync.d.ts.map +1 -0
  174. package/lib/State/Sync.js +49 -0
  175. package/lib/State/Sync.js.map +1 -0
  176. package/lib/State/index.d.ts +4 -0
  177. package/lib/State/index.d.ts.map +1 -1
  178. package/lib/State/index.js +4 -0
  179. package/lib/State/index.js.map +1 -1
  180. package/lib/Test/Bun.d.ts +13 -17
  181. package/lib/Test/Bun.d.ts.map +1 -1
  182. package/lib/Test/Bun.js +16 -33
  183. package/lib/Test/Bun.js.map +1 -1
  184. package/lib/Test/TestState.d.ts +1 -2
  185. package/lib/Test/TestState.d.ts.map +1 -1
  186. package/lib/Test/TestState.js +5 -9
  187. package/lib/Test/TestState.js.map +1 -1
  188. package/lib/Test/Vitest.d.ts +3 -2
  189. package/lib/Test/Vitest.d.ts.map +1 -1
  190. package/lib/Test/Vitest.js +20 -15
  191. package/lib/Test/Vitest.js.map +1 -1
  192. package/lib/Test/index.d.ts +0 -1
  193. package/lib/Test/index.d.ts.map +1 -1
  194. package/lib/Test/index.js +0 -1
  195. package/lib/Test/index.js.map +1 -1
  196. package/lib/Util/FileLogger.d.ts +2 -2
  197. package/lib/Util/FileLogger.d.ts.map +1 -1
  198. package/lib/Util/FileLogger.js +2 -2
  199. package/lib/Util/FileLogger.js.map +1 -1
  200. package/lib/cli/index.d.ts +10 -1
  201. package/lib/cli/index.d.ts.map +1 -1
  202. package/lib/cli/index.js +9 -4
  203. package/lib/cli/index.js.map +1 -1
  204. package/lib/index.d.ts +2 -1
  205. package/lib/index.d.ts.map +1 -1
  206. package/lib/index.js +2 -1
  207. package/lib/index.js.map +1 -1
  208. package/package.json +26 -11
  209. package/src/AWS/ECS/Task.ts +4 -4
  210. package/src/AWS/Lambda/Function.ts +2 -3
  211. package/src/AWS/SQS/QueueSink.ts +1 -2
  212. package/src/Agent/coding/bash.ts +2 -3
  213. package/src/Agent/coding/glob.ts +1 -2
  214. package/src/Agent/coding/grep.ts +1 -2
  215. package/src/AlchemyContext.ts +27 -0
  216. package/src/Auth/AuthProvider.ts +1 -1
  217. package/src/Auth/index.ts +3 -1
  218. package/src/Cli/InkCLI.tsx +10 -0
  219. package/src/{Test/TestCli.ts → Cli/LoggingCli.ts} +2 -2
  220. package/src/Cloudflare/Container/ContainerApplication.ts +8 -2
  221. package/src/Cloudflare/EdgeSession.ts +144 -0
  222. package/src/Cloudflare/StateStore/Api.ts +157 -0
  223. package/src/Cloudflare/StateStore/State.ts +318 -0
  224. package/src/Cloudflare/StateStore/Store.ts +288 -0
  225. package/src/Cloudflare/StateStore/Token.ts +62 -0
  226. package/src/Cloudflare/StateStore/index.ts +1 -0
  227. package/src/Cloudflare/Workers/DurableObjectNamespace.ts +1 -6
  228. package/src/Cloudflare/Workers/HttpServer.ts +1 -1
  229. package/src/Cloudflare/Workers/Worker.ts +7 -7
  230. package/src/Cloudflare/index.ts +3 -1
  231. package/src/Deploy.ts +25 -0
  232. package/src/Destroy.ts +25 -13
  233. package/src/Http.ts +31 -16
  234. package/src/Output.ts +0 -2
  235. package/src/Platform.ts +8 -2
  236. package/src/Provider.ts +0 -2
  237. package/src/Stack.ts +122 -47
  238. package/src/State/HttpStateApi.ts +155 -0
  239. package/src/State/HttpStateStore.ts +155 -0
  240. package/src/State/InMemoryState.ts +15 -17
  241. package/src/State/LocalState.ts +13 -45
  242. package/src/State/State.ts +13 -4
  243. package/src/State/StateEncoding.ts +83 -0
  244. package/src/State/Sync.ts +89 -0
  245. package/src/State/index.ts +4 -0
  246. package/src/Test/Bun.ts +29 -87
  247. package/src/Test/TestState.ts +6 -8
  248. package/src/Test/Vitest.ts +32 -23
  249. package/src/Test/index.ts +0 -1
  250. package/src/Util/FileLogger.ts +2 -2
  251. package/src/index.ts +2 -1
  252. package/lib/Config.d.ts +0 -20
  253. package/lib/Config.d.ts.map +0 -1
  254. package/lib/Config.js +0 -22
  255. package/lib/Config.js.map +0 -1
  256. package/lib/Observability.d.ts +0 -2
  257. package/lib/Observability.d.ts.map +0 -1
  258. package/lib/Observability.js +0 -1
  259. package/lib/Observability.js.map +0 -1
  260. package/lib/Test/TestCli.d.ts +0 -4
  261. package/lib/Test/TestCli.d.ts.map +0 -1
  262. package/lib/Test/TestCli.js.map +0 -1
  263. package/src/Config.ts +0 -31
  264. package/src/Observability.ts +0 -0
package/src/Stack.ts CHANGED
@@ -1,20 +1,29 @@
1
+ import { ConfigProvider } from "effect/ConfigProvider";
1
2
  import * as Context from "effect/Context";
2
3
  import * as Effect from "effect/Effect";
3
4
  import { FileSystem } from "effect/FileSystem";
4
5
  import * as Layer from "effect/Layer";
6
+ import * as Logger from "effect/Logger";
5
7
  import * as Option from "effect/Option";
6
8
  import { Path } from "effect/Path";
7
9
  import type { Scope } from "effect/Scope";
10
+ import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
8
11
  import type { HttpClient } from "effect/unstable/http/HttpClient";
9
12
  import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
10
- import type { AuthProviders } from "./Auth/AuthProvider.ts";
11
- import { DotAlchemy } from "./Config.ts";
13
+ import { AlchemyContext, AlchemyContextLive } from "./AlchemyContext.ts";
14
+ import { provideFreshArtifactStore } from "./Artifacts.ts";
15
+ import { AuthProviders } from "./Auth/AuthProvider.ts";
16
+ import { inkCLIAutoApprove } from "./Cli/InkCLI.tsx";
12
17
  import type { Input, InputProps } from "./Input.ts";
13
18
  import * as Output from "./Output.ts";
14
19
  import { ref } from "./Ref.ts";
15
20
  import type { ResourceBinding, ResourceLike } from "./Resource.ts";
16
21
  import { Stage } from "./Stage.ts";
22
+ import type { State } from "./State/State.ts";
23
+ import { loadConfigProvider } from "./Util/ConfigProvider.ts";
17
24
  import { taggedFunction } from "./Util/effect.ts";
25
+ import { fileLogger } from "./Util/FileLogger.ts";
26
+ import { PlatformServices } from "./Util/PlatformServices.ts";
18
27
 
19
28
  export type StackServices =
20
29
  | Stack
@@ -22,16 +31,33 @@ export type StackServices =
22
31
  | Scope
23
32
  | FileSystem
24
33
  | Path
25
- | DotAlchemy
34
+ | AlchemyContext
26
35
  | HttpClient
27
36
  | ChildProcessSpawner
28
37
  | AuthProviders;
29
38
 
39
+ export type StackEffect<A, Err = never, Req = never> = Effect.Effect<
40
+ A,
41
+ Err,
42
+ | PlatformServices
43
+ | HttpClient
44
+ | Scope
45
+ | AuthProviders
46
+ | AlchemyContext
47
+ | State
48
+ | Req
49
+ >;
50
+
30
51
  export type Stack = Context.ServiceClass.Shape<
31
52
  "Stack",
32
53
  Omit<StackSpec, "output">
33
54
  >;
34
55
 
56
+ export interface StackProps<Req> {
57
+ providers: Layer.Layer<NoInfer<Req>, never, StackServices>;
58
+ state: Layer.Layer<State, never, StackServices>;
59
+ }
60
+
35
61
  export const Stack: Context.ServiceClass<
36
62
  Stack,
37
63
  "Stack",
@@ -50,9 +76,7 @@ export const Stack: Context.ServiceClass<
50
76
  <Self>(): {
51
77
  <A, Req>(
52
78
  stackName: string,
53
- options: {
54
- providers: Layer.Layer<NoInfer<Req>, never, StackServices>;
55
- },
79
+ options: StackProps<NoInfer<Req>>,
56
80
  eff: Effect.Effect<A, never, Req>,
57
81
  ): Effect.Effect<Self> & {
58
82
  new (_: never): A extends object ? A : {};
@@ -75,9 +99,7 @@ export const Stack: Context.ServiceClass<
75
99
  };
76
100
  <A, Req>(
77
101
  stackName: string,
78
- options: {
79
- providers: Layer.Layer<NoInfer<Req>, never, StackServices>;
80
- },
102
+ options: StackProps<NoInfer<Req>>,
81
103
  eff: Effect.Effect<A, never, Req | StackServices>,
82
104
  ): Effect.Effect<CompiledStack<A>>;
83
105
  } = Object.assign(
@@ -87,23 +109,29 @@ export const Stack: Context.ServiceClass<
87
109
  stackName: string,
88
110
  options: {
89
111
  providers: Layer.Layer<NoInfer<Req>, never, StackServices>;
112
+ state: Layer.Layer<State, never, StackServices>;
90
113
  },
91
114
  eff: Effect.Effect<A, never, Req>,
92
115
  ) =>
93
- eff.pipe(make(stackName, options.providers), (eff) =>
94
- Object.assign(eff, {
95
- stage: new Proxy(
96
- {},
97
- {
98
- get: (_, stage: string) =>
99
- ref({
100
- stack: stackName,
101
- stage,
102
- id: stackName,
103
- }),
104
- },
105
- ),
116
+ eff.pipe(
117
+ make({
118
+ name: stackName,
119
+ ...options,
106
120
  }),
121
+ (eff) =>
122
+ Object.assign(eff, {
123
+ stage: new Proxy(
124
+ {},
125
+ {
126
+ get: (_, stage: string) =>
127
+ ref({
128
+ stack: stackName,
129
+ stage,
130
+ id: stackName,
131
+ }),
132
+ },
133
+ ),
134
+ }),
107
135
  ),
108
136
  ),
109
137
  ) as any;
@@ -130,38 +158,42 @@ export interface CompiledStack<
130
158
 
131
159
  export const StackName = Stack.use((stack) => Effect.succeed(stack.name));
132
160
 
161
+ export interface MakeStackProps<ROut = never> {
162
+ name: string;
163
+ providers: Layer.Layer<ROut, never, StackServices>;
164
+ state: Layer.Layer<State, never, StackServices>;
165
+ /** @internal */
166
+ stack?: StackSpec;
167
+ }
168
+
133
169
  export const make =
134
- <ROut = never>(
135
- name: string,
136
- providers: Layer.Layer<ROut, never, StackServices>,
137
- /** @internal */
138
- stack?: StackSpec,
139
- ) =>
170
+ <ROut = never>(options: MakeStackProps<ROut>) =>
140
171
  <A, Err = never, Req extends ROut | StackServices = never>(
141
172
  effect: Effect.Effect<A, Err, Req>,
142
173
  ) =>
143
174
  Effect.scope.pipe(
144
- Effect.flatMap((scope) => {
145
- const stackLayer = Layer.effect(
146
- Stack,
147
- Stage.asEffect().pipe(
148
- Effect.map(
149
- (stage) =>
150
- stack ?? {
151
- name,
152
- stage,
153
- resources: {},
154
- bindings: {},
155
- },
175
+ Effect.flatMap((scope) =>
176
+ options.providers.pipe(
177
+ Layer.provideMerge(options.state),
178
+ Layer.provideMerge(
179
+ Layer.effect(
180
+ Stack,
181
+ Stage.asEffect().pipe(
182
+ Effect.map(
183
+ (stage) =>
184
+ options.stack ?? {
185
+ name: options.name,
186
+ stage,
187
+ resources: {},
188
+ bindings: {},
189
+ },
190
+ ),
191
+ ),
156
192
  ),
157
- Effect.tap(Effect.logInfo),
158
193
  ),
159
- );
160
- return providers.pipe(
161
- Layer.provideMerge(stackLayer),
162
194
  Layer.buildWithScope(scope),
163
- );
164
- }),
195
+ ),
196
+ ),
165
197
  Effect.flatMap((context) =>
166
198
  Effect.all([
167
199
  effect,
@@ -175,7 +207,7 @@ export const make =
175
207
  > => ({
176
208
  ...stack,
177
209
  output,
178
- services,
210
+ services: Context.merge(services, context),
179
211
  }),
180
212
  ),
181
213
  Effect.provideContext(context),
@@ -186,3 +218,46 @@ export const make =
186
218
  export const CurrentStack = Effect.serviceOption(Stack)
187
219
  .asEffect()
188
220
  .pipe(Effect.map(Option.getOrUndefined));
221
+
222
+ const platform = Layer.mergeAll(
223
+ PlatformServices,
224
+ FetchHttpClient.layer,
225
+ Logger.layer([fileLogger("out")]),
226
+ );
227
+ // override alchemy state store, CLI/reporting, state, and Config
228
+ const alchemy = Layer.mergeAll(
229
+ // CLI.inkCLI(),
230
+ // optional
231
+ AlchemyContextLive,
232
+ );
233
+
234
+ export const evalStack = <A, B, Err, Req>(
235
+ effect: StackEffect<CompiledStack<A>, Stage | AlchemyContext>,
236
+ fn: (stack: CompiledStack<A>) => Effect.Effect<B, Err, Req>,
237
+ options: {
238
+ stage: string;
239
+ },
240
+ ) =>
241
+ Effect.gen(function* () {
242
+ const stack = yield* effect;
243
+ const configProvider = yield* loadConfigProvider(Option.none());
244
+
245
+ return yield* fn(stack).pipe(
246
+ provideFreshArtifactStore,
247
+ Effect.provide(stack.services),
248
+ Effect.provide(Layer.succeed(ConfigProvider, configProvider)),
249
+ );
250
+ }).pipe(
251
+ Effect.provide(
252
+ Layer.effect(
253
+ AuthProviders,
254
+ Effect.serviceOption(AuthProviders).pipe(
255
+ Effect.map(Option.getOrElse(() => ({}))),
256
+ ),
257
+ ),
258
+ ),
259
+ Effect.provide(Layer.succeed(Stage, options.stage)),
260
+ Effect.provide(inkCLIAutoApprove()),
261
+ Effect.provide(Layer.provideMerge(alchemy, platform)),
262
+ Effect.scoped,
263
+ );
@@ -0,0 +1,155 @@
1
+ import * as Context from "effect/Context";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Layer from "effect/Layer";
4
+ import * as Redacted from "effect/Redacted";
5
+ import * as Schema from "effect/Schema";
6
+ import * as HttpApi from "effect/unstable/httpapi/HttpApi";
7
+ import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
8
+ import * as HttpApiError from "effect/unstable/httpapi/HttpApiError";
9
+ import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
10
+ import * as HttpApiMiddleware from "effect/unstable/httpapi/HttpApiMiddleware";
11
+ import * as HttpApiSchema from "effect/unstable/httpapi/HttpApiSchema";
12
+ import * as HttpApiSecurity from "effect/unstable/httpapi/HttpApiSecurity";
13
+
14
+ export const ResourceStateSchema = Schema.Any;
15
+
16
+ export class BearerTokenValidator extends Context.Service<
17
+ BearerTokenValidator,
18
+ {
19
+ readonly validate: (
20
+ token: string,
21
+ ) => Effect.Effect<void, HttpApiError.Unauthorized>;
22
+ }
23
+ >()("alchemy/State/BearerTokenValidator") {}
24
+
25
+ export class StateAuth extends HttpApiMiddleware.Service<
26
+ StateAuth,
27
+ { requires: BearerTokenValidator }
28
+ >()("alchemy/State/StateAuth", {
29
+ security: {
30
+ bearer: HttpApiSecurity.bearer,
31
+ },
32
+ error: HttpApiError.UnauthorizedNoContent,
33
+ }) {}
34
+
35
+ export const StateAuthLive: Layer.Layer<
36
+ StateAuth,
37
+ never,
38
+ BearerTokenValidator
39
+ > = Layer.effect(
40
+ StateAuth,
41
+ Effect.gen(function* () {
42
+ const validator = yield* BearerTokenValidator;
43
+ return {
44
+ bearer: (httpEffect, { credential }) =>
45
+ validator
46
+ .validate(Redacted.value(credential))
47
+ .pipe(Effect.flatMap(() => httpEffect)),
48
+ };
49
+ }),
50
+ );
51
+
52
+ /** `stack` path segment for nested REST resources. */
53
+ const StackParams = Schema.Struct({
54
+ stack: Schema.String,
55
+ });
56
+
57
+ /** Optional stage selector for stack deletion. */
58
+ const OptionalStageQuery = Schema.Struct({
59
+ stage: Schema.optional(Schema.String),
60
+ });
61
+
62
+ /** `(stack, stage)` path segments shared by stage-scoped endpoints. */
63
+ const StackStage = Schema.Struct({
64
+ stack: Schema.String,
65
+ stage: Schema.String,
66
+ });
67
+
68
+ /** `(stack, stage, fqn)` path segments for a single resource. */
69
+ const ResourceKey = Schema.Struct({
70
+ stack: Schema.String,
71
+ stage: Schema.String,
72
+ fqn: Schema.String,
73
+ });
74
+
75
+ export const ListStacks = HttpApiEndpoint.get("listStacks", "/state/stacks", {
76
+ success: Schema.Array(Schema.String),
77
+ });
78
+
79
+ export const ListStages = HttpApiEndpoint.get(
80
+ "listStages",
81
+ "/state/stacks/:stack/stages",
82
+ {
83
+ params: StackParams,
84
+ success: Schema.Array(Schema.String),
85
+ },
86
+ );
87
+
88
+ export const ListResources = HttpApiEndpoint.get(
89
+ "listResources",
90
+ "/state/stacks/:stack/stages/:stage/resources",
91
+ {
92
+ params: StackStage,
93
+ success: Schema.Array(Schema.String),
94
+ },
95
+ );
96
+
97
+ export const GetState = HttpApiEndpoint.get(
98
+ "getState",
99
+ "/state/stacks/:stack/stages/:stage/resources/:fqn",
100
+ {
101
+ params: ResourceKey,
102
+ success: Schema.UndefinedOr(ResourceStateSchema),
103
+ },
104
+ );
105
+
106
+ export const SetState = HttpApiEndpoint.put(
107
+ "setState",
108
+ "/state/stacks/:stack/stages/:stage/resources/:fqn",
109
+ {
110
+ params: ResourceKey,
111
+ payload: ResourceStateSchema,
112
+ success: ResourceStateSchema,
113
+ },
114
+ );
115
+
116
+ export const DeleteState = HttpApiEndpoint.delete(
117
+ "deleteState",
118
+ "/state/stacks/:stack/stages/:stage/resources/:fqn",
119
+ {
120
+ params: ResourceKey,
121
+ success: HttpApiSchema.NoContent,
122
+ },
123
+ );
124
+
125
+ export const DeleteStack = HttpApiEndpoint.delete(
126
+ "deleteStack",
127
+ "/state/stacks/:stack",
128
+ {
129
+ params: StackParams,
130
+ query: OptionalStageQuery,
131
+ success: HttpApiSchema.NoContent,
132
+ },
133
+ );
134
+
135
+ export const GetReplacedResources = HttpApiEndpoint.get(
136
+ "getReplacedResources",
137
+ "/state/stacks/:stack/stages/:stage/replaced-resources",
138
+ {
139
+ params: StackStage,
140
+ success: Schema.Array(ResourceStateSchema),
141
+ },
142
+ );
143
+
144
+ export class StateGroup extends HttpApiGroup.make("state")
145
+ .add(ListStacks)
146
+ .add(ListStages)
147
+ .add(ListResources)
148
+ .add(GetState)
149
+ .add(SetState)
150
+ .add(DeleteState)
151
+ .add(GetReplacedResources)
152
+ .add(DeleteStack)
153
+ .middleware(StateAuth) {}
154
+
155
+ export class StateApi extends HttpApi.make("alchemy-state").add(StateGroup) {}
@@ -0,0 +1,155 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schedule from "effect/Schedule";
3
+ import * as HttpClient from "effect/unstable/http/HttpClient";
4
+ import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
5
+ import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";
6
+ import { StateApi } from "./HttpStateApi.ts";
7
+
8
+ import type { ReplacedResourceState, ResourceState } from "./ResourceState.ts";
9
+ import { StateStoreError, type StateService } from "./State.ts";
10
+ import { encodeState, reviveStateRecursive } from "./StateEncoding.ts";
11
+
12
+ export interface HttpStateStoreProps {
13
+ url: string;
14
+ /** Bearer token used to authenticate every request. */
15
+ authToken: string;
16
+ }
17
+
18
+ export const makeHttpStateStore = ({ url, authToken }: HttpStateStoreProps) =>
19
+ Effect.gen(function* () {
20
+ const apiClient = yield* HttpApiClient.make(StateApi, {
21
+ baseUrl: url,
22
+ transformClient: HttpClient.mapRequest(
23
+ HttpClientRequest.bearerToken(authToken),
24
+ ),
25
+ });
26
+ const state = apiClient.state;
27
+
28
+ const service: StateService = {
29
+ listStacks: () =>
30
+ state.listStacks().pipe(
31
+ Effect.map((stacks) => [...stacks]),
32
+ mapStateStoreError,
33
+ ),
34
+ listStages: (stack) =>
35
+ state.listStages({ params: { stack } }).pipe(mapStateStoreError),
36
+ list: (request) =>
37
+ state.listResources({ params: request }).pipe(mapStateStoreError),
38
+ get: (request) =>
39
+ state
40
+ .getState({
41
+ params: {
42
+ stack: request.stack,
43
+ stage: request.stage,
44
+ fqn: encodeURIComponent(request.fqn),
45
+ },
46
+ })
47
+ .pipe(
48
+ Effect.map((s) =>
49
+ s == null
50
+ ? undefined
51
+ : (reviveStateRecursive(s) as ResourceState),
52
+ ),
53
+ mapStateStoreError,
54
+ ),
55
+ getReplacedResources: (request) =>
56
+ state.getReplacedResources({ params: request }).pipe(
57
+ Effect.map((resources) =>
58
+ resources.map(
59
+ (s) => reviveStateRecursive(s) as ReplacedResourceState,
60
+ ),
61
+ ),
62
+ mapStateStoreError,
63
+ ),
64
+ set: <V extends ResourceState>(request: {
65
+ stack: string;
66
+ stage: string;
67
+ fqn: string;
68
+ value: V;
69
+ }) =>
70
+ state
71
+ .setState({
72
+ params: {
73
+ stack: request.stack,
74
+ stage: request.stage,
75
+ fqn: encodeURIComponent(request.fqn),
76
+ },
77
+ payload: encodeState(request.value),
78
+ })
79
+ .pipe(
80
+ // Server echoes the stored value, but the client already
81
+ // has the canonical object (including any Redacted<T>
82
+ // instances); returning the input avoids a lossy round-trip.
83
+ Effect.map(() => request.value),
84
+ mapStateStoreError,
85
+ ),
86
+ delete: (request) =>
87
+ state
88
+ .deleteState({
89
+ params: {
90
+ stack: request.stack,
91
+ stage: request.stage,
92
+ fqn: encodeURIComponent(request.fqn),
93
+ },
94
+ })
95
+ .pipe(Effect.asVoid, mapStateStoreError),
96
+ deleteStack: (request) =>
97
+ state
98
+ .deleteStack({
99
+ params: { stack: request.stack },
100
+ query: request.stage === undefined ? {} : { stage: request.stage },
101
+ })
102
+ .pipe(Effect.asVoid, mapStateStoreError),
103
+ };
104
+ return service;
105
+ });
106
+
107
+ /**
108
+ * Predicate over an `HttpClientError`-shaped failure that returns `true`
109
+ * for failures we expect to clear up on their own.
110
+ *
111
+ * We retry:
112
+ * - all transport-level errors (no `response` — DNS, TCP reset, TLS,
113
+ * abort, etc.)
114
+ * - 408 (request timeout) and 429 (rate limit)
115
+ * - 404, which is normal in the seconds after a worker is first
116
+ * deployed and the route hasn't propagated yet
117
+ * - every 5xx
118
+ *
119
+ * Anything else (400/401/403/etc.) is a real client-side problem and
120
+ * shouldn't be hidden behind retries.
121
+ */
122
+ const isTransient = (e: any): boolean => {
123
+ if (e?._tag !== "HttpClientError") return false;
124
+ const status: number | undefined = e.response?.status;
125
+ if (status == null) return true;
126
+ if (status === 404 || status === 408 || status === 429) return true;
127
+ return status >= 500 && status < 600;
128
+ };
129
+
130
+ const retryTransient = <A, Err, Req>(eff: Effect.Effect<A, Err, Req>) =>
131
+ Effect.retry(eff, {
132
+ while: isTransient,
133
+ // Exponential backoff capped at 2s, no attempt limit. The state
134
+ // store is on the deploy critical path; we'd rather block than
135
+ // surface a transient blip as a hard failure (which historically
136
+ // stranded `syncState` mid-flight, leaving the remote store
137
+ // partially populated).
138
+ schedule: Schedule.exponential(100).pipe(
139
+ Schedule.either(Schedule.spaced("2 seconds")),
140
+ ),
141
+ });
142
+
143
+ /** Collapse any client failure into a {@link StateStoreError}. */
144
+ const mapStateStoreError = <A, E, R>(eff: Effect.Effect<A, E, R>) =>
145
+ eff.pipe(
146
+ retryTransient,
147
+ Effect.catch((e: E) =>
148
+ Effect.fail(
149
+ new StateStoreError({
150
+ message: e instanceof Error ? e.message : String(e),
151
+ cause: e instanceof Error ? e : undefined,
152
+ }),
153
+ ),
154
+ ),
155
+ ) as Effect.Effect<A, StateStoreError, R>;
@@ -1,32 +1,23 @@
1
1
  import * as Effect from "effect/Effect";
2
2
  import * as Layer from "effect/Layer";
3
3
  import type { ResourceState } from "./ResourceState.ts";
4
- import { State, type StateService } from "./State.ts";
4
+ import { State } from "./State.ts";
5
5
 
6
6
  type StackId = string;
7
7
  type StageId = string;
8
8
  type Fqn = string;
9
9
 
10
- export const InMemory = (
10
+ export const inMemoryState = (
11
11
  initialState: Record<
12
12
  StackId,
13
13
  Record<StageId, Record<Fqn, ResourceState>>
14
14
  > = {},
15
- ) =>
16
- Layer.succeed(State, InMemoryService(initialState)) as Layer.Layer<
17
- State,
18
- never,
19
- never
20
- >;
15
+ ) => Layer.succeed(State, InMemoryService(initialState));
21
16
 
22
17
  export const InMemoryService = (
23
- initialState: Record<
24
- StackId,
25
- Record<StageId, Record<Fqn, ResourceState>>
26
- > = {},
27
- ) => {
28
- const state = initialState;
29
- return {
18
+ state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
19
+ ) =>
20
+ State.of({
30
21
  listStacks: () => Effect.succeed(Array.from(Object.keys(state))),
31
22
  listStages: (stack: string) =>
32
23
  Effect.succeed(
@@ -78,9 +69,16 @@ export const InMemoryService = (
78
69
  stage: string;
79
70
  fqn: string;
80
71
  }) => Effect.succeed(delete state[stack]?.[stage]?.[fqn]),
72
+ deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
73
+ Effect.sync(() => {
74
+ if (stage === undefined) {
75
+ delete state[stack];
76
+ } else {
77
+ delete state[stack]?.[stage];
78
+ }
79
+ }),
81
80
  list: ({ stack, stage }: { stack: string; stage: string }) =>
82
81
  Effect.succeed(
83
82
  Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
84
83
  ),
85
- } satisfies StateService;
86
- };
84
+ });
@@ -3,53 +3,13 @@ import * as FileSystem from "effect/FileSystem";
3
3
  import * as Layer from "effect/Layer";
4
4
  import * as Path from "effect/Path";
5
5
  import type { PlatformError } from "effect/PlatformError";
6
- import * as Redacted from "effect/Redacted";
7
6
  import { decodeFqn, encodeFqn } from "../FQN.ts";
8
- import { isResource } from "../Resource.ts";
9
7
  import { State, StateStoreError, type StateService } from "./State.ts";
8
+ import { encodeState, reviveState } from "./StateEncoding.ts";
10
9
 
11
- const REDACTED_MARKER = "__redacted__";
10
+ export const localState = () => Layer.effect(State, makeLocalState());
12
11
 
13
- const encodeState = (value: unknown): unknown => {
14
- if (value === null || value === undefined) return value;
15
- if (Redacted.isRedacted(value)) {
16
- return {
17
- [REDACTED_MARKER]: encodeState(Redacted.value(value)),
18
- };
19
- }
20
- if (isResource(value)) {
21
- return {
22
- id: value.LogicalId,
23
- type: value.Type,
24
- props: encodeState(value.Props),
25
- attr: encodeState(value.Attributes),
26
- };
27
- }
28
- if (Array.isArray(value)) return value.map(encodeState);
29
- if (typeof value === "object") {
30
- const result: Record<string, unknown> = {};
31
- for (const [k, v] of Object.entries(value)) {
32
- result[k] = encodeState(v);
33
- }
34
- return result;
35
- }
36
- return value;
37
- };
38
-
39
- const reviveState = (_key: string, value: unknown): unknown => {
40
- if (
41
- value !== null &&
42
- typeof value === "object" &&
43
- !Array.isArray(value) &&
44
- REDACTED_MARKER in value
45
- ) {
46
- return Redacted.make((value as Record<string, unknown>)[REDACTED_MARKER]);
47
- }
48
- return value;
49
- };
50
-
51
- export const LocalState = Layer.effect(
52
- State,
12
+ export const makeLocalState = () =>
53
13
  Effect.gen(function* () {
54
14
  const fs = yield* FileSystem.FileSystem;
55
15
  const path = yield* Path.Path;
@@ -132,6 +92,15 @@ export const LocalState = Layer.effect(
132
92
  Effect.map(() => request.value),
133
93
  ),
134
94
  delete: (request) => fs.remove(resource(request)).pipe(recover),
95
+ deleteStack: ({ stack, stage }) =>
96
+ fs
97
+ .remove(
98
+ stage === undefined
99
+ ? path.join(stateDir, stack)
100
+ : stageDir({ stack, stage }),
101
+ { recursive: true },
102
+ )
103
+ .pipe(recover),
135
104
  list: (request) =>
136
105
  fs.readDirectory(stageDir(request)).pipe(
137
106
  recover,
@@ -143,5 +112,4 @@ export const LocalState = Layer.effect(
143
112
  ),
144
113
  };
145
114
  return state;
146
- }),
147
- );
115
+ });