@watasu/sdk 0.1.5 → 0.1.24
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 +145 -2
- package/dist/commands.d.ts +33 -4
- package/dist/commands.js +68 -17
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +8 -0
- package/dist/filesystem.d.ts +80 -13
- package/dist/filesystem.js +184 -9
- package/dist/git.d.ts +171 -0
- package/dist/git.js +277 -0
- package/dist/index.d.ts +16 -6
- package/dist/index.js +9 -4
- package/dist/process.d.ts +56 -0
- package/dist/process.js +137 -0
- package/dist/processSocket.d.ts +2 -0
- package/dist/processSocket.js +9 -1
- package/dist/pty.d.ts +37 -0
- package/dist/pty.js +90 -0
- package/dist/sandbox.d.ts +145 -25
- package/dist/sandbox.js +312 -45
- package/dist/template.d.ts +232 -0
- package/dist/template.js +624 -0
- package/dist/terminal.d.ts +41 -0
- package/dist/terminal.js +74 -0
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,8 +16,8 @@ Set `WATASU_API_KEY` before using the SDK.
|
|
|
16
16
|
import { Sandbox } from '@watasu/sdk'
|
|
17
17
|
|
|
18
18
|
const sbx = await Sandbox.create()
|
|
19
|
-
await sbx.
|
|
20
|
-
const result = await sbx.
|
|
19
|
+
await sbx.filesystem.write('/home/user/a.js', 'console.log(2 + 2)')
|
|
20
|
+
const result = await sbx.process.startAndWait('node /home/user/a.js')
|
|
21
21
|
console.log(result.stdout)
|
|
22
22
|
console.log(await sbx.isRunning())
|
|
23
23
|
await sbx.kill()
|
|
@@ -26,6 +26,147 @@ 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
|
+
```ts
|
|
30
|
+
await sbx.betaPause()
|
|
31
|
+
await sbx.resume({ timeoutMs: 300_000 })
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## MCP Gateway
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const sbx = await Sandbox.create({
|
|
38
|
+
mcp: {
|
|
39
|
+
github: {
|
|
40
|
+
command: 'github-mcp-server',
|
|
41
|
+
args: ['stdio'],
|
|
42
|
+
env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
console.log(sbx.getMcpUrl())
|
|
48
|
+
console.log(await sbx.getMcpToken())
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Listing Sandboxes
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { Sandbox } from '@watasu/sdk'
|
|
55
|
+
|
|
56
|
+
const paginator = Sandbox.list({
|
|
57
|
+
query: { metadata: { purpose: 'ci' }, state: ['running'] },
|
|
58
|
+
limit: 20,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
for (const sandbox of await paginator.listItems()) {
|
|
62
|
+
console.log(sandbox.sandboxId, sandbox.state)
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Git, Watch, PTY, And Signed File URLs
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const sbx = await Sandbox.create()
|
|
70
|
+
|
|
71
|
+
await sbx.git.init('/workspace/new-project', { initialBranch: 'main' })
|
|
72
|
+
await sbx.git.clone('https://github.com/acme/project.git', {
|
|
73
|
+
path: '/workspace/project',
|
|
74
|
+
branch: 'main',
|
|
75
|
+
depth: 1,
|
|
76
|
+
})
|
|
77
|
+
const status = await sbx.git.status('/workspace/project')
|
|
78
|
+
await sbx.git.configureUser('Watasu Bot', 'bot@watasu.local', {
|
|
79
|
+
scope: 'local',
|
|
80
|
+
path: '/workspace/project',
|
|
81
|
+
})
|
|
82
|
+
await sbx.git.createBranch('/workspace/project', 'feature/docs')
|
|
83
|
+
await sbx.git.add('/workspace/project', { files: ['README.md'] })
|
|
84
|
+
await sbx.git.commit('/workspace/project', 'Update docs', {
|
|
85
|
+
authorName: 'Watasu Bot',
|
|
86
|
+
authorEmail: 'bot@watasu.local',
|
|
87
|
+
})
|
|
88
|
+
await sbx.git.push('/workspace/project', {
|
|
89
|
+
remote: 'origin',
|
|
90
|
+
branch: 'feature/docs',
|
|
91
|
+
setUpstream: true,
|
|
92
|
+
})
|
|
93
|
+
const remoteUrl = await sbx.git.remoteGet('/workspace/project', 'origin')
|
|
94
|
+
await sbx.git.restore('/workspace/project', { paths: ['README.md'] })
|
|
95
|
+
await sbx.git.reset('/workspace/project', { mode: 'hard', target: 'HEAD' })
|
|
96
|
+
|
|
97
|
+
await sbx.filesystem.writeFiles([
|
|
98
|
+
{ path: '/workspace/project/a.txt', data: 'alpha' },
|
|
99
|
+
{ path: '/workspace/project/b.bin', data: new Uint8Array([0, 1, 2]) },
|
|
100
|
+
])
|
|
101
|
+
|
|
102
|
+
const watcher = sbx.filesystem.watchDir('/workspace/project')
|
|
103
|
+
watcher.addEventListener((event) => {
|
|
104
|
+
console.log(event.type, event.path)
|
|
105
|
+
})
|
|
106
|
+
await watcher.start({ recursive: true })
|
|
107
|
+
|
|
108
|
+
const terminal = await sbx.terminal.start({
|
|
109
|
+
size: { cols: 100, rows: 30 },
|
|
110
|
+
onData: (data) => process.stdout.write(data),
|
|
111
|
+
})
|
|
112
|
+
await terminal.sendData('echo hello\n')
|
|
113
|
+
|
|
114
|
+
const uploadUrl = await sbx.uploadUrl('/workspace/input.bin')
|
|
115
|
+
const downloadUrl = await sbx.downloadUrl('/workspace/output.bin')
|
|
116
|
+
|
|
117
|
+
watcher.stop()
|
|
118
|
+
await terminal.kill()
|
|
119
|
+
await sbx.kill()
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Network Policy
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const sbx = await Sandbox.create({
|
|
126
|
+
network: {
|
|
127
|
+
allowOut: ['pypi.org:443'],
|
|
128
|
+
denyOut: ['169.254.169.254'],
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
await sbx.updateNetwork({
|
|
133
|
+
allowInternetAccess: false,
|
|
134
|
+
allowPackageRegistryAccess: true,
|
|
135
|
+
allowOut: ['pypi.org:443', 'registry.npmjs.org:443'],
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Template Builds
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { Template } from '@watasu/sdk'
|
|
143
|
+
|
|
144
|
+
const template = Template()
|
|
145
|
+
.fromPythonImage('3.12')
|
|
146
|
+
.copy('requirements.txt', '/workspace/requirements.txt')
|
|
147
|
+
.aptInstall(['git'])
|
|
148
|
+
.pipInstall(['pytest'])
|
|
149
|
+
.setEnvs({ PIP_DISABLE_PIP_VERSION_CHECK: '1' })
|
|
150
|
+
.runCmd('echo ready')
|
|
151
|
+
|
|
152
|
+
const build = await Template.buildInBackground(template, 'python-ci:stable', {
|
|
153
|
+
tags: ['stable'],
|
|
154
|
+
cpuCount: 2,
|
|
155
|
+
memoryMB: 2048,
|
|
156
|
+
})
|
|
157
|
+
const status = await Template.getBuildStatus(build)
|
|
158
|
+
|
|
159
|
+
await Template.assignTags('python-ci:stable', ['prod'])
|
|
160
|
+
console.log(await Template.exists('python-ci'))
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Template names resolve server-side. `python-ci` starts the latest ready build;
|
|
164
|
+
`python-ci:stable` starts the tagged build.
|
|
165
|
+
|
|
166
|
+
`Template({ fileContextPath: process.cwd() }).fromDockerfile('Dockerfile')`
|
|
167
|
+
parses common `FROM`, `WORKDIR`, `COPY`, `RUN`, `ENV`, `CMD`, and `ENTRYPOINT`
|
|
168
|
+
instructions into Watasu's package-spec builder.
|
|
169
|
+
|
|
29
170
|
## Metrics And Snapshots
|
|
30
171
|
|
|
31
172
|
```ts
|
|
@@ -35,7 +176,9 @@ const sbx = await Sandbox.create()
|
|
|
35
176
|
const metrics = await sbx.getMetrics()
|
|
36
177
|
const snapshot = await sbx.createSnapshot({ name: 'ready' })
|
|
37
178
|
const snapshots = await sbx.listSnapshots().nextItems()
|
|
179
|
+
const allSnapshots = await Sandbox.listSnapshots({ limit: 100 }).nextItems()
|
|
38
180
|
const restored = await sbx.restore({ snapshotId: snapshot.snapshotId })
|
|
181
|
+
await sbx.deleteSnapshot(snapshot.snapshotId)
|
|
39
182
|
await sbx.kill()
|
|
40
183
|
```
|
|
41
184
|
|
package/dist/commands.d.ts
CHANGED
|
@@ -29,13 +29,26 @@ export interface ProcessInfo {
|
|
|
29
29
|
export interface CommandStartOpts {
|
|
30
30
|
/** Return a `CommandHandle` immediately instead of waiting for exit. */
|
|
31
31
|
background?: boolean;
|
|
32
|
+
/** Executable to start directly. When omitted, `cmd` strings run through a login shell. */
|
|
33
|
+
cmd?: string;
|
|
34
|
+
/** Arguments for `cmd` when starting a direct executable. */
|
|
35
|
+
args?: string[];
|
|
32
36
|
cwd?: string;
|
|
37
|
+
/** Deprecated alias for `cwd`. */
|
|
38
|
+
rootDir?: string;
|
|
33
39
|
user?: string;
|
|
34
40
|
envs?: Record<string, string>;
|
|
41
|
+
/** Alias for `envs`. */
|
|
42
|
+
envVars?: Record<string, string>;
|
|
35
43
|
onStdout?: (data: string) => void | Promise<void>;
|
|
36
44
|
onStderr?: (data: string) => void | Promise<void>;
|
|
45
|
+
onPty?: (data: Uint8Array) => void | Promise<void>;
|
|
46
|
+
onExit?: (exitCode: number) => void | Promise<void>;
|
|
37
47
|
stdin?: boolean;
|
|
38
48
|
timeoutMs?: number;
|
|
49
|
+
/** Alias for `timeoutMs`. */
|
|
50
|
+
timeout?: number;
|
|
51
|
+
processID?: string;
|
|
39
52
|
requestTimeoutMs?: number;
|
|
40
53
|
}
|
|
41
54
|
/** Live handle for one sandbox process stream. */
|
|
@@ -46,23 +59,32 @@ export declare class CommandHandle implements Partial<CommandResult> {
|
|
|
46
59
|
private readonly events;
|
|
47
60
|
private readonly onStdout?;
|
|
48
61
|
private readonly onStderr?;
|
|
62
|
+
private readonly onPty?;
|
|
63
|
+
private readonly onExit?;
|
|
49
64
|
private _stdout;
|
|
50
65
|
private _stderr;
|
|
51
66
|
private result?;
|
|
52
67
|
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);
|
|
68
|
+
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, onExit?: ((exitCode: number) => void | Promise<void>) | undefined);
|
|
54
69
|
get stdout(): string;
|
|
55
70
|
get stderr(): string;
|
|
56
71
|
get exitCode(): number | undefined;
|
|
57
72
|
get error(): string | undefined;
|
|
58
73
|
/** Wait until the process exits and return captured output. */
|
|
59
|
-
wait(): Promise<CommandResult>;
|
|
74
|
+
wait(timeoutMs?: number): Promise<CommandResult>;
|
|
60
75
|
/** Kill the process. */
|
|
61
76
|
kill(): Promise<boolean>;
|
|
62
77
|
/** Send stdin bytes or text to the process. */
|
|
63
78
|
sendStdin(data: string | Uint8Array): Promise<void>;
|
|
79
|
+
/** Close the stdin stream and signal EOF to the process. */
|
|
80
|
+
closeStdin(): Promise<void>;
|
|
81
|
+
/** Resize the attached PTY stream when this handle was created as a PTY. */
|
|
82
|
+
resize(size: {
|
|
83
|
+
cols: number;
|
|
84
|
+
rows: number;
|
|
85
|
+
}): Promise<void>;
|
|
64
86
|
/** Detach the local stream without killing the process. */
|
|
65
|
-
disconnect(): void
|
|
87
|
+
disconnect(): Promise<void>;
|
|
66
88
|
private handleEvents;
|
|
67
89
|
}
|
|
68
90
|
/** Command runner for a sandbox data-plane session. */
|
|
@@ -71,6 +93,8 @@ export declare class Commands {
|
|
|
71
93
|
private readonly config;
|
|
72
94
|
private readonly sandboxEnvs;
|
|
73
95
|
constructor(dataPlane: DataPlaneClient, config: ConnectionConfig, sandboxEnvs?: Record<string, string>);
|
|
96
|
+
/** Whether this runtime supports stdin EOF frames. */
|
|
97
|
+
get supportsStdinClose(): boolean;
|
|
74
98
|
/** List processes currently known by the sandbox runtime. */
|
|
75
99
|
list(opts?: {
|
|
76
100
|
requestTimeoutMs?: number;
|
|
@@ -83,11 +107,16 @@ export declare class Commands {
|
|
|
83
107
|
sendStdin(pid: number | string, data: string | Uint8Array, opts?: {
|
|
84
108
|
requestTimeoutMs?: number;
|
|
85
109
|
}): Promise<void>;
|
|
110
|
+
/** Attach to a process and close stdin, signalling EOF. */
|
|
111
|
+
closeStdin(pid: number | string, opts?: {
|
|
112
|
+
requestTimeoutMs?: number;
|
|
113
|
+
}): Promise<void>;
|
|
86
114
|
run(cmd: string, opts: CommandStartOpts & {
|
|
87
115
|
background: true;
|
|
88
116
|
}): Promise<CommandHandle>;
|
|
89
117
|
run(cmd: string, opts?: CommandStartOpts): Promise<CommandResult>;
|
|
90
118
|
/** Reconnect to a live process stream by pid. */
|
|
91
119
|
connect(pid: number | string, opts?: CommandStartOpts): Promise<CommandHandle>;
|
|
92
|
-
|
|
120
|
+
/** Start a command and return a live handle immediately. */
|
|
121
|
+
start(cmd: string, opts?: CommandStartOpts): Promise<CommandHandle>;
|
|
93
122
|
}
|
package/dist/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ProcessSocket, base64DecodeText } from './processSocket.js';
|
|
2
|
-
import { SandboxError } from './errors.js';
|
|
1
|
+
import { ProcessSocket, base64DecodeBytes, base64DecodeText } from './processSocket.js';
|
|
2
|
+
import { SandboxError, TimeoutError } from './errors.js';
|
|
3
3
|
/** Error thrown by `CommandHandle.wait()` when a process exits non-zero. */
|
|
4
4
|
export class CommandExitError extends SandboxError {
|
|
5
5
|
result;
|
|
@@ -21,17 +21,21 @@ export class CommandHandle {
|
|
|
21
21
|
events;
|
|
22
22
|
onStdout;
|
|
23
23
|
onStderr;
|
|
24
|
+
onPty;
|
|
25
|
+
onExit;
|
|
24
26
|
_stdout = '';
|
|
25
27
|
_stderr = '';
|
|
26
28
|
result;
|
|
27
29
|
pending;
|
|
28
|
-
constructor(pid, socket, handleKill, events, onStdout, onStderr) {
|
|
30
|
+
constructor(pid, socket, handleKill, events, onStdout, onStderr, onPty, onExit) {
|
|
29
31
|
this.pid = pid;
|
|
30
32
|
this.socket = socket;
|
|
31
33
|
this.handleKill = handleKill;
|
|
32
34
|
this.events = events;
|
|
33
35
|
this.onStdout = onStdout;
|
|
34
36
|
this.onStderr = onStderr;
|
|
37
|
+
this.onPty = onPty;
|
|
38
|
+
this.onExit = onExit;
|
|
35
39
|
this.pending = this.handleEvents();
|
|
36
40
|
}
|
|
37
41
|
get stdout() { return this._stdout; }
|
|
@@ -39,8 +43,8 @@ export class CommandHandle {
|
|
|
39
43
|
get exitCode() { return this.result?.exitCode; }
|
|
40
44
|
get error() { return this.result?.error; }
|
|
41
45
|
/** Wait until the process exits and return captured output. */
|
|
42
|
-
async wait() {
|
|
43
|
-
await this.pending;
|
|
46
|
+
async wait(timeoutMs) {
|
|
47
|
+
await waitFor(this.pending, timeoutMs);
|
|
44
48
|
if (!this.result)
|
|
45
49
|
throw new SandboxError('Command ended without an exit event');
|
|
46
50
|
if (this.result.exitCode !== 0)
|
|
@@ -55,8 +59,16 @@ export class CommandHandle {
|
|
|
55
59
|
async sendStdin(data) {
|
|
56
60
|
this.socket.sendStdin(data);
|
|
57
61
|
}
|
|
62
|
+
/** Close the stdin stream and signal EOF to the process. */
|
|
63
|
+
async closeStdin() {
|
|
64
|
+
this.socket.closeStdin();
|
|
65
|
+
}
|
|
66
|
+
/** Resize the attached PTY stream when this handle was created as a PTY. */
|
|
67
|
+
async resize(size) {
|
|
68
|
+
this.socket.sendJson({ type: 'resize', cols: size.cols, rows: size.rows });
|
|
69
|
+
}
|
|
58
70
|
/** Detach the local stream without killing the process. */
|
|
59
|
-
disconnect() {
|
|
71
|
+
async disconnect() {
|
|
60
72
|
this.socket.close();
|
|
61
73
|
}
|
|
62
74
|
async handleEvents() {
|
|
@@ -75,13 +87,21 @@ export class CommandHandle {
|
|
|
75
87
|
this._stderr += out;
|
|
76
88
|
await this.onStderr?.(out);
|
|
77
89
|
}
|
|
90
|
+
else if (type === 'pty') {
|
|
91
|
+
const bytes = base64DecodeBytes(frame.data);
|
|
92
|
+
const out = new TextDecoder().decode(bytes);
|
|
93
|
+
this._stdout += out;
|
|
94
|
+
await this.onPty?.(bytes);
|
|
95
|
+
}
|
|
78
96
|
else if (type === 'exit') {
|
|
97
|
+
const exitCode = Number(frame.exit_code ?? frame.exitCode ?? 0);
|
|
79
98
|
this.result = {
|
|
80
|
-
exitCode
|
|
99
|
+
exitCode,
|
|
81
100
|
error: typeof frame.error === 'string' ? frame.error : undefined,
|
|
82
101
|
stdout: this._stdout,
|
|
83
102
|
stderr: this._stderr,
|
|
84
103
|
};
|
|
104
|
+
await this.onExit?.(exitCode);
|
|
85
105
|
return;
|
|
86
106
|
}
|
|
87
107
|
else if (type === 'error') {
|
|
@@ -104,6 +124,10 @@ export class Commands {
|
|
|
104
124
|
this.config = config;
|
|
105
125
|
this.sandboxEnvs = sandboxEnvs;
|
|
106
126
|
}
|
|
127
|
+
/** Whether this runtime supports stdin EOF frames. */
|
|
128
|
+
get supportsStdinClose() {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
107
131
|
/** List processes currently known by the sandbox runtime. */
|
|
108
132
|
async list(opts = {}) {
|
|
109
133
|
const payload = await this.dataPlane.getJson('/runtime/v1/process', opts);
|
|
@@ -125,7 +149,17 @@ export class Commands {
|
|
|
125
149
|
await handle.sendStdin(data);
|
|
126
150
|
}
|
|
127
151
|
finally {
|
|
128
|
-
handle.disconnect();
|
|
152
|
+
await handle.disconnect();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Attach to a process and close stdin, signalling EOF. */
|
|
156
|
+
async closeStdin(pid, opts = {}) {
|
|
157
|
+
const handle = await this.connect(pid, opts);
|
|
158
|
+
try {
|
|
159
|
+
await handle.closeStdin();
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await handle.disconnect();
|
|
129
163
|
}
|
|
130
164
|
}
|
|
131
165
|
/** Run a shell command over the WebSocket process runtime. */
|
|
@@ -133,36 +167,53 @@ export class Commands {
|
|
|
133
167
|
const handle = await this.start(cmd, opts);
|
|
134
168
|
if (opts.background)
|
|
135
169
|
return handle;
|
|
136
|
-
return handle.wait();
|
|
170
|
+
return handle.wait(opts.timeoutMs ?? opts.timeout);
|
|
137
171
|
}
|
|
138
172
|
/** Reconnect to a live process stream by pid. */
|
|
139
173
|
async connect(pid, opts = {}) {
|
|
140
174
|
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
175
|
const first = await nextStarted(socket);
|
|
142
176
|
const actualPid = framePid(first) ?? pid;
|
|
143
|
-
return new CommandHandle(actualPid, socket, () => this.kill(actualPid), socket, opts.onStdout, opts.onStderr);
|
|
177
|
+
return new CommandHandle(actualPid, socket, () => this.kill(actualPid), socket, opts.onStdout, opts.onStderr, opts.onPty);
|
|
144
178
|
}
|
|
145
|
-
|
|
179
|
+
/** Start a command and return a live handle immediately. */
|
|
180
|
+
async start(cmd, opts = {}) {
|
|
146
181
|
const socket = await new ProcessSocket(this.dataPlane.baseUrl, this.dataPlane.token, '/runtime/v1/process', opts.requestTimeoutMs ?? this.config.requestTimeoutMs).connect();
|
|
147
|
-
const environment = { ...this.sandboxEnvs, ...(opts.envs ?? {}) };
|
|
182
|
+
const environment = { ...this.sandboxEnvs, ...(opts.envVars ?? opts.envs ?? {}) };
|
|
183
|
+
const processConfig = processStartConfig(cmd, opts);
|
|
148
184
|
socket.sendJson({
|
|
149
185
|
type: 'start',
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
186
|
+
id: opts.processID,
|
|
187
|
+
cmd: processConfig.cmd,
|
|
188
|
+
args: processConfig.args,
|
|
189
|
+
cwd: opts.cwd ?? opts.rootDir,
|
|
153
190
|
user: opts.user,
|
|
154
191
|
environment,
|
|
155
192
|
envs: environment,
|
|
156
193
|
stdin: opts.stdin ?? false,
|
|
157
|
-
timeout_ms: opts.timeoutMs ?? 60_000,
|
|
194
|
+
timeout_ms: opts.timeoutMs ?? opts.timeout ?? 60_000,
|
|
158
195
|
});
|
|
159
196
|
const first = await nextStarted(socket);
|
|
160
197
|
const pid = framePid(first);
|
|
161
198
|
if (pid === undefined)
|
|
162
199
|
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);
|
|
200
|
+
return new CommandHandle(pid, socket, () => this.kill(pid), withFirst(first, socket), opts.onStdout, opts.onStderr, opts.onPty, opts.onExit);
|
|
164
201
|
}
|
|
165
202
|
}
|
|
203
|
+
function processStartConfig(cmd, opts) {
|
|
204
|
+
if (opts.args !== undefined || opts.cmd !== undefined) {
|
|
205
|
+
return { cmd: opts.cmd ?? cmd, args: opts.args ?? [] };
|
|
206
|
+
}
|
|
207
|
+
return { cmd: '/bin/bash', args: ['-l', '-c', cmd] };
|
|
208
|
+
}
|
|
209
|
+
function waitFor(promise, timeoutMs) {
|
|
210
|
+
if (timeoutMs === undefined || timeoutMs <= 0)
|
|
211
|
+
return promise;
|
|
212
|
+
return new Promise((resolve, reject) => {
|
|
213
|
+
const timer = setTimeout(() => reject(new TimeoutError()), timeoutMs);
|
|
214
|
+
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
215
|
+
});
|
|
216
|
+
}
|
|
166
217
|
async function nextStarted(events) {
|
|
167
218
|
for await (const frame of events) {
|
|
168
219
|
if (frame.type === 'started')
|
package/dist/errors.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export declare class AuthenticationError extends SandboxError {
|
|
|
7
7
|
export declare class NotFoundError extends SandboxError {
|
|
8
8
|
constructor(message?: string);
|
|
9
9
|
}
|
|
10
|
+
export declare class ConflictError extends SandboxError {
|
|
11
|
+
constructor(message?: string);
|
|
12
|
+
}
|
|
10
13
|
export declare class TimeoutError extends SandboxError {
|
|
11
14
|
constructor(message?: string);
|
|
12
15
|
}
|
package/dist/errors.js
CHANGED
|
@@ -16,6 +16,12 @@ export class NotFoundError extends SandboxError {
|
|
|
16
16
|
this.name = 'NotFoundError';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
export class ConflictError extends SandboxError {
|
|
20
|
+
constructor(message = 'Conflict') {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ConflictError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
19
25
|
export class TimeoutError extends SandboxError {
|
|
20
26
|
constructor(message = 'Request timed out') {
|
|
21
27
|
super(message);
|
|
@@ -76,6 +82,8 @@ export function errorFromResponse(status, payload) {
|
|
|
76
82
|
return new AuthenticationError(message);
|
|
77
83
|
if (status === 404)
|
|
78
84
|
return new NotFoundError(message);
|
|
85
|
+
if (status === 409)
|
|
86
|
+
return new ConflictError(message);
|
|
79
87
|
if (status === 408 || status === 504)
|
|
80
88
|
return new TimeoutError(message);
|
|
81
89
|
if (status === 422 || status === 400)
|
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,22 +21,86 @@ export interface EntryInfo {
|
|
|
20
21
|
metadata?: Record<string, string>;
|
|
21
22
|
}
|
|
22
23
|
export type WriteInfo = EntryInfo;
|
|
24
|
+
export type WriteData = string | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array>;
|
|
25
|
+
export interface WriteEntry {
|
|
26
|
+
path: string;
|
|
27
|
+
data: WriteData;
|
|
28
|
+
}
|
|
29
|
+
export interface FilesystemEvent {
|
|
30
|
+
type: 'create' | 'write' | 'modify' | 'remove' | 'delete' | 'rename' | string;
|
|
31
|
+
path: string;
|
|
32
|
+
entry?: EntryInfo;
|
|
33
|
+
raw: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export interface WatchOpts {
|
|
36
|
+
recursive?: boolean;
|
|
37
|
+
includeEntry?: boolean;
|
|
38
|
+
requestTimeoutMs?: number;
|
|
39
|
+
onExit?: (error?: Error) => void | Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
export interface FilesystemRequestOpts {
|
|
42
|
+
requestTimeoutMs?: number;
|
|
43
|
+
user?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface FilesystemReadOpts extends FilesystemRequestOpts {
|
|
46
|
+
gzip?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface FilesystemWriteOpts extends FilesystemRequestOpts {
|
|
49
|
+
gzip?: boolean;
|
|
50
|
+
metadata?: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
/** Live filesystem watcher. Call `stop()` to close the local watch stream. */
|
|
53
|
+
export declare class WatchHandle {
|
|
54
|
+
private readonly socket;
|
|
55
|
+
private readonly done;
|
|
56
|
+
constructor(socket: ProcessSocket, events: AsyncIterable<ProcessFrame>, onEvent: (event: FilesystemEvent) => void | Promise<void>, onExit?: (error?: Error) => void | Promise<void>);
|
|
57
|
+
/** Stop watching the directory. */
|
|
58
|
+
stop(): void;
|
|
59
|
+
/** Alias for `stop`. */
|
|
60
|
+
close(): void;
|
|
61
|
+
/** Resolves when the watcher stream exits. */
|
|
62
|
+
wait(): Promise<void>;
|
|
63
|
+
private pump;
|
|
64
|
+
}
|
|
65
|
+
/** Lazy filesystem watcher. Add listeners, then call `start()`. */
|
|
66
|
+
export declare class FilesystemWatcher {
|
|
67
|
+
private readonly dataPlane;
|
|
68
|
+
private readonly path;
|
|
69
|
+
private readonly opts;
|
|
70
|
+
private handle?;
|
|
71
|
+
private listeners;
|
|
72
|
+
constructor(dataPlane: DataPlaneClient, path: string, opts?: WatchOpts);
|
|
73
|
+
start(opts?: WatchOpts): Promise<void>;
|
|
74
|
+
stop(): Promise<void>;
|
|
75
|
+
addEventListener(listener: (event: FilesystemEvent) => void | Promise<void>): () => boolean;
|
|
76
|
+
wait(): Promise<void>;
|
|
77
|
+
}
|
|
23
78
|
/** Filesystem helper for a sandbox data-plane session. */
|
|
24
79
|
export declare class Filesystem {
|
|
25
80
|
private readonly dataPlane;
|
|
26
81
|
constructor(dataPlane: DataPlaneClient);
|
|
27
|
-
/** Read
|
|
28
|
-
read(path: string, opts?: {
|
|
29
|
-
format?: 'text'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
/** Read file content as text, bytes, a `Blob`, or a `ReadableStream`. */
|
|
83
|
+
read(path: string, opts?: FilesystemReadOpts & {
|
|
84
|
+
format?: 'text';
|
|
85
|
+
}): Promise<string>;
|
|
86
|
+
read(path: string, opts: FilesystemReadOpts & {
|
|
87
|
+
format: 'bytes';
|
|
88
|
+
}): Promise<Uint8Array>;
|
|
89
|
+
read(path: string, opts: FilesystemReadOpts & {
|
|
90
|
+
format: 'blob';
|
|
91
|
+
}): Promise<Blob>;
|
|
92
|
+
read(path: string, opts: FilesystemReadOpts & {
|
|
93
|
+
format: 'stream';
|
|
94
|
+
}): Promise<ReadableStream<Uint8Array>>;
|
|
95
|
+
/** Read a file as raw bytes. */
|
|
96
|
+
readBytes(path: string, opts?: FilesystemReadOpts): Promise<Uint8Array>;
|
|
97
|
+
/** Write UTF-8 text, bytes, browser data objects, or a batch of file entries. */
|
|
98
|
+
write(path: string, data: WriteData, opts?: FilesystemWriteOpts): Promise<WriteInfo>;
|
|
99
|
+
write(files: WriteEntry[], opts?: FilesystemWriteOpts): Promise<WriteInfo[]>;
|
|
100
|
+
/** Write raw bytes to a file. */
|
|
101
|
+
writeBytes(path: string, data: Uint8Array | ArrayBuffer, opts?: FilesystemWriteOpts): Promise<WriteInfo>;
|
|
102
|
+
/** Write several files in one runtime API call. */
|
|
103
|
+
writeFiles(files: WriteEntry[], opts?: FilesystemWriteOpts): Promise<WriteInfo[]>;
|
|
39
104
|
/** List directory entries below `path`. */
|
|
40
105
|
list(path: string, opts?: {
|
|
41
106
|
requestTimeoutMs?: number;
|
|
@@ -61,5 +126,7 @@ export declare class Filesystem {
|
|
|
61
126
|
makeDir(path: string, opts?: {
|
|
62
127
|
requestTimeoutMs?: number;
|
|
63
128
|
}): Promise<boolean>;
|
|
64
|
-
|
|
129
|
+
/** Start watching a directory for filesystem events. */
|
|
130
|
+
watchDir(path: string): FilesystemWatcher;
|
|
131
|
+
watchDir(path: string, onEvent: (event: FilesystemEvent) => void | Promise<void>, opts?: WatchOpts): Promise<FilesystemWatcher>;
|
|
65
132
|
}
|