@alchemy.run/boat 0.0.0 → 2.0.0-beta.86
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 +186 -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 +249 -0
- package/lib/internal/SandboxHandle.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 +199 -0
- package/src/SandboxEvents.ts +125 -0
- package/src/SandboxEventsLive.ts +88 -0
- package/src/Snapshot.ts +284 -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 +425 -0
- package/src/internal/WebhookDelivery.ts +179 -0
package/lib/Providers.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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.js";
|
|
7
|
+
import { ApiKey, ApiKeyProvider } from "./ApiKey.js";
|
|
8
|
+
import { BoatAuth } from "./AuthProvider.js";
|
|
9
|
+
import { CredentialProfile, CredentialProfileProvider } from "./CredentialProfile.js";
|
|
10
|
+
import * as Credentials from "./Credentials.js";
|
|
11
|
+
import { Environment, EnvironmentProvider } from "./Environment.js";
|
|
12
|
+
import { Sandbox, SandboxProvider } from "./Sandbox.js";
|
|
13
|
+
import { Snapshot, SnapshotProvider } from "./Snapshot.js";
|
|
14
|
+
import { Webhook, WebhookProvider } from "./Webhook.js";
|
|
15
|
+
/**
|
|
16
|
+
* Service tag bundling all Boat providers + auth + credentials. Use
|
|
17
|
+
* `Boat.providers()` to materialize the full layer.
|
|
18
|
+
*/
|
|
19
|
+
export class Providers extends Provider.ProviderCollection()("Boat") {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the complete Boat providers Layer: every Boat resource, the Boat
|
|
23
|
+
* AuthProvider registration (so `alchemy profile edit` can configure it),
|
|
24
|
+
* and credential resolution.
|
|
25
|
+
*
|
|
26
|
+
* `org` picks the organization (wallet) the stack's resources bill and the
|
|
27
|
+
* one whose shared environments `Boat.Environment` manages: an org id
|
|
28
|
+
* (`team_…`), an org name, or `personal`. It overrides the profile's `org`
|
|
29
|
+
* (or `BOAT_ORG`); with neither, Boat uses the account's active wallet and
|
|
30
|
+
* environments are your personal ones.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* Alchemy.Stack("App", { providers: Boat.providers() }, ...)
|
|
35
|
+
* Alchemy.Stack("App", { providers: Boat.providers({ org: "team_abc" }) }, ...)
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export const providers = (options) => Layer.effect(Providers, Provider.collection([
|
|
39
|
+
AiProxy,
|
|
40
|
+
ApiKey,
|
|
41
|
+
CredentialProfile,
|
|
42
|
+
Environment,
|
|
43
|
+
Sandbox,
|
|
44
|
+
Snapshot,
|
|
45
|
+
Webhook,
|
|
46
|
+
])).pipe(Layer.provide(Layer.mergeAll(AiProxyProvider(), ApiKeyProvider(), CredentialProfileProvider(), EnvironmentProvider(), SandboxProvider(), SnapshotProvider(), WebhookProvider())), Layer.provideMerge(Credentials.fromAuthProvider(options)), Layer.provideMerge(FetchHttpClient.layer), Layer.provideMerge(BoatAuth), Layer.provideMerge(ProfileStoreLive), Layer.provideMerge(CredentialsStoreLive), Layer.orDie);
|
|
47
|
+
//# sourceMappingURL=Providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Providers.js","sources":["../src/Providers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,QAAQ,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,eAAe,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACtF,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACpE,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAExD;;;GAGG;AACH,MAAM,OAAO,SAAU,SAAQ,QAAQ,CAAC,kBAAkB,EAAa,CAAC,MAAM,CAAC;CAAG;AAIlF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAmC,EAAE,EAAE,CAC/D,KAAK,CAAC,MAAM,CACV,SAAS,EACT,QAAQ,CAAC,UAAU,CAAC;IAClB,OAAO;IACP,MAAM;IACN,iBAAiB;IACjB,WAAW;IACX,OAAO;IACP,QAAQ;IACR,OAAO;CACR,CAAC,CACH,CAAC,IAAI,CACJ,KAAK,CAAC,OAAO,CACX,KAAK,CAAC,QAAQ,CACZ,eAAe,EAAE,EACjB,cAAc,EAAE,EAChB,yBAAyB,EAAE,EAC3B,mBAAmB,EAAE,EACrB,eAAe,EAAE,EACjB,gBAAgB,EAAE,EAClB,eAAe,EAAE,CAClB,CACF,EACD,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,EACzD,KAAK,CAAC,YAAY,CAAC,eAAe,CAAC,KAAK,CAAC,EACzC,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,EAC5B,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,EACpC,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,EACxC,KAAK,CAAC,KAAK,CACZ,CAAC"}
|
package/lib/Sandbox.d.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as boat from "@distilled.cloud/boat-dev/boat";
|
|
2
|
+
import type * as Bundle from "alchemy/Bundle/Bundle";
|
|
3
|
+
import { Platform, type Main, type PlatformProps } from "alchemy/Platform";
|
|
4
|
+
import * as Provider from "alchemy/Provider";
|
|
5
|
+
import { Resource } from "alchemy/Resource";
|
|
6
|
+
import { type HostRuntimeContext, type ServerHost } from "alchemy/Runtime/Process";
|
|
7
|
+
import { Stack } from "alchemy/Stack";
|
|
8
|
+
import * as Duration from "effect/Duration";
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import { type ProgramState } from "./internal/Program.ts";
|
|
12
|
+
import { type Ref } from "./internal/Ref.ts";
|
|
13
|
+
import type { Providers } from "./Providers.ts";
|
|
14
|
+
import type { SnapshotBase } from "./Snapshot.ts";
|
|
15
|
+
/**
|
|
16
|
+
* Machine size. `small` runs at half the default rate and `large` at
|
|
17
|
+
* twice; `xlarge` needs the $100/month plan or higher.
|
|
18
|
+
*/
|
|
19
|
+
export type SandboxType = "small" | "default" | "large" | "xlarge";
|
|
20
|
+
/** Where a sandbox is in its life. `archived` is stopped, with its disk saved. */
|
|
21
|
+
export type SandboxState = "init" | "provisioning" | "provisioned" | "cloning" | "ready" | "idle" | "running" | "archiving" | "archived" | "error" | "cancelled";
|
|
22
|
+
/** How the setup script went: `pending`, `running`, `done` or `failed`. */
|
|
23
|
+
export type SandboxSetupStatus = "pending" | "running" | "done" | "failed";
|
|
24
|
+
/** An environment, by name. Accepts a `Boat.Environment` or `{ name }`. */
|
|
25
|
+
export interface SandboxEnvironmentRef {
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
/** What bindings attach to a sandbox that runs a program: its process's environment. */
|
|
29
|
+
export interface SandboxBinding {
|
|
30
|
+
env?: Record<string, any>;
|
|
31
|
+
}
|
|
32
|
+
export interface SandboxProps extends PlatformProps {
|
|
33
|
+
/**
|
|
34
|
+
* Display name. If omitted, a unique name is generated from the stack,
|
|
35
|
+
* stage and logical ID. Renamed in place.
|
|
36
|
+
*/
|
|
37
|
+
name?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Machine size. Changing it replaces the sandbox.
|
|
40
|
+
* @default "default"
|
|
41
|
+
*/
|
|
42
|
+
type?: SandboxType;
|
|
43
|
+
/**
|
|
44
|
+
* How long the sandbox runs before Boat stops it, saving its disk;
|
|
45
|
+
* `false` never stops it. Updated in place: the next stop is then
|
|
46
|
+
* `autoStop` from the deploy.
|
|
47
|
+
* @default "1 hour", or `false` for a sandbox with `main`
|
|
48
|
+
*/
|
|
49
|
+
autoStop?: Duration.Input | false;
|
|
50
|
+
/**
|
|
51
|
+
* Environment variables. On a sandbox without `main` they are the
|
|
52
|
+
* sandbox's, on top of its environment's, and changing them replaces it.
|
|
53
|
+
* On a sandbox with `main` they are the program's, and changing them
|
|
54
|
+
* restarts it.
|
|
55
|
+
*/
|
|
56
|
+
env?: Record<string, string | Redacted.Redacted<string>>;
|
|
57
|
+
/**
|
|
58
|
+
* The `Boat.Environment` to attach (or `{ name }` of one): its
|
|
59
|
+
* variables, secret files and credential settings. Omit for the account's
|
|
60
|
+
* default environment (`base`). Changing it replaces the sandbox.
|
|
61
|
+
*/
|
|
62
|
+
environment?: Ref<SandboxEnvironmentRef>;
|
|
63
|
+
/**
|
|
64
|
+
* Attach none of the account's secrets (no environment variables, secret
|
|
65
|
+
* files or credentials), and confine the sandbox so it can't act on the
|
|
66
|
+
* account. For sandboxes you give to your own users. Changing it replaces
|
|
67
|
+
* the sandbox.
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
noEnv?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* `false` never snapshots the sandbox: stopping it erases its disk, and it
|
|
73
|
+
* can't be resumed, forked or saved. Changing it replaces the sandbox.
|
|
74
|
+
* @default true
|
|
75
|
+
*/
|
|
76
|
+
snapshots?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* `false` keeps no command output, process list or metrics for the
|
|
79
|
+
* sandbox. Changing it replaces the sandbox.
|
|
80
|
+
* @default true
|
|
81
|
+
*/
|
|
82
|
+
monitoring?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* A shell script run once, in the background, after the sandbox starts.
|
|
85
|
+
* Its outcome is `setupStatus`. Changing it replaces the sandbox.
|
|
86
|
+
*/
|
|
87
|
+
setupScript?: string;
|
|
88
|
+
/**
|
|
89
|
+
* The `Boat.Snapshot` to start from (or `{ name }` of one saved
|
|
90
|
+
* elsewhere, or a share code `bsh_...`). Changing or rebuilding it
|
|
91
|
+
* replaces the sandbox, so it starts from the new disk.
|
|
92
|
+
*/
|
|
93
|
+
snapshot?: Ref<SnapshotBase>;
|
|
94
|
+
/**
|
|
95
|
+
* Your own labels, at most 17 (Alchemy adds three). Boat sets labels
|
|
96
|
+
* only at create, so changing them replaces the sandbox.
|
|
97
|
+
*/
|
|
98
|
+
labels?: Record<string, string>;
|
|
99
|
+
/**
|
|
100
|
+
* The sandbox's subdomain under `on.boat.dev`. Port `p` answers at
|
|
101
|
+
* `https://<subdomain>-<p>.on.boat.dev`. Renamed in place.
|
|
102
|
+
*/
|
|
103
|
+
subdomain?: string;
|
|
104
|
+
/**
|
|
105
|
+
* Module entrypoint of a program to run in the sandbox, typically
|
|
106
|
+
* `import.meta.url` in the class form. Alchemy bundles it, uploads it
|
|
107
|
+
* into the sandbox and runs it with Node 24; a code change uploads it
|
|
108
|
+
* again and restarts the process, without replacing the sandbox.
|
|
109
|
+
*/
|
|
110
|
+
main?: string;
|
|
111
|
+
/**
|
|
112
|
+
* Port the program listens on. Written to `PORT`; Boat serves it at
|
|
113
|
+
* `url`.
|
|
114
|
+
* @default 3000
|
|
115
|
+
*/
|
|
116
|
+
port?: number;
|
|
117
|
+
/**
|
|
118
|
+
* Serve the program's port to anyone. Otherwise requests need `urlToken`
|
|
119
|
+
* (as the `_token` query parameter, or the `_port_auth` cookie).
|
|
120
|
+
* Updated in place.
|
|
121
|
+
* @default false
|
|
122
|
+
*/
|
|
123
|
+
public?: boolean;
|
|
124
|
+
/**
|
|
125
|
+
* Named export to load from `main`.
|
|
126
|
+
* @default "default"
|
|
127
|
+
*/
|
|
128
|
+
handler?: string;
|
|
129
|
+
/** Bundler configuration for `main`. */
|
|
130
|
+
build?: Bundle.BundleConfig;
|
|
131
|
+
/**
|
|
132
|
+
* Who in the organization that pays for the sandbox can use it:
|
|
133
|
+
* `private`, only you; `shared`, every member. Omitted, the organization's
|
|
134
|
+
* default for new sandboxes applies. Updated in place. Sharing a sandbox
|
|
135
|
+
* that holds your logins (GitHub, Claude and Codex logins, environment
|
|
136
|
+
* secrets) wipes them at its next start and never pushes them again, so
|
|
137
|
+
* share one with `noEnv` or an environment that passes nothing. A
|
|
138
|
+
* personal sandbox is always private, and Boat refuses `shared` for one.
|
|
139
|
+
*/
|
|
140
|
+
visibility?: "shared" | "private";
|
|
141
|
+
}
|
|
142
|
+
export interface SandboxAttributes {
|
|
143
|
+
/** Boat's sandbox id (`bx_…`). */
|
|
144
|
+
sandboxId: string;
|
|
145
|
+
name: string;
|
|
146
|
+
/** `ready`, `running`, `idle`, `archived` (stopped), … */
|
|
147
|
+
state: SandboxState;
|
|
148
|
+
type: SandboxType | undefined;
|
|
149
|
+
/** vCPUs and RAM (GB) the machine size guarantees. */
|
|
150
|
+
vcpu: number | undefined;
|
|
151
|
+
memoryGB: number | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* The subdomain its ports are served under: port `p` answers at
|
|
154
|
+
* `https://<subdomain>-<p>.on.boat.dev`.
|
|
155
|
+
*/
|
|
156
|
+
subdomain: string | undefined;
|
|
157
|
+
/** The machine's URL. */
|
|
158
|
+
machineUrl: string | undefined;
|
|
159
|
+
/** The environment the sandbox runs, and its version. */
|
|
160
|
+
environment: string | undefined;
|
|
161
|
+
environmentVersion: number | undefined;
|
|
162
|
+
/** Seconds it runs before stopping, or `undefined` when it never stops. */
|
|
163
|
+
ttlSeconds: number | undefined;
|
|
164
|
+
/** Your labels, without Alchemy's. */
|
|
165
|
+
labels: Record<string, string>;
|
|
166
|
+
/** The setup script's outcome: `pending`, `running`, `done` or `failed`. */
|
|
167
|
+
setupStatus: SandboxSetupStatus | undefined;
|
|
168
|
+
/** The program's URL, for a sandbox with `main`. */
|
|
169
|
+
url: string | undefined;
|
|
170
|
+
/**
|
|
171
|
+
* The token a request to `url` needs unless the sandbox is `public`: the
|
|
172
|
+
* `_token` query parameter, or the `_port_auth` cookie.
|
|
173
|
+
*/
|
|
174
|
+
urlToken: Redacted.Redacted<string> | undefined;
|
|
175
|
+
/** The program the sandbox runs, for a sandbox with `main`. */
|
|
176
|
+
program: ProgramState | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* The organization that pays for it, fixed when it was created;
|
|
179
|
+
* `undefined` when you do.
|
|
180
|
+
*/
|
|
181
|
+
org: {
|
|
182
|
+
id: string;
|
|
183
|
+
name: string;
|
|
184
|
+
} | undefined;
|
|
185
|
+
/** `shared` or `private` for an organization's sandbox; `undefined` for yours. */
|
|
186
|
+
visibility: "shared" | "private" | undefined;
|
|
187
|
+
}
|
|
188
|
+
export type Sandbox = Resource<"Boat.Sandbox", SandboxProps, SandboxAttributes, SandboxBinding, Providers, {
|
|
189
|
+
sandboxId: string;
|
|
190
|
+
}>;
|
|
191
|
+
export type SandboxServices = ServerHost;
|
|
192
|
+
/** A program's shape: a long-running server returning `{ fetch }`, or `host.run` loops. */
|
|
193
|
+
export type SandboxShape = Main<SandboxServices>;
|
|
194
|
+
export type SandboxRuntimeContext = HostRuntimeContext;
|
|
195
|
+
export declare const Sandbox: Platform<Sandbox, SandboxServices, SandboxShape, SandboxRuntimeContext> & {
|
|
196
|
+
/** References a sandbox the stack doesn't manage, by id. */
|
|
197
|
+
readonly lookup: ReturnType<typeof Resource<Sandbox>>["lookup"];
|
|
198
|
+
};
|
|
199
|
+
/** A sandbox, or `undefined` when Boat no longer has it. */
|
|
200
|
+
export declare const observeSandbox: (sandboxId: string) => Effect.Effect<boat.Sandbox | undefined, import("@distilled.cloud/boat-dev").BadGateway | import("@distilled.cloud/core/errors").BadRequest | import("@distilled.cloud/boat-dev").BoatParseError | import("@distilled.cloud/boat-dev").ConfigError | import("@distilled.cloud/core/errors").Conflict | import("@distilled.cloud/core/errors").Forbidden | import("@distilled.cloud/boat-dev").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/boat-dev").InternalServerError | import("@distilled.cloud/core/protocol-http").InvalidInput | import("@distilled.cloud/boat-dev").Locked | import("@distilled.cloud/core/sdk-error").SdkError | import("@distilled.cloud/boat-dev").ServiceUnavailable | import("@distilled.cloud/boat-dev").TooManyRequests | import("@distilled.cloud/boat-dev").Unauthorized | import("@distilled.cloud/boat-dev").UnknownBoatError | import("@distilled.cloud/boat-dev").UnprocessableEntity, boat.BoatOpContext>;
|
|
201
|
+
/** Delete a sandbox; deleting one that is gone succeeds. */
|
|
202
|
+
export declare const deleteSandbox: (sandboxId: string) => Effect.Effect<void, import("@distilled.cloud/boat-dev").BadGateway | import("@distilled.cloud/core/errors").BadRequest | import("@distilled.cloud/boat-dev").BoatParseError | import("@distilled.cloud/boat-dev").ConfigError | import("@distilled.cloud/core/errors").Conflict | boat.Conflict | import("@distilled.cloud/core/errors").Forbidden | boat.Forbidden | import("@distilled.cloud/boat-dev").GatewayTimeout | import("effect/http/HttpClientError").HttpClientError | import("@distilled.cloud/boat-dev").InternalServerError | import("@distilled.cloud/core/protocol-http").InvalidInput | import("@distilled.cloud/boat-dev").Locked | import("@distilled.cloud/core/sdk-error").SdkError | import("@distilled.cloud/boat-dev").ServiceUnavailable | import("@distilled.cloud/boat-dev").TooManyRequests | import("@distilled.cloud/boat-dev").Unauthorized | import("@distilled.cloud/boat-dev").UnknownBoatError | import("@distilled.cloud/boat-dev").UnprocessableEntity, boat.BoatOpContext>;
|
|
203
|
+
export declare const toAttributes: (sandbox: boat.Sandbox) => SandboxAttributes;
|
|
204
|
+
export declare const SandboxProvider: () => import("effect/Layer").Layer<Provider.Provider<Sandbox>, never, import("effect/FileSystem").FileSystem | import("effect/Path").Path | Stack | import("alchemy/Stage").Stage | boat.BoatOpContext>;
|
|
205
|
+
//# sourceMappingURL=Sandbox.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Sandbox.d.ts","sources":["../src/Sandbox.ts"],"names":[],"mappings":"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"}
|
package/lib/Sandbox.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import * as boat from "@distilled.cloud/boat-dev/boat";
|
|
2
|
+
import { Unowned } from "alchemy/AdoptPolicy";
|
|
3
|
+
import { isResolved } from "alchemy/Diff";
|
|
4
|
+
import { Platform } from "alchemy/Platform";
|
|
5
|
+
import * as Provider from "alchemy/Provider";
|
|
6
|
+
import { Resource } from "alchemy/Resource";
|
|
7
|
+
import { createHostRuntimeContext, } from "alchemy/Runtime/Process";
|
|
8
|
+
import { Stack } from "alchemy/Stack";
|
|
9
|
+
import * as Duration from "effect/Duration";
|
|
10
|
+
import * as Effect from "effect/Effect";
|
|
11
|
+
import * as Redacted from "effect/Redacted";
|
|
12
|
+
import * as Stream from "effect/Stream";
|
|
13
|
+
import { isDeclaration } from "./internal/Declared.js";
|
|
14
|
+
import { boatName, hasAnyAlchemyLabel, hasOwnershipLabels, ownershipLabels, userLabels, } from "./internal/Ownership.js";
|
|
15
|
+
import { bundleProgram, DEFAULT_PORT, deployProgram, isProgramRunning, } from "./internal/Program.js";
|
|
16
|
+
import { resolved } from "./internal/Ref.js";
|
|
17
|
+
/**
|
|
18
|
+
* A Boat sandbox the stack owns: a Linux machine with Docker, passwordless
|
|
19
|
+
* `sudo` and a desktop, that stops itself after `autoStop` and keeps its
|
|
20
|
+
* disk until it is resumed.
|
|
21
|
+
*
|
|
22
|
+
* Use it for a workspace that outlives requests (an agent's home, a dev
|
|
23
|
+
* box, a preview environment). Bindings create short-lived sandboxes at
|
|
24
|
+
* run time with `Boat.CreateSandbox`.
|
|
25
|
+
*
|
|
26
|
+
* Alchemy labels the sandbox with its stack, stage and logical ID
|
|
27
|
+
* (`alchemy.stack`, `alchemy.stage`, `alchemy.id`) to recognise the
|
|
28
|
+
* sandboxes it owns.
|
|
29
|
+
*
|
|
30
|
+
* The sandbox bills the stack's organization (`Boat.providers({ org })`,
|
|
31
|
+
* or the profile's), else your active wallet. Boat fixes the wallet at
|
|
32
|
+
* create, so changing the stack's `org` later leaves the sandbox where it
|
|
33
|
+
* is; `org` says which.
|
|
34
|
+
*
|
|
35
|
+
* :::caution
|
|
36
|
+
* Replacing the sandbox (a new `type`, `env`, `environment`, `noEnv`,
|
|
37
|
+
* `snapshots`, `monitoring`, `setupScript`, `labels`, or a new or rebuilt
|
|
38
|
+
* `snapshot`) creates a fresh sandbox and deletes the old one with its disk.
|
|
39
|
+
* :::
|
|
40
|
+
*
|
|
41
|
+
* ### Creating a Sandbox
|
|
42
|
+
* **Example:** A dev box that stops after an idle hour
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import * as Boat from "@alchemy.run/boat";
|
|
45
|
+
*
|
|
46
|
+
* export const DevBox = Boat.Sandbox("DevBox", {
|
|
47
|
+
* type: "large",
|
|
48
|
+
* autoStop: "1 hour",
|
|
49
|
+
* setupScript: "git clone https://github.com/alchemy-run/alchemy ~/alchemy",
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* **Example:** A sandbox with none of the account's secrets
|
|
54
|
+
* ```typescript
|
|
55
|
+
* export const Untrusted = Boat.Sandbox("Untrusted", {
|
|
56
|
+
* noEnv: true,
|
|
57
|
+
* env: { TASK: "lint" },
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* ### Running a Program
|
|
62
|
+
* The class form runs an Effect program in the sandbox: give it `main` and
|
|
63
|
+
* an implementation, and Alchemy bundles the program, uploads it and runs
|
|
64
|
+
* it with Node, served at `url`. A code change uploads the new bundle and
|
|
65
|
+
* restarts the process; the sandbox and its URL stay the same. A sandbox
|
|
66
|
+
* with `main` never stops on its own unless `autoStop` says so; if it
|
|
67
|
+
* stopped anyway, the next deploy resumes it and starts the program.
|
|
68
|
+
*
|
|
69
|
+
* **Example:** An HTTP server
|
|
70
|
+
* ```typescript
|
|
71
|
+
* export default class Api extends Boat.Sandbox<Api>()(
|
|
72
|
+
* "Api",
|
|
73
|
+
* { main: import.meta.url, public: true },
|
|
74
|
+
* Effect.gen(function* () {
|
|
75
|
+
* return {
|
|
76
|
+
* fetch: Effect.gen(function* () {
|
|
77
|
+
* return yield* HttpServerResponse.json({ ok: true });
|
|
78
|
+
* }),
|
|
79
|
+
* };
|
|
80
|
+
* }),
|
|
81
|
+
* ) {}
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ### Starting from a snapshot and an environment
|
|
85
|
+
* **Example:** A sandbox with a project set up and its secrets
|
|
86
|
+
* ```typescript
|
|
87
|
+
* export const Golden = Boat.Snapshot("Golden", { run: ["npm i -g pnpm"] });
|
|
88
|
+
* export const MyEnv = Boat.Environment("MyEnv", {
|
|
89
|
+
* env: { API_TOKEN: Config.Redacted("API_TOKEN") },
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* export const Box = Boat.Sandbox("Box", {
|
|
93
|
+
* snapshot: Golden,
|
|
94
|
+
* environment: MyEnv,
|
|
95
|
+
* autoStop: "30 minutes",
|
|
96
|
+
* });
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* ### Sharing with an organization
|
|
100
|
+
* **Example:** A sandbox every member of the team can use
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // with Boat.providers({ org: "acme" }) on the stack
|
|
103
|
+
* export const TeamBox = Boat.Sandbox("TeamBox", {
|
|
104
|
+
* noEnv: true,
|
|
105
|
+
* visibility: "shared",
|
|
106
|
+
* });
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* ### Using it at runtime
|
|
110
|
+
* **Example:** Run a command in it from a Worker
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const devBox = yield* Boat.ConnectSandbox(DevBox);
|
|
113
|
+
* // in fetch:
|
|
114
|
+
* yield* devBox.resume();
|
|
115
|
+
* const result = yield* devBox.exec("git pull && npm test", { cwd: "/home/user/repo" });
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* ### Referencing a sandbox the stack doesn't manage
|
|
119
|
+
* **Example:** Look one up by id
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const shared = Boat.Sandbox.lookup({ sandboxId: "bx_abc123" });
|
|
122
|
+
* const box = yield* Boat.ConnectSandbox(shared);
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @resource
|
|
126
|
+
* @product Sandbox
|
|
127
|
+
*/
|
|
128
|
+
/** The plain resource behind {@link Sandbox}, which carries `lookup`. */
|
|
129
|
+
const SandboxResource = Resource("Boat.Sandbox");
|
|
130
|
+
export const Sandbox = Object.assign(Platform("Boat.Sandbox", {
|
|
131
|
+
createRuntimeContext: createHostRuntimeContext("Boat.Sandbox"),
|
|
132
|
+
// A declaration passed as a prop (`snapshot: Golden`) is an Effect until
|
|
133
|
+
// yielded. Yield it here so it is registered and the prop resolves to
|
|
134
|
+
// its attributes, as with `yield* Golden` inside Effect.gen.
|
|
135
|
+
transformProps: (_id, props) => Effect.gen(function* () {
|
|
136
|
+
if (globalThis.__ALCHEMY_RUNTIME__)
|
|
137
|
+
return props;
|
|
138
|
+
// A lookup (`Boat.Environment.lookup({ name })`) is an Output, which
|
|
139
|
+
// the engine resolves; only a declaration is yielded.
|
|
140
|
+
const ref = (value) => isDeclaration(value)
|
|
141
|
+
? value
|
|
142
|
+
: Effect.succeed(value);
|
|
143
|
+
return {
|
|
144
|
+
...props,
|
|
145
|
+
snapshot: props.snapshot === undefined ? undefined : yield* ref(props.snapshot),
|
|
146
|
+
environment: props.environment === undefined ? undefined : yield* ref(props.environment),
|
|
147
|
+
};
|
|
148
|
+
}),
|
|
149
|
+
}), { lookup: (props) => SandboxResource.lookup(props) });
|
|
150
|
+
/** A sandbox, or `undefined` when Boat no longer has it. */
|
|
151
|
+
export const observeSandbox = (sandboxId) => boat.getSandbox({ sandboxId }).pipe(Effect.map((got) => got.sandbox === undefined || got.sandbox.state === "cancelled" ? undefined : got.sandbox), Effect.catchTag("NotFound", () => Effect.undefined));
|
|
152
|
+
/** Delete a sandbox; deleting one that is gone succeeds. */
|
|
153
|
+
export const deleteSandbox = (sandboxId) => boat.deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId }).pipe(Effect.asVoid, Effect.catchTag("NotFound", () => Effect.void));
|
|
154
|
+
const plainEnv = (env) => env === undefined
|
|
155
|
+
? undefined
|
|
156
|
+
: Object.fromEntries(Object.entries(env).map(([key, value]) => [
|
|
157
|
+
key,
|
|
158
|
+
Redacted.isRedacted(value) ? Redacted.value(value) : value,
|
|
159
|
+
]));
|
|
160
|
+
export const toAttributes = (sandbox) => ({
|
|
161
|
+
sandboxId: sandbox.id,
|
|
162
|
+
name: sandbox.name,
|
|
163
|
+
state: sandbox.state,
|
|
164
|
+
type: sandbox.type,
|
|
165
|
+
vcpu: sandbox.vcpu,
|
|
166
|
+
memoryGB: sandbox.memoryGB,
|
|
167
|
+
subdomain: sandbox.subdomain ?? undefined,
|
|
168
|
+
machineUrl: sandbox.url ?? undefined,
|
|
169
|
+
environment: sandbox.environment ?? undefined,
|
|
170
|
+
environmentVersion: sandbox.environmentVersion ?? undefined,
|
|
171
|
+
ttlSeconds: sandbox.ttlSeconds ?? undefined,
|
|
172
|
+
labels: userLabels(sandbox.labels),
|
|
173
|
+
setupStatus: sandbox.setupStatus ?? undefined,
|
|
174
|
+
url: undefined,
|
|
175
|
+
urlToken: undefined,
|
|
176
|
+
program: undefined,
|
|
177
|
+
org: sandbox.team ? { id: sandbox.team.id, name: sandbox.team.name } : undefined,
|
|
178
|
+
visibility: sandbox.visibility ?? undefined,
|
|
179
|
+
});
|
|
180
|
+
/** The bindings still attached (a removed binding arrives as a `delete`). */
|
|
181
|
+
const activeBindings = (bindings) => bindings.filter((binding) => binding.action !== "delete");
|
|
182
|
+
/** The program's environment from its bindings. */
|
|
183
|
+
const bindingEnv = (bindings) => activeBindings(bindings).reduce((acc, binding) => ({ ...acc, ...binding.data?.env }), {});
|
|
184
|
+
/**
|
|
185
|
+
* Boat's `ttlSeconds` for `autoStop`: `null` never stops. Without
|
|
186
|
+
* `autoStop`, a program sandbox never stops and others keep Boat's default.
|
|
187
|
+
*/
|
|
188
|
+
const ttlSecondsOf = (autoStop, program) => autoStop === false
|
|
189
|
+
? null
|
|
190
|
+
: autoStop === undefined
|
|
191
|
+
? program
|
|
192
|
+
? null
|
|
193
|
+
: undefined
|
|
194
|
+
: Math.max(1, Math.round(Duration.toMillis(Duration.fromInputUnsafe(autoStop)) / 1000));
|
|
195
|
+
/** Props whose change means a new sandbox (Boat can't change them on a live one). */
|
|
196
|
+
const replacingProps = (props) => JSON.stringify({
|
|
197
|
+
type: props.type ?? "default",
|
|
198
|
+
// A program's env is its process's: changing it restarts the program.
|
|
199
|
+
env: props.main === undefined ? (plainEnv(props.env) ?? {}) : undefined,
|
|
200
|
+
program: props.main !== undefined,
|
|
201
|
+
environment: props.environment === undefined ? undefined : resolved(props.environment).name,
|
|
202
|
+
noEnv: props.noEnv ?? false,
|
|
203
|
+
snapshots: props.snapshots ?? true,
|
|
204
|
+
monitoring: props.monitoring ?? true,
|
|
205
|
+
setupScript: props.setupScript,
|
|
206
|
+
snapshot: props.snapshot === undefined
|
|
207
|
+
? undefined
|
|
208
|
+
: [resolved(props.snapshot).name, resolved(props.snapshot).setupHash ?? null],
|
|
209
|
+
labels: props.labels ?? {},
|
|
210
|
+
});
|
|
211
|
+
export const SandboxProvider = () => Provider.effect(Sandbox, Effect.gen(function* () {
|
|
212
|
+
const stack = yield* Stack;
|
|
213
|
+
// The bundled program's bootstrap reads these to rebuild the Stack at runtime.
|
|
214
|
+
const alchemyEnv = {
|
|
215
|
+
ALCHEMY_STACK_NAME: stack.name,
|
|
216
|
+
ALCHEMY_STAGE: stack.stage,
|
|
217
|
+
ALCHEMY_PHASE: "runtime",
|
|
218
|
+
HOST: "0.0.0.0",
|
|
219
|
+
};
|
|
220
|
+
return {
|
|
221
|
+
stables: ["sandboxId"],
|
|
222
|
+
diff: Effect.fn(function* ({ news, olds, output }) {
|
|
223
|
+
if (olds === undefined)
|
|
224
|
+
return undefined;
|
|
225
|
+
// A program's props are partly unresolved at plan (its bindings'
|
|
226
|
+
// env holds Outputs); the replacing ones are compared once known.
|
|
227
|
+
if (isResolved(news) && replacingProps(olds) !== replacingProps(news)) {
|
|
228
|
+
return { action: "replace" };
|
|
229
|
+
}
|
|
230
|
+
// The bundle depends only on `main`, `handler` and `build`, which
|
|
231
|
+
// are plain. A code change, or a process that ended (the sandbox
|
|
232
|
+
// stopped or rebooted), redeploys the program in place.
|
|
233
|
+
const props = news;
|
|
234
|
+
if (typeof props.main === "string" && output !== undefined) {
|
|
235
|
+
const { hash } = yield* bundleProgram({ ...props, main: props.main });
|
|
236
|
+
if (output.program === undefined || hash !== output.program.codeHash) {
|
|
237
|
+
return { action: "update" };
|
|
238
|
+
}
|
|
239
|
+
if (!(yield* isProgramRunning(output.sandboxId, output.program))) {
|
|
240
|
+
return { action: "update" };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}),
|
|
245
|
+
read: Effect.fn(function* ({ id, output }) {
|
|
246
|
+
if (output === undefined)
|
|
247
|
+
return undefined;
|
|
248
|
+
const sandbox = yield* observeSandbox(output.sandboxId);
|
|
249
|
+
if (sandbox === undefined)
|
|
250
|
+
return undefined;
|
|
251
|
+
// The program isn't visible on the sandbox; keep what the last deploy recorded.
|
|
252
|
+
const attrs = {
|
|
253
|
+
...toAttributes(sandbox),
|
|
254
|
+
url: output.url,
|
|
255
|
+
urlToken: output.urlToken,
|
|
256
|
+
program: output.program,
|
|
257
|
+
};
|
|
258
|
+
return (yield* hasOwnershipLabels(id, sandbox.labels)) ? attrs : Unowned(attrs);
|
|
259
|
+
}),
|
|
260
|
+
lookup: Effect.fn(function* ({ sandboxId }) {
|
|
261
|
+
const sandbox = yield* observeSandbox(sandboxId);
|
|
262
|
+
return sandbox === undefined ? undefined : toAttributes(sandbox);
|
|
263
|
+
}),
|
|
264
|
+
reconcile: Effect.fn(function* ({ id, news, output, bindings, session }) {
|
|
265
|
+
const name = yield* boatName(id, news.name ?? output?.name, 120);
|
|
266
|
+
const owner = yield* ownershipLabels(id);
|
|
267
|
+
const program = news.main !== undefined;
|
|
268
|
+
// A program serves for as long as the sandbox runs: don't stop it on a timer.
|
|
269
|
+
const ttlSeconds = ttlSecondsOf(news.autoStop, program);
|
|
270
|
+
// Observe — `output` caches the id; the sandbox may be gone.
|
|
271
|
+
let sandbox = output === undefined ? undefined : yield* observeSandbox(output.sandboxId);
|
|
272
|
+
// Ensure
|
|
273
|
+
if (sandbox === undefined) {
|
|
274
|
+
yield* session.note(`Creating sandbox ${name}...`);
|
|
275
|
+
const created = yield* boat.createSandbox({
|
|
276
|
+
...(news.type === undefined ? {} : { type: news.type }),
|
|
277
|
+
...(ttlSeconds === undefined ? {} : { ttlSeconds }),
|
|
278
|
+
// A program's env is its process's, written at deploy, not the sandbox's.
|
|
279
|
+
...(news.env === undefined || program ? {} : { env: plainEnv(news.env) }),
|
|
280
|
+
...(news.environment === undefined
|
|
281
|
+
? {}
|
|
282
|
+
: { environment: resolved(news.environment).name }),
|
|
283
|
+
...(news.noEnv === undefined ? {} : { noEnv: news.noEnv }),
|
|
284
|
+
...(news.snapshots === undefined ? {} : { snapshots: news.snapshots }),
|
|
285
|
+
...(news.monitoring === undefined ? {} : { monitoring: news.monitoring }),
|
|
286
|
+
...(news.setupScript === undefined ? {} : { setupScript: news.setupScript }),
|
|
287
|
+
...(news.snapshot === undefined ? {} : { from: resolved(news.snapshot).name }),
|
|
288
|
+
...(news.visibility === undefined ? {} : { visibility: news.visibility }),
|
|
289
|
+
labels: { ...news.labels, ...owner },
|
|
290
|
+
});
|
|
291
|
+
sandbox = created.sandbox;
|
|
292
|
+
}
|
|
293
|
+
// Sync — a stopped program sandbox is resumed, so the program can run.
|
|
294
|
+
if (program && sandbox.state === "archived") {
|
|
295
|
+
yield* session.note(`Resuming sandbox ${name}...`);
|
|
296
|
+
const resumed = yield* boat.resumeSandbox({ sandboxId: sandbox.id });
|
|
297
|
+
sandbox = resumed.sandbox ?? sandbox;
|
|
298
|
+
}
|
|
299
|
+
// Sync — name, subdomain and auto-stop, against what Boat reports.
|
|
300
|
+
const ttlChanged = ttlSeconds !== undefined && (sandbox.ttlSeconds ?? null) !== ttlSeconds;
|
|
301
|
+
const subdomainChanged = news.subdomain !== undefined && sandbox.subdomain !== news.subdomain;
|
|
302
|
+
if (sandbox.name !== name || ttlChanged || subdomainChanged) {
|
|
303
|
+
const updated = yield* boat.updateSandbox({
|
|
304
|
+
sandboxId: sandbox.id,
|
|
305
|
+
...(sandbox.name !== name ? { name } : {}),
|
|
306
|
+
...(ttlChanged ? { ttlSeconds } : {}),
|
|
307
|
+
...(subdomainChanged ? { subdomain: news.subdomain } : {}),
|
|
308
|
+
});
|
|
309
|
+
sandbox = updated.sandbox ?? sandbox;
|
|
310
|
+
}
|
|
311
|
+
// Sync — visibility, against what Boat reports.
|
|
312
|
+
if (news.visibility !== undefined &&
|
|
313
|
+
(sandbox.visibility ?? "private") !== news.visibility) {
|
|
314
|
+
yield* session.note(`Making sandbox ${name} ${news.visibility === "shared" ? "shared" : "private"}...`);
|
|
315
|
+
if (news.visibility === "shared") {
|
|
316
|
+
const shared = yield* boat.shareSandbox({ sandboxId: sandbox.id });
|
|
317
|
+
if (shared.restartRequired) {
|
|
318
|
+
yield* session.note(`Sandbox ${name} is shared from its next start: stop and resume it to share it now.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
yield* boat.unshareSandbox({ sandboxId: sandbox.id });
|
|
323
|
+
}
|
|
324
|
+
sandbox = (yield* observeSandbox(sandbox.id)) ?? sandbox;
|
|
325
|
+
}
|
|
326
|
+
const attrs = toAttributes(sandbox);
|
|
327
|
+
if (news.main === undefined)
|
|
328
|
+
return attrs;
|
|
329
|
+
// Deploy the program into the running sandbox, then host its port.
|
|
330
|
+
const port = news.port ?? DEFAULT_PORT;
|
|
331
|
+
const state = yield* deployProgram({
|
|
332
|
+
id,
|
|
333
|
+
sandboxId: sandbox.id,
|
|
334
|
+
source: { ...news, main: news.main },
|
|
335
|
+
port,
|
|
336
|
+
env: { ...bindingEnv(bindings), ...alchemyEnv, PORT: String(port), ...news.env },
|
|
337
|
+
// A replaced sandbox runs nothing yet.
|
|
338
|
+
previous: output?.sandboxId === sandbox.id ? output.program : undefined,
|
|
339
|
+
note: session.note,
|
|
340
|
+
});
|
|
341
|
+
const hosted = yield* boat.hostPort({
|
|
342
|
+
sandboxId: sandbox.id,
|
|
343
|
+
port,
|
|
344
|
+
public: news.public ?? false,
|
|
345
|
+
});
|
|
346
|
+
const parsed = hosted.url === undefined ? undefined : new URL(hosted.url);
|
|
347
|
+
const token = parsed?.searchParams.get("_token") ?? undefined;
|
|
348
|
+
return {
|
|
349
|
+
...attrs,
|
|
350
|
+
url: parsed?.origin,
|
|
351
|
+
urlToken: token === undefined ? undefined : Redacted.make(token),
|
|
352
|
+
program: state,
|
|
353
|
+
};
|
|
354
|
+
}),
|
|
355
|
+
delete: Effect.fn(function* ({ output }) {
|
|
356
|
+
yield* deleteSandbox(output.sandboxId);
|
|
357
|
+
}),
|
|
358
|
+
// Declared sandboxes and the ones bindings create at run time both
|
|
359
|
+
// carry `alchemy.stack`, so `nuke` finds both.
|
|
360
|
+
list: Effect.fn(function* () {
|
|
361
|
+
const all = yield* Stream.runCollect(boat.listSandboxes.items({ limit: 100 }));
|
|
362
|
+
return Array.from(all)
|
|
363
|
+
.filter((sandbox) => sandbox.state !== "cancelled" && hasAnyAlchemyLabel(sandbox.labels))
|
|
364
|
+
.map(toAttributes);
|
|
365
|
+
}),
|
|
366
|
+
};
|
|
367
|
+
}));
|
|
368
|
+
//# sourceMappingURL=Sandbox.js.map
|