@computesdk/boat 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 computesdk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,96 @@
1
- # Temporary Holding Version
1
+ # @computesdk/boat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Boat provider for ComputeSDK: Ubuntu cloud sandboxes with streaming commands, signed file transfers, named snapshots and templates, and HTTPS preview URLs.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install computesdk @computesdk/provider @computesdk/boat
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Create an API key in the [Boat dashboard](https://boat.dev/dashboard?tab=api-keys) and export it:
14
+
15
+ ```bash
16
+ export BOAT_API_KEY=boat_...
17
+ # Optional: custom API endpoint (defaults to https://boat.dev/api/v1)
18
+ export BOAT_API_BASE=https://boat.dev/api/v1
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```typescript
24
+ import { createCompute } from '@computesdk/provider';
25
+ import { boat } from '@computesdk/boat';
26
+
27
+ const compute = createCompute({ provider: boat({}) });
28
+ const sandbox = await compute.instances.create({ type: 'small' });
29
+
30
+ try {
31
+ const result = await sandbox.runCommand('echo "Hello from Boat"');
32
+ console.log(result.stdout);
33
+ await sandbox.filesystem.writeFile('/tmp/hello.txt', 'Hello from Boat');
34
+ console.log(await sandbox.filesystem.readFile('/tmp/hello.txt'));
35
+ } finally {
36
+ await sandbox.destroy();
37
+ }
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ | Option | Meaning |
43
+ | --- | --- |
44
+ | `apiKey` | API key; falls back to `BOAT_API_KEY` |
45
+ | `baseURL` | API URL; falls back to `BOAT_API_BASE`, then `https://boat.dev/api/v1` |
46
+ | `timeout` | Default sandbox lifetime in milliseconds; omitted uses Boat's one-hour default |
47
+ | `readyTimeout` | Provisioning wait deadline in milliseconds; defaults to 300000 |
48
+ | `snapshotTimeout` | Named snapshot save deadline in milliseconds; defaults to 600000 |
49
+ | `preview` | Default preview settings: `public`, `localhost`, and `title` |
50
+ | `createOptions` | Native Boat creation defaults, such as `type`, `environment`, `noEnv`, `from`, `org`, and `ttlSeconds` |
51
+
52
+ ## Features
53
+
54
+ - Sandbox create, get, list, and permanent destroy
55
+ - Foreground commands with reconnecting output streams, optional timeouts, and remote cleanup; background commands
56
+ - Interactive processes through the shared daemon API
57
+ - Filesystem access, with signed binary transfers under `/home/user` and `/tmp` (`readBoatFile`, `writeBoatFile`)
58
+ - Named snapshots and templates through `provider.snapshot` and `provider.template`
59
+ - Token-protected HTTPS preview URLs, with typed controls through `getBoatUrl`
60
+
61
+ ## Limitations
62
+
63
+ - Fixed machine types: CPU, memory, disk, GPU, image, region, and arbitrary metadata options are rejected
64
+ - Preview URLs are HTTPS only
65
+ - Snapshots keep disk changes, not process memory; `/tmp` is excluded
66
+ - Setup scripts run in the background, so readiness does not mean setup has finished
67
+
68
+ See the [Boat provider guide](https://github.com/computesdk/computesdk/blob/main/docs/providers/boat.md) for details and examples.
69
+
70
+ ## Development
71
+
72
+ For local live tests, put `BOAT_API_KEY=boat_...` in the repository root `.env` file, which Git ignores. Tests use the live Boat API whenever `BOAT_API_KEY` is present.
73
+
74
+ ```bash
75
+ # Unit tests and real-SDK/local-HTTP transport tests; no cloud resources.
76
+ pnpm --filter @computesdk/boat test:unit
77
+
78
+ # Shared conformance, shared CRUD, and Boat-specific integration tests.
79
+ # BOAT_API_KEY in the environment or root .env enables live calls.
80
+ pnpm --filter @computesdk/boat test:integration
81
+
82
+ # All tests (also live when credentials are available).
83
+ pnpm --filter @computesdk/boat test
84
+ pnpm --filter @computesdk/boat typecheck
85
+ pnpm --filter @computesdk/boat lint
86
+ ```
87
+
88
+ `index.test.ts` uses `runProviderTestSuite` and `crud.test.ts` uses `runProviderCrudTest`. The shared contract checks commands, filesystem, streaming, and interactive processes; its baseline tests use shared mocks without credentials. CRUD and Boat-specific integration tests skip without credentials. No separate live opt-in flag is required.
89
+
90
+ `unit.test.ts` uses SDK mocks; `transport.test.ts` uses the real Boat SDK against a local HTTP server. The separate `integration.test.ts` covers signed large-text/binary transfers, protected/public localhost preview routing, and named snapshot restore. Test files run sequentially to avoid concurrent account-quota pressure. The shared conformance entry point also spaces sandbox starts to stay below Boat's standard 12-starts-per-minute limit; accounts with lower limits may need slower pacing.
91
+
92
+ Credentialed runs create multiple small billable sandboxes. Shared suite hooks clean up their instances; the Boat-specific test attempts cleanup of both sandboxes and its named snapshot in a finally block. To run every suite without cloud access even when root .env contains a key, use `BOAT_API_KEY= pnpm --filter @computesdk/boat test`.
93
+
94
+ ## License
95
+
96
+ MIT
@@ -0,0 +1,64 @@
1
+ import { NamedSnapshot, CreateSandboxRequest, BoatApi } from '@boatdev/sdk';
2
+ export { CreateSandboxRequest as BoatCreateOptions } from '@boatdev/sdk';
3
+ import { CreateTemplateOptions, Provider, ProviderSnapshotManager, ProviderTemplateManager } from '@computesdk/provider';
4
+
5
+ interface BoatSnapshot extends NamedSnapshot {
6
+ /** Reusable snapshot name; the native snapshotId is a different, non-bootable artifact ID. */
7
+ id: string;
8
+ provider: 'boat';
9
+ }
10
+ interface BoatTemplate extends BoatSnapshot {
11
+ templateId: string;
12
+ }
13
+
14
+ /** Binary download. Signed URLs support files under /home/user and /tmp. */
15
+ declare function readBoatFile(sandbox: BoatSandbox, path: string): Promise<Uint8Array>;
16
+ /** Binary or UTF-8 upload, without embedding the file in a command argument. */
17
+ declare function writeBoatFile(sandbox: BoatSandbox, path: string, content: string | Uint8Array): Promise<void>;
18
+
19
+ interface BoatConfig {
20
+ /** Falls back to BOAT_API_KEY. */
21
+ apiKey?: string;
22
+ /** Falls back to BOAT_API_BASE, then https://boat.dev/api/v1. */
23
+ baseURL?: string;
24
+ /** Default sandbox lifetime in milliseconds. Boat defaults to one hour. */
25
+ timeout?: number;
26
+ /** Provisioning deadline in milliseconds (default: 300000). */
27
+ readyTimeout?: number;
28
+ /** Native defaults, including type, environment, noEnv, and from. */
29
+ createOptions?: CreateSandboxRequest;
30
+ /** Named snapshot save deadline in milliseconds (default: 600000). */
31
+ snapshotTimeout?: number;
32
+ /** Default HTTPS preview settings. */
33
+ preview?: BoatPreviewDefaults;
34
+ }
35
+ /** Boat has an account-level client; retain it alongside the sandbox ID. */
36
+ interface BoatSandbox {
37
+ api: BoatApi;
38
+ id: string;
39
+ /** Boat's sandbox name, so callers can match list() results without a getInfo() call each. */
40
+ name: string;
41
+ preview?: BoatPreviewDefaults;
42
+ }
43
+ interface BoatPreviewDefaults {
44
+ public?: boolean;
45
+ localhost?: boolean;
46
+ title?: string;
47
+ }
48
+ interface BoatPreviewOptions extends BoatPreviewDefaults {
49
+ port: number;
50
+ protocol?: string;
51
+ }
52
+ /** Typed preview controls beyond the portable getUrl interface. */
53
+ declare function getBoatUrl(sandbox: BoatSandbox, options: BoatPreviewOptions): Promise<string>;
54
+ interface BoatCreateTemplateOptions extends CreateTemplateOptions {
55
+ /** Sandbox whose disk to freeze as a reusable named template. */
56
+ sandboxId: string;
57
+ }
58
+ interface BoatProvider extends Provider<BoatSandbox, BoatTemplate, BoatSnapshot> {
59
+ snapshot: ProviderSnapshotManager<BoatSnapshot>;
60
+ template: ProviderTemplateManager<BoatTemplate, BoatCreateTemplateOptions>;
61
+ }
62
+ declare function boat(config?: BoatConfig): BoatProvider;
63
+
64
+ export { type BoatConfig, type BoatCreateTemplateOptions, type BoatPreviewDefaults, type BoatPreviewOptions, type BoatProvider, type BoatSandbox, type BoatSnapshot, type BoatTemplate, boat, getBoatUrl, readBoatFile, writeBoatFile };
@@ -0,0 +1,64 @@
1
+ import { NamedSnapshot, CreateSandboxRequest, BoatApi } from '@boatdev/sdk';
2
+ export { CreateSandboxRequest as BoatCreateOptions } from '@boatdev/sdk';
3
+ import { CreateTemplateOptions, Provider, ProviderSnapshotManager, ProviderTemplateManager } from '@computesdk/provider';
4
+
5
+ interface BoatSnapshot extends NamedSnapshot {
6
+ /** Reusable snapshot name; the native snapshotId is a different, non-bootable artifact ID. */
7
+ id: string;
8
+ provider: 'boat';
9
+ }
10
+ interface BoatTemplate extends BoatSnapshot {
11
+ templateId: string;
12
+ }
13
+
14
+ /** Binary download. Signed URLs support files under /home/user and /tmp. */
15
+ declare function readBoatFile(sandbox: BoatSandbox, path: string): Promise<Uint8Array>;
16
+ /** Binary or UTF-8 upload, without embedding the file in a command argument. */
17
+ declare function writeBoatFile(sandbox: BoatSandbox, path: string, content: string | Uint8Array): Promise<void>;
18
+
19
+ interface BoatConfig {
20
+ /** Falls back to BOAT_API_KEY. */
21
+ apiKey?: string;
22
+ /** Falls back to BOAT_API_BASE, then https://boat.dev/api/v1. */
23
+ baseURL?: string;
24
+ /** Default sandbox lifetime in milliseconds. Boat defaults to one hour. */
25
+ timeout?: number;
26
+ /** Provisioning deadline in milliseconds (default: 300000). */
27
+ readyTimeout?: number;
28
+ /** Native defaults, including type, environment, noEnv, and from. */
29
+ createOptions?: CreateSandboxRequest;
30
+ /** Named snapshot save deadline in milliseconds (default: 600000). */
31
+ snapshotTimeout?: number;
32
+ /** Default HTTPS preview settings. */
33
+ preview?: BoatPreviewDefaults;
34
+ }
35
+ /** Boat has an account-level client; retain it alongside the sandbox ID. */
36
+ interface BoatSandbox {
37
+ api: BoatApi;
38
+ id: string;
39
+ /** Boat's sandbox name, so callers can match list() results without a getInfo() call each. */
40
+ name: string;
41
+ preview?: BoatPreviewDefaults;
42
+ }
43
+ interface BoatPreviewDefaults {
44
+ public?: boolean;
45
+ localhost?: boolean;
46
+ title?: string;
47
+ }
48
+ interface BoatPreviewOptions extends BoatPreviewDefaults {
49
+ port: number;
50
+ protocol?: string;
51
+ }
52
+ /** Typed preview controls beyond the portable getUrl interface. */
53
+ declare function getBoatUrl(sandbox: BoatSandbox, options: BoatPreviewOptions): Promise<string>;
54
+ interface BoatCreateTemplateOptions extends CreateTemplateOptions {
55
+ /** Sandbox whose disk to freeze as a reusable named template. */
56
+ sandboxId: string;
57
+ }
58
+ interface BoatProvider extends Provider<BoatSandbox, BoatTemplate, BoatSnapshot> {
59
+ snapshot: ProviderSnapshotManager<BoatSnapshot>;
60
+ template: ProviderTemplateManager<BoatTemplate, BoatCreateTemplateOptions>;
61
+ }
62
+ declare function boat(config?: BoatConfig): BoatProvider;
63
+
64
+ export { type BoatConfig, type BoatCreateTemplateOptions, type BoatPreviewDefaults, type BoatPreviewOptions, type BoatProvider, type BoatSandbox, type BoatSnapshot, type BoatTemplate, boat, getBoatUrl, readBoatFile, writeBoatFile };
package/dist/index.js ADDED
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ boat: () => boat,
24
+ getBoatUrl: () => getBoatUrl,
25
+ readBoatFile: () => readBoatFile,
26
+ writeBoatFile: () => writeBoatFile
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_sdk2 = require("@boatdev/sdk");
30
+ var import_provider3 = require("@computesdk/provider");
31
+
32
+ // src/commands.ts
33
+ var import_sdk = require("@boatdev/sdk");
34
+ var import_provider = require("@computesdk/provider");
35
+ function commandText(command, options) {
36
+ let text = command;
37
+ if (options?.env && Object.keys(options.env).length > 0) {
38
+ const env = Object.entries(options.env).map(([key, value]) => {
39
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);
40
+ return `"${(0, import_provider.escapeShellArg)(`${key}=${value}`)}"`;
41
+ }).join(" ");
42
+ text = `env ${env} /bin/sh -c "${(0, import_provider.escapeShellArg)(command)}"`;
43
+ }
44
+ return options?.cwd ? `cd -- "${(0, import_provider.escapeShellArg)(options.cwd)}" || exit
45
+ ${text}` : text;
46
+ }
47
+ async function run(sandbox, command, options) {
48
+ const start = Date.now();
49
+ const text = commandText(command, options);
50
+ if (options?.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout < 0)) {
51
+ throw new Error("Boat command timeout must be non-negative milliseconds; 0 means unlimited.");
52
+ }
53
+ if (options?.background) {
54
+ await sandbox.api.command({ sandboxId: sandbox.id, commandRequest: { command: text, detached: true } });
55
+ return { stdout: "", stderr: "", exitCode: 0, durationMs: Date.now() - start };
56
+ }
57
+ const controller = new AbortController();
58
+ let timer;
59
+ let processId;
60
+ let timedOut = false;
61
+ let stdout = "";
62
+ let stderr = "";
63
+ const result = (exitCode) => ({ stdout, stderr, exitCode, durationMs: Date.now() - start });
64
+ try {
65
+ for await (const frame of (0, import_sdk.runCommand)(sandbox.api, sandbox.id, text, { signal: controller.signal })) {
66
+ if (frame.type === "started") {
67
+ if (typeof frame.processId !== "number") throw new Error("Boat did not return a process ID.");
68
+ processId = frame.processId;
69
+ if (options?.timeout) {
70
+ const deadline = Date.now() + options.timeout;
71
+ const expire = () => {
72
+ const remaining = deadline - Date.now();
73
+ if (remaining <= 0) {
74
+ timedOut = true;
75
+ controller.abort();
76
+ } else timer = setTimeout(expire, Math.min(remaining, 2147483647));
77
+ };
78
+ expire();
79
+ }
80
+ }
81
+ if (frame.type === "stdout") {
82
+ stdout += frame.data ?? "";
83
+ options?.onStdout?.(frame.data ?? "");
84
+ }
85
+ if (frame.type === "stderr") {
86
+ stderr += frame.data ?? "";
87
+ options?.onStderr?.(frame.data ?? "");
88
+ }
89
+ if (frame.type === "error") throw new Error(frame.message || frame.error || "Boat command stream failed.");
90
+ if (frame.type === "exit" && !timedOut) return result(frame.timedOut ? 124 : frame.exitCode ?? 1);
91
+ }
92
+ throw new Error("Boat command stream ended without an exit frame.");
93
+ } catch (error) {
94
+ clearTimeout(timer);
95
+ if (processId !== void 0) {
96
+ try {
97
+ await sandbox.api.stopCommand({ sandboxId: sandbox.id, processId });
98
+ } catch (cleanupError) {
99
+ if (cleanupError?.response?.status !== 404) {
100
+ const reason = timedOut ? "timeout" : `stream failure (${error instanceof Error ? error.message : String(error)})`;
101
+ throw new Error(`Failed to stop Boat process ${processId} after ${reason}: ${String(cleanupError)}`);
102
+ }
103
+ }
104
+ }
105
+ if (timedOut) return result(124);
106
+ throw error;
107
+ } finally {
108
+ clearTimeout(timer);
109
+ }
110
+ }
111
+ async function checked(sandbox, command) {
112
+ const result = await run(sandbox, command);
113
+ if (result.exitCode !== 0) throw new Error(`Boat filesystem operation failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
114
+ return result.stdout;
115
+ }
116
+
117
+ // src/files.ts
118
+ var import_node_path = require("path");
119
+ var import_provider2 = require("@computesdk/provider");
120
+ function signedPath(path) {
121
+ const absolute = import_node_path.posix.resolve("/home/user", path);
122
+ return absolute.startsWith("/home/user/") || absolute.startsWith("/tmp/");
123
+ }
124
+ async function readBoatFile(sandbox, path) {
125
+ if (!signedPath(path)) {
126
+ return new Uint8Array(Buffer.from(await checked(sandbox, `base64 < "${(0, import_provider2.escapeShellArg)(path)}"`), "base64"));
127
+ }
128
+ const signed = await sandbox.api.createDownloadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });
129
+ const response = await fetch(signed.url);
130
+ if (!response.ok) throw new Error(`Boat file download failed (HTTP ${response.status}).`);
131
+ return new Uint8Array(await response.arrayBuffer());
132
+ }
133
+ async function writeBoatFile(sandbox, path, content) {
134
+ if (!signedPath(path)) {
135
+ const encoded = Buffer.from(content).toString("base64");
136
+ await checked(sandbox, `: > "${(0, import_provider2.escapeShellArg)(path)}"`);
137
+ for (let offset = 0; offset < encoded.length; offset += 48 * 1024) {
138
+ await checked(sandbox, `printf %s "${encoded.slice(offset, offset + 48 * 1024)}" | base64 -d >> "${(0, import_provider2.escapeShellArg)(path)}"`);
139
+ }
140
+ return;
141
+ }
142
+ const signed = await sandbox.api.createUploadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });
143
+ const response = await fetch(signed.url, { method: signed.method, body: Buffer.from(content) });
144
+ if (!response.ok) throw new Error(`Boat file upload failed (HTTP ${response.status}).`);
145
+ await response.body?.cancel();
146
+ }
147
+
148
+ // src/snapshots.ts
149
+ var import_node_crypto = require("crypto");
150
+ function snapshotInfo(snapshot) {
151
+ return { ...snapshot, id: snapshot.name, provider: "boat" };
152
+ }
153
+ function templateInfo(snapshot) {
154
+ return { ...snapshot, templateId: snapshot.name };
155
+ }
156
+ async function saveSnapshot(api, config, sandboxId, options) {
157
+ if (options?.metadata !== void 0) throw new Error("Boat named snapshots do not support arbitrary metadata.");
158
+ const name = options?.name ?? `compute-${(0, import_node_crypto.randomUUID)()}`;
159
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name) || ["latest", "tree", "pull", "rm", "save", "current", "self", "new"].includes(name)) {
160
+ throw new Error("Boat snapshot names must be 1-63 lowercase letters, digits, or dashes, start with a letter or digit, and not be reserved.");
161
+ }
162
+ const timeout = config.snapshotTimeout ?? 6e5;
163
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("Boat snapshotTimeout must be positive milliseconds.");
164
+ const deadline = Date.now() + timeout;
165
+ let snapshot = (await api.saveNamedSnapshot({ namedSnapshotSaveRequest: { sandboxId, name } })).snapshot;
166
+ while (snapshot.status !== "ready") {
167
+ if (snapshot.status === "failed") throw new Error(`Boat snapshot ${name} failed: ${snapshot.error ?? "unknown error"}`);
168
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat snapshot ${name}; inspect or delete it through the snapshot manager.`);
169
+ await new Promise((resolve) => setTimeout(resolve, Math.min(2e3, Math.max(0, deadline - Date.now()))));
170
+ snapshot = (await api.getNamedSnapshot({ name })).snapshot;
171
+ }
172
+ return snapshotInfo(snapshot);
173
+ }
174
+ function limitItems(items, limit) {
175
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new Error("Boat list limit must be a non-negative integer.");
176
+ return limit === void 0 ? items : items.slice(0, limit);
177
+ }
178
+ async function listSnapshots(api, options) {
179
+ limitItems([], options?.limit);
180
+ if (options?.limit === 0) return [];
181
+ const sandboxId = options?.instanceId ?? options?.sandboxId;
182
+ return limitItems((await api.listNamedSnapshots()).snapshots.filter((snapshot) => sandboxId === void 0 || snapshot.sourceSandboxId === sandboxId).map(snapshotInfo), options?.limit);
183
+ }
184
+
185
+ // src/index.ts
186
+ var BoatClient = class extends import_sdk2.BoatApi {
187
+ async request(context, initOverrides) {
188
+ try {
189
+ return await super.request(context, initOverrides);
190
+ } catch (error) {
191
+ if (!(error instanceof import_sdk2.ResponseError)) throw error;
192
+ const { response } = error;
193
+ const text = (await response.clone().text().catch(() => "")).replace(/\s+/g, " ").trim();
194
+ let detail = text.slice(0, 300);
195
+ try {
196
+ const body = JSON.parse(text);
197
+ const parts = [body.error, body.message].filter((part) => typeof part === "string" && part !== "");
198
+ if (parts.length > 0) detail = parts.join(": ").slice(0, 300);
199
+ } catch {
200
+ }
201
+ throw new import_sdk2.ResponseError(response, `Boat API error: ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`);
202
+ }
203
+ }
204
+ };
205
+ function client(config) {
206
+ const apiKey = config.apiKey || process.env.BOAT_API_KEY;
207
+ if (!apiKey) throw new Error("Missing Boat API key. Provide 'apiKey' or set BOAT_API_KEY. Create a key at https://boat.dev/dashboard?tab=api-keys");
208
+ return new BoatClient(new import_sdk2.Configuration({
209
+ accessToken: apiKey,
210
+ basePath: config.baseURL || process.env.BOAT_API_BASE || "https://boat.dev/api/v1"
211
+ }));
212
+ }
213
+ function seconds(ms) {
214
+ if (!Number.isFinite(ms) || ms <= 0) throw new Error("Boat timeout must be a positive number of milliseconds.");
215
+ return Math.ceil(ms / 1e3);
216
+ }
217
+ function isNotFound(error) {
218
+ return error?.response?.status === 404;
219
+ }
220
+ async function waitUntilReady(api, sandboxId, timeoutMs, signal) {
221
+ const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
222
+ for (; ; ) {
223
+ if (signal?.aborted) throw signal.reason ?? new Error("Boat creation aborted.");
224
+ const { state } = (await api.get({ sandboxId })).sandbox;
225
+ if (["ready", "idle", "running"].includes(state)) return;
226
+ if (["archived", "archiving", "error", "cancelled"].includes(state)) throw new Error(`Boat sandbox ${sandboxId} entered terminal state ${state}.`);
227
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat sandbox ${sandboxId} to become ready.`);
228
+ await new Promise((resolve) => setTimeout(resolve, Math.min(2e3, deadline - Date.now())));
229
+ }
230
+ }
231
+ function wrap(api, sandbox, preview) {
232
+ return { instance: { api, id: sandbox.id, name: sandbox.name, ...preview ? { preview } : {} }, instanceId: sandbox.id };
233
+ }
234
+ async function getBoatUrl(sandbox, options) {
235
+ if (options.protocol && options.protocol !== "https") throw new Error("Boat preview URLs support HTTPS only.");
236
+ const settings = { ...sandbox.preview, ...options };
237
+ const hostPortRequest = {
238
+ port: settings.port,
239
+ ...settings.public === void 0 ? {} : { _public: settings.public },
240
+ ...settings.localhost === void 0 ? {} : { localhost: settings.localhost },
241
+ ...settings.title === void 0 ? {} : { title: settings.title }
242
+ };
243
+ const result = await sandbox.api.hostPort({ sandboxId: sandbox.id, hostPortRequest });
244
+ if (!result.url) throw new Error("Boat did not return a preview URL.");
245
+ return result.url;
246
+ }
247
+ var createBoatProvider = (0, import_provider3.defineProvider)({
248
+ name: "boat",
249
+ methods: {
250
+ instances: {
251
+ create: async (config, options) => {
252
+ if (options?.templateId && options?.snapshotId) throw new Error("Specify either templateId or snapshotId, not both.");
253
+ if (options?.metadata !== void 0) throw new Error("Boat does not support arbitrary instance metadata.");
254
+ for (const key of ["cpu", "cpuLimit", "cpus", "vcpus", "memory", "memoryMiB", "memMiB", "memoryMb", "diskMiB", "diskMb", "diskSize", "resources", "launch_parameters", "deploymentPlan", "size", "vmTier", "gpu", "image", "region", "architecture"]) {
255
+ if (options?.[key] !== void 0) throw new Error(`Boat does not support ${key}; select a fixed machine type with type.`);
256
+ }
257
+ const api = client(config);
258
+ const lifetime = options?.timeout ?? (options?.ttlSeconds === void 0 ? config.timeout : void 0);
259
+ const defaults = config.createOptions;
260
+ const request = {
261
+ ...defaults,
262
+ type: options?.type ?? defaults?.type,
263
+ environment: options?.environment ?? defaults?.environment,
264
+ noEnv: options?.noEnv ?? defaults?.noEnv,
265
+ snapshots: options?.snapshots ?? defaults?.snapshots,
266
+ monitoring: options?.monitoring ?? defaults?.monitoring,
267
+ failFast: options?.failFast ?? defaults?.failFast,
268
+ setupScript: options?.setupScript ?? defaults?.setupScript,
269
+ org: options?.org ?? options?.teamId ?? defaults?.org,
270
+ from: options?.snapshotId ?? options?.templateId ?? options?.from ?? defaults?.from,
271
+ env: options?.envs ?? options?.env ?? defaults?.env,
272
+ ttlSeconds: lifetime === void 0 ? options?.ttlSeconds === void 0 ? defaults?.ttlSeconds : options.ttlSeconds : seconds(lifetime)
273
+ };
274
+ if (options?.signal?.aborted) throw options.signal.reason ?? new Error("Boat creation aborted.");
275
+ const { sandbox } = await api.create({ createSandboxRequest: request });
276
+ try {
277
+ await waitUntilReady(api, sandbox.id, config.readyTimeout ?? 3e5, options?.signal);
278
+ if (options?.signal?.aborted) throw options.signal.reason ?? new Error("Boat creation aborted.");
279
+ if (options?.name) await api.update({ sandboxId: sandbox.id, updateSandboxRequest: { name: options.name } });
280
+ return wrap(api, options?.name ? { ...sandbox, name: options.name } : sandbox, config.preview);
281
+ } catch (error) {
282
+ try {
283
+ await api.deleteSandbox({ sandboxId: sandbox.id, xAsciiConfirmDelete: sandbox.id });
284
+ } catch (cleanupError) {
285
+ if (isNotFound(cleanupError)) throw error;
286
+ throw new Error(`Boat creation failed (${String(error)}); cleanup of ${sandbox.id} also failed: ${String(cleanupError)}`);
287
+ }
288
+ throw error;
289
+ }
290
+ },
291
+ getById: async (config, sandboxId) => {
292
+ const api = client(config);
293
+ try {
294
+ return wrap(api, (await api.get({ sandboxId })).sandbox, config.preview);
295
+ } catch (error) {
296
+ if (isNotFound(error)) return null;
297
+ throw error;
298
+ }
299
+ },
300
+ list: async (config) => {
301
+ const api = client(config);
302
+ const items = [];
303
+ let cursor;
304
+ do {
305
+ const page = await api.sandboxes({ cursor });
306
+ items.push(...page.sandboxes.filter((s) => ["ready", "idle", "running"].includes(s.state)).map((s) => wrap(api, s, config.preview)));
307
+ cursor = page.pageInfo?.hasMore ? page.pageInfo.nextCursor ?? void 0 : void 0;
308
+ } while (cursor);
309
+ return items;
310
+ },
311
+ destroy: async (config, sandboxId) => {
312
+ try {
313
+ await client(config).deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId });
314
+ } catch (error) {
315
+ if (!isNotFound(error)) throw error;
316
+ }
317
+ },
318
+ runCommand: run,
319
+ streamCommand: run,
320
+ getInfo: async (sandbox) => {
321
+ const info = (await sandbox.api.get({ sandboxId: sandbox.id })).sandbox;
322
+ return {
323
+ id: info.id,
324
+ provider: "boat",
325
+ status: ["error", "cancelled"].includes(info.state) ? "error" : ["archived", "archiving"].includes(info.state) ? "stopped" : "running",
326
+ createdAt: info.createdAt ?? /* @__PURE__ */ new Date(0),
327
+ timeout: info.ttlSeconds == null ? 0 : info.ttlSeconds * 1e3,
328
+ metadata: { name: info.name, state: info.state, type: info.type, ip: info.ip, environment: info.environment, archiveAfter: info.archiveAfter }
329
+ };
330
+ },
331
+ getUrl: getBoatUrl,
332
+ filesystem: {
333
+ readFile: async (sandbox, path) => Buffer.from(await readBoatFile(sandbox, path)).toString("utf8"),
334
+ writeFile: writeBoatFile,
335
+ mkdir: async (sandbox, path) => {
336
+ await checked(sandbox, `mkdir -p -- "${(0, import_provider3.escapeShellArg)(path)}"`);
337
+ },
338
+ readdir: async (sandbox, path) => {
339
+ const script = 'import os,json,sys; p=sys.argv[1]; print(json.dumps([dict(name=e.name,type="directory" if e.is_dir() else "file",size=e.stat(follow_symlinks=False).st_size,modified=e.stat(follow_symlinks=False).st_mtime) for e in os.scandir(p)]))';
340
+ const entries = JSON.parse(await checked(sandbox, `python3 -c "${(0, import_provider3.escapeShellArg)(script)}" "${(0, import_provider3.escapeShellArg)(path)}"`));
341
+ return entries.map((entry) => ({ ...entry, modified: new Date(entry.modified * 1e3) }));
342
+ },
343
+ exists: async (sandbox, path) => {
344
+ const result = await run(sandbox, `test -e "${(0, import_provider3.escapeShellArg)(path)}"`);
345
+ if (result.exitCode > 1) throw new Error(`Boat filesystem exists failed: ${result.stderr}`);
346
+ return result.exitCode === 0;
347
+ },
348
+ remove: async (sandbox, path) => {
349
+ await checked(sandbox, `rm -rf -- "${(0, import_provider3.escapeShellArg)(path)}"`);
350
+ }
351
+ },
352
+ getInstance: (sandbox) => sandbox
353
+ },
354
+ snapshot: {
355
+ create: (config, sandboxId, options) => saveSnapshot(client(config), config, sandboxId, options),
356
+ list: (config, options) => listSnapshots(client(config), options),
357
+ delete: async (config, name) => {
358
+ try {
359
+ await client(config).deleteNamedSnapshot({ name });
360
+ } catch (error) {
361
+ if (!isNotFound(error)) throw error;
362
+ }
363
+ }
364
+ },
365
+ template: {
366
+ create: async (config, baseOptions) => {
367
+ const options = baseOptions;
368
+ if (!options.sandboxId) throw new Error("Boat template creation requires sandboxId.");
369
+ if (options.description !== void 0) throw new Error("Boat named templates do not support descriptions.");
370
+ return templateInfo(await saveSnapshot(client(config), config, options.sandboxId, options));
371
+ },
372
+ list: async (config, options) => (await listSnapshots(client(config), options)).map(templateInfo),
373
+ delete: async (config, name) => {
374
+ try {
375
+ await client(config).deleteNamedSnapshot({ name });
376
+ } catch (error) {
377
+ if (!isNotFound(error)) throw error;
378
+ }
379
+ }
380
+ }
381
+ }
382
+ });
383
+ function boat(config = {}) {
384
+ const provider = createBoatProvider(config);
385
+ return { ...provider, snapshot: provider.snapshot, template: provider.template };
386
+ }
387
+ // Annotate the CommonJS export names for ESM import in node:
388
+ 0 && (module.exports = {
389
+ boat,
390
+ getBoatUrl,
391
+ readBoatFile,
392
+ writeBoatFile
393
+ });
394
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands.ts","../src/files.ts","../src/snapshots.ts"],"sourcesContent":["import { BoatApi, Configuration, ResponseError } from '@boatdev/sdk';\nimport type { CreateSandboxRequest, InitOverrideFunction, RequestOpts, Sandbox } from '@boatdev/sdk';\nimport { defineProvider, escapeShellArg } from '@computesdk/provider';\nimport type { Provider, ProviderSnapshotManager, ProviderTemplateManager, CreateTemplateOptions } from '@computesdk/provider';\nimport { checked, run } from './commands';\nimport { readBoatFile, writeBoatFile } from './files';\nimport { saveSnapshot, listSnapshots, templateInfo } from './snapshots';\nimport type { BoatSnapshot, BoatTemplate } from './snapshots';\nimport type { FileEntry } from 'computesdk';\n\nexport interface BoatConfig {\n /** Falls back to BOAT_API_KEY. */\n apiKey?: string;\n /** Falls back to BOAT_API_BASE, then https://boat.dev/api/v1. */\n baseURL?: string;\n /** Default sandbox lifetime in milliseconds. Boat defaults to one hour. */\n timeout?: number;\n /** Provisioning deadline in milliseconds (default: 300000). */\n readyTimeout?: number;\n /** Native defaults, including type, environment, noEnv, and from. */\n createOptions?: CreateSandboxRequest;\n /** Named snapshot save deadline in milliseconds (default: 600000). */\n snapshotTimeout?: number;\n /** Default HTTPS preview settings. */\n preview?: BoatPreviewDefaults;\n}\n\n/** Boat has an account-level client; retain it alongside the sandbox ID. */\nexport interface BoatSandbox {\n api: BoatApi;\n id: string;\n /** Boat's sandbox name, so callers can match list() results without a getInfo() call each. */\n name: string;\n preview?: BoatPreviewDefaults;\n}\n\n/**\n * The SDK's errors say only \"Response returned an error code\". Add the status and Boat's\n * error body, and keep an unread `response` for the SDK's retry checks and 404 handling.\n * Overriding request() leaves successful responses, including command streams, untouched.\n */\nclass BoatClient extends BoatApi {\n protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {\n try {\n return await super.request(context, initOverrides);\n } catch (error) {\n if (!(error instanceof ResponseError)) throw error;\n const { response } = error;\n const text = (await response.clone().text().catch(() => '')).replace(/\\s+/g, ' ').trim();\n let detail = text.slice(0, 300);\n try {\n const body = JSON.parse(text) as { error?: unknown; message?: unknown };\n const parts = [body.error, body.message].filter((part): part is string => typeof part === 'string' && part !== '');\n if (parts.length > 0) detail = parts.join(': ').slice(0, 300);\n } catch { /* Not JSON; keep the raw text. */ }\n throw new ResponseError(response, `Boat API error: ${response.status} ${response.statusText}${detail ? `: ${detail}` : ''}`);\n }\n }\n}\n\nfunction client(config: BoatConfig): BoatApi {\n const apiKey = config.apiKey || process.env.BOAT_API_KEY;\n if (!apiKey) throw new Error(\"Missing Boat API key. Provide 'apiKey' or set BOAT_API_KEY. Create a key at https://boat.dev/dashboard?tab=api-keys\");\n return new BoatClient(new Configuration({\n accessToken: apiKey,\n basePath: config.baseURL || process.env.BOAT_API_BASE || 'https://boat.dev/api/v1',\n }));\n}\n\nfunction seconds(ms: number): number {\n if (!Number.isFinite(ms) || ms <= 0) throw new Error('Boat timeout must be a positive number of milliseconds.');\n return Math.ceil(ms / 1000);\n}\n\nfunction isNotFound(error: unknown): boolean {\n return (error as { response?: { status?: number } })?.response?.status === 404;\n}\n\n/** Unlike the SDK's waitUntilReady, stop on cancelled sandboxes and honour the abort signal. */\nasync function waitUntilReady(api: BoatApi, sandboxId: string, timeoutMs: number, signal?: AbortSignal): Promise<void> {\n const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;\n for (;;) {\n if (signal?.aborted) throw signal.reason ?? new Error('Boat creation aborted.');\n const { state } = (await api.get({ sandboxId })).sandbox;\n if (['ready', 'idle', 'running'].includes(state)) return;\n if (['archived', 'archiving', 'error', 'cancelled'].includes(state)) throw new Error(`Boat sandbox ${sandboxId} entered terminal state ${state}.`);\n if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat sandbox ${sandboxId} to become ready.`);\n await new Promise(resolve => setTimeout(resolve, Math.min(2000, deadline - Date.now())));\n }\n}\n\nfunction wrap(api: BoatApi, sandbox: Sandbox, preview?: BoatPreviewDefaults) {\n return { instance: { api, id: sandbox.id, name: sandbox.name, ...(preview ? { preview } : {}) }, instanceId: sandbox.id };\n}\n\nexport interface BoatPreviewDefaults {\n public?: boolean;\n localhost?: boolean;\n title?: string;\n}\nexport interface BoatPreviewOptions extends BoatPreviewDefaults { port: number; protocol?: string }\n\n/** Typed preview controls beyond the portable getUrl interface. */\nexport async function getBoatUrl(sandbox: BoatSandbox, options: BoatPreviewOptions): Promise<string> {\n if (options.protocol && options.protocol !== 'https') throw new Error('Boat preview URLs support HTTPS only.');\n const settings = { ...sandbox.preview, ...options };\n const hostPortRequest = {\n port: settings.port,\n ...(settings.public === undefined ? {} : { _public: settings.public }),\n ...(settings.localhost === undefined ? {} : { localhost: settings.localhost }),\n ...(settings.title === undefined ? {} : { title: settings.title }),\n };\n const result = await sandbox.api.hostPort({ sandboxId: sandbox.id, hostPortRequest });\n if (!result.url) throw new Error('Boat did not return a preview URL.');\n return result.url;\n}\n\nexport interface BoatCreateTemplateOptions extends CreateTemplateOptions {\n /** Sandbox whose disk to freeze as a reusable named template. */\n sandboxId: string;\n}\nexport interface BoatProvider extends Provider<BoatSandbox, BoatTemplate, BoatSnapshot> {\n snapshot: ProviderSnapshotManager<BoatSnapshot>;\n template: ProviderTemplateManager<BoatTemplate, BoatCreateTemplateOptions>;\n}\n\nconst createBoatProvider = defineProvider<BoatSandbox, BoatConfig, BoatTemplate, BoatSnapshot>({\n name: 'boat',\n methods: {\n instances: {\n create: async (config, options) => {\n if (options?.templateId && options?.snapshotId) throw new Error('Specify either templateId or snapshotId, not both.');\n if (options?.metadata !== undefined) throw new Error('Boat does not support arbitrary instance metadata.');\n for (const key of ['cpu', 'cpuLimit', 'cpus', 'vcpus', 'memory', 'memoryMiB', 'memMiB', 'memoryMb', 'diskMiB', 'diskMb', 'diskSize', 'resources', 'launch_parameters', 'deploymentPlan', 'size', 'vmTier', 'gpu', 'image', 'region', 'architecture']) {\n if (options?.[key] !== undefined) throw new Error(`Boat does not support ${key}; select a fixed machine type with type.`);\n }\n const api = client(config);\n const lifetime = options?.timeout ?? (options?.ttlSeconds === undefined ? config.timeout : undefined);\n const defaults = config.createOptions;\n const request: CreateSandboxRequest = {\n ...defaults,\n type: options?.type ?? defaults?.type,\n environment: options?.environment ?? defaults?.environment,\n noEnv: options?.noEnv ?? defaults?.noEnv,\n snapshots: options?.snapshots ?? defaults?.snapshots,\n monitoring: options?.monitoring ?? defaults?.monitoring,\n failFast: options?.failFast ?? defaults?.failFast,\n setupScript: options?.setupScript ?? defaults?.setupScript,\n org: options?.org ?? options?.teamId ?? defaults?.org,\n from: options?.snapshotId ?? options?.templateId ?? options?.from ?? defaults?.from,\n env: options?.envs ?? options?.env ?? defaults?.env,\n ttlSeconds: lifetime === undefined ? (options?.ttlSeconds === undefined ? defaults?.ttlSeconds : options.ttlSeconds) : seconds(lifetime),\n };\n if (options?.signal?.aborted) throw options.signal.reason ?? new Error('Boat creation aborted.');\n const { sandbox } = await api.create({ createSandboxRequest: request });\n try {\n await waitUntilReady(api, sandbox.id, config.readyTimeout ?? 300_000, options?.signal);\n if (options?.signal?.aborted) throw options.signal.reason ?? new Error('Boat creation aborted.');\n if (options?.name) await api.update({ sandboxId: sandbox.id, updateSandboxRequest: { name: options.name } });\n return wrap(api, options?.name ? { ...sandbox, name: options.name } : sandbox, config.preview);\n } catch (error) {\n // A provisioning or rename failure must not leave a billable VM behind.\n try { await api.deleteSandbox({ sandboxId: sandbox.id, xAsciiConfirmDelete: sandbox.id }); }\n catch (cleanupError) {\n // Boat removes cancelled sandboxes itself, so a 404 means there is nothing left to clean up.\n if (isNotFound(cleanupError)) throw error;\n throw new Error(`Boat creation failed (${String(error)}); cleanup of ${sandbox.id} also failed: ${String(cleanupError)}`);\n }\n throw error;\n }\n },\n getById: async (config, sandboxId) => {\n const api = client(config);\n try { return wrap(api, (await api.get({ sandboxId })).sandbox, config.preview); }\n catch (error) { if (isNotFound(error)) return null; throw error; }\n },\n list: async config => {\n const api = client(config);\n const items: ReturnType<typeof wrap>[] = [];\n let cursor: string | undefined;\n do {\n const page = await api.sandboxes({ cursor });\n items.push(...page.sandboxes.filter(s => ['ready', 'idle', 'running'].includes(s.state)).map(s => wrap(api, s, config.preview)));\n cursor = page.pageInfo?.hasMore ? page.pageInfo.nextCursor ?? undefined : undefined;\n } while (cursor);\n return items;\n },\n destroy: async (config, sandboxId) => {\n try { await client(config).deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId }); }\n catch (error) { if (!isNotFound(error)) throw error; }\n },\n runCommand: run,\n streamCommand: run,\n getInfo: async sandbox => {\n const info = (await sandbox.api.get({ sandboxId: sandbox.id })).sandbox;\n return {\n id: info.id, provider: 'boat',\n status: ['error', 'cancelled'].includes(info.state) ? 'error' : ['archived', 'archiving'].includes(info.state) ? 'stopped' : 'running',\n createdAt: info.createdAt ?? new Date(0),\n timeout: info.ttlSeconds == null ? 0 : info.ttlSeconds * 1000,\n metadata: { name: info.name, state: info.state, type: info.type, ip: info.ip, environment: info.environment, archiveAfter: info.archiveAfter },\n };\n },\n getUrl: getBoatUrl,\n filesystem: {\n readFile: async (sandbox, path) => Buffer.from(await readBoatFile(sandbox, path)).toString('utf8'),\n writeFile: writeBoatFile,\n mkdir: async (sandbox, path) => { await checked(sandbox, `mkdir -p -- \"${escapeShellArg(path)}\"`); },\n readdir: async (sandbox, path): Promise<FileEntry[]> => {\n const script = 'import os,json,sys; p=sys.argv[1]; print(json.dumps([dict(name=e.name,type=\"directory\" if e.is_dir() else \"file\",size=e.stat(follow_symlinks=False).st_size,modified=e.stat(follow_symlinks=False).st_mtime) for e in os.scandir(p)]))';\n const entries: Array<Omit<FileEntry, 'modified'> & { modified: number }> = JSON.parse(await checked(sandbox, `python3 -c \"${escapeShellArg(script)}\" \"${escapeShellArg(path)}\"`));\n return entries.map(entry => ({ ...entry, modified: new Date(entry.modified * 1000) }));\n },\n exists: async (sandbox, path) => {\n const result = await run(sandbox, `test -e \"${escapeShellArg(path)}\"`);\n if (result.exitCode > 1) throw new Error(`Boat filesystem exists failed: ${result.stderr}`);\n return result.exitCode === 0;\n },\n remove: async (sandbox, path) => { await checked(sandbox, `rm -rf -- \"${escapeShellArg(path)}\"`); },\n },\n getInstance: sandbox => sandbox,\n },\n snapshot: {\n create: (config, sandboxId, options) => saveSnapshot(client(config), config, sandboxId, options),\n list: (config, options) => listSnapshots(client(config), options),\n delete: async (config, name) => {\n try { await client(config).deleteNamedSnapshot({ name }); }\n catch (error) { if (!isNotFound(error)) throw error; }\n },\n },\n template: {\n create: async (config, baseOptions) => {\n const options = baseOptions as BoatCreateTemplateOptions;\n if (!options.sandboxId) throw new Error('Boat template creation requires sandboxId.');\n if (options.description !== undefined) throw new Error('Boat named templates do not support descriptions.');\n return templateInfo(await saveSnapshot(client(config), config, options.sandboxId, options));\n },\n list: async (config, options) => (await listSnapshots(client(config), options)).map(templateInfo),\n delete: async (config, name) => {\n try { await client(config).deleteNamedSnapshot({ name }); }\n catch (error) { if (!isNotFound(error)) throw error; }\n },\n },\n },\n});\n\nexport function boat(config: BoatConfig = {}): BoatProvider {\n const provider = createBoatProvider(config);\n return { ...provider, snapshot: provider.snapshot!, template: provider.template! };\n}\n\nexport { readBoatFile, writeBoatFile } from './files';\nexport type { BoatSnapshot, BoatTemplate } from './snapshots';\nexport type { CreateSandboxRequest as BoatCreateOptions } from '@boatdev/sdk';\n","import { runCommand as runBoatCommand } from '@boatdev/sdk';\nimport { escapeShellArg } from '@computesdk/provider';\nimport type { CommandResult, RunCommandOptions } from 'computesdk';\nimport type { BoatSandbox } from './index';\n\n\nfunction commandText(command: string, options?: RunCommandOptions): string {\n let text = command;\n if (options?.env && Object.keys(options.env).length > 0) {\n const env = Object.entries(options.env).map(([key, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);\n return `\"${escapeShellArg(`${key}=${value}`)}\"`;\n }).join(' ');\n // Absolute path: env resolves its command with the caller's PATH, which may have no shell.\n text = `env ${env} /bin/sh -c \"${escapeShellArg(command)}\"`;\n }\n // Exit on a failed cd so no statement of a multi-statement command runs elsewhere.\n return options?.cwd ? `cd -- \"${escapeShellArg(options.cwd)}\" || exit\\n${text}` : text;\n}\n\n/** Stream every foreground command to avoid synchronous output limits. */\nexport async function run(sandbox: BoatSandbox, command: string, options?: RunCommandOptions): Promise<CommandResult> {\n const start = Date.now();\n const text = commandText(command, options);\n if (options?.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout < 0)) {\n throw new Error('Boat command timeout must be non-negative milliseconds; 0 means unlimited.');\n }\n if (options?.background) {\n await sandbox.api.command({ sandboxId: sandbox.id, commandRequest: { command: text, detached: true } });\n return { stdout: '', stderr: '', exitCode: 0, durationMs: Date.now() - start };\n }\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let processId: number | undefined;\n let timedOut = false;\n let stdout = '';\n let stderr = '';\n const result = (exitCode: number) => ({ stdout, stderr, exitCode, durationMs: Date.now() - start });\n try {\n for await (const frame of runBoatCommand(sandbox.api, sandbox.id, text, { signal: controller.signal })) {\n if (frame.type === 'started') {\n if (typeof frame.processId !== 'number') throw new Error('Boat did not return a process ID.');\n processId = frame.processId;\n if (options?.timeout) {\n const deadline = Date.now() + options.timeout;\n const expire = () => {\n const remaining = deadline - Date.now();\n if (remaining <= 0) { timedOut = true; controller.abort(); }\n else timer = setTimeout(expire, Math.min(remaining, 2_147_483_647));\n };\n expire();\n }\n }\n if (frame.type === 'stdout') { stdout += frame.data ?? ''; options?.onStdout?.(frame.data ?? ''); }\n if (frame.type === 'stderr') { stderr += frame.data ?? ''; options?.onStderr?.(frame.data ?? ''); }\n if (frame.type === 'error') throw new Error(frame.message || frame.error || 'Boat command stream failed.');\n if (frame.type === 'exit' && !timedOut) return result(frame.timedOut ? 124 : frame.exitCode ?? 1);\n }\n throw new Error('Boat command stream ended without an exit frame.');\n } catch (error) {\n // A deadline passing during cleanup must not turn this failure into a timeout result.\n clearTimeout(timer);\n // Detached commands survive a lost stream; do not orphan a failed or timed-out foreground job.\n if (processId !== undefined) {\n try { await sandbox.api.stopCommand({ sandboxId: sandbox.id, processId }); }\n catch (cleanupError) {\n if ((cleanupError as { response?: { status?: number } })?.response?.status !== 404) {\n const reason = timedOut ? 'timeout' : `stream failure (${error instanceof Error ? error.message : String(error)})`;\n throw new Error(`Failed to stop Boat process ${processId} after ${reason}: ${String(cleanupError)}`);\n }\n }\n }\n if (timedOut) return result(124);\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function checked(sandbox: BoatSandbox, command: string): Promise<string> {\n const result = await run(sandbox, command);\n if (result.exitCode !== 0) throw new Error(`Boat filesystem operation failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);\n return result.stdout;\n}\n","import { posix } from 'node:path';\nimport type { BoatSandbox } from './index';\nimport { escapeShellArg } from '@computesdk/provider';\nimport { checked } from './commands';\n\nfunction signedPath(path: string): boolean {\n const absolute = posix.resolve('/home/user', path);\n return absolute.startsWith('/home/user/') || absolute.startsWith('/tmp/');\n}\n\n/** Binary download. Signed URLs support files under /home/user and /tmp. */\nexport async function readBoatFile(sandbox: BoatSandbox, path: string): Promise<Uint8Array> {\n if (!signedPath(path)) {\n return new Uint8Array(Buffer.from(await checked(sandbox, `base64 < \"${escapeShellArg(path)}\"`), 'base64'));\n }\n const signed = await sandbox.api.createDownloadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });\n const response = await fetch(signed.url);\n // Do not include the signed URL in errors or send the account bearer token to it.\n if (!response.ok) throw new Error(`Boat file download failed (HTTP ${response.status}).`);\n return new Uint8Array(await response.arrayBuffer());\n}\n\n/** Binary or UTF-8 upload, without embedding the file in a command argument. */\nexport async function writeBoatFile(sandbox: BoatSandbox, path: string, content: string | Uint8Array): Promise<void> {\n if (!signedPath(path)) {\n // The signed API confines paths. Preserve shell access elsewhere with bounded command payloads.\n const encoded = Buffer.from(content).toString('base64');\n await checked(sandbox, `: > \"${escapeShellArg(path)}\"`);\n for (let offset = 0; offset < encoded.length; offset += 48 * 1024) {\n await checked(sandbox, `printf %s \"${encoded.slice(offset, offset + 48 * 1024)}\" | base64 -d >> \"${escapeShellArg(path)}\"`);\n }\n return;\n }\n const signed = await sandbox.api.createUploadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });\n const response = await fetch(signed.url, { method: signed.method, body: Buffer.from(content) });\n if (!response.ok) throw new Error(`Boat file upload failed (HTTP ${response.status}).`);\n await response.body?.cancel();\n}\n","import { randomUUID } from 'node:crypto';\nimport type { BoatApi, NamedSnapshot } from '@boatdev/sdk';\nimport type { CreateSnapshotOptions, ListSnapshotsOptions } from '@computesdk/provider';\nimport type { BoatConfig } from './index';\n\nexport interface BoatSnapshot extends NamedSnapshot {\n /** Reusable snapshot name; the native snapshotId is a different, non-bootable artifact ID. */\n id: string;\n provider: 'boat';\n}\nexport interface BoatTemplate extends BoatSnapshot { templateId: string }\n\nexport function snapshotInfo(snapshot: NamedSnapshot): BoatSnapshot {\n return { ...snapshot, id: snapshot.name, provider: 'boat' };\n}\nexport function templateInfo(snapshot: BoatSnapshot): BoatTemplate {\n return { ...snapshot, templateId: snapshot.name };\n}\n\nexport async function saveSnapshot(api: BoatApi, config: BoatConfig, sandboxId: string, options?: CreateSnapshotOptions): Promise<BoatSnapshot> {\n if (options?.metadata !== undefined) throw new Error('Boat named snapshots do not support arbitrary metadata.');\n const name = options?.name ?? `compute-${randomUUID()}`;\n if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name) || ['latest', 'tree', 'pull', 'rm', 'save', 'current', 'self', 'new'].includes(name)) {\n throw new Error('Boat snapshot names must be 1-63 lowercase letters, digits, or dashes, start with a letter or digit, and not be reserved.');\n }\n const timeout = config.snapshotTimeout ?? 600_000;\n if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Boat snapshotTimeout must be positive milliseconds.');\n const deadline = Date.now() + timeout;\n let snapshot = (await api.saveNamedSnapshot({ namedSnapshotSaveRequest: { sandboxId, name } })).snapshot;\n while (snapshot.status !== 'ready') {\n if (snapshot.status === 'failed') throw new Error(`Boat snapshot ${name} failed: ${snapshot.error ?? 'unknown error'}`);\n if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat snapshot ${name}; inspect or delete it through the snapshot manager.`);\n await new Promise(resolve => setTimeout(resolve, Math.min(2000, Math.max(0, deadline - Date.now()))));\n snapshot = (await api.getNamedSnapshot({ name })).snapshot;\n }\n return snapshotInfo(snapshot);\n}\n\nexport function limitItems<T>(items: T[], limit?: number): T[] {\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) throw new Error('Boat list limit must be a non-negative integer.');\n return limit === undefined ? items : items.slice(0, limit);\n}\n\nexport async function listSnapshots(api: BoatApi, options?: ListSnapshotsOptions): Promise<BoatSnapshot[]> {\n limitItems([], options?.limit);\n if (options?.limit === 0) return [];\n const sandboxId = options?.instanceId ?? options?.sandboxId;\n return limitItems((await api.listNamedSnapshots()).snapshots\n .filter(snapshot => sandboxId === undefined || snapshot.sourceSandboxId === sandboxId)\n .map(snapshotInfo), options?.limit);\n}\n"],"mappings":"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","names":["import_sdk","import_provider","runBoatCommand","import_provider"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,366 @@
1
+ // src/index.ts
2
+ import { BoatApi, Configuration, ResponseError } from "@boatdev/sdk";
3
+ import { defineProvider, escapeShellArg as escapeShellArg3 } from "@computesdk/provider";
4
+
5
+ // src/commands.ts
6
+ import { runCommand as runBoatCommand } from "@boatdev/sdk";
7
+ import { escapeShellArg } from "@computesdk/provider";
8
+ function commandText(command, options) {
9
+ let text = command;
10
+ if (options?.env && Object.keys(options.env).length > 0) {
11
+ const env = Object.entries(options.env).map(([key, value]) => {
12
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);
13
+ return `"${escapeShellArg(`${key}=${value}`)}"`;
14
+ }).join(" ");
15
+ text = `env ${env} /bin/sh -c "${escapeShellArg(command)}"`;
16
+ }
17
+ return options?.cwd ? `cd -- "${escapeShellArg(options.cwd)}" || exit
18
+ ${text}` : text;
19
+ }
20
+ async function run(sandbox, command, options) {
21
+ const start = Date.now();
22
+ const text = commandText(command, options);
23
+ if (options?.timeout !== void 0 && (!Number.isFinite(options.timeout) || options.timeout < 0)) {
24
+ throw new Error("Boat command timeout must be non-negative milliseconds; 0 means unlimited.");
25
+ }
26
+ if (options?.background) {
27
+ await sandbox.api.command({ sandboxId: sandbox.id, commandRequest: { command: text, detached: true } });
28
+ return { stdout: "", stderr: "", exitCode: 0, durationMs: Date.now() - start };
29
+ }
30
+ const controller = new AbortController();
31
+ let timer;
32
+ let processId;
33
+ let timedOut = false;
34
+ let stdout = "";
35
+ let stderr = "";
36
+ const result = (exitCode) => ({ stdout, stderr, exitCode, durationMs: Date.now() - start });
37
+ try {
38
+ for await (const frame of runBoatCommand(sandbox.api, sandbox.id, text, { signal: controller.signal })) {
39
+ if (frame.type === "started") {
40
+ if (typeof frame.processId !== "number") throw new Error("Boat did not return a process ID.");
41
+ processId = frame.processId;
42
+ if (options?.timeout) {
43
+ const deadline = Date.now() + options.timeout;
44
+ const expire = () => {
45
+ const remaining = deadline - Date.now();
46
+ if (remaining <= 0) {
47
+ timedOut = true;
48
+ controller.abort();
49
+ } else timer = setTimeout(expire, Math.min(remaining, 2147483647));
50
+ };
51
+ expire();
52
+ }
53
+ }
54
+ if (frame.type === "stdout") {
55
+ stdout += frame.data ?? "";
56
+ options?.onStdout?.(frame.data ?? "");
57
+ }
58
+ if (frame.type === "stderr") {
59
+ stderr += frame.data ?? "";
60
+ options?.onStderr?.(frame.data ?? "");
61
+ }
62
+ if (frame.type === "error") throw new Error(frame.message || frame.error || "Boat command stream failed.");
63
+ if (frame.type === "exit" && !timedOut) return result(frame.timedOut ? 124 : frame.exitCode ?? 1);
64
+ }
65
+ throw new Error("Boat command stream ended without an exit frame.");
66
+ } catch (error) {
67
+ clearTimeout(timer);
68
+ if (processId !== void 0) {
69
+ try {
70
+ await sandbox.api.stopCommand({ sandboxId: sandbox.id, processId });
71
+ } catch (cleanupError) {
72
+ if (cleanupError?.response?.status !== 404) {
73
+ const reason = timedOut ? "timeout" : `stream failure (${error instanceof Error ? error.message : String(error)})`;
74
+ throw new Error(`Failed to stop Boat process ${processId} after ${reason}: ${String(cleanupError)}`);
75
+ }
76
+ }
77
+ }
78
+ if (timedOut) return result(124);
79
+ throw error;
80
+ } finally {
81
+ clearTimeout(timer);
82
+ }
83
+ }
84
+ async function checked(sandbox, command) {
85
+ const result = await run(sandbox, command);
86
+ if (result.exitCode !== 0) throw new Error(`Boat filesystem operation failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
87
+ return result.stdout;
88
+ }
89
+
90
+ // src/files.ts
91
+ import { posix } from "path";
92
+ import { escapeShellArg as escapeShellArg2 } from "@computesdk/provider";
93
+ function signedPath(path) {
94
+ const absolute = posix.resolve("/home/user", path);
95
+ return absolute.startsWith("/home/user/") || absolute.startsWith("/tmp/");
96
+ }
97
+ async function readBoatFile(sandbox, path) {
98
+ if (!signedPath(path)) {
99
+ return new Uint8Array(Buffer.from(await checked(sandbox, `base64 < "${escapeShellArg2(path)}"`), "base64"));
100
+ }
101
+ const signed = await sandbox.api.createDownloadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });
102
+ const response = await fetch(signed.url);
103
+ if (!response.ok) throw new Error(`Boat file download failed (HTTP ${response.status}).`);
104
+ return new Uint8Array(await response.arrayBuffer());
105
+ }
106
+ async function writeBoatFile(sandbox, path, content) {
107
+ if (!signedPath(path)) {
108
+ const encoded = Buffer.from(content).toString("base64");
109
+ await checked(sandbox, `: > "${escapeShellArg2(path)}"`);
110
+ for (let offset = 0; offset < encoded.length; offset += 48 * 1024) {
111
+ await checked(sandbox, `printf %s "${encoded.slice(offset, offset + 48 * 1024)}" | base64 -d >> "${escapeShellArg2(path)}"`);
112
+ }
113
+ return;
114
+ }
115
+ const signed = await sandbox.api.createUploadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });
116
+ const response = await fetch(signed.url, { method: signed.method, body: Buffer.from(content) });
117
+ if (!response.ok) throw new Error(`Boat file upload failed (HTTP ${response.status}).`);
118
+ await response.body?.cancel();
119
+ }
120
+
121
+ // src/snapshots.ts
122
+ import { randomUUID } from "crypto";
123
+ function snapshotInfo(snapshot) {
124
+ return { ...snapshot, id: snapshot.name, provider: "boat" };
125
+ }
126
+ function templateInfo(snapshot) {
127
+ return { ...snapshot, templateId: snapshot.name };
128
+ }
129
+ async function saveSnapshot(api, config, sandboxId, options) {
130
+ if (options?.metadata !== void 0) throw new Error("Boat named snapshots do not support arbitrary metadata.");
131
+ const name = options?.name ?? `compute-${randomUUID()}`;
132
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name) || ["latest", "tree", "pull", "rm", "save", "current", "self", "new"].includes(name)) {
133
+ throw new Error("Boat snapshot names must be 1-63 lowercase letters, digits, or dashes, start with a letter or digit, and not be reserved.");
134
+ }
135
+ const timeout = config.snapshotTimeout ?? 6e5;
136
+ if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("Boat snapshotTimeout must be positive milliseconds.");
137
+ const deadline = Date.now() + timeout;
138
+ let snapshot = (await api.saveNamedSnapshot({ namedSnapshotSaveRequest: { sandboxId, name } })).snapshot;
139
+ while (snapshot.status !== "ready") {
140
+ if (snapshot.status === "failed") throw new Error(`Boat snapshot ${name} failed: ${snapshot.error ?? "unknown error"}`);
141
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat snapshot ${name}; inspect or delete it through the snapshot manager.`);
142
+ await new Promise((resolve) => setTimeout(resolve, Math.min(2e3, Math.max(0, deadline - Date.now()))));
143
+ snapshot = (await api.getNamedSnapshot({ name })).snapshot;
144
+ }
145
+ return snapshotInfo(snapshot);
146
+ }
147
+ function limitItems(items, limit) {
148
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 0)) throw new Error("Boat list limit must be a non-negative integer.");
149
+ return limit === void 0 ? items : items.slice(0, limit);
150
+ }
151
+ async function listSnapshots(api, options) {
152
+ limitItems([], options?.limit);
153
+ if (options?.limit === 0) return [];
154
+ const sandboxId = options?.instanceId ?? options?.sandboxId;
155
+ return limitItems((await api.listNamedSnapshots()).snapshots.filter((snapshot) => sandboxId === void 0 || snapshot.sourceSandboxId === sandboxId).map(snapshotInfo), options?.limit);
156
+ }
157
+
158
+ // src/index.ts
159
+ var BoatClient = class extends BoatApi {
160
+ async request(context, initOverrides) {
161
+ try {
162
+ return await super.request(context, initOverrides);
163
+ } catch (error) {
164
+ if (!(error instanceof ResponseError)) throw error;
165
+ const { response } = error;
166
+ const text = (await response.clone().text().catch(() => "")).replace(/\s+/g, " ").trim();
167
+ let detail = text.slice(0, 300);
168
+ try {
169
+ const body = JSON.parse(text);
170
+ const parts = [body.error, body.message].filter((part) => typeof part === "string" && part !== "");
171
+ if (parts.length > 0) detail = parts.join(": ").slice(0, 300);
172
+ } catch {
173
+ }
174
+ throw new ResponseError(response, `Boat API error: ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`);
175
+ }
176
+ }
177
+ };
178
+ function client(config) {
179
+ const apiKey = config.apiKey || process.env.BOAT_API_KEY;
180
+ if (!apiKey) throw new Error("Missing Boat API key. Provide 'apiKey' or set BOAT_API_KEY. Create a key at https://boat.dev/dashboard?tab=api-keys");
181
+ return new BoatClient(new Configuration({
182
+ accessToken: apiKey,
183
+ basePath: config.baseURL || process.env.BOAT_API_BASE || "https://boat.dev/api/v1"
184
+ }));
185
+ }
186
+ function seconds(ms) {
187
+ if (!Number.isFinite(ms) || ms <= 0) throw new Error("Boat timeout must be a positive number of milliseconds.");
188
+ return Math.ceil(ms / 1e3);
189
+ }
190
+ function isNotFound(error) {
191
+ return error?.response?.status === 404;
192
+ }
193
+ async function waitUntilReady(api, sandboxId, timeoutMs, signal) {
194
+ const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
195
+ for (; ; ) {
196
+ if (signal?.aborted) throw signal.reason ?? new Error("Boat creation aborted.");
197
+ const { state } = (await api.get({ sandboxId })).sandbox;
198
+ if (["ready", "idle", "running"].includes(state)) return;
199
+ if (["archived", "archiving", "error", "cancelled"].includes(state)) throw new Error(`Boat sandbox ${sandboxId} entered terminal state ${state}.`);
200
+ if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat sandbox ${sandboxId} to become ready.`);
201
+ await new Promise((resolve) => setTimeout(resolve, Math.min(2e3, deadline - Date.now())));
202
+ }
203
+ }
204
+ function wrap(api, sandbox, preview) {
205
+ return { instance: { api, id: sandbox.id, name: sandbox.name, ...preview ? { preview } : {} }, instanceId: sandbox.id };
206
+ }
207
+ async function getBoatUrl(sandbox, options) {
208
+ if (options.protocol && options.protocol !== "https") throw new Error("Boat preview URLs support HTTPS only.");
209
+ const settings = { ...sandbox.preview, ...options };
210
+ const hostPortRequest = {
211
+ port: settings.port,
212
+ ...settings.public === void 0 ? {} : { _public: settings.public },
213
+ ...settings.localhost === void 0 ? {} : { localhost: settings.localhost },
214
+ ...settings.title === void 0 ? {} : { title: settings.title }
215
+ };
216
+ const result = await sandbox.api.hostPort({ sandboxId: sandbox.id, hostPortRequest });
217
+ if (!result.url) throw new Error("Boat did not return a preview URL.");
218
+ return result.url;
219
+ }
220
+ var createBoatProvider = defineProvider({
221
+ name: "boat",
222
+ methods: {
223
+ instances: {
224
+ create: async (config, options) => {
225
+ if (options?.templateId && options?.snapshotId) throw new Error("Specify either templateId or snapshotId, not both.");
226
+ if (options?.metadata !== void 0) throw new Error("Boat does not support arbitrary instance metadata.");
227
+ for (const key of ["cpu", "cpuLimit", "cpus", "vcpus", "memory", "memoryMiB", "memMiB", "memoryMb", "diskMiB", "diskMb", "diskSize", "resources", "launch_parameters", "deploymentPlan", "size", "vmTier", "gpu", "image", "region", "architecture"]) {
228
+ if (options?.[key] !== void 0) throw new Error(`Boat does not support ${key}; select a fixed machine type with type.`);
229
+ }
230
+ const api = client(config);
231
+ const lifetime = options?.timeout ?? (options?.ttlSeconds === void 0 ? config.timeout : void 0);
232
+ const defaults = config.createOptions;
233
+ const request = {
234
+ ...defaults,
235
+ type: options?.type ?? defaults?.type,
236
+ environment: options?.environment ?? defaults?.environment,
237
+ noEnv: options?.noEnv ?? defaults?.noEnv,
238
+ snapshots: options?.snapshots ?? defaults?.snapshots,
239
+ monitoring: options?.monitoring ?? defaults?.monitoring,
240
+ failFast: options?.failFast ?? defaults?.failFast,
241
+ setupScript: options?.setupScript ?? defaults?.setupScript,
242
+ org: options?.org ?? options?.teamId ?? defaults?.org,
243
+ from: options?.snapshotId ?? options?.templateId ?? options?.from ?? defaults?.from,
244
+ env: options?.envs ?? options?.env ?? defaults?.env,
245
+ ttlSeconds: lifetime === void 0 ? options?.ttlSeconds === void 0 ? defaults?.ttlSeconds : options.ttlSeconds : seconds(lifetime)
246
+ };
247
+ if (options?.signal?.aborted) throw options.signal.reason ?? new Error("Boat creation aborted.");
248
+ const { sandbox } = await api.create({ createSandboxRequest: request });
249
+ try {
250
+ await waitUntilReady(api, sandbox.id, config.readyTimeout ?? 3e5, options?.signal);
251
+ if (options?.signal?.aborted) throw options.signal.reason ?? new Error("Boat creation aborted.");
252
+ if (options?.name) await api.update({ sandboxId: sandbox.id, updateSandboxRequest: { name: options.name } });
253
+ return wrap(api, options?.name ? { ...sandbox, name: options.name } : sandbox, config.preview);
254
+ } catch (error) {
255
+ try {
256
+ await api.deleteSandbox({ sandboxId: sandbox.id, xAsciiConfirmDelete: sandbox.id });
257
+ } catch (cleanupError) {
258
+ if (isNotFound(cleanupError)) throw error;
259
+ throw new Error(`Boat creation failed (${String(error)}); cleanup of ${sandbox.id} also failed: ${String(cleanupError)}`);
260
+ }
261
+ throw error;
262
+ }
263
+ },
264
+ getById: async (config, sandboxId) => {
265
+ const api = client(config);
266
+ try {
267
+ return wrap(api, (await api.get({ sandboxId })).sandbox, config.preview);
268
+ } catch (error) {
269
+ if (isNotFound(error)) return null;
270
+ throw error;
271
+ }
272
+ },
273
+ list: async (config) => {
274
+ const api = client(config);
275
+ const items = [];
276
+ let cursor;
277
+ do {
278
+ const page = await api.sandboxes({ cursor });
279
+ items.push(...page.sandboxes.filter((s) => ["ready", "idle", "running"].includes(s.state)).map((s) => wrap(api, s, config.preview)));
280
+ cursor = page.pageInfo?.hasMore ? page.pageInfo.nextCursor ?? void 0 : void 0;
281
+ } while (cursor);
282
+ return items;
283
+ },
284
+ destroy: async (config, sandboxId) => {
285
+ try {
286
+ await client(config).deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId });
287
+ } catch (error) {
288
+ if (!isNotFound(error)) throw error;
289
+ }
290
+ },
291
+ runCommand: run,
292
+ streamCommand: run,
293
+ getInfo: async (sandbox) => {
294
+ const info = (await sandbox.api.get({ sandboxId: sandbox.id })).sandbox;
295
+ return {
296
+ id: info.id,
297
+ provider: "boat",
298
+ status: ["error", "cancelled"].includes(info.state) ? "error" : ["archived", "archiving"].includes(info.state) ? "stopped" : "running",
299
+ createdAt: info.createdAt ?? /* @__PURE__ */ new Date(0),
300
+ timeout: info.ttlSeconds == null ? 0 : info.ttlSeconds * 1e3,
301
+ metadata: { name: info.name, state: info.state, type: info.type, ip: info.ip, environment: info.environment, archiveAfter: info.archiveAfter }
302
+ };
303
+ },
304
+ getUrl: getBoatUrl,
305
+ filesystem: {
306
+ readFile: async (sandbox, path) => Buffer.from(await readBoatFile(sandbox, path)).toString("utf8"),
307
+ writeFile: writeBoatFile,
308
+ mkdir: async (sandbox, path) => {
309
+ await checked(sandbox, `mkdir -p -- "${escapeShellArg3(path)}"`);
310
+ },
311
+ readdir: async (sandbox, path) => {
312
+ const script = 'import os,json,sys; p=sys.argv[1]; print(json.dumps([dict(name=e.name,type="directory" if e.is_dir() else "file",size=e.stat(follow_symlinks=False).st_size,modified=e.stat(follow_symlinks=False).st_mtime) for e in os.scandir(p)]))';
313
+ const entries = JSON.parse(await checked(sandbox, `python3 -c "${escapeShellArg3(script)}" "${escapeShellArg3(path)}"`));
314
+ return entries.map((entry) => ({ ...entry, modified: new Date(entry.modified * 1e3) }));
315
+ },
316
+ exists: async (sandbox, path) => {
317
+ const result = await run(sandbox, `test -e "${escapeShellArg3(path)}"`);
318
+ if (result.exitCode > 1) throw new Error(`Boat filesystem exists failed: ${result.stderr}`);
319
+ return result.exitCode === 0;
320
+ },
321
+ remove: async (sandbox, path) => {
322
+ await checked(sandbox, `rm -rf -- "${escapeShellArg3(path)}"`);
323
+ }
324
+ },
325
+ getInstance: (sandbox) => sandbox
326
+ },
327
+ snapshot: {
328
+ create: (config, sandboxId, options) => saveSnapshot(client(config), config, sandboxId, options),
329
+ list: (config, options) => listSnapshots(client(config), options),
330
+ delete: async (config, name) => {
331
+ try {
332
+ await client(config).deleteNamedSnapshot({ name });
333
+ } catch (error) {
334
+ if (!isNotFound(error)) throw error;
335
+ }
336
+ }
337
+ },
338
+ template: {
339
+ create: async (config, baseOptions) => {
340
+ const options = baseOptions;
341
+ if (!options.sandboxId) throw new Error("Boat template creation requires sandboxId.");
342
+ if (options.description !== void 0) throw new Error("Boat named templates do not support descriptions.");
343
+ return templateInfo(await saveSnapshot(client(config), config, options.sandboxId, options));
344
+ },
345
+ list: async (config, options) => (await listSnapshots(client(config), options)).map(templateInfo),
346
+ delete: async (config, name) => {
347
+ try {
348
+ await client(config).deleteNamedSnapshot({ name });
349
+ } catch (error) {
350
+ if (!isNotFound(error)) throw error;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ });
356
+ function boat(config = {}) {
357
+ const provider = createBoatProvider(config);
358
+ return { ...provider, snapshot: provider.snapshot, template: provider.template };
359
+ }
360
+ export {
361
+ boat,
362
+ getBoatUrl,
363
+ readBoatFile,
364
+ writeBoatFile
365
+ };
366
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/commands.ts","../src/files.ts","../src/snapshots.ts"],"sourcesContent":["import { BoatApi, Configuration, ResponseError } from '@boatdev/sdk';\nimport type { CreateSandboxRequest, InitOverrideFunction, RequestOpts, Sandbox } from '@boatdev/sdk';\nimport { defineProvider, escapeShellArg } from '@computesdk/provider';\nimport type { Provider, ProviderSnapshotManager, ProviderTemplateManager, CreateTemplateOptions } from '@computesdk/provider';\nimport { checked, run } from './commands';\nimport { readBoatFile, writeBoatFile } from './files';\nimport { saveSnapshot, listSnapshots, templateInfo } from './snapshots';\nimport type { BoatSnapshot, BoatTemplate } from './snapshots';\nimport type { FileEntry } from 'computesdk';\n\nexport interface BoatConfig {\n /** Falls back to BOAT_API_KEY. */\n apiKey?: string;\n /** Falls back to BOAT_API_BASE, then https://boat.dev/api/v1. */\n baseURL?: string;\n /** Default sandbox lifetime in milliseconds. Boat defaults to one hour. */\n timeout?: number;\n /** Provisioning deadline in milliseconds (default: 300000). */\n readyTimeout?: number;\n /** Native defaults, including type, environment, noEnv, and from. */\n createOptions?: CreateSandboxRequest;\n /** Named snapshot save deadline in milliseconds (default: 600000). */\n snapshotTimeout?: number;\n /** Default HTTPS preview settings. */\n preview?: BoatPreviewDefaults;\n}\n\n/** Boat has an account-level client; retain it alongside the sandbox ID. */\nexport interface BoatSandbox {\n api: BoatApi;\n id: string;\n /** Boat's sandbox name, so callers can match list() results without a getInfo() call each. */\n name: string;\n preview?: BoatPreviewDefaults;\n}\n\n/**\n * The SDK's errors say only \"Response returned an error code\". Add the status and Boat's\n * error body, and keep an unread `response` for the SDK's retry checks and 404 handling.\n * Overriding request() leaves successful responses, including command streams, untouched.\n */\nclass BoatClient extends BoatApi {\n protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise<Response> {\n try {\n return await super.request(context, initOverrides);\n } catch (error) {\n if (!(error instanceof ResponseError)) throw error;\n const { response } = error;\n const text = (await response.clone().text().catch(() => '')).replace(/\\s+/g, ' ').trim();\n let detail = text.slice(0, 300);\n try {\n const body = JSON.parse(text) as { error?: unknown; message?: unknown };\n const parts = [body.error, body.message].filter((part): part is string => typeof part === 'string' && part !== '');\n if (parts.length > 0) detail = parts.join(': ').slice(0, 300);\n } catch { /* Not JSON; keep the raw text. */ }\n throw new ResponseError(response, `Boat API error: ${response.status} ${response.statusText}${detail ? `: ${detail}` : ''}`);\n }\n }\n}\n\nfunction client(config: BoatConfig): BoatApi {\n const apiKey = config.apiKey || process.env.BOAT_API_KEY;\n if (!apiKey) throw new Error(\"Missing Boat API key. Provide 'apiKey' or set BOAT_API_KEY. Create a key at https://boat.dev/dashboard?tab=api-keys\");\n return new BoatClient(new Configuration({\n accessToken: apiKey,\n basePath: config.baseURL || process.env.BOAT_API_BASE || 'https://boat.dev/api/v1',\n }));\n}\n\nfunction seconds(ms: number): number {\n if (!Number.isFinite(ms) || ms <= 0) throw new Error('Boat timeout must be a positive number of milliseconds.');\n return Math.ceil(ms / 1000);\n}\n\nfunction isNotFound(error: unknown): boolean {\n return (error as { response?: { status?: number } })?.response?.status === 404;\n}\n\n/** Unlike the SDK's waitUntilReady, stop on cancelled sandboxes and honour the abort signal. */\nasync function waitUntilReady(api: BoatApi, sandboxId: string, timeoutMs: number, signal?: AbortSignal): Promise<void> {\n const deadline = timeoutMs === 0 ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;\n for (;;) {\n if (signal?.aborted) throw signal.reason ?? new Error('Boat creation aborted.');\n const { state } = (await api.get({ sandboxId })).sandbox;\n if (['ready', 'idle', 'running'].includes(state)) return;\n if (['archived', 'archiving', 'error', 'cancelled'].includes(state)) throw new Error(`Boat sandbox ${sandboxId} entered terminal state ${state}.`);\n if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat sandbox ${sandboxId} to become ready.`);\n await new Promise(resolve => setTimeout(resolve, Math.min(2000, deadline - Date.now())));\n }\n}\n\nfunction wrap(api: BoatApi, sandbox: Sandbox, preview?: BoatPreviewDefaults) {\n return { instance: { api, id: sandbox.id, name: sandbox.name, ...(preview ? { preview } : {}) }, instanceId: sandbox.id };\n}\n\nexport interface BoatPreviewDefaults {\n public?: boolean;\n localhost?: boolean;\n title?: string;\n}\nexport interface BoatPreviewOptions extends BoatPreviewDefaults { port: number; protocol?: string }\n\n/** Typed preview controls beyond the portable getUrl interface. */\nexport async function getBoatUrl(sandbox: BoatSandbox, options: BoatPreviewOptions): Promise<string> {\n if (options.protocol && options.protocol !== 'https') throw new Error('Boat preview URLs support HTTPS only.');\n const settings = { ...sandbox.preview, ...options };\n const hostPortRequest = {\n port: settings.port,\n ...(settings.public === undefined ? {} : { _public: settings.public }),\n ...(settings.localhost === undefined ? {} : { localhost: settings.localhost }),\n ...(settings.title === undefined ? {} : { title: settings.title }),\n };\n const result = await sandbox.api.hostPort({ sandboxId: sandbox.id, hostPortRequest });\n if (!result.url) throw new Error('Boat did not return a preview URL.');\n return result.url;\n}\n\nexport interface BoatCreateTemplateOptions extends CreateTemplateOptions {\n /** Sandbox whose disk to freeze as a reusable named template. */\n sandboxId: string;\n}\nexport interface BoatProvider extends Provider<BoatSandbox, BoatTemplate, BoatSnapshot> {\n snapshot: ProviderSnapshotManager<BoatSnapshot>;\n template: ProviderTemplateManager<BoatTemplate, BoatCreateTemplateOptions>;\n}\n\nconst createBoatProvider = defineProvider<BoatSandbox, BoatConfig, BoatTemplate, BoatSnapshot>({\n name: 'boat',\n methods: {\n instances: {\n create: async (config, options) => {\n if (options?.templateId && options?.snapshotId) throw new Error('Specify either templateId or snapshotId, not both.');\n if (options?.metadata !== undefined) throw new Error('Boat does not support arbitrary instance metadata.');\n for (const key of ['cpu', 'cpuLimit', 'cpus', 'vcpus', 'memory', 'memoryMiB', 'memMiB', 'memoryMb', 'diskMiB', 'diskMb', 'diskSize', 'resources', 'launch_parameters', 'deploymentPlan', 'size', 'vmTier', 'gpu', 'image', 'region', 'architecture']) {\n if (options?.[key] !== undefined) throw new Error(`Boat does not support ${key}; select a fixed machine type with type.`);\n }\n const api = client(config);\n const lifetime = options?.timeout ?? (options?.ttlSeconds === undefined ? config.timeout : undefined);\n const defaults = config.createOptions;\n const request: CreateSandboxRequest = {\n ...defaults,\n type: options?.type ?? defaults?.type,\n environment: options?.environment ?? defaults?.environment,\n noEnv: options?.noEnv ?? defaults?.noEnv,\n snapshots: options?.snapshots ?? defaults?.snapshots,\n monitoring: options?.monitoring ?? defaults?.monitoring,\n failFast: options?.failFast ?? defaults?.failFast,\n setupScript: options?.setupScript ?? defaults?.setupScript,\n org: options?.org ?? options?.teamId ?? defaults?.org,\n from: options?.snapshotId ?? options?.templateId ?? options?.from ?? defaults?.from,\n env: options?.envs ?? options?.env ?? defaults?.env,\n ttlSeconds: lifetime === undefined ? (options?.ttlSeconds === undefined ? defaults?.ttlSeconds : options.ttlSeconds) : seconds(lifetime),\n };\n if (options?.signal?.aborted) throw options.signal.reason ?? new Error('Boat creation aborted.');\n const { sandbox } = await api.create({ createSandboxRequest: request });\n try {\n await waitUntilReady(api, sandbox.id, config.readyTimeout ?? 300_000, options?.signal);\n if (options?.signal?.aborted) throw options.signal.reason ?? new Error('Boat creation aborted.');\n if (options?.name) await api.update({ sandboxId: sandbox.id, updateSandboxRequest: { name: options.name } });\n return wrap(api, options?.name ? { ...sandbox, name: options.name } : sandbox, config.preview);\n } catch (error) {\n // A provisioning or rename failure must not leave a billable VM behind.\n try { await api.deleteSandbox({ sandboxId: sandbox.id, xAsciiConfirmDelete: sandbox.id }); }\n catch (cleanupError) {\n // Boat removes cancelled sandboxes itself, so a 404 means there is nothing left to clean up.\n if (isNotFound(cleanupError)) throw error;\n throw new Error(`Boat creation failed (${String(error)}); cleanup of ${sandbox.id} also failed: ${String(cleanupError)}`);\n }\n throw error;\n }\n },\n getById: async (config, sandboxId) => {\n const api = client(config);\n try { return wrap(api, (await api.get({ sandboxId })).sandbox, config.preview); }\n catch (error) { if (isNotFound(error)) return null; throw error; }\n },\n list: async config => {\n const api = client(config);\n const items: ReturnType<typeof wrap>[] = [];\n let cursor: string | undefined;\n do {\n const page = await api.sandboxes({ cursor });\n items.push(...page.sandboxes.filter(s => ['ready', 'idle', 'running'].includes(s.state)).map(s => wrap(api, s, config.preview)));\n cursor = page.pageInfo?.hasMore ? page.pageInfo.nextCursor ?? undefined : undefined;\n } while (cursor);\n return items;\n },\n destroy: async (config, sandboxId) => {\n try { await client(config).deleteSandbox({ sandboxId, xAsciiConfirmDelete: sandboxId }); }\n catch (error) { if (!isNotFound(error)) throw error; }\n },\n runCommand: run,\n streamCommand: run,\n getInfo: async sandbox => {\n const info = (await sandbox.api.get({ sandboxId: sandbox.id })).sandbox;\n return {\n id: info.id, provider: 'boat',\n status: ['error', 'cancelled'].includes(info.state) ? 'error' : ['archived', 'archiving'].includes(info.state) ? 'stopped' : 'running',\n createdAt: info.createdAt ?? new Date(0),\n timeout: info.ttlSeconds == null ? 0 : info.ttlSeconds * 1000,\n metadata: { name: info.name, state: info.state, type: info.type, ip: info.ip, environment: info.environment, archiveAfter: info.archiveAfter },\n };\n },\n getUrl: getBoatUrl,\n filesystem: {\n readFile: async (sandbox, path) => Buffer.from(await readBoatFile(sandbox, path)).toString('utf8'),\n writeFile: writeBoatFile,\n mkdir: async (sandbox, path) => { await checked(sandbox, `mkdir -p -- \"${escapeShellArg(path)}\"`); },\n readdir: async (sandbox, path): Promise<FileEntry[]> => {\n const script = 'import os,json,sys; p=sys.argv[1]; print(json.dumps([dict(name=e.name,type=\"directory\" if e.is_dir() else \"file\",size=e.stat(follow_symlinks=False).st_size,modified=e.stat(follow_symlinks=False).st_mtime) for e in os.scandir(p)]))';\n const entries: Array<Omit<FileEntry, 'modified'> & { modified: number }> = JSON.parse(await checked(sandbox, `python3 -c \"${escapeShellArg(script)}\" \"${escapeShellArg(path)}\"`));\n return entries.map(entry => ({ ...entry, modified: new Date(entry.modified * 1000) }));\n },\n exists: async (sandbox, path) => {\n const result = await run(sandbox, `test -e \"${escapeShellArg(path)}\"`);\n if (result.exitCode > 1) throw new Error(`Boat filesystem exists failed: ${result.stderr}`);\n return result.exitCode === 0;\n },\n remove: async (sandbox, path) => { await checked(sandbox, `rm -rf -- \"${escapeShellArg(path)}\"`); },\n },\n getInstance: sandbox => sandbox,\n },\n snapshot: {\n create: (config, sandboxId, options) => saveSnapshot(client(config), config, sandboxId, options),\n list: (config, options) => listSnapshots(client(config), options),\n delete: async (config, name) => {\n try { await client(config).deleteNamedSnapshot({ name }); }\n catch (error) { if (!isNotFound(error)) throw error; }\n },\n },\n template: {\n create: async (config, baseOptions) => {\n const options = baseOptions as BoatCreateTemplateOptions;\n if (!options.sandboxId) throw new Error('Boat template creation requires sandboxId.');\n if (options.description !== undefined) throw new Error('Boat named templates do not support descriptions.');\n return templateInfo(await saveSnapshot(client(config), config, options.sandboxId, options));\n },\n list: async (config, options) => (await listSnapshots(client(config), options)).map(templateInfo),\n delete: async (config, name) => {\n try { await client(config).deleteNamedSnapshot({ name }); }\n catch (error) { if (!isNotFound(error)) throw error; }\n },\n },\n },\n});\n\nexport function boat(config: BoatConfig = {}): BoatProvider {\n const provider = createBoatProvider(config);\n return { ...provider, snapshot: provider.snapshot!, template: provider.template! };\n}\n\nexport { readBoatFile, writeBoatFile } from './files';\nexport type { BoatSnapshot, BoatTemplate } from './snapshots';\nexport type { CreateSandboxRequest as BoatCreateOptions } from '@boatdev/sdk';\n","import { runCommand as runBoatCommand } from '@boatdev/sdk';\nimport { escapeShellArg } from '@computesdk/provider';\nimport type { CommandResult, RunCommandOptions } from 'computesdk';\nimport type { BoatSandbox } from './index';\n\n\nfunction commandText(command: string, options?: RunCommandOptions): string {\n let text = command;\n if (options?.env && Object.keys(options.env).length > 0) {\n const env = Object.entries(options.env).map(([key, value]) => {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) throw new Error(`Invalid environment variable name: ${key}`);\n return `\"${escapeShellArg(`${key}=${value}`)}\"`;\n }).join(' ');\n // Absolute path: env resolves its command with the caller's PATH, which may have no shell.\n text = `env ${env} /bin/sh -c \"${escapeShellArg(command)}\"`;\n }\n // Exit on a failed cd so no statement of a multi-statement command runs elsewhere.\n return options?.cwd ? `cd -- \"${escapeShellArg(options.cwd)}\" || exit\\n${text}` : text;\n}\n\n/** Stream every foreground command to avoid synchronous output limits. */\nexport async function run(sandbox: BoatSandbox, command: string, options?: RunCommandOptions): Promise<CommandResult> {\n const start = Date.now();\n const text = commandText(command, options);\n if (options?.timeout !== undefined && (!Number.isFinite(options.timeout) || options.timeout < 0)) {\n throw new Error('Boat command timeout must be non-negative milliseconds; 0 means unlimited.');\n }\n if (options?.background) {\n await sandbox.api.command({ sandboxId: sandbox.id, commandRequest: { command: text, detached: true } });\n return { stdout: '', stderr: '', exitCode: 0, durationMs: Date.now() - start };\n }\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let processId: number | undefined;\n let timedOut = false;\n let stdout = '';\n let stderr = '';\n const result = (exitCode: number) => ({ stdout, stderr, exitCode, durationMs: Date.now() - start });\n try {\n for await (const frame of runBoatCommand(sandbox.api, sandbox.id, text, { signal: controller.signal })) {\n if (frame.type === 'started') {\n if (typeof frame.processId !== 'number') throw new Error('Boat did not return a process ID.');\n processId = frame.processId;\n if (options?.timeout) {\n const deadline = Date.now() + options.timeout;\n const expire = () => {\n const remaining = deadline - Date.now();\n if (remaining <= 0) { timedOut = true; controller.abort(); }\n else timer = setTimeout(expire, Math.min(remaining, 2_147_483_647));\n };\n expire();\n }\n }\n if (frame.type === 'stdout') { stdout += frame.data ?? ''; options?.onStdout?.(frame.data ?? ''); }\n if (frame.type === 'stderr') { stderr += frame.data ?? ''; options?.onStderr?.(frame.data ?? ''); }\n if (frame.type === 'error') throw new Error(frame.message || frame.error || 'Boat command stream failed.');\n if (frame.type === 'exit' && !timedOut) return result(frame.timedOut ? 124 : frame.exitCode ?? 1);\n }\n throw new Error('Boat command stream ended without an exit frame.');\n } catch (error) {\n // A deadline passing during cleanup must not turn this failure into a timeout result.\n clearTimeout(timer);\n // Detached commands survive a lost stream; do not orphan a failed or timed-out foreground job.\n if (processId !== undefined) {\n try { await sandbox.api.stopCommand({ sandboxId: sandbox.id, processId }); }\n catch (cleanupError) {\n if ((cleanupError as { response?: { status?: number } })?.response?.status !== 404) {\n const reason = timedOut ? 'timeout' : `stream failure (${error instanceof Error ? error.message : String(error)})`;\n throw new Error(`Failed to stop Boat process ${processId} after ${reason}: ${String(cleanupError)}`);\n }\n }\n }\n if (timedOut) return result(124);\n throw error;\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport async function checked(sandbox: BoatSandbox, command: string): Promise<string> {\n const result = await run(sandbox, command);\n if (result.exitCode !== 0) throw new Error(`Boat filesystem operation failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);\n return result.stdout;\n}\n","import { posix } from 'node:path';\nimport type { BoatSandbox } from './index';\nimport { escapeShellArg } from '@computesdk/provider';\nimport { checked } from './commands';\n\nfunction signedPath(path: string): boolean {\n const absolute = posix.resolve('/home/user', path);\n return absolute.startsWith('/home/user/') || absolute.startsWith('/tmp/');\n}\n\n/** Binary download. Signed URLs support files under /home/user and /tmp. */\nexport async function readBoatFile(sandbox: BoatSandbox, path: string): Promise<Uint8Array> {\n if (!signedPath(path)) {\n return new Uint8Array(Buffer.from(await checked(sandbox, `base64 < \"${escapeShellArg(path)}\"`), 'base64'));\n }\n const signed = await sandbox.api.createDownloadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });\n const response = await fetch(signed.url);\n // Do not include the signed URL in errors or send the account bearer token to it.\n if (!response.ok) throw new Error(`Boat file download failed (HTTP ${response.status}).`);\n return new Uint8Array(await response.arrayBuffer());\n}\n\n/** Binary or UTF-8 upload, without embedding the file in a command argument. */\nexport async function writeBoatFile(sandbox: BoatSandbox, path: string, content: string | Uint8Array): Promise<void> {\n if (!signedPath(path)) {\n // The signed API confines paths. Preserve shell access elsewhere with bounded command payloads.\n const encoded = Buffer.from(content).toString('base64');\n await checked(sandbox, `: > \"${escapeShellArg(path)}\"`);\n for (let offset = 0; offset < encoded.length; offset += 48 * 1024) {\n await checked(sandbox, `printf %s \"${encoded.slice(offset, offset + 48 * 1024)}\" | base64 -d >> \"${escapeShellArg(path)}\"`);\n }\n return;\n }\n const signed = await sandbox.api.createUploadUrl({ sandboxId: sandbox.id, fileUrlRequest: { path } });\n const response = await fetch(signed.url, { method: signed.method, body: Buffer.from(content) });\n if (!response.ok) throw new Error(`Boat file upload failed (HTTP ${response.status}).`);\n await response.body?.cancel();\n}\n","import { randomUUID } from 'node:crypto';\nimport type { BoatApi, NamedSnapshot } from '@boatdev/sdk';\nimport type { CreateSnapshotOptions, ListSnapshotsOptions } from '@computesdk/provider';\nimport type { BoatConfig } from './index';\n\nexport interface BoatSnapshot extends NamedSnapshot {\n /** Reusable snapshot name; the native snapshotId is a different, non-bootable artifact ID. */\n id: string;\n provider: 'boat';\n}\nexport interface BoatTemplate extends BoatSnapshot { templateId: string }\n\nexport function snapshotInfo(snapshot: NamedSnapshot): BoatSnapshot {\n return { ...snapshot, id: snapshot.name, provider: 'boat' };\n}\nexport function templateInfo(snapshot: BoatSnapshot): BoatTemplate {\n return { ...snapshot, templateId: snapshot.name };\n}\n\nexport async function saveSnapshot(api: BoatApi, config: BoatConfig, sandboxId: string, options?: CreateSnapshotOptions): Promise<BoatSnapshot> {\n if (options?.metadata !== undefined) throw new Error('Boat named snapshots do not support arbitrary metadata.');\n const name = options?.name ?? `compute-${randomUUID()}`;\n if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name) || ['latest', 'tree', 'pull', 'rm', 'save', 'current', 'self', 'new'].includes(name)) {\n throw new Error('Boat snapshot names must be 1-63 lowercase letters, digits, or dashes, start with a letter or digit, and not be reserved.');\n }\n const timeout = config.snapshotTimeout ?? 600_000;\n if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('Boat snapshotTimeout must be positive milliseconds.');\n const deadline = Date.now() + timeout;\n let snapshot = (await api.saveNamedSnapshot({ namedSnapshotSaveRequest: { sandboxId, name } })).snapshot;\n while (snapshot.status !== 'ready') {\n if (snapshot.status === 'failed') throw new Error(`Boat snapshot ${name} failed: ${snapshot.error ?? 'unknown error'}`);\n if (Date.now() >= deadline) throw new Error(`Timed out waiting for Boat snapshot ${name}; inspect or delete it through the snapshot manager.`);\n await new Promise(resolve => setTimeout(resolve, Math.min(2000, Math.max(0, deadline - Date.now()))));\n snapshot = (await api.getNamedSnapshot({ name })).snapshot;\n }\n return snapshotInfo(snapshot);\n}\n\nexport function limitItems<T>(items: T[], limit?: number): T[] {\n if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) throw new Error('Boat list limit must be a non-negative integer.');\n return limit === undefined ? items : items.slice(0, limit);\n}\n\nexport async function listSnapshots(api: BoatApi, options?: ListSnapshotsOptions): Promise<BoatSnapshot[]> {\n limitItems([], options?.limit);\n if (options?.limit === 0) return [];\n const sandboxId = options?.instanceId ?? options?.sandboxId;\n return limitItems((await api.listNamedSnapshots()).snapshots\n .filter(snapshot => sandboxId === undefined || snapshot.sourceSandboxId === sandboxId)\n .map(snapshotInfo), options?.limit);\n}\n"],"mappings":"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","names":["escapeShellArg","escapeShellArg","escapeShellArg","escapeShellArg"]}
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@computesdk/boat",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.1",
4
+ "description": "Boat provider for ComputeSDK - Ubuntu cloud sandboxes with streaming, named snapshots, signed file transfers, and preview URLs",
5
+ "author": "Dao Hoang Son",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.mjs",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.mjs",
14
+ "require": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "dependencies": {
21
+ "@boatdev/sdk": "^1.9.9",
22
+ "@computesdk/provider": "2.1.23",
23
+ "computesdk": "4.1.17"
24
+ },
25
+ "keywords": [
26
+ "computesdk",
27
+ "boat",
28
+ "sandbox",
29
+ "code-execution",
30
+ "python",
31
+ "javascript",
32
+ "cloud",
33
+ "compute",
34
+ "provider",
35
+ "containers"
36
+ ],
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/computesdk/computesdk.git",
40
+ "directory": "packages/boat"
41
+ },
42
+ "homepage": "https://www.computesdk.com",
43
+ "bugs": {
44
+ "url": "https://github.com/computesdk/computesdk/issues"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20.0.0",
48
+ "@vitest/coverage-v8": "^1.0.0",
49
+ "eslint": "^8.37.0",
50
+ "rimraf": "^5.0.0",
51
+ "tsup": "^8.0.0",
52
+ "typescript": "^5.0.0",
53
+ "vitest": "^1.0.0",
54
+ "@computesdk/test-utils": "2.0.6"
55
+ },
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "clean": "rimraf dist",
62
+ "dev": "tsup --watch",
63
+ "test": "vitest run",
64
+ "test:unit": "vitest run src/__tests__/unit.test.ts src/__tests__/transport.test.ts",
65
+ "test:integration": "vitest run src/__tests__/index.test.ts src/__tests__/crud.test.ts src/__tests__/integration.test.ts",
66
+ "test:watch": "vitest watch",
67
+ "test:coverage": "vitest run --coverage",
68
+ "typecheck": "tsc --noEmit",
69
+ "lint": "eslint src --ext .ts"
70
+ }
6
71
  }