@alchemy.run/boat 0.0.0 → 2.0.0-beta.87
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/LICENSE +201 -0
- package/README.md +66 -4
- package/lib/AiProxy.d.ts +89 -0
- package/lib/AiProxy.d.ts.map +1 -0
- package/lib/AiProxy.js +125 -0
- package/lib/AiProxy.js.map +1 -0
- package/lib/ApiKey.d.ts +105 -0
- package/lib/ApiKey.d.ts.map +1 -0
- package/lib/ApiKey.js +134 -0
- package/lib/ApiKey.js.map +1 -0
- package/lib/AuthProvider.d.ts +71 -0
- package/lib/AuthProvider.d.ts.map +1 -0
- package/lib/AuthProvider.js +322 -0
- package/lib/AuthProvider.js.map +1 -0
- package/lib/BoatBindings.d.ts +52 -0
- package/lib/BoatBindings.d.ts.map +1 -0
- package/lib/BoatBindings.js +104 -0
- package/lib/BoatBindings.js.map +1 -0
- package/lib/Bootstrap.d.ts +8 -0
- package/lib/Bootstrap.d.ts.map +1 -0
- package/lib/Bootstrap.js +28 -0
- package/lib/Bootstrap.js.map +1 -0
- package/lib/ConnectSandbox.d.ts +56 -0
- package/lib/ConnectSandbox.d.ts.map +1 -0
- package/lib/ConnectSandbox.js +3 -0
- package/lib/ConnectSandbox.js.map +1 -0
- package/lib/ConnectSandboxHttp.d.ts +21 -0
- package/lib/ConnectSandboxHttp.d.ts.map +1 -0
- package/lib/ConnectSandboxHttp.js +46 -0
- package/lib/ConnectSandboxHttp.js.map +1 -0
- package/lib/Contributions.d.ts +7 -0
- package/lib/Contributions.d.ts.map +1 -0
- package/lib/Contributions.js +6 -0
- package/lib/Contributions.js.map +1 -0
- package/lib/CreateSandbox.d.ts +134 -0
- package/lib/CreateSandbox.d.ts.map +1 -0
- package/lib/CreateSandbox.js +3 -0
- package/lib/CreateSandbox.js.map +1 -0
- package/lib/CreateSandboxHttp.d.ts +21 -0
- package/lib/CreateSandboxHttp.d.ts.map +1 -0
- package/lib/CreateSandboxHttp.js +93 -0
- package/lib/CreateSandboxHttp.js.map +1 -0
- package/lib/CredentialProfile.d.ts +157 -0
- package/lib/CredentialProfile.d.ts.map +1 -0
- package/lib/CredentialProfile.js +245 -0
- package/lib/CredentialProfile.js.map +1 -0
- package/lib/Credentials.d.ts +15 -0
- package/lib/Credentials.d.ts.map +1 -0
- package/lib/Credentials.js +23 -0
- package/lib/Credentials.js.map +1 -0
- package/lib/Environment.d.ts +219 -0
- package/lib/Environment.d.ts.map +1 -0
- package/lib/Environment.js +273 -0
- package/lib/Environment.js.map +1 -0
- package/lib/Errors.d.ts +50 -0
- package/lib/Errors.d.ts.map +1 -0
- package/lib/Errors.js +17 -0
- package/lib/Errors.js.map +1 -0
- package/lib/Providers.d.ts +33 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +47 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Sandbox.d.ts +205 -0
- package/lib/Sandbox.d.ts.map +1 -0
- package/lib/Sandbox.js +368 -0
- package/lib/Sandbox.js.map +1 -0
- package/lib/SandboxClient.d.ts +159 -0
- package/lib/SandboxClient.d.ts.map +1 -0
- package/lib/SandboxClient.js +1 -0
- package/lib/SandboxClient.js.map +1 -0
- package/lib/SandboxEvents.d.ts +111 -0
- package/lib/SandboxEvents.d.ts.map +1 -0
- package/lib/SandboxEvents.js +60 -0
- package/lib/SandboxEvents.js.map +1 -0
- package/lib/SandboxEventsLive.d.ts +13 -0
- package/lib/SandboxEventsLive.d.ts.map +1 -0
- package/lib/SandboxEventsLive.js +63 -0
- package/lib/SandboxEventsLive.js.map +1 -0
- package/lib/Snapshot.d.ts +123 -0
- package/lib/Snapshot.d.ts.map +1 -0
- package/lib/Snapshot.js +173 -0
- package/lib/Snapshot.js.map +1 -0
- package/lib/Webhook.d.ts +56 -0
- package/lib/Webhook.d.ts.map +1 -0
- package/lib/Webhook.js +98 -0
- package/lib/Webhook.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +19 -0
- package/lib/index.js.map +1 -0
- package/lib/internal/Declared.d.ts +34 -0
- package/lib/internal/Declared.d.ts.map +1 -0
- package/lib/internal/Declared.js +38 -0
- package/lib/internal/Declared.js.map +1 -0
- package/lib/internal/Org.d.ts +22 -0
- package/lib/internal/Org.d.ts.map +1 -0
- package/lib/internal/Org.js +23 -0
- package/lib/internal/Org.js.map +1 -0
- package/lib/internal/Ownership.d.ts +29 -0
- package/lib/internal/Ownership.d.ts.map +1 -0
- package/lib/internal/Ownership.js +50 -0
- package/lib/internal/Ownership.js.map +1 -0
- package/lib/internal/Program.d.ts +69 -0
- package/lib/internal/Program.d.ts.map +1 -0
- package/lib/internal/Program.js +178 -0
- package/lib/internal/Program.js.map +1 -0
- package/lib/internal/Ref.d.ts +14 -0
- package/lib/internal/Ref.d.ts.map +1 -0
- package/lib/internal/Ref.js +7 -0
- package/lib/internal/Ref.js.map +1 -0
- package/lib/internal/SandboxHandle.d.ts +17 -0
- package/lib/internal/SandboxHandle.d.ts.map +1 -0
- package/lib/internal/SandboxHandle.js +238 -0
- package/lib/internal/SandboxHandle.js.map +1 -0
- package/lib/internal/SavedSnapshot.d.ts +10 -0
- package/lib/internal/SavedSnapshot.d.ts.map +1 -0
- package/lib/internal/SavedSnapshot.js +22 -0
- package/lib/internal/SavedSnapshot.js.map +1 -0
- package/lib/internal/WebhookDelivery.d.ts +16 -0
- package/lib/internal/WebhookDelivery.d.ts.map +1 -0
- package/lib/internal/WebhookDelivery.js +109 -0
- package/lib/internal/WebhookDelivery.js.map +1 -0
- package/package.json +67 -3
- package/src/AiProxy.ts +171 -0
- package/src/ApiKey.ts +250 -0
- package/src/AuthProvider.ts +472 -0
- package/src/BoatBindings.ts +136 -0
- package/src/Bootstrap.ts +55 -0
- package/src/ConnectSandbox.ts +60 -0
- package/src/ConnectSandboxHttp.ts +59 -0
- package/src/Contributions.ts +9 -0
- package/src/CreateSandbox.ts +143 -0
- package/src/CreateSandboxHttp.ts +115 -0
- package/src/CredentialProfile.ts +350 -0
- package/src/Credentials.ts +33 -0
- package/src/Environment.ts +416 -0
- package/src/Errors.ts +34 -0
- package/src/Providers.ts +71 -0
- package/src/Sandbox.ts +647 -0
- package/src/SandboxClient.ts +200 -0
- package/src/SandboxEvents.ts +125 -0
- package/src/SandboxEventsLive.ts +88 -0
- package/src/Snapshot.ts +264 -0
- package/src/Webhook.ts +163 -0
- package/src/globals.d.ts +2 -0
- package/src/index.ts +18 -0
- package/src/internal/Declared.ts +87 -0
- package/src/internal/Org.ts +31 -0
- package/src/internal/Ownership.ts +75 -0
- package/src/internal/Program.ts +250 -0
- package/src/internal/Ref.ts +15 -0
- package/src/internal/SandboxHandle.ts +407 -0
- package/src/internal/SavedSnapshot.ts +34 -0
- package/src/internal/WebhookDelivery.ts +179 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import * as boat from "@distilled.cloud/boat-dev/boat";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import { Resource } from "alchemy/Resource";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Redacted from "effect/Redacted";
|
|
7
|
+
import { BoatUnexpectedResponse, InvalidEnvironmentValue } from "./Errors.ts";
|
|
8
|
+
import { declarable } from "./internal/Declared.ts";
|
|
9
|
+
import { inOrg, sameOrg, stackOrg } from "./internal/Org.ts";
|
|
10
|
+
import { boatName, isGeneratedName } from "./internal/Ownership.ts";
|
|
11
|
+
import type { Providers } from "./Providers.ts";
|
|
12
|
+
|
|
13
|
+
/** A file written into every sandbox on the environment. */
|
|
14
|
+
export interface EnvironmentSecretFile {
|
|
15
|
+
/** Where it goes, under the home directory: `.npmrc`, `~/.npmrc` or `/home/user/.npmrc`. */
|
|
16
|
+
path: string;
|
|
17
|
+
contents: string | Redacted.Redacted<string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A credential profile, by id. Accepts a `Boat.CredentialProfile`. */
|
|
21
|
+
export interface EnvironmentCredentialProfileRef {
|
|
22
|
+
credentialProfileId: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** An AI credentials proxy switch. Accepts a `Boat.AiProxy`. */
|
|
26
|
+
export interface EnvironmentAiProxyRef {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface EnvironmentProps {
|
|
31
|
+
/**
|
|
32
|
+
* Environment name: letters, digits, `.`, `-` and `_`, up to 64. If
|
|
33
|
+
* omitted, a unique name is generated from the stack, stage and logical
|
|
34
|
+
* ID. Renamed in place.
|
|
35
|
+
*/
|
|
36
|
+
name?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Environment variables every sandbox on it gets. Updated in place;
|
|
39
|
+
* running sandboxes keep the version they started with. A value can't
|
|
40
|
+
* hold a newline, or both `'` and `"`.
|
|
41
|
+
*/
|
|
42
|
+
env?: Record<string, string | Redacted.Redacted<string>>;
|
|
43
|
+
/** Files written into every sandbox on it. Updated in place. */
|
|
44
|
+
secretFiles?: EnvironmentSecretFile[];
|
|
45
|
+
/**
|
|
46
|
+
* Pass nothing to sandboxes: no repositories, secrets or credentials,
|
|
47
|
+
* whatever the flags below say. For sandboxes you give to third parties.
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
safeForThirdParties?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Attach `env` and `secretFiles`.
|
|
53
|
+
* @default true
|
|
54
|
+
*/
|
|
55
|
+
passSecrets?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Attach your GitHub token, the repositories selected on the dashboard,
|
|
58
|
+
* and their setup scripts. A sandbox with it can push as you.
|
|
59
|
+
* @default false
|
|
60
|
+
*/
|
|
61
|
+
passGithub?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Attach a Boat API key, so a sandbox can snapshot, stop and resume
|
|
64
|
+
* itself.
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
passSandboxCredentials?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Attach the keys and sign-ins of `credentialProfile`, or of the sandbox
|
|
70
|
+
* owner's default profile when there is none.
|
|
71
|
+
* @default true with a `credentialProfile`, else false
|
|
72
|
+
*/
|
|
73
|
+
passAgentsCredentials?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* The `Boat.CredentialProfile` whose keys the sandboxes get (or
|
|
76
|
+
* `{ credentialProfileId }` of one). Omit for the sandbox owner's default
|
|
77
|
+
* profile.
|
|
78
|
+
*/
|
|
79
|
+
credentialProfile?: EnvironmentCredentialProfileRef;
|
|
80
|
+
/**
|
|
81
|
+
* With `safeForThirdParties`, still give sandboxes the credential
|
|
82
|
+
* profile, as placeholders the AI credentials proxy swaps for the real
|
|
83
|
+
* keys. Real keys never enter them. Boat rejects it while the proxy is
|
|
84
|
+
* off for the profile's owner, so pass the `Boat.AiProxy` that turns it
|
|
85
|
+
* on: the environment then waits for it, and proxies only while it is on.
|
|
86
|
+
* @default false
|
|
87
|
+
*/
|
|
88
|
+
proxyAiCredentials?: boolean | EnvironmentAiProxyRef;
|
|
89
|
+
/**
|
|
90
|
+
* Make it the account's default: sandboxes created without an
|
|
91
|
+
* `environment` use it.
|
|
92
|
+
* @default false
|
|
93
|
+
*/
|
|
94
|
+
isDefault?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface EnvironmentAttributes {
|
|
98
|
+
/** Boat's environment id. */
|
|
99
|
+
environmentId: string;
|
|
100
|
+
name: string;
|
|
101
|
+
/** Each change mints a version; new sandboxes start on the latest. */
|
|
102
|
+
latestVersionNumber: number;
|
|
103
|
+
isDefault: boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The organization whose shared environment this is, as the stack's
|
|
106
|
+
* `org` named it; `undefined` for a personal environment.
|
|
107
|
+
*/
|
|
108
|
+
org: string | undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface Environment extends Resource<
|
|
112
|
+
"Boat.Environment",
|
|
113
|
+
EnvironmentProps,
|
|
114
|
+
EnvironmentAttributes,
|
|
115
|
+
never,
|
|
116
|
+
Providers,
|
|
117
|
+
{ name: string }
|
|
118
|
+
> {}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A Boat environment: the environment variables, secret files and
|
|
122
|
+
* credential settings a sandbox starts with.
|
|
123
|
+
*
|
|
124
|
+
* Every change mints a new version. New sandboxes start on the latest
|
|
125
|
+
* version; running ones keep the version they started with until they are
|
|
126
|
+
* upgraded on the dashboard.
|
|
127
|
+
*
|
|
128
|
+
* An environment is personal, or shared by an organization. With an `org`
|
|
129
|
+
* on the stack (`Boat.providers({ org })`, or the profile's), it is that
|
|
130
|
+
* organization's: every member can use it, and owners and admins change
|
|
131
|
+
* it. Without one it is yours alone, even when your active wallet is an
|
|
132
|
+
* organization's. Changing the stack's `org` replaces it.
|
|
133
|
+
*
|
|
134
|
+
* An environment passes a sandbox its own `env` and `secretFiles`, and
|
|
135
|
+
* nothing of your account. Your GitHub token, a Boat API key and your AI
|
|
136
|
+
* credentials are each one flag, off until you turn it on.
|
|
137
|
+
*
|
|
138
|
+
* ### Creating an environment
|
|
139
|
+
* **Example:** Variables and a secret file
|
|
140
|
+
* ```typescript
|
|
141
|
+
* export const MyEnv = Boat.Environment("MyEnv", {
|
|
142
|
+
* env: { NODE_ENV: "production", API_TOKEN: Config.Redacted("API_TOKEN") },
|
|
143
|
+
* secretFiles: [{ path: ".npmrc", contents: Config.Redacted("NPMRC") }],
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* export const Box = Boat.Sandbox("Box", { environment: MyEnv });
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* **Example:** An environment for sandboxes you hand to users
|
|
150
|
+
* ```typescript
|
|
151
|
+
* export const Tenants = Boat.Environment("Tenants", { safeForThirdParties: true });
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* ### Passing account credentials
|
|
155
|
+
* **Example:** Let sandboxes push to GitHub as you, and manage themselves
|
|
156
|
+
* ```typescript
|
|
157
|
+
* export const Mine = Boat.Environment("Mine", {
|
|
158
|
+
* passGithub: true,
|
|
159
|
+
* passSandboxCredentials: true,
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* ### Giving sandboxes AI credentials
|
|
164
|
+
* **Example:** The keys of a credential profile
|
|
165
|
+
* ```typescript
|
|
166
|
+
* export const MyProfile = Boat.CredentialProfile("MyProfile", {
|
|
167
|
+
* anthropic: Config.Redacted("ANTHROPIC_API_KEY"),
|
|
168
|
+
* });
|
|
169
|
+
*
|
|
170
|
+
* export const MyEnv = Boat.Environment("MyEnv", { credentialProfile: MyProfile });
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* **Example:** The sign-ins you made on the dashboard
|
|
174
|
+
* ```typescript
|
|
175
|
+
* export const MyEnv = Boat.Environment("MyEnv", {
|
|
176
|
+
* credentialProfile: Boat.CredentialProfile.lookup({ name: "default" }),
|
|
177
|
+
* });
|
|
178
|
+
* ```
|
|
179
|
+
*
|
|
180
|
+
* **Example:** Placeholders, for sandboxes you hand to users
|
|
181
|
+
* ```typescript
|
|
182
|
+
* export const MyProfile = Boat.CredentialProfile("MyProfile", {
|
|
183
|
+
* anthropic: Config.Redacted("ANTHROPIC_API_KEY"),
|
|
184
|
+
* });
|
|
185
|
+
*
|
|
186
|
+
* // Billed: $1 a day, plus $1 per 1,000 proxied requests.
|
|
187
|
+
* export const Proxy = Boat.AiProxy("Proxy", { enabled: true });
|
|
188
|
+
*
|
|
189
|
+
* export const Tenants = Boat.Environment("Tenants", {
|
|
190
|
+
* safeForThirdParties: true,
|
|
191
|
+
* credentialProfile: MyProfile,
|
|
192
|
+
* proxyAiCredentials: Proxy,
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*
|
|
196
|
+
* ### Sharing with an organization
|
|
197
|
+
* **Example:** An environment the whole team uses
|
|
198
|
+
* ```typescript
|
|
199
|
+
* // alchemy.run.ts
|
|
200
|
+
* export default Alchemy.Stack(
|
|
201
|
+
* "Team",
|
|
202
|
+
* { providers: Boat.providers({ org: "acme" }) },
|
|
203
|
+
* Effect.gen(function* () {
|
|
204
|
+
* const env = yield* Boat.Environment("MyEnv", { env: { MODE: "agent" } });
|
|
205
|
+
* return { environment: env.name };
|
|
206
|
+
* }),
|
|
207
|
+
* );
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* ### Referencing an environment the stack doesn't manage
|
|
211
|
+
* **Example:** Look one up by name
|
|
212
|
+
* ```typescript
|
|
213
|
+
* export const Box = Boat.Sandbox("Box", {
|
|
214
|
+
* environment: Boat.Environment.lookup({ name: "base" }),
|
|
215
|
+
* });
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* @resource
|
|
219
|
+
* @product Environment
|
|
220
|
+
*/
|
|
221
|
+
export const Environment = declarable<Environment>()(Resource<Environment>("Boat.Environment"));
|
|
222
|
+
|
|
223
|
+
const reveal = (value: string | Redacted.Redacted<string>) =>
|
|
224
|
+
Redacted.isRedacted(value) ? Redacted.value(value) : value;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* One `KEY=value` line as Boat reads it: single quotes keep a value
|
|
228
|
+
* verbatim; double quotes are stripped without escapes. A value holding a
|
|
229
|
+
* newline, or both kinds of quote, has no form Boat reads back intact.
|
|
230
|
+
*/
|
|
231
|
+
const envLine = (key: string, value: string) =>
|
|
232
|
+
/^[A-Za-z0-9_./:@+,=%-]*$/.test(value)
|
|
233
|
+
? Effect.succeed(`${key}=${value}`)
|
|
234
|
+
: value.includes("\n") || (value.includes("'") && value.includes('"'))
|
|
235
|
+
? Effect.fail(
|
|
236
|
+
new InvalidEnvironmentValue({
|
|
237
|
+
key,
|
|
238
|
+
message: `Boat can't hold environment variable ${key}: its value has a newline, or both ' and "`,
|
|
239
|
+
}),
|
|
240
|
+
)
|
|
241
|
+
: Effect.succeed(value.includes("'") ? `${key}="${value}"` : `${key}='${value}'`);
|
|
242
|
+
|
|
243
|
+
const envContentsOf = (env: EnvironmentProps["env"]) =>
|
|
244
|
+
Effect.map(
|
|
245
|
+
Effect.forEach(Object.entries(env ?? {}), ([key, value]) => envLine(key, reveal(value))),
|
|
246
|
+
(lines) => lines.join("\n"),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
/** Boat writes secret files relative to the home directory and doesn't expand `~`. */
|
|
250
|
+
const relativePath = (path: string) => path.replace(/^~\//, "").replace(/^\/home\/user\//, "");
|
|
251
|
+
|
|
252
|
+
const secretFilesOf = (files: EnvironmentProps["secretFiles"]) =>
|
|
253
|
+
(files ?? []).map((file) => ({
|
|
254
|
+
path: relativePath(file.path),
|
|
255
|
+
contents: reveal(file.contents),
|
|
256
|
+
}));
|
|
257
|
+
|
|
258
|
+
const listEnvironments = Effect.fn(function* (org: string | undefined) {
|
|
259
|
+
const { environments } = yield* boat.listEnvironments(inOrg(org));
|
|
260
|
+
return environments;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const findEnvironment = Effect.fn(function* (
|
|
264
|
+
org: string | undefined,
|
|
265
|
+
match: { environmentId?: string; name: string },
|
|
266
|
+
) {
|
|
267
|
+
const environments = yield* listEnvironments(org);
|
|
268
|
+
return (
|
|
269
|
+
environments.find((environment) => environment.id === match.environmentId) ??
|
|
270
|
+
environments.find((environment) => environment.name === match.name)
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const toAttributes = (
|
|
275
|
+
environment: boat.SandboxEnvironment,
|
|
276
|
+
org: string | undefined,
|
|
277
|
+
): EnvironmentAttributes => ({
|
|
278
|
+
environmentId: environment.id,
|
|
279
|
+
name: environment.name,
|
|
280
|
+
latestVersionNumber: environment.latestVersionNumber ?? 0,
|
|
281
|
+
isDefault: environment.isDefault,
|
|
282
|
+
org,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* What the props set, as the fields `updateEnvironment` takes. Nothing of
|
|
287
|
+
* the account is passed unless a prop asks for it: Boat's own defaults pass
|
|
288
|
+
* everything.
|
|
289
|
+
*/
|
|
290
|
+
const desiredOf = (name: string, news: EnvironmentProps, envContents: string) => ({
|
|
291
|
+
name,
|
|
292
|
+
envContents,
|
|
293
|
+
secretFiles: secretFilesOf(news.secretFiles),
|
|
294
|
+
safeForThirdParties: news.safeForThirdParties ?? false,
|
|
295
|
+
passSecrets: news.passSecrets ?? true,
|
|
296
|
+
passGithub: news.passGithub ?? false,
|
|
297
|
+
passSandboxCredentials: news.passSandboxCredentials ?? false,
|
|
298
|
+
passAgentsCredentials: news.passAgentsCredentials ?? news.credentialProfile !== undefined,
|
|
299
|
+
proxyAiCredentials:
|
|
300
|
+
typeof news.proxyAiCredentials === "object"
|
|
301
|
+
? news.proxyAiCredentials.enabled
|
|
302
|
+
: (news.proxyAiCredentials ?? false),
|
|
303
|
+
credentialProfileId: news.credentialProfile?.credentialProfileId ?? null,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const sortedFiles = (files: ReadonlyArray<{ path: string; contents: string }>) =>
|
|
307
|
+
JSON.stringify([...files].sort((a, b) => a.path.localeCompare(b.path)));
|
|
308
|
+
|
|
309
|
+
export const EnvironmentProvider = () =>
|
|
310
|
+
Provider.effect(
|
|
311
|
+
Environment,
|
|
312
|
+
Effect.gen(function* () {
|
|
313
|
+
return {
|
|
314
|
+
stables: ["environmentId", "org"],
|
|
315
|
+
// An environment belongs to one organization (or to you) for life.
|
|
316
|
+
diff: Effect.fn(function* ({ output }) {
|
|
317
|
+
if (output !== undefined && !sameOrg(output.org, yield* stackOrg)) {
|
|
318
|
+
return { action: "replace" } as const;
|
|
319
|
+
}
|
|
320
|
+
}),
|
|
321
|
+
read: Effect.fn(function* ({ id, olds, output }) {
|
|
322
|
+
const name = output?.name ?? (yield* boatName(id, olds?.name, 64));
|
|
323
|
+
const org = output ? output.org : yield* stackOrg;
|
|
324
|
+
const environment = yield* findEnvironment(org, {
|
|
325
|
+
environmentId: output?.environmentId,
|
|
326
|
+
name,
|
|
327
|
+
});
|
|
328
|
+
if (environment === undefined) return undefined;
|
|
329
|
+
const attrs = toAttributes(environment, org);
|
|
330
|
+
return output !== undefined || isGeneratedName(environment.name) ? attrs : Unowned(attrs);
|
|
331
|
+
}),
|
|
332
|
+
lookup: Effect.fn(function* ({ name }) {
|
|
333
|
+
const org = yield* stackOrg;
|
|
334
|
+
const environment = yield* findEnvironment(org, { name });
|
|
335
|
+
return environment === undefined ? undefined : toAttributes(environment, org);
|
|
336
|
+
}),
|
|
337
|
+
reconcile: Effect.fn(function* ({ id, news, output, session }) {
|
|
338
|
+
const name = yield* boatName(id, news.name ?? output?.name, 64);
|
|
339
|
+
// A different org replaces (see diff), so this is the org it lives in.
|
|
340
|
+
const org = yield* stackOrg;
|
|
341
|
+
|
|
342
|
+
// Observe — by id once we have one (it survives renames), else by name.
|
|
343
|
+
let environment = yield* findEnvironment(org, {
|
|
344
|
+
environmentId: output?.environmentId,
|
|
345
|
+
name,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Ensure — a concurrent create of the same name is a race, not a failure.
|
|
349
|
+
if (environment === undefined) {
|
|
350
|
+
yield* session.note(`Creating environment ${name}...`);
|
|
351
|
+
const created = yield* boat
|
|
352
|
+
.createEnvironment({ name, ...inOrg(org) })
|
|
353
|
+
.pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
|
|
354
|
+
environment =
|
|
355
|
+
created?.environments.find((candidate) => candidate.name === name) ??
|
|
356
|
+
(yield* findEnvironment(org, { name }));
|
|
357
|
+
if (environment === undefined) {
|
|
358
|
+
return yield* new BoatUnexpectedResponse({
|
|
359
|
+
message: `Boat environment ${name} vanished after create`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Sync — each update mints a version, so send only what differs.
|
|
365
|
+
const desired = desiredOf(name, news, yield* envContentsOf(news.env));
|
|
366
|
+
const delta: Partial<typeof desired> = {};
|
|
367
|
+
if (environment.name !== desired.name) delta.name = desired.name;
|
|
368
|
+
if (environment.envContents.trim() !== desired.envContents.trim()) {
|
|
369
|
+
delta.envContents = desired.envContents;
|
|
370
|
+
}
|
|
371
|
+
if (sortedFiles(environment.secretFiles) !== sortedFiles(desired.secretFiles)) {
|
|
372
|
+
delta.secretFiles = desired.secretFiles;
|
|
373
|
+
}
|
|
374
|
+
for (const flag of [
|
|
375
|
+
"safeForThirdParties",
|
|
376
|
+
"passGithub",
|
|
377
|
+
"passSecrets",
|
|
378
|
+
"passSandboxCredentials",
|
|
379
|
+
"passAgentsCredentials",
|
|
380
|
+
] as const) {
|
|
381
|
+
if (environment[flag] !== desired[flag]) delta[flag] = desired[flag];
|
|
382
|
+
}
|
|
383
|
+
if ((environment.proxyAiCredentials ?? false) !== desired.proxyAiCredentials) {
|
|
384
|
+
delta.proxyAiCredentials = desired.proxyAiCredentials;
|
|
385
|
+
}
|
|
386
|
+
if ((environment.credentialProfileId ?? null) !== desired.credentialProfileId) {
|
|
387
|
+
delta.credentialProfileId = desired.credentialProfileId;
|
|
388
|
+
}
|
|
389
|
+
const makeDefault = news.isDefault === true && !environment.isDefault;
|
|
390
|
+
if (Object.keys(delta).length > 0 || makeDefault) {
|
|
391
|
+
yield* session.note(`Updating environment ${name}...`);
|
|
392
|
+
const updated = yield* boat.updateEnvironment({
|
|
393
|
+
environmentId: environment.id,
|
|
394
|
+
...inOrg(org),
|
|
395
|
+
...delta,
|
|
396
|
+
...(makeDefault ? { isDefault: true } : {}),
|
|
397
|
+
});
|
|
398
|
+
environment = updated.environment ?? environment;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return toAttributes(environment, org);
|
|
402
|
+
}),
|
|
403
|
+
delete: Effect.fn(function* ({ output }) {
|
|
404
|
+
yield* boat
|
|
405
|
+
.deleteEnvironment({ environmentId: output.environmentId, ...inOrg(output.org) })
|
|
406
|
+
.pipe(Effect.catchTag("NotFound", () => Effect.void));
|
|
407
|
+
}),
|
|
408
|
+
list: Effect.fn(function* () {
|
|
409
|
+
const org = yield* stackOrg;
|
|
410
|
+
return (yield* listEnvironments(org))
|
|
411
|
+
.filter((environment) => isGeneratedName(environment.name))
|
|
412
|
+
.map((environment) => toAttributes(environment, org));
|
|
413
|
+
}),
|
|
414
|
+
};
|
|
415
|
+
}),
|
|
416
|
+
);
|
package/src/Errors.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as Data from "effect/Data";
|
|
2
|
+
|
|
3
|
+
/** Boat answered with something Alchemy can't use (a missing field, a vanished object). */
|
|
4
|
+
export class BoatUnexpectedResponse extends Data.TaggedError("Boat.UnexpectedResponse")<{
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}> {}
|
|
7
|
+
|
|
8
|
+
/** A command ran past its `timeout` and Boat stopped it. */
|
|
9
|
+
export class ExecTimedOut extends Data.TaggedError("Boat.ExecTimedOut")<{
|
|
10
|
+
readonly message: string;
|
|
11
|
+
readonly stdout: string;
|
|
12
|
+
readonly stderr: string;
|
|
13
|
+
}> {}
|
|
14
|
+
|
|
15
|
+
/** A saved snapshot Boat could not save. */
|
|
16
|
+
export class SnapshotFailed extends Data.TaggedError("Boat.SnapshotFailed")<{
|
|
17
|
+
readonly message: string;
|
|
18
|
+
/** The snapshot's name, or the sandbox whose setup it waited on. */
|
|
19
|
+
readonly snapshot: string;
|
|
20
|
+
}> {}
|
|
21
|
+
|
|
22
|
+
/** An environment variable value Boat's `.env` format can't hold. */
|
|
23
|
+
export class InvalidEnvironmentValue extends Data.TaggedError("Boat.InvalidEnvironmentValue")<{
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly key: string;
|
|
26
|
+
}> {}
|
|
27
|
+
|
|
28
|
+
/** A snapshot's setup command failed or ran past 10 minutes. */
|
|
29
|
+
export class SnapshotSetupFailed extends Data.TaggedError("Boat.SnapshotSetupFailed")<{
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly command: string;
|
|
32
|
+
readonly exitCode: number;
|
|
33
|
+
readonly stderr: string;
|
|
34
|
+
}> {}
|
package/src/Providers.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CredentialsStoreLive } from "alchemy/Auth/Credentials";
|
|
2
|
+
import { ProfileStoreLive } from "alchemy/Auth/Profile";
|
|
3
|
+
import * as Provider from "alchemy/Provider";
|
|
4
|
+
import * as FetchHttpClient from "effect/http/FetchHttpClient";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import { AiProxy, AiProxyProvider } from "./AiProxy.ts";
|
|
7
|
+
import { ApiKey, ApiKeyProvider } from "./ApiKey.ts";
|
|
8
|
+
import { BoatAuth } from "./AuthProvider.ts";
|
|
9
|
+
import { CredentialProfile, CredentialProfileProvider } from "./CredentialProfile.ts";
|
|
10
|
+
import * as Credentials from "./Credentials.ts";
|
|
11
|
+
import { Environment, EnvironmentProvider } from "./Environment.ts";
|
|
12
|
+
import { Sandbox, SandboxProvider } from "./Sandbox.ts";
|
|
13
|
+
import { Snapshot, SnapshotProvider } from "./Snapshot.ts";
|
|
14
|
+
import { Webhook, WebhookProvider } from "./Webhook.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Service tag bundling all Boat providers + auth + credentials. Use
|
|
18
|
+
* `Boat.providers()` to materialize the full layer.
|
|
19
|
+
*/
|
|
20
|
+
export class Providers extends Provider.ProviderCollection<Providers>()("Boat") {}
|
|
21
|
+
|
|
22
|
+
export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build the complete Boat providers Layer: every Boat resource, the Boat
|
|
26
|
+
* AuthProvider registration (so `alchemy profile edit` can configure it),
|
|
27
|
+
* and credential resolution.
|
|
28
|
+
*
|
|
29
|
+
* `org` picks the organization (wallet) the stack's resources bill and the
|
|
30
|
+
* one whose shared environments `Boat.Environment` manages: an org id
|
|
31
|
+
* (`team_…`), an org name, or `personal`. It overrides the profile's `org`
|
|
32
|
+
* (or `BOAT_ORG`); with neither, Boat uses the account's active wallet and
|
|
33
|
+
* environments are your personal ones.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* Alchemy.Stack("App", { providers: Boat.providers() }, ...)
|
|
38
|
+
* Alchemy.Stack("App", { providers: Boat.providers({ org: "team_abc" }) }, ...)
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export const providers = (options?: { readonly org?: string }) =>
|
|
42
|
+
Layer.effect(
|
|
43
|
+
Providers,
|
|
44
|
+
Provider.collection([
|
|
45
|
+
AiProxy,
|
|
46
|
+
ApiKey,
|
|
47
|
+
CredentialProfile,
|
|
48
|
+
Environment,
|
|
49
|
+
Sandbox,
|
|
50
|
+
Snapshot,
|
|
51
|
+
Webhook,
|
|
52
|
+
]),
|
|
53
|
+
).pipe(
|
|
54
|
+
Layer.provide(
|
|
55
|
+
Layer.mergeAll(
|
|
56
|
+
AiProxyProvider(),
|
|
57
|
+
ApiKeyProvider(),
|
|
58
|
+
CredentialProfileProvider(),
|
|
59
|
+
EnvironmentProvider(),
|
|
60
|
+
SandboxProvider(),
|
|
61
|
+
SnapshotProvider(),
|
|
62
|
+
WebhookProvider(),
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
Layer.provideMerge(Credentials.fromAuthProvider(options)),
|
|
66
|
+
Layer.provideMerge(FetchHttpClient.layer),
|
|
67
|
+
Layer.provideMerge(BoatAuth),
|
|
68
|
+
Layer.provideMerge(ProfileStoreLive),
|
|
69
|
+
Layer.provideMerge(CredentialsStoreLive),
|
|
70
|
+
Layer.orDie,
|
|
71
|
+
);
|