@watasu/sdk 0.1.4 → 0.1.6
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/README.md +63 -0
- package/dist/commands.d.ts +8 -1
- package/dist/commands.js +16 -4
- package/dist/filesystem.d.ts +28 -1
- package/dist/filesystem.js +62 -3
- package/dist/git.d.ts +144 -0
- package/dist/git.js +236 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +5 -3
- package/dist/processSocket.d.ts +1 -0
- package/dist/processSocket.js +6 -1
- package/dist/pty.d.ts +33 -0
- package/dist/pty.js +88 -0
- package/dist/sandbox.d.ts +89 -9
- package/dist/sandbox.js +211 -5
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -26,4 +26,67 @@ await sbx.kill()
|
|
|
26
26
|
`Sandbox.create` and `Sandbox.connect` return only after the Watasu API supplies
|
|
27
27
|
a usable data-plane session. The SDK does not poll sandbox readiness.
|
|
28
28
|
|
|
29
|
+
## Git, Watch, PTY, And Signed File URLs
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
const sbx = await Sandbox.create()
|
|
33
|
+
|
|
34
|
+
await sbx.git.clone('https://github.com/acme/project.git', {
|
|
35
|
+
path: '/workspace/project',
|
|
36
|
+
branch: 'main',
|
|
37
|
+
depth: 1,
|
|
38
|
+
})
|
|
39
|
+
const status = await sbx.git.status('/workspace/project')
|
|
40
|
+
await sbx.git.configureUser('Watasu Bot', 'bot@watasu.local', {
|
|
41
|
+
scope: 'local',
|
|
42
|
+
path: '/workspace/project',
|
|
43
|
+
})
|
|
44
|
+
await sbx.git.createBranch('/workspace/project', 'feature/docs')
|
|
45
|
+
await sbx.git.add('/workspace/project', { files: ['README.md'] })
|
|
46
|
+
await sbx.git.commit('/workspace/project', 'Update docs', {
|
|
47
|
+
authorName: 'Watasu Bot',
|
|
48
|
+
authorEmail: 'bot@watasu.local',
|
|
49
|
+
})
|
|
50
|
+
await sbx.git.push('/workspace/project', {
|
|
51
|
+
remote: 'origin',
|
|
52
|
+
branch: 'feature/docs',
|
|
53
|
+
setUpstream: true,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const watcher = await sbx.files.watchDir('/workspace/project', (event) => {
|
|
57
|
+
console.log(event.type, event.path)
|
|
58
|
+
}, { recursive: true })
|
|
59
|
+
|
|
60
|
+
const terminal = await sbx.pty.create({
|
|
61
|
+
cols: 100,
|
|
62
|
+
rows: 30,
|
|
63
|
+
onData: (data) => process.stdout.write(data),
|
|
64
|
+
})
|
|
65
|
+
await terminal.sendStdin('echo hello\n')
|
|
66
|
+
|
|
67
|
+
const uploadUrl = await sbx.uploadUrl('/workspace/input.bin')
|
|
68
|
+
const downloadUrl = await sbx.downloadUrl('/workspace/output.bin')
|
|
69
|
+
|
|
70
|
+
watcher.stop()
|
|
71
|
+
await terminal.kill()
|
|
72
|
+
await sbx.kill()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Metrics And Snapshots
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { Sandbox } from '@watasu/sdk'
|
|
79
|
+
|
|
80
|
+
const sbx = await Sandbox.create()
|
|
81
|
+
const metrics = await sbx.getMetrics()
|
|
82
|
+
const snapshot = await sbx.createSnapshot({ name: 'ready' })
|
|
83
|
+
const snapshots = await sbx.listSnapshots().nextItems()
|
|
84
|
+
const restored = await sbx.restore({ snapshotId: snapshot.snapshotId })
|
|
85
|
+
await sbx.deleteSnapshot(snapshot.snapshotId)
|
|
86
|
+
await sbx.kill()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Watasu snapshots are backed by sandbox checkpoints. Use the returned
|
|
90
|
+
`snapshotId` when restoring from a checkpoint.
|
|
91
|
+
|
|
29
92
|
The SDK is ESM-first and ships TypeScript declarations.
|
package/dist/commands.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface CommandStartOpts {
|
|
|
34
34
|
envs?: Record<string, string>;
|
|
35
35
|
onStdout?: (data: string) => void | Promise<void>;
|
|
36
36
|
onStderr?: (data: string) => void | Promise<void>;
|
|
37
|
+
onPty?: (data: Uint8Array) => void | Promise<void>;
|
|
37
38
|
stdin?: boolean;
|
|
38
39
|
timeoutMs?: number;
|
|
39
40
|
requestTimeoutMs?: number;
|
|
@@ -46,11 +47,12 @@ export declare class CommandHandle implements Partial<CommandResult> {
|
|
|
46
47
|
private readonly events;
|
|
47
48
|
private readonly onStdout?;
|
|
48
49
|
private readonly onStderr?;
|
|
50
|
+
private readonly onPty?;
|
|
49
51
|
private _stdout;
|
|
50
52
|
private _stderr;
|
|
51
53
|
private result?;
|
|
52
54
|
private readonly pending;
|
|
53
|
-
constructor(pid: number | string, socket: ProcessSocket, handleKill: () => Promise<boolean>, events: AsyncIterable<ProcessFrame>, onStdout?: ((data: string) => void | Promise<void>) | undefined, onStderr?: ((data: string) => void | Promise<void>) | undefined);
|
|
55
|
+
constructor(pid: number | string, socket: ProcessSocket, handleKill: () => Promise<boolean>, events: AsyncIterable<ProcessFrame>, onStdout?: ((data: string) => void | Promise<void>) | undefined, onStderr?: ((data: string) => void | Promise<void>) | undefined, onPty?: ((data: Uint8Array) => void | Promise<void>) | undefined);
|
|
54
56
|
get stdout(): string;
|
|
55
57
|
get stderr(): string;
|
|
56
58
|
get exitCode(): number | undefined;
|
|
@@ -61,6 +63,11 @@ export declare class CommandHandle implements Partial<CommandResult> {
|
|
|
61
63
|
kill(): Promise<boolean>;
|
|
62
64
|
/** Send stdin bytes or text to the process. */
|
|
63
65
|
sendStdin(data: string | Uint8Array): Promise<void>;
|
|
66
|
+
/** Resize the attached PTY stream when this handle was created as a PTY. */
|
|
67
|
+
resize(size: {
|
|
68
|
+
cols: number;
|
|
69
|
+
rows: number;
|
|
70
|
+
}): Promise<void>;
|
|
64
71
|
/** Detach the local stream without killing the process. */
|
|
65
72
|
disconnect(): void;
|
|
66
73
|
private handleEvents;
|
package/dist/commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ProcessSocket, base64DecodeText } from './processSocket.js';
|
|
1
|
+
import { ProcessSocket, base64DecodeBytes, base64DecodeText } from './processSocket.js';
|
|
2
2
|
import { SandboxError } from './errors.js';
|
|
3
3
|
/** Error thrown by `CommandHandle.wait()` when a process exits non-zero. */
|
|
4
4
|
export class CommandExitError extends SandboxError {
|
|
@@ -21,17 +21,19 @@ export class CommandHandle {
|
|
|
21
21
|
events;
|
|
22
22
|
onStdout;
|
|
23
23
|
onStderr;
|
|
24
|
+
onPty;
|
|
24
25
|
_stdout = '';
|
|
25
26
|
_stderr = '';
|
|
26
27
|
result;
|
|
27
28
|
pending;
|
|
28
|
-
constructor(pid, socket, handleKill, events, onStdout, onStderr) {
|
|
29
|
+
constructor(pid, socket, handleKill, events, onStdout, onStderr, onPty) {
|
|
29
30
|
this.pid = pid;
|
|
30
31
|
this.socket = socket;
|
|
31
32
|
this.handleKill = handleKill;
|
|
32
33
|
this.events = events;
|
|
33
34
|
this.onStdout = onStdout;
|
|
34
35
|
this.onStderr = onStderr;
|
|
36
|
+
this.onPty = onPty;
|
|
35
37
|
this.pending = this.handleEvents();
|
|
36
38
|
}
|
|
37
39
|
get stdout() { return this._stdout; }
|
|
@@ -55,6 +57,10 @@ export class CommandHandle {
|
|
|
55
57
|
async sendStdin(data) {
|
|
56
58
|
this.socket.sendStdin(data);
|
|
57
59
|
}
|
|
60
|
+
/** Resize the attached PTY stream when this handle was created as a PTY. */
|
|
61
|
+
async resize(size) {
|
|
62
|
+
this.socket.sendJson({ type: 'resize', cols: size.cols, rows: size.rows });
|
|
63
|
+
}
|
|
58
64
|
/** Detach the local stream without killing the process. */
|
|
59
65
|
disconnect() {
|
|
60
66
|
this.socket.close();
|
|
@@ -75,6 +81,12 @@ export class CommandHandle {
|
|
|
75
81
|
this._stderr += out;
|
|
76
82
|
await this.onStderr?.(out);
|
|
77
83
|
}
|
|
84
|
+
else if (type === 'pty') {
|
|
85
|
+
const bytes = base64DecodeBytes(frame.data);
|
|
86
|
+
const out = new TextDecoder().decode(bytes);
|
|
87
|
+
this._stdout += out;
|
|
88
|
+
await this.onPty?.(bytes);
|
|
89
|
+
}
|
|
78
90
|
else if (type === 'exit') {
|
|
79
91
|
this.result = {
|
|
80
92
|
exitCode: Number(frame.exit_code ?? frame.exitCode ?? 0),
|
|
@@ -140,7 +152,7 @@ export class Commands {
|
|
|
140
152
|
const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, `/runtime/v1/process/${pid}/connect?since=0`, opts.requestTimeoutMs ?? this.config.requestTimeoutMs).connect();
|
|
141
153
|
const first = await nextStarted(socket);
|
|
142
154
|
const actualPid = framePid(first) ?? pid;
|
|
143
|
-
return new CommandHandle(actualPid, socket, () => this.kill(actualPid), socket, opts.onStdout, opts.onStderr);
|
|
155
|
+
return new CommandHandle(actualPid, socket, () => this.kill(actualPid), socket, opts.onStdout, opts.onStderr, opts.onPty);
|
|
144
156
|
}
|
|
145
157
|
async start(cmd, opts) {
|
|
146
158
|
const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, '/runtime/v1/process', opts.requestTimeoutMs ?? this.config.requestTimeoutMs).connect();
|
|
@@ -160,7 +172,7 @@ export class Commands {
|
|
|
160
172
|
const pid = framePid(first);
|
|
161
173
|
if (pid === undefined)
|
|
162
174
|
throw new SandboxError('process started frame did not include pid');
|
|
163
|
-
return new CommandHandle(pid, socket, () => this.kill(pid), withFirst(first, socket), opts.onStdout, opts.onStderr);
|
|
175
|
+
return new CommandHandle(pid, socket, () => this.kill(pid), withFirst(first, socket), opts.onStdout, opts.onStderr, opts.onPty);
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
async function nextStarted(events) {
|
package/dist/filesystem.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DataPlaneClient } from './transport.js';
|
|
2
|
+
import { ProcessFrame, ProcessSocket } from './processSocket.js';
|
|
2
3
|
export declare enum FileType {
|
|
3
4
|
/** Regular file. */
|
|
4
5
|
FILE = "file",
|
|
@@ -20,6 +21,31 @@ export interface EntryInfo {
|
|
|
20
21
|
metadata?: Record<string, string>;
|
|
21
22
|
}
|
|
22
23
|
export type WriteInfo = EntryInfo;
|
|
24
|
+
export interface FilesystemEvent {
|
|
25
|
+
type: 'create' | 'write' | 'modify' | 'remove' | 'delete' | 'rename' | string;
|
|
26
|
+
path: string;
|
|
27
|
+
entry?: EntryInfo;
|
|
28
|
+
raw: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
export interface WatchOpts {
|
|
31
|
+
recursive?: boolean;
|
|
32
|
+
includeEntry?: boolean;
|
|
33
|
+
requestTimeoutMs?: number;
|
|
34
|
+
onExit?: (error?: Error) => void | Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/** Live filesystem watcher. Call `stop()` to close the local watch stream. */
|
|
37
|
+
export declare class WatchHandle {
|
|
38
|
+
private readonly socket;
|
|
39
|
+
private readonly done;
|
|
40
|
+
constructor(socket: ProcessSocket, events: AsyncIterable<ProcessFrame>, onEvent: (event: FilesystemEvent) => void | Promise<void>, onExit?: (error?: Error) => void | Promise<void>);
|
|
41
|
+
/** Stop watching the directory. */
|
|
42
|
+
stop(): void;
|
|
43
|
+
/** Alias for `stop`. */
|
|
44
|
+
close(): void;
|
|
45
|
+
/** Resolves when the watcher stream exits. */
|
|
46
|
+
wait(): Promise<void>;
|
|
47
|
+
private pump;
|
|
48
|
+
}
|
|
23
49
|
/** Filesystem helper for a sandbox data-plane session. */
|
|
24
50
|
export declare class Filesystem {
|
|
25
51
|
private readonly dataPlane;
|
|
@@ -61,5 +87,6 @@ export declare class Filesystem {
|
|
|
61
87
|
makeDir(path: string, opts?: {
|
|
62
88
|
requestTimeoutMs?: number;
|
|
63
89
|
}): Promise<boolean>;
|
|
64
|
-
|
|
90
|
+
/** Start watching a directory for filesystem events. */
|
|
91
|
+
watchDir(path: string, onEvent: (event: FilesystemEvent) => void | Promise<void>, opts?: WatchOpts): Promise<WatchHandle>;
|
|
65
92
|
}
|
package/dist/filesystem.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { withQuery } from './transport.js';
|
|
2
|
-
import { FileNotFoundError
|
|
2
|
+
import { FileNotFoundError } from './errors.js';
|
|
3
|
+
import { ProcessSocket } from './processSocket.js';
|
|
3
4
|
export var FileType;
|
|
4
5
|
(function (FileType) {
|
|
5
6
|
/** Regular file. */
|
|
@@ -9,6 +10,46 @@ export var FileType;
|
|
|
9
10
|
/** Symbolic link. */
|
|
10
11
|
FileType["SYMLINK"] = "symlink";
|
|
11
12
|
})(FileType || (FileType = {}));
|
|
13
|
+
/** Live filesystem watcher. Call `stop()` to close the local watch stream. */
|
|
14
|
+
export class WatchHandle {
|
|
15
|
+
socket;
|
|
16
|
+
done;
|
|
17
|
+
constructor(socket, events, onEvent, onExit) {
|
|
18
|
+
this.socket = socket;
|
|
19
|
+
this.done = this.pump(events, onEvent, onExit);
|
|
20
|
+
}
|
|
21
|
+
/** Stop watching the directory. */
|
|
22
|
+
stop() {
|
|
23
|
+
this.socket.close();
|
|
24
|
+
}
|
|
25
|
+
/** Alias for `stop`. */
|
|
26
|
+
close() {
|
|
27
|
+
this.stop();
|
|
28
|
+
}
|
|
29
|
+
/** Resolves when the watcher stream exits. */
|
|
30
|
+
wait() {
|
|
31
|
+
return this.done;
|
|
32
|
+
}
|
|
33
|
+
async pump(events, onEvent, onExit) {
|
|
34
|
+
let error;
|
|
35
|
+
try {
|
|
36
|
+
for await (const frame of events) {
|
|
37
|
+
if (frame.type !== 'events' || !Array.isArray(frame.events))
|
|
38
|
+
continue;
|
|
39
|
+
for (const item of frame.events) {
|
|
40
|
+
await onEvent(filesystemEvent(item));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (caught) {
|
|
45
|
+
error = caught instanceof Error ? caught : new Error(String(caught));
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await onExit?.(error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
12
53
|
/** Filesystem helper for a sandbox data-plane session. */
|
|
13
54
|
export class Filesystem {
|
|
14
55
|
dataPlane;
|
|
@@ -70,8 +111,10 @@ export class Filesystem {
|
|
|
70
111
|
await this.dataPlane.postJson(withQuery('/runtime/v1/directories', { path }), opts);
|
|
71
112
|
return true;
|
|
72
113
|
}
|
|
73
|
-
|
|
74
|
-
|
|
114
|
+
/** Start watching a directory for filesystem events. */
|
|
115
|
+
async watchDir(path, onEvent, opts = {}) {
|
|
116
|
+
const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, withQuery('/runtime/v1/files/watch', { path, recursive: opts.recursive ?? false, include_entry: opts.includeEntry }), opts.requestTimeoutMs).connect();
|
|
117
|
+
return new WatchHandle(socket, socket, onEvent, opts.onExit);
|
|
75
118
|
}
|
|
76
119
|
}
|
|
77
120
|
function entryInfo(value) {
|
|
@@ -96,3 +139,19 @@ function recordOfStrings(value) {
|
|
|
96
139
|
return undefined;
|
|
97
140
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, String(item)]));
|
|
98
141
|
}
|
|
142
|
+
function filesystemEvent(value) {
|
|
143
|
+
const item = value && typeof value === 'object' ? value : {};
|
|
144
|
+
return {
|
|
145
|
+
type: normalizeEventType(String(item.type ?? 'modify')),
|
|
146
|
+
path: String(item.path ?? ''),
|
|
147
|
+
entry: item.file && typeof item.file === 'object' ? entryInfo(item.file) : undefined,
|
|
148
|
+
raw: item,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function normalizeEventType(value) {
|
|
152
|
+
if (value === 'delete')
|
|
153
|
+
return 'remove';
|
|
154
|
+
if (value === 'modify')
|
|
155
|
+
return 'write';
|
|
156
|
+
return value;
|
|
157
|
+
}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { DataPlaneClient } from './transport.js';
|
|
2
|
+
export interface GitCommandResult {
|
|
3
|
+
path?: string;
|
|
4
|
+
url?: string;
|
|
5
|
+
ref?: string;
|
|
6
|
+
branch?: string;
|
|
7
|
+
remote?: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
value?: string;
|
|
10
|
+
branches?: string[];
|
|
11
|
+
currentBranch?: string;
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
command?: Record<string, unknown>;
|
|
15
|
+
raw: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface GitAuthOpts {
|
|
18
|
+
username?: string;
|
|
19
|
+
password?: string;
|
|
20
|
+
envs?: Record<string, string>;
|
|
21
|
+
timeout?: number;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
requestTimeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
export interface GitCloneOpts extends GitAuthOpts {
|
|
26
|
+
path?: string;
|
|
27
|
+
branch?: string;
|
|
28
|
+
depth?: number;
|
|
29
|
+
recursive?: boolean;
|
|
30
|
+
submodules?: boolean;
|
|
31
|
+
dangerouslyStoreCredentials?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface GitRequestOpts extends GitAuthOpts {
|
|
34
|
+
}
|
|
35
|
+
export interface GitPullOpts extends GitAuthOpts {
|
|
36
|
+
branch?: string;
|
|
37
|
+
remote?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface GitPushOpts extends GitAuthOpts {
|
|
40
|
+
branch?: string;
|
|
41
|
+
remote?: string;
|
|
42
|
+
setUpstream?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface GitCredentialOpts extends GitRequestOpts {
|
|
45
|
+
host?: string;
|
|
46
|
+
protocol?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface GitConfigureUserOpts extends GitRequestOpts {
|
|
49
|
+
scope?: 'global' | 'local';
|
|
50
|
+
path?: string;
|
|
51
|
+
}
|
|
52
|
+
export interface GitBranchOpts extends GitRequestOpts {
|
|
53
|
+
force?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export interface GitAddOpts extends GitRequestOpts {
|
|
56
|
+
files?: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface GitCommitOpts extends GitRequestOpts {
|
|
59
|
+
authorName?: string;
|
|
60
|
+
authorEmail?: string;
|
|
61
|
+
allowEmpty?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface GitRemoteAddOpts extends GitRequestOpts {
|
|
64
|
+
fetch?: boolean;
|
|
65
|
+
overwrite?: boolean;
|
|
66
|
+
}
|
|
67
|
+
export interface GitConfigOpts extends GitRequestOpts {
|
|
68
|
+
scope?: 'global' | 'local';
|
|
69
|
+
path?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface GitBranches {
|
|
72
|
+
path?: string;
|
|
73
|
+
branches: string[];
|
|
74
|
+
currentBranch?: string;
|
|
75
|
+
result: GitCommandResult;
|
|
76
|
+
}
|
|
77
|
+
export interface GitFileStatus {
|
|
78
|
+
name: string;
|
|
79
|
+
status: string;
|
|
80
|
+
indexStatus: string;
|
|
81
|
+
workingTreeStatus: string;
|
|
82
|
+
staged: boolean;
|
|
83
|
+
renamedFrom?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface GitStatus {
|
|
86
|
+
currentBranch?: string;
|
|
87
|
+
upstream?: string;
|
|
88
|
+
ahead: number;
|
|
89
|
+
behind: number;
|
|
90
|
+
detached: boolean;
|
|
91
|
+
fileStatus: GitFileStatus[];
|
|
92
|
+
isClean: boolean;
|
|
93
|
+
hasChanges: boolean;
|
|
94
|
+
hasStaged: boolean;
|
|
95
|
+
hasUntracked: boolean;
|
|
96
|
+
hasConflicts: boolean;
|
|
97
|
+
totalCount: number;
|
|
98
|
+
stagedCount: number;
|
|
99
|
+
unstagedCount: number;
|
|
100
|
+
untrackedCount: number;
|
|
101
|
+
conflictCount: number;
|
|
102
|
+
result: GitCommandResult;
|
|
103
|
+
}
|
|
104
|
+
/** Git helper backed by sandbox data-plane routes. */
|
|
105
|
+
export declare class Git {
|
|
106
|
+
private readonly dataPlane;
|
|
107
|
+
constructor(dataPlane: DataPlaneClient);
|
|
108
|
+
/** Clone a repository into the sandbox. */
|
|
109
|
+
clone(url: string, opts?: GitCloneOpts): Promise<GitCommandResult>;
|
|
110
|
+
/** Store Git credentials in the sandbox credential helper. */
|
|
111
|
+
dangerouslyAuthenticate(opts: GitCredentialOpts & {
|
|
112
|
+
username: string;
|
|
113
|
+
password: string;
|
|
114
|
+
}): Promise<GitCommandResult>;
|
|
115
|
+
/** Configure Git author identity globally or for one repository. */
|
|
116
|
+
configureUser(name: string, email: string, opts?: GitConfigureUserOpts): Promise<GitCommandResult>;
|
|
117
|
+
/** Return parsed repository status for `path`. */
|
|
118
|
+
status(path: string, opts?: GitRequestOpts): Promise<GitStatus>;
|
|
119
|
+
/** Return branches and the current branch for `path`. */
|
|
120
|
+
branches(path: string, opts?: GitRequestOpts): Promise<GitBranches>;
|
|
121
|
+
/** Create and check out a new branch. */
|
|
122
|
+
createBranch(path: string, branch: string, opts?: GitRequestOpts): Promise<GitCommandResult>;
|
|
123
|
+
/** Delete a branch. */
|
|
124
|
+
deleteBranch(path: string, branch: string, opts?: GitBranchOpts): Promise<GitCommandResult>;
|
|
125
|
+
/** Stage files. Defaults to all files. */
|
|
126
|
+
add(path: string, opts?: GitAddOpts): Promise<GitCommandResult>;
|
|
127
|
+
/** Commit staged files. */
|
|
128
|
+
commit(path: string, message: string, opts?: GitCommitOpts): Promise<GitCommandResult>;
|
|
129
|
+
/** Pull the current branch with a fast-forward-only merge. */
|
|
130
|
+
pull(path: string, opts?: GitPullOpts): Promise<GitCommandResult>;
|
|
131
|
+
/** Push the current branch or a selected branch. */
|
|
132
|
+
push(path: string, opts?: GitPushOpts): Promise<GitCommandResult>;
|
|
133
|
+
/** Check out an arbitrary ref in a repository. */
|
|
134
|
+
checkout(path: string, ref: string, opts?: GitRequestOpts): Promise<GitCommandResult>;
|
|
135
|
+
/** Check out an existing branch in a repository. */
|
|
136
|
+
checkoutBranch(path: string, branch: string, opts?: GitRequestOpts): Promise<GitCommandResult>;
|
|
137
|
+
/** Add a remote. */
|
|
138
|
+
remoteAdd(path: string, name: string, url: string, opts?: GitRemoteAddOpts): Promise<GitCommandResult>;
|
|
139
|
+
/** Set a Git config value. */
|
|
140
|
+
setConfig(key: string, value: string, opts?: GitConfigOpts): Promise<GitCommandResult>;
|
|
141
|
+
/** Read a Git config value. */
|
|
142
|
+
getConfig(key: string, opts?: GitConfigOpts): Promise<string>;
|
|
143
|
+
private run;
|
|
144
|
+
}
|
package/dist/git.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/** Git helper backed by sandbox data-plane routes. */
|
|
2
|
+
export class Git {
|
|
3
|
+
dataPlane;
|
|
4
|
+
constructor(dataPlane) {
|
|
5
|
+
this.dataPlane = dataPlane;
|
|
6
|
+
}
|
|
7
|
+
/** Clone a repository into the sandbox. */
|
|
8
|
+
async clone(url, opts = {}) {
|
|
9
|
+
return this.run('/runtime/v1/git/clone', {
|
|
10
|
+
url,
|
|
11
|
+
...gitOpts(opts),
|
|
12
|
+
...pick(opts, ['path', 'branch', 'depth', 'recursive', 'submodules', 'username', 'password']),
|
|
13
|
+
dangerously_store_credentials: opts.dangerouslyStoreCredentials,
|
|
14
|
+
}, opts);
|
|
15
|
+
}
|
|
16
|
+
/** Store Git credentials in the sandbox credential helper. */
|
|
17
|
+
async dangerouslyAuthenticate(opts) {
|
|
18
|
+
return this.run('/runtime/v1/git/dangerously_authenticate', {
|
|
19
|
+
...gitOpts(opts),
|
|
20
|
+
username: opts.username,
|
|
21
|
+
password: opts.password,
|
|
22
|
+
host: opts.host,
|
|
23
|
+
protocol: opts.protocol,
|
|
24
|
+
}, opts);
|
|
25
|
+
}
|
|
26
|
+
/** Configure Git author identity globally or for one repository. */
|
|
27
|
+
async configureUser(name, email, opts = {}) {
|
|
28
|
+
return this.run('/runtime/v1/git/configure_user', {
|
|
29
|
+
...gitOpts(opts),
|
|
30
|
+
name,
|
|
31
|
+
email,
|
|
32
|
+
scope: opts.scope,
|
|
33
|
+
path: opts.path,
|
|
34
|
+
}, opts);
|
|
35
|
+
}
|
|
36
|
+
/** Return parsed repository status for `path`. */
|
|
37
|
+
async status(path, opts = {}) {
|
|
38
|
+
const result = await this.run('/runtime/v1/git/status', { path, ...gitOpts(opts) }, opts);
|
|
39
|
+
return parseGitStatus(result);
|
|
40
|
+
}
|
|
41
|
+
/** Return branches and the current branch for `path`. */
|
|
42
|
+
async branches(path, opts = {}) {
|
|
43
|
+
const result = await this.run('/runtime/v1/git/branches', { path, ...gitOpts(opts) }, opts);
|
|
44
|
+
return {
|
|
45
|
+
path: result.path,
|
|
46
|
+
branches: Array.isArray(result.raw.branches) ? result.raw.branches.map(String) : result.branches ?? [],
|
|
47
|
+
currentBranch: stringValue(result.raw.current_branch) ?? result.currentBranch,
|
|
48
|
+
result,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** Create and check out a new branch. */
|
|
52
|
+
async createBranch(path, branch, opts = {}) {
|
|
53
|
+
return this.run('/runtime/v1/git/create_branch', { path, branch, ...gitOpts(opts) }, opts);
|
|
54
|
+
}
|
|
55
|
+
/** Delete a branch. */
|
|
56
|
+
async deleteBranch(path, branch, opts = {}) {
|
|
57
|
+
return this.run('/runtime/v1/git/delete_branch', { path, branch, force: opts.force, ...gitOpts(opts) }, opts);
|
|
58
|
+
}
|
|
59
|
+
/** Stage files. Defaults to all files. */
|
|
60
|
+
async add(path, opts = {}) {
|
|
61
|
+
return this.run('/runtime/v1/git/add', { path, files: opts.files, ...gitOpts(opts) }, opts);
|
|
62
|
+
}
|
|
63
|
+
/** Commit staged files. */
|
|
64
|
+
async commit(path, message, opts = {}) {
|
|
65
|
+
return this.run('/runtime/v1/git/commit', {
|
|
66
|
+
path,
|
|
67
|
+
message,
|
|
68
|
+
author_name: opts.authorName,
|
|
69
|
+
author_email: opts.authorEmail,
|
|
70
|
+
allow_empty: opts.allowEmpty,
|
|
71
|
+
...gitOpts(opts),
|
|
72
|
+
}, opts);
|
|
73
|
+
}
|
|
74
|
+
/** Pull the current branch with a fast-forward-only merge. */
|
|
75
|
+
async pull(path, opts = {}) {
|
|
76
|
+
return this.run('/runtime/v1/git/pull', { path, ...gitOpts(opts), ...pick(opts, ['remote', 'branch', 'username', 'password']) }, opts);
|
|
77
|
+
}
|
|
78
|
+
/** Push the current branch or a selected branch. */
|
|
79
|
+
async push(path, opts = {}) {
|
|
80
|
+
return this.run('/runtime/v1/git/push', {
|
|
81
|
+
path,
|
|
82
|
+
...gitOpts(opts),
|
|
83
|
+
...pick(opts, ['remote', 'branch', 'username', 'password']),
|
|
84
|
+
set_upstream: opts.setUpstream,
|
|
85
|
+
}, opts);
|
|
86
|
+
}
|
|
87
|
+
/** Check out an arbitrary ref in a repository. */
|
|
88
|
+
async checkout(path, ref, opts = {}) {
|
|
89
|
+
return this.run('/runtime/v1/git/checkout', { path, ref, ...gitOpts(opts) }, opts);
|
|
90
|
+
}
|
|
91
|
+
/** Check out an existing branch in a repository. */
|
|
92
|
+
async checkoutBranch(path, branch, opts = {}) {
|
|
93
|
+
return this.checkout(path, branch, opts);
|
|
94
|
+
}
|
|
95
|
+
/** Add a remote. */
|
|
96
|
+
async remoteAdd(path, name, url, opts = {}) {
|
|
97
|
+
return this.run('/runtime/v1/git/remote_add', {
|
|
98
|
+
path,
|
|
99
|
+
name,
|
|
100
|
+
url,
|
|
101
|
+
fetch: opts.fetch,
|
|
102
|
+
overwrite: opts.overwrite,
|
|
103
|
+
...gitOpts(opts),
|
|
104
|
+
}, opts);
|
|
105
|
+
}
|
|
106
|
+
/** Set a Git config value. */
|
|
107
|
+
async setConfig(key, value, opts = {}) {
|
|
108
|
+
return this.run('/runtime/v1/git/set_config', {
|
|
109
|
+
key,
|
|
110
|
+
value,
|
|
111
|
+
scope: opts.scope,
|
|
112
|
+
path: opts.path,
|
|
113
|
+
...gitOpts(opts),
|
|
114
|
+
}, opts);
|
|
115
|
+
}
|
|
116
|
+
/** Read a Git config value. */
|
|
117
|
+
async getConfig(key, opts = {}) {
|
|
118
|
+
const result = await this.run('/runtime/v1/git/get_config', {
|
|
119
|
+
key,
|
|
120
|
+
scope: opts.scope,
|
|
121
|
+
path: opts.path,
|
|
122
|
+
...gitOpts(opts),
|
|
123
|
+
}, opts);
|
|
124
|
+
return String(result.value ?? '');
|
|
125
|
+
}
|
|
126
|
+
async run(path, json, opts) {
|
|
127
|
+
const payload = await this.dataPlane.postJson(path, { json: compact(json), requestTimeoutMs: opts.requestTimeoutMs });
|
|
128
|
+
return gitResult(payload.git ?? payload);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function gitOpts(opts) {
|
|
132
|
+
return {
|
|
133
|
+
env_vars: opts.envs,
|
|
134
|
+
timeout_seconds: opts.timeout ?? (opts.timeoutMs === undefined ? undefined : Math.ceil(opts.timeoutMs / 1000)),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function pick(source, keys) {
|
|
138
|
+
const record = source;
|
|
139
|
+
return Object.fromEntries(keys.map((key) => [key, record[key]]));
|
|
140
|
+
}
|
|
141
|
+
function compact(value) {
|
|
142
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
143
|
+
}
|
|
144
|
+
function gitResult(value) {
|
|
145
|
+
const item = value && typeof value === 'object' ? value : {};
|
|
146
|
+
return {
|
|
147
|
+
path: stringValue(item.path),
|
|
148
|
+
url: stringValue(item.url),
|
|
149
|
+
ref: stringValue(item.ref),
|
|
150
|
+
branch: stringValue(item.branch),
|
|
151
|
+
remote: stringValue(item.remote),
|
|
152
|
+
name: stringValue(item.name),
|
|
153
|
+
value: stringValue(item.value),
|
|
154
|
+
branches: Array.isArray(item.branches) ? item.branches.map(String) : undefined,
|
|
155
|
+
currentBranch: stringValue(item.current_branch),
|
|
156
|
+
stdout: String(item.stdout ?? ''),
|
|
157
|
+
stderr: String(item.stderr ?? ''),
|
|
158
|
+
command: item.command && typeof item.command === 'object' ? item.command : undefined,
|
|
159
|
+
raw: item,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function parseGitStatus(result) {
|
|
163
|
+
const fileStatus = [];
|
|
164
|
+
let currentBranch;
|
|
165
|
+
let upstream;
|
|
166
|
+
let ahead = 0;
|
|
167
|
+
let behind = 0;
|
|
168
|
+
let detached = false;
|
|
169
|
+
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
170
|
+
if (line.startsWith('## ')) {
|
|
171
|
+
const branchLine = line.slice(3);
|
|
172
|
+
detached = branchLine.includes('HEAD') && branchLine.includes('no branch');
|
|
173
|
+
const [branchPart, trackingPart] = branchLine.split('...');
|
|
174
|
+
currentBranch = branchPart?.replace(/\s+\[.*\]$/, '') || undefined;
|
|
175
|
+
if (trackingPart) {
|
|
176
|
+
const match = trackingPart.match(/^([^\s[]+)(?:\s+\[(.*)\])?/);
|
|
177
|
+
upstream = match?.[1];
|
|
178
|
+
const details = match?.[2] ?? '';
|
|
179
|
+
ahead = numberFrom(details, /ahead\s+(\d+)/);
|
|
180
|
+
behind = numberFrom(details, /behind\s+(\d+)/);
|
|
181
|
+
}
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const indexStatus = line[0] ?? ' ';
|
|
185
|
+
const workingTreeStatus = line[1] ?? ' ';
|
|
186
|
+
const path = line.slice(3);
|
|
187
|
+
const [name, renamedFrom] = path.includes(' -> ') ? path.split(' -> ').reverse() : [path, undefined];
|
|
188
|
+
const status = statusName(indexStatus, workingTreeStatus);
|
|
189
|
+
fileStatus.push({ name, status, indexStatus, workingTreeStatus, staged: indexStatus !== ' ' && indexStatus !== '?', renamedFrom });
|
|
190
|
+
}
|
|
191
|
+
const stagedCount = fileStatus.filter((item) => item.staged).length;
|
|
192
|
+
const untrackedCount = fileStatus.filter((item) => item.status === 'untracked').length;
|
|
193
|
+
const conflictCount = fileStatus.filter((item) => item.status === 'conflict').length;
|
|
194
|
+
const totalCount = fileStatus.length;
|
|
195
|
+
return {
|
|
196
|
+
currentBranch,
|
|
197
|
+
upstream,
|
|
198
|
+
ahead,
|
|
199
|
+
behind,
|
|
200
|
+
detached,
|
|
201
|
+
fileStatus,
|
|
202
|
+
isClean: totalCount === 0,
|
|
203
|
+
hasChanges: totalCount > 0,
|
|
204
|
+
hasStaged: stagedCount > 0,
|
|
205
|
+
hasUntracked: untrackedCount > 0,
|
|
206
|
+
hasConflicts: conflictCount > 0,
|
|
207
|
+
totalCount,
|
|
208
|
+
stagedCount,
|
|
209
|
+
unstagedCount: totalCount - stagedCount,
|
|
210
|
+
untrackedCount,
|
|
211
|
+
conflictCount,
|
|
212
|
+
result,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function numberFrom(value, pattern) {
|
|
216
|
+
const match = value.match(pattern);
|
|
217
|
+
return match ? Number(match[1]) : 0;
|
|
218
|
+
}
|
|
219
|
+
function statusName(indexStatus, workingTreeStatus) {
|
|
220
|
+
if (indexStatus === '?' && workingTreeStatus === '?')
|
|
221
|
+
return 'untracked';
|
|
222
|
+
if (indexStatus === 'U' || workingTreeStatus === 'U' || indexStatus === 'A' && workingTreeStatus === 'A')
|
|
223
|
+
return 'conflict';
|
|
224
|
+
if (indexStatus === 'D' || workingTreeStatus === 'D')
|
|
225
|
+
return 'deleted';
|
|
226
|
+
if (indexStatus === 'R')
|
|
227
|
+
return 'renamed';
|
|
228
|
+
if (indexStatus === 'A')
|
|
229
|
+
return 'added';
|
|
230
|
+
if (indexStatus === 'M' || workingTreeStatus === 'M')
|
|
231
|
+
return 'modified';
|
|
232
|
+
return 'changed';
|
|
233
|
+
}
|
|
234
|
+
function stringValue(value) {
|
|
235
|
+
return typeof value === 'string' ? value : undefined;
|
|
236
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
export { ApiError, AuthenticationError, FileNotFoundError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, NotImplementedError, RateLimitError, SandboxError, TimeoutError, } from './errors.js';
|
|
2
2
|
export { ConnectionConfig, KEEPALIVE_PING_INTERVAL_SEC } from './connectionConfig.js';
|
|
3
|
-
export { Sandbox } from './sandbox.js';
|
|
4
|
-
export type { SandboxCreateOpts, SandboxConnectOpts, SandboxInfo } from './sandbox.js';
|
|
3
|
+
export { Sandbox, SnapshotPaginator } from './sandbox.js';
|
|
4
|
+
export type { CreateSnapshotOpts, RestoreSnapshotOpts, SandboxCreateOpts, SandboxConnectOpts, SandboxInfo, SandboxMetrics, SandboxUrlOpts, SnapshotInfo, FileUrlInfo, } from './sandbox.js';
|
|
5
5
|
export { CommandExitError, CommandHandle, Commands } from './commands.js';
|
|
6
6
|
export type { CommandResult, CommandStartOpts, ProcessInfo } from './commands.js';
|
|
7
|
-
export { FileType, Filesystem } from './filesystem.js';
|
|
8
|
-
export type { EntryInfo, WriteInfo } from './filesystem.js';
|
|
9
|
-
export {
|
|
7
|
+
export { FileType, Filesystem, WatchHandle } from './filesystem.js';
|
|
8
|
+
export type { EntryInfo, FilesystemEvent, WatchOpts, WriteInfo } from './filesystem.js';
|
|
9
|
+
export { Git } from './git.js';
|
|
10
|
+
export type { GitAddOpts, GitAuthOpts, GitBranches, GitBranchOpts, GitCloneOpts, GitCommandResult, GitConfigOpts, GitConfigureUserOpts, GitCredentialOpts, GitCommitOpts, GitFileStatus, GitPullOpts, GitPushOpts, GitRemoteAddOpts, GitRequestOpts, GitStatus, } from './git.js';
|
|
11
|
+
export { Pty } from './pty.js';
|
|
12
|
+
export type { PtyConnectOpts, PtyCreateOpts, PtySize } from './pty.js';
|
|
13
|
+
export { ProcessSocket, base64DecodeBytes, base64DecodeText, base64Encode } from './processSocket.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { ApiError, AuthenticationError, FileNotFoundError, InvalidArgumentError, NotEnoughSpaceError, NotFoundError, NotImplementedError, RateLimitError, SandboxError, TimeoutError, } from './errors.js';
|
|
2
2
|
export { ConnectionConfig, KEEPALIVE_PING_INTERVAL_SEC } from './connectionConfig.js';
|
|
3
|
-
export { Sandbox } from './sandbox.js';
|
|
3
|
+
export { Sandbox, SnapshotPaginator } from './sandbox.js';
|
|
4
4
|
export { CommandExitError, CommandHandle, Commands } from './commands.js';
|
|
5
|
-
export { FileType, Filesystem } from './filesystem.js';
|
|
6
|
-
export {
|
|
5
|
+
export { FileType, Filesystem, WatchHandle } from './filesystem.js';
|
|
6
|
+
export { Git } from './git.js';
|
|
7
|
+
export { Pty } from './pty.js';
|
|
8
|
+
export { ProcessSocket, base64DecodeBytes, base64DecodeText, base64Encode } from './processSocket.js';
|
package/dist/processSocket.d.ts
CHANGED
|
@@ -23,3 +23,4 @@ export declare class ProcessSocket implements AsyncIterable<ProcessFrame> {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function base64Encode(bytes: Uint8Array): string;
|
|
25
25
|
export declare function base64DecodeText(value: unknown): string;
|
|
26
|
+
export declare function base64DecodeBytes(value: unknown): Uint8Array;
|
package/dist/processSocket.js
CHANGED
|
@@ -122,12 +122,17 @@ export function base64DecodeText(value) {
|
|
|
122
122
|
if (typeof value !== 'string')
|
|
123
123
|
return String(value ?? '');
|
|
124
124
|
try {
|
|
125
|
-
return Buffer.from(value
|
|
125
|
+
return Buffer.from(base64DecodeBytes(value)).toString('utf8');
|
|
126
126
|
}
|
|
127
127
|
catch {
|
|
128
128
|
return value;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
export function base64DecodeBytes(value) {
|
|
132
|
+
if (typeof value !== 'string')
|
|
133
|
+
return new TextEncoder().encode(String(value ?? ''));
|
|
134
|
+
return new Uint8Array(Buffer.from(value, 'base64'));
|
|
135
|
+
}
|
|
131
136
|
function rawDataToText(message) {
|
|
132
137
|
if (typeof message === 'string')
|
|
133
138
|
return message;
|
package/dist/pty.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CommandHandle, CommandStartOpts } from './commands.js';
|
|
2
|
+
import { ConnectionConfig } from './connectionConfig.js';
|
|
3
|
+
import { DataPlaneClient } from './transport.js';
|
|
4
|
+
export interface PtySize {
|
|
5
|
+
cols: number;
|
|
6
|
+
rows: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PtyCreateOpts extends Omit<CommandStartOpts, 'background' | 'onStdout' | 'onStderr'>, PtySize {
|
|
9
|
+
onData?: (data: Uint8Array) => void | Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface PtyConnectOpts {
|
|
12
|
+
onData?: (data: Uint8Array) => void | Promise<void>;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
requestTimeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/** PTY helper backed by the sandbox process WebSocket runtime. */
|
|
17
|
+
export declare class Pty {
|
|
18
|
+
private readonly dataPlane;
|
|
19
|
+
private readonly config;
|
|
20
|
+
constructor(dataPlane: DataPlaneClient, config: ConnectionConfig);
|
|
21
|
+
/** Create an interactive shell PTY and return its live command handle. */
|
|
22
|
+
create(opts: PtyCreateOpts): Promise<CommandHandle>;
|
|
23
|
+
/** Connect to a running PTY by pid. */
|
|
24
|
+
connect(pid: number | string, opts?: PtyConnectOpts): Promise<CommandHandle>;
|
|
25
|
+
/** Send input bytes or text to a PTY. */
|
|
26
|
+
sendStdin(pid: number | string, data: string | Uint8Array, opts?: PtyConnectOpts): Promise<void>;
|
|
27
|
+
/** Alias for `sendStdin`. */
|
|
28
|
+
sendInput(pid: number | string, data: string | Uint8Array, opts?: PtyConnectOpts): Promise<void>;
|
|
29
|
+
/** Resize a running PTY. */
|
|
30
|
+
resize(pid: number | string, size: PtySize, opts?: PtyConnectOpts): Promise<void>;
|
|
31
|
+
/** Kill a running PTY. */
|
|
32
|
+
kill(pid: number | string): Promise<boolean>;
|
|
33
|
+
}
|
package/dist/pty.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CommandHandle } from './commands.js';
|
|
2
|
+
import { ProcessSocket } from './processSocket.js';
|
|
3
|
+
import { SandboxError } from './errors.js';
|
|
4
|
+
/** PTY helper backed by the sandbox process WebSocket runtime. */
|
|
5
|
+
export class Pty {
|
|
6
|
+
dataPlane;
|
|
7
|
+
config;
|
|
8
|
+
constructor(dataPlane, config) {
|
|
9
|
+
this.dataPlane = dataPlane;
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
/** Create an interactive shell PTY and return its live command handle. */
|
|
13
|
+
async create(opts) {
|
|
14
|
+
const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, '/runtime/v1/process', opts.requestTimeoutMs ?? this.config.requestTimeoutMs).connect();
|
|
15
|
+
const envs = { TERM: 'xterm-256color', LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8', ...(opts.envs ?? {}) };
|
|
16
|
+
socket.sendJson({
|
|
17
|
+
type: 'start',
|
|
18
|
+
cmd: '/bin/bash',
|
|
19
|
+
args: ['-i', '-l'],
|
|
20
|
+
cwd: opts.cwd,
|
|
21
|
+
user: opts.user,
|
|
22
|
+
environment: envs,
|
|
23
|
+
envs,
|
|
24
|
+
stdin: true,
|
|
25
|
+
pty: { cols: opts.cols, rows: opts.rows },
|
|
26
|
+
timeout_ms: opts.timeoutMs ?? 60_000,
|
|
27
|
+
});
|
|
28
|
+
const first = await nextStarted(socket);
|
|
29
|
+
const pid = framePid(first);
|
|
30
|
+
if (pid === undefined)
|
|
31
|
+
throw new SandboxError('PTY started frame did not include pid');
|
|
32
|
+
return new CommandHandle(pid, socket, () => this.kill(pid), withFirst(first, socket), undefined, undefined, opts.onData ?? opts.onPty);
|
|
33
|
+
}
|
|
34
|
+
/** Connect to a running PTY by pid. */
|
|
35
|
+
async connect(pid, opts = {}) {
|
|
36
|
+
const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, `/runtime/v1/process/${pid}/connect?since=0`, opts.requestTimeoutMs ?? this.config.requestTimeoutMs).connect();
|
|
37
|
+
const first = await nextStarted(socket);
|
|
38
|
+
const actualPid = framePid(first) ?? pid;
|
|
39
|
+
return new CommandHandle(actualPid, socket, () => this.kill(actualPid), withFirst(first, socket), undefined, undefined, opts.onData);
|
|
40
|
+
}
|
|
41
|
+
/** Send input bytes or text to a PTY. */
|
|
42
|
+
async sendStdin(pid, data, opts = {}) {
|
|
43
|
+
const handle = await this.connect(pid, opts);
|
|
44
|
+
try {
|
|
45
|
+
await handle.sendStdin(data);
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
handle.disconnect();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Alias for `sendStdin`. */
|
|
52
|
+
async sendInput(pid, data, opts = {}) {
|
|
53
|
+
return this.sendStdin(pid, data, opts);
|
|
54
|
+
}
|
|
55
|
+
/** Resize a running PTY. */
|
|
56
|
+
async resize(pid, size, opts = {}) {
|
|
57
|
+
const handle = await this.connect(pid, opts);
|
|
58
|
+
try {
|
|
59
|
+
await handle.resize(size);
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
handle.disconnect();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Kill a running PTY. */
|
|
66
|
+
async kill(pid) {
|
|
67
|
+
await this.dataPlane.postJson(`/runtime/v1/process/${pid}/signal`, {
|
|
68
|
+
json: { signal: 'SIGKILL' },
|
|
69
|
+
});
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function nextStarted(events) {
|
|
74
|
+
for await (const frame of events) {
|
|
75
|
+
if (frame.type === 'started')
|
|
76
|
+
return frame;
|
|
77
|
+
}
|
|
78
|
+
throw new SandboxError('PTY ended before started frame');
|
|
79
|
+
}
|
|
80
|
+
async function* withFirst(first, rest) {
|
|
81
|
+
yield first;
|
|
82
|
+
yield* rest;
|
|
83
|
+
}
|
|
84
|
+
function framePid(frame) {
|
|
85
|
+
const process = frame.process && typeof frame.process === 'object' ? frame.process : {};
|
|
86
|
+
const pid = frame.pid ?? process.pid ?? process.id;
|
|
87
|
+
return typeof pid === 'number' || typeof pid === 'string' ? pid : undefined;
|
|
88
|
+
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { Commands } from './commands.js';
|
|
|
2
2
|
import { ConnectionConfig, ConnectionOpts } from './connectionConfig.js';
|
|
3
3
|
import { ControlClient } from './transport.js';
|
|
4
4
|
import { Filesystem } from './filesystem.js';
|
|
5
|
+
import { Git } from './git.js';
|
|
6
|
+
import { Pty } from './pty.js';
|
|
5
7
|
export interface SandboxCreateOpts extends ConnectionOpts {
|
|
6
8
|
/** Template slug to create. Defaults to "base". */
|
|
7
9
|
template?: string;
|
|
@@ -28,19 +30,66 @@ export interface SandboxInfo {
|
|
|
28
30
|
startedAt?: string;
|
|
29
31
|
endAt?: string;
|
|
30
32
|
}
|
|
33
|
+
export interface SandboxMetrics {
|
|
34
|
+
sandboxId?: string;
|
|
35
|
+
state?: string;
|
|
36
|
+
node?: string;
|
|
37
|
+
backend?: string;
|
|
38
|
+
cpuCount?: number;
|
|
39
|
+
memoryMb?: number;
|
|
40
|
+
raw: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
export interface SnapshotInfo {
|
|
43
|
+
snapshotId: string;
|
|
44
|
+
sandboxId?: string;
|
|
45
|
+
name?: string;
|
|
46
|
+
status?: string;
|
|
47
|
+
sizeBytes?: number;
|
|
48
|
+
createdAt?: string;
|
|
49
|
+
expiresAt?: string;
|
|
50
|
+
raw: Record<string, unknown>;
|
|
51
|
+
}
|
|
52
|
+
export interface FileUrlInfo {
|
|
53
|
+
method: string;
|
|
54
|
+
path: string;
|
|
55
|
+
url: string;
|
|
56
|
+
expiresAt?: string;
|
|
57
|
+
raw: Record<string, unknown>;
|
|
58
|
+
}
|
|
59
|
+
export interface SandboxUrlOpts extends ConnectionOpts {
|
|
60
|
+
user?: string;
|
|
61
|
+
useSignatureExpiration?: number;
|
|
62
|
+
expiresInSeconds?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface CreateSnapshotOpts extends ConnectionOpts {
|
|
65
|
+
name?: string;
|
|
66
|
+
metadata?: Record<string, string>;
|
|
67
|
+
expiresAt?: string;
|
|
68
|
+
quiesceMode?: string;
|
|
69
|
+
}
|
|
70
|
+
export interface RestoreSnapshotOpts extends ConnectionOpts {
|
|
71
|
+
checkpointId?: string | number;
|
|
72
|
+
snapshotId?: string | number;
|
|
73
|
+
timeout?: number;
|
|
74
|
+
timeoutMs?: number;
|
|
75
|
+
}
|
|
76
|
+
export declare class SnapshotPaginator {
|
|
77
|
+
private readonly loadItems;
|
|
78
|
+
private consumed;
|
|
79
|
+
hasNext: boolean;
|
|
80
|
+
nextToken: string | undefined;
|
|
81
|
+
constructor(loadItems: () => Promise<SnapshotInfo[]>);
|
|
82
|
+
nextItems(): Promise<SnapshotInfo[]>;
|
|
83
|
+
}
|
|
31
84
|
/** Running Watasu sandbox with ready `files` and `commands` helpers. */
|
|
32
85
|
export declare class Sandbox {
|
|
33
86
|
/** Default template slug used when create is called without a template. */
|
|
34
87
|
static readonly defaultTemplate = "base";
|
|
35
88
|
files: Filesystem;
|
|
36
89
|
commands: Commands;
|
|
90
|
+
pty: Pty;
|
|
91
|
+
git: Git;
|
|
37
92
|
readonly sandboxId: string;
|
|
38
|
-
readonly pty: {
|
|
39
|
-
create: () => never;
|
|
40
|
-
};
|
|
41
|
-
readonly git: {
|
|
42
|
-
clone: () => never;
|
|
43
|
-
};
|
|
44
93
|
private readonly config;
|
|
45
94
|
private readonly control;
|
|
46
95
|
private readonly envs;
|
|
@@ -62,6 +111,16 @@ export declare class Sandbox {
|
|
|
62
111
|
connect(opts?: SandboxConnectOpts): Promise<this>;
|
|
63
112
|
/** Destroy a sandbox by id. */
|
|
64
113
|
static kill(sandboxId: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
114
|
+
/** Fetch sandbox metrics by id. */
|
|
115
|
+
static getMetrics(sandboxId: string, opts?: ConnectionOpts): Promise<SandboxMetrics[]>;
|
|
116
|
+
/** Deprecated alias for `getInfo`. */
|
|
117
|
+
static getFullInfo(sandboxId: string, opts?: ConnectionOpts): Promise<SandboxInfo>;
|
|
118
|
+
/** Create a Watasu checkpoint using snapshot naming. */
|
|
119
|
+
static createSnapshot(sandboxId: string, opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
120
|
+
/** List checkpoints for one sandbox using snapshot naming. */
|
|
121
|
+
static listSnapshots(sandboxId: string, opts?: ConnectionOpts): SnapshotPaginator;
|
|
122
|
+
/** Delete a snapshot by id. Returns `false` when the snapshot does not exist. */
|
|
123
|
+
static deleteSnapshot(snapshotId: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
65
124
|
/** Destroy this sandbox. */
|
|
66
125
|
kill(): Promise<boolean>;
|
|
67
126
|
/** Check if this sandbox is in a runtime-active lifecycle state. */
|
|
@@ -74,15 +133,36 @@ export declare class Sandbox {
|
|
|
74
133
|
static getInfo(sandboxId: string, opts?: ConnectionOpts): Promise<SandboxInfo>;
|
|
75
134
|
/** Fetch the latest control-plane metadata for this sandbox. */
|
|
76
135
|
getInfo(): Promise<SandboxInfo>;
|
|
136
|
+
/** Fetch latest sandbox metrics. */
|
|
137
|
+
getMetrics(opts?: ConnectionOpts): Promise<SandboxMetrics[]>;
|
|
138
|
+
/** Create a Watasu checkpoint using snapshot naming. */
|
|
139
|
+
createSnapshot(opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
140
|
+
/** Delete a snapshot by id. */
|
|
141
|
+
deleteSnapshot(snapshotId: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
142
|
+
/** Watasu-native alias for `createSnapshot`. */
|
|
143
|
+
checkpoint(opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
144
|
+
/** List checkpoints for this sandbox using snapshot naming. */
|
|
145
|
+
listSnapshots(opts?: ConnectionOpts): SnapshotPaginator;
|
|
146
|
+
/** Restore a checkpoint into a new sandbox and return its control-plane info. */
|
|
147
|
+
restore(opts?: RestoreSnapshotOpts | string | number): Promise<SandboxInfo>;
|
|
77
148
|
/** List sandboxes visible to the configured API key. */
|
|
78
149
|
static list(opts?: ConnectionOpts & {
|
|
79
150
|
team?: string;
|
|
80
151
|
}): Promise<SandboxInfo[]>;
|
|
81
152
|
/** Return the public hostname for an exposed sandbox port. */
|
|
82
153
|
getHost(port: number): string;
|
|
154
|
+
/** Get a signed URL that accepts a POST upload for a sandbox file path. */
|
|
155
|
+
uploadUrl(path: string, opts?: SandboxUrlOpts): Promise<string>;
|
|
156
|
+
/** Get a signed URL that accepts a GET download for a sandbox file path. */
|
|
157
|
+
downloadUrl(path: string, opts?: SandboxUrlOpts): Promise<string>;
|
|
158
|
+
/** Get signed upload URL metadata for a sandbox file path. */
|
|
159
|
+
uploadUrlInfo(path: string, opts?: SandboxUrlOpts): Promise<FileUrlInfo>;
|
|
160
|
+
/** Get signed download URL metadata for a sandbox file path. */
|
|
161
|
+
downloadUrlInfo(path: string, opts?: SandboxUrlOpts): Promise<FileUrlInfo>;
|
|
162
|
+
updateNetwork(..._args: unknown[]): never;
|
|
83
163
|
pause(): never;
|
|
164
|
+
betaPause(): never;
|
|
84
165
|
resume(): never;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
restore(): never;
|
|
166
|
+
private fileUrl;
|
|
167
|
+
private configOptions;
|
|
88
168
|
}
|
package/dist/sandbox.js
CHANGED
|
@@ -3,15 +3,33 @@ import { ConnectionConfig, SESSION_OPERATION_REQUEST_TIMEOUT_MS } from './connec
|
|
|
3
3
|
import { DataPlaneClient, ControlClient } from './transport.js';
|
|
4
4
|
import { NotFoundError, SandboxError, unsupported } from './errors.js';
|
|
5
5
|
import { Filesystem } from './filesystem.js';
|
|
6
|
+
import { Git } from './git.js';
|
|
7
|
+
import { Pty } from './pty.js';
|
|
8
|
+
export class SnapshotPaginator {
|
|
9
|
+
loadItems;
|
|
10
|
+
consumed = false;
|
|
11
|
+
hasNext = true;
|
|
12
|
+
nextToken;
|
|
13
|
+
constructor(loadItems) {
|
|
14
|
+
this.loadItems = loadItems;
|
|
15
|
+
}
|
|
16
|
+
async nextItems() {
|
|
17
|
+
if (this.consumed)
|
|
18
|
+
throw new SandboxError('No more snapshots to fetch');
|
|
19
|
+
this.consumed = true;
|
|
20
|
+
this.hasNext = false;
|
|
21
|
+
return this.loadItems();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
6
24
|
/** Running Watasu sandbox with ready `files` and `commands` helpers. */
|
|
7
25
|
export class Sandbox {
|
|
8
26
|
/** Default template slug used when create is called without a template. */
|
|
9
27
|
static defaultTemplate = 'base';
|
|
10
28
|
files;
|
|
11
29
|
commands;
|
|
30
|
+
pty;
|
|
31
|
+
git;
|
|
12
32
|
sandboxId;
|
|
13
|
-
pty = { create: () => unsupported('sandbox.pty') };
|
|
14
|
-
git = { clone: () => unsupported('sandbox.git') };
|
|
15
33
|
config;
|
|
16
34
|
control;
|
|
17
35
|
envs;
|
|
@@ -27,6 +45,8 @@ export class Sandbox {
|
|
|
27
45
|
this.dataPlane = dataPlane;
|
|
28
46
|
this.files = new Filesystem(dataPlane);
|
|
29
47
|
this.commands = new Commands(dataPlane, this.config, this.envs);
|
|
48
|
+
this.pty = new Pty(dataPlane, this.config);
|
|
49
|
+
this.git = new Git(dataPlane);
|
|
30
50
|
}
|
|
31
51
|
/** Create a sandbox and return it only after the API supplies a data-plane session. */
|
|
32
52
|
static async create(templateOrOpts, opts = {}) {
|
|
@@ -94,6 +114,8 @@ export class Sandbox {
|
|
|
94
114
|
this.dataPlane = dataPlane;
|
|
95
115
|
this.files = new Filesystem(dataPlane);
|
|
96
116
|
this.commands = new Commands(dataPlane, this.config, this.envs);
|
|
117
|
+
this.pty = new Pty(dataPlane, this.config);
|
|
118
|
+
this.git = new Git(dataPlane);
|
|
97
119
|
return this;
|
|
98
120
|
}
|
|
99
121
|
/** Destroy a sandbox by id. */
|
|
@@ -102,6 +124,53 @@ export class Sandbox {
|
|
|
102
124
|
await control.delete(`/sandboxes/${sandboxId}`);
|
|
103
125
|
return true;
|
|
104
126
|
}
|
|
127
|
+
/** Fetch sandbox metrics by id. */
|
|
128
|
+
static async getMetrics(sandboxId, opts = {}) {
|
|
129
|
+
const control = new ControlClient(new ConnectionConfig(opts));
|
|
130
|
+
const payload = await control.get(`/sandboxes/${sandboxId}/metrics`, {
|
|
131
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
132
|
+
});
|
|
133
|
+
return metricsList(payload.metrics ?? payload);
|
|
134
|
+
}
|
|
135
|
+
/** Deprecated alias for `getInfo`. */
|
|
136
|
+
static async getFullInfo(sandboxId, opts = {}) {
|
|
137
|
+
return this.getInfo(sandboxId, opts);
|
|
138
|
+
}
|
|
139
|
+
/** Create a Watasu checkpoint using snapshot naming. */
|
|
140
|
+
static async createSnapshot(sandboxId, opts = {}) {
|
|
141
|
+
const control = new ControlClient(new ConnectionConfig(opts));
|
|
142
|
+
const payload = await control.post(`/sandboxes/${sandboxId}/snapshots`, {
|
|
143
|
+
json: snapshotPayload(opts),
|
|
144
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
145
|
+
});
|
|
146
|
+
return snapshotInfo(record(payload.sandbox_checkpoint ?? payload.snapshot ?? payload));
|
|
147
|
+
}
|
|
148
|
+
/** List checkpoints for one sandbox using snapshot naming. */
|
|
149
|
+
static listSnapshots(sandboxId, opts = {}) {
|
|
150
|
+
return new SnapshotPaginator(async () => {
|
|
151
|
+
const control = new ControlClient(new ConnectionConfig(opts));
|
|
152
|
+
const payload = await control.get(`/sandboxes/${sandboxId}/checkpoints`, {
|
|
153
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
154
|
+
});
|
|
155
|
+
const snapshots = Array.isArray(payload.sandbox_checkpoints) ? payload.sandbox_checkpoints : [];
|
|
156
|
+
return snapshots.map((item) => snapshotInfo(record(item)));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/** Delete a snapshot by id. Returns `false` when the snapshot does not exist. */
|
|
160
|
+
static async deleteSnapshot(snapshotId, opts = {}) {
|
|
161
|
+
const control = new ControlClient(new ConnectionConfig(opts));
|
|
162
|
+
try {
|
|
163
|
+
await control.delete(`/sandbox_snapshots/${snapshotId}`, {
|
|
164
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
165
|
+
});
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (error instanceof NotFoundError)
|
|
170
|
+
return false;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
105
174
|
/** Destroy this sandbox. */
|
|
106
175
|
async kill() {
|
|
107
176
|
await this.control.delete(`/sandboxes/${this.sandboxId}`);
|
|
@@ -146,6 +215,45 @@ export class Sandbox {
|
|
|
146
215
|
const payload = await this.control.get(`/sandboxes/${this.sandboxId}`);
|
|
147
216
|
return sandboxInfo(record(payload.sandbox ?? payload));
|
|
148
217
|
}
|
|
218
|
+
/** Fetch latest sandbox metrics. */
|
|
219
|
+
async getMetrics(opts = {}) {
|
|
220
|
+
return Sandbox.getMetrics(this.sandboxId, { ...this.configOptions(), ...opts });
|
|
221
|
+
}
|
|
222
|
+
/** Create a Watasu checkpoint using snapshot naming. */
|
|
223
|
+
async createSnapshot(opts = {}) {
|
|
224
|
+
return Sandbox.createSnapshot(this.sandboxId, { ...this.configOptions(), ...opts });
|
|
225
|
+
}
|
|
226
|
+
/** Delete a snapshot by id. */
|
|
227
|
+
async deleteSnapshot(snapshotId, opts = {}) {
|
|
228
|
+
return Sandbox.deleteSnapshot(snapshotId, { ...this.configOptions(), ...opts });
|
|
229
|
+
}
|
|
230
|
+
/** Watasu-native alias for `createSnapshot`. */
|
|
231
|
+
async checkpoint(opts = {}) {
|
|
232
|
+
return this.createSnapshot(opts);
|
|
233
|
+
}
|
|
234
|
+
/** List checkpoints for this sandbox using snapshot naming. */
|
|
235
|
+
listSnapshots(opts = {}) {
|
|
236
|
+
return Sandbox.listSnapshots(this.sandboxId, { ...this.configOptions(), ...opts });
|
|
237
|
+
}
|
|
238
|
+
/** Restore a checkpoint into a new sandbox and return its control-plane info. */
|
|
239
|
+
async restore(opts = {}) {
|
|
240
|
+
const restoreOpts = typeof opts === 'string' || typeof opts === 'number'
|
|
241
|
+
? { checkpointId: opts }
|
|
242
|
+
: opts;
|
|
243
|
+
const checkpointId = restoreOpts.checkpointId ?? restoreOpts.snapshotId;
|
|
244
|
+
if (checkpointId === undefined)
|
|
245
|
+
throw new SandboxError('checkpointId or snapshotId is required');
|
|
246
|
+
const payload = { checkpoint_id: checkpointId };
|
|
247
|
+
if (restoreOpts.timeout !== undefined)
|
|
248
|
+
payload.timeout_seconds = restoreOpts.timeout;
|
|
249
|
+
if (restoreOpts.timeoutMs !== undefined)
|
|
250
|
+
payload.timeout_seconds = Math.ceil(restoreOpts.timeoutMs / 1000);
|
|
251
|
+
const response = await this.control.post(`/sandboxes/${this.sandboxId}/restore`, {
|
|
252
|
+
json: payload,
|
|
253
|
+
requestTimeoutMs: restoreOpts.requestTimeoutMs,
|
|
254
|
+
});
|
|
255
|
+
return sandboxInfo(record(response.sandbox ?? response));
|
|
256
|
+
}
|
|
149
257
|
/** List sandboxes visible to the configured API key. */
|
|
150
258
|
static async list(opts = {}) {
|
|
151
259
|
const control = new ControlClient(new ConnectionConfig(opts));
|
|
@@ -162,11 +270,48 @@ export class Sandbox {
|
|
|
162
270
|
throw new SandboxError('port response did not include host or url');
|
|
163
271
|
return `p${port}-${routeToken}.sandbox.${this.config.dataPlaneDomain}`;
|
|
164
272
|
}
|
|
273
|
+
/** Get a signed URL that accepts a POST upload for a sandbox file path. */
|
|
274
|
+
async uploadUrl(path, opts = {}) {
|
|
275
|
+
const fileUrl = await this.fileUrl('/upload_url', path, opts);
|
|
276
|
+
return fileUrl.url;
|
|
277
|
+
}
|
|
278
|
+
/** Get a signed URL that accepts a GET download for a sandbox file path. */
|
|
279
|
+
async downloadUrl(path, opts = {}) {
|
|
280
|
+
const fileUrl = await this.fileUrl('/download_url', path, opts);
|
|
281
|
+
return fileUrl.url;
|
|
282
|
+
}
|
|
283
|
+
/** Get signed upload URL metadata for a sandbox file path. */
|
|
284
|
+
async uploadUrlInfo(path, opts = {}) {
|
|
285
|
+
return this.fileUrl('/upload_url', path, opts);
|
|
286
|
+
}
|
|
287
|
+
/** Get signed download URL metadata for a sandbox file path. */
|
|
288
|
+
async downloadUrlInfo(path, opts = {}) {
|
|
289
|
+
return this.fileUrl('/download_url', path, opts);
|
|
290
|
+
}
|
|
291
|
+
updateNetwork(..._args) { unsupported('Sandbox.updateNetwork'); }
|
|
165
292
|
pause() { unsupported('Sandbox.pause'); }
|
|
293
|
+
betaPause() { unsupported('Sandbox.betaPause'); }
|
|
166
294
|
resume() { unsupported('Sandbox.resume'); }
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
295
|
+
async fileUrl(route, path, opts) {
|
|
296
|
+
const payload = await this.control.post(`/sandboxes/${this.sandboxId}/files${route}`, {
|
|
297
|
+
json: compactRecord({
|
|
298
|
+
path,
|
|
299
|
+
user: opts.user,
|
|
300
|
+
use_signature_expiration: opts.useSignatureExpiration,
|
|
301
|
+
expires_in_seconds: opts.expiresInSeconds,
|
|
302
|
+
}),
|
|
303
|
+
requestTimeoutMs: opts.requestTimeoutMs,
|
|
304
|
+
});
|
|
305
|
+
return fileUrlInfo(record(payload.file_url ?? payload));
|
|
306
|
+
}
|
|
307
|
+
configOptions() {
|
|
308
|
+
return {
|
|
309
|
+
apiKey: this.config.apiKey,
|
|
310
|
+
apiUrl: this.config.apiUrl,
|
|
311
|
+
dataPlaneDomain: this.config.dataPlaneDomain,
|
|
312
|
+
requestTimeoutMs: this.config.requestTimeoutMs,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
170
315
|
}
|
|
171
316
|
function dataPlaneFromSession(session, config) {
|
|
172
317
|
const item = record(session);
|
|
@@ -179,6 +324,18 @@ function dataPlaneFromSession(session, config) {
|
|
|
179
324
|
}
|
|
180
325
|
return new DataPlaneClient(url, token, config);
|
|
181
326
|
}
|
|
327
|
+
function fileUrlInfo(payload) {
|
|
328
|
+
return {
|
|
329
|
+
method: String(payload.method ?? ''),
|
|
330
|
+
path: String(payload.path ?? ''),
|
|
331
|
+
url: String(payload.url ?? ''),
|
|
332
|
+
expiresAt: typeof payload.expires_at === 'string' ? payload.expires_at : typeof payload.expiresAt === 'string' ? payload.expiresAt : undefined,
|
|
333
|
+
raw: payload,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function compactRecord(payload) {
|
|
337
|
+
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== undefined));
|
|
338
|
+
}
|
|
182
339
|
function sessionOperationRequestTimeout(config, opts) {
|
|
183
340
|
if (opts.requestTimeoutMs !== undefined)
|
|
184
341
|
return opts.requestTimeoutMs;
|
|
@@ -199,6 +356,55 @@ function sandboxInfo(payload) {
|
|
|
199
356
|
: typeof payload.deadline_at === 'string' ? payload.deadline_at : undefined,
|
|
200
357
|
};
|
|
201
358
|
}
|
|
359
|
+
function metricsList(value) {
|
|
360
|
+
if (Array.isArray(value))
|
|
361
|
+
return value.map((item) => metricsInfo(record(item)));
|
|
362
|
+
return [metricsInfo(record(value))];
|
|
363
|
+
}
|
|
364
|
+
function metricsInfo(value) {
|
|
365
|
+
return {
|
|
366
|
+
sandboxId: stringValue(value.sandbox_id ?? value.sandboxId),
|
|
367
|
+
state: stringValue(value.state),
|
|
368
|
+
node: stringValue(value.node),
|
|
369
|
+
backend: stringValue(value.backend),
|
|
370
|
+
cpuCount: numberValue(value.cpu_count ?? value.cpuCount),
|
|
371
|
+
memoryMb: numberValue(value.memory_mb ?? value.memoryMb),
|
|
372
|
+
raw: value,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function snapshotPayload(opts) {
|
|
376
|
+
const payload = {};
|
|
377
|
+
putIfPresent(payload, 'name', opts.name);
|
|
378
|
+
putIfPresent(payload, 'metadata', opts.metadata);
|
|
379
|
+
putIfPresent(payload, 'expires_at', opts.expiresAt);
|
|
380
|
+
putIfPresent(payload, 'quiesce_mode', opts.quiesceMode);
|
|
381
|
+
return payload;
|
|
382
|
+
}
|
|
383
|
+
function snapshotInfo(value) {
|
|
384
|
+
const id = value.snapshot_id ?? value.snapshotId ?? value.checkpoint_id ?? value.checkpointId ?? value.id;
|
|
385
|
+
if (id === undefined)
|
|
386
|
+
throw new SandboxError('snapshot response did not include id');
|
|
387
|
+
return {
|
|
388
|
+
snapshotId: String(id),
|
|
389
|
+
sandboxId: stringValue(value.sandbox_id ?? value.sandboxId),
|
|
390
|
+
name: stringValue(value.name),
|
|
391
|
+
status: stringValue(value.status),
|
|
392
|
+
sizeBytes: numberValue(value.size_bytes ?? value.sizeBytes),
|
|
393
|
+
createdAt: stringValue(value.created_at ?? value.createdAt),
|
|
394
|
+
expiresAt: stringValue(value.expires_at ?? value.expiresAt),
|
|
395
|
+
raw: value,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function stringValue(value) {
|
|
399
|
+
if (typeof value === 'string')
|
|
400
|
+
return value;
|
|
401
|
+
if (typeof value === 'number')
|
|
402
|
+
return String(value);
|
|
403
|
+
return undefined;
|
|
404
|
+
}
|
|
405
|
+
function numberValue(value) {
|
|
406
|
+
return typeof value === 'number' ? value : undefined;
|
|
407
|
+
}
|
|
202
408
|
function templateSlug(value) {
|
|
203
409
|
const template = record(value);
|
|
204
410
|
return typeof template.slug === 'string' ? template.slug : undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@watasu/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"description": "TypeScript SDK for Watasu",
|
|
@@ -28,5 +28,10 @@
|
|
|
28
28
|
"@types/node": "^24.0.3",
|
|
29
29
|
"@types/ws": "^8.18.1",
|
|
30
30
|
"typescript": "^5.7.0"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/watasuio/sdk.git",
|
|
35
|
+
"directory": "ts"
|
|
31
36
|
}
|
|
32
37
|
}
|