@watasu/sdk 0.1.5 → 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 +47 -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 +8 -4
- package/dist/index.js +4 -2
- 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 +29 -8
- package/dist/sandbox.js +69 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,52 @@ 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
|
+
|
|
29
75
|
## Metrics And Snapshots
|
|
30
76
|
|
|
31
77
|
```ts
|
|
@@ -36,6 +82,7 @@ const metrics = await sbx.getMetrics()
|
|
|
36
82
|
const snapshot = await sbx.createSnapshot({ name: 'ready' })
|
|
37
83
|
const snapshots = await sbx.listSnapshots().nextItems()
|
|
38
84
|
const restored = await sbx.restore({ snapshotId: snapshot.snapshotId })
|
|
85
|
+
await sbx.deleteSnapshot(snapshot.snapshotId)
|
|
39
86
|
await sbx.kill()
|
|
40
87
|
```
|
|
41
88
|
|
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
3
|
export { Sandbox, SnapshotPaginator } from './sandbox.js';
|
|
4
|
-
export type { CreateSnapshotOpts, RestoreSnapshotOpts, SandboxCreateOpts, SandboxConnectOpts, SandboxInfo, SandboxMetrics, SnapshotInfo, } 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
|
@@ -2,5 +2,7 @@ export { ApiError, AuthenticationError, FileNotFoundError, InvalidArgumentError,
|
|
|
2
2
|
export { ConnectionConfig, KEEPALIVE_PING_INTERVAL_SEC } from './connectionConfig.js';
|
|
3
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;
|
|
@@ -47,6 +49,18 @@ export interface SnapshotInfo {
|
|
|
47
49
|
expiresAt?: string;
|
|
48
50
|
raw: Record<string, unknown>;
|
|
49
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
|
+
}
|
|
50
64
|
export interface CreateSnapshotOpts extends ConnectionOpts {
|
|
51
65
|
name?: string;
|
|
52
66
|
metadata?: Record<string, string>;
|
|
@@ -73,13 +87,9 @@ export declare class Sandbox {
|
|
|
73
87
|
static readonly defaultTemplate = "base";
|
|
74
88
|
files: Filesystem;
|
|
75
89
|
commands: Commands;
|
|
90
|
+
pty: Pty;
|
|
91
|
+
git: Git;
|
|
76
92
|
readonly sandboxId: string;
|
|
77
|
-
readonly pty: {
|
|
78
|
-
create: () => never;
|
|
79
|
-
};
|
|
80
|
-
readonly git: {
|
|
81
|
-
clone: () => never;
|
|
82
|
-
};
|
|
83
93
|
private readonly config;
|
|
84
94
|
private readonly control;
|
|
85
95
|
private readonly envs;
|
|
@@ -109,8 +119,8 @@ export declare class Sandbox {
|
|
|
109
119
|
static createSnapshot(sandboxId: string, opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
110
120
|
/** List checkpoints for one sandbox using snapshot naming. */
|
|
111
121
|
static listSnapshots(sandboxId: string, opts?: ConnectionOpts): SnapshotPaginator;
|
|
112
|
-
/**
|
|
113
|
-
static deleteSnapshot(
|
|
122
|
+
/** Delete a snapshot by id. Returns `false` when the snapshot does not exist. */
|
|
123
|
+
static deleteSnapshot(snapshotId: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
114
124
|
/** Destroy this sandbox. */
|
|
115
125
|
kill(): Promise<boolean>;
|
|
116
126
|
/** Check if this sandbox is in a runtime-active lifecycle state. */
|
|
@@ -127,6 +137,8 @@ export declare class Sandbox {
|
|
|
127
137
|
getMetrics(opts?: ConnectionOpts): Promise<SandboxMetrics[]>;
|
|
128
138
|
/** Create a Watasu checkpoint using snapshot naming. */
|
|
129
139
|
createSnapshot(opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
140
|
+
/** Delete a snapshot by id. */
|
|
141
|
+
deleteSnapshot(snapshotId: string, opts?: ConnectionOpts): Promise<boolean>;
|
|
130
142
|
/** Watasu-native alias for `createSnapshot`. */
|
|
131
143
|
checkpoint(opts?: CreateSnapshotOpts): Promise<SnapshotInfo>;
|
|
132
144
|
/** List checkpoints for this sandbox using snapshot naming. */
|
|
@@ -139,9 +151,18 @@ export declare class Sandbox {
|
|
|
139
151
|
}): Promise<SandboxInfo[]>;
|
|
140
152
|
/** Return the public hostname for an exposed sandbox port. */
|
|
141
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>;
|
|
142
162
|
updateNetwork(..._args: unknown[]): never;
|
|
143
163
|
pause(): never;
|
|
144
164
|
betaPause(): never;
|
|
145
165
|
resume(): never;
|
|
166
|
+
private fileUrl;
|
|
146
167
|
private configOptions;
|
|
147
168
|
}
|
package/dist/sandbox.js
CHANGED
|
@@ -3,6 +3,8 @@ 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';
|
|
6
8
|
export class SnapshotPaginator {
|
|
7
9
|
loadItems;
|
|
8
10
|
consumed = false;
|
|
@@ -25,9 +27,9 @@ export class Sandbox {
|
|
|
25
27
|
static defaultTemplate = 'base';
|
|
26
28
|
files;
|
|
27
29
|
commands;
|
|
30
|
+
pty;
|
|
31
|
+
git;
|
|
28
32
|
sandboxId;
|
|
29
|
-
pty = { create: () => unsupported('sandbox.pty') };
|
|
30
|
-
git = { clone: () => unsupported('sandbox.git') };
|
|
31
33
|
config;
|
|
32
34
|
control;
|
|
33
35
|
envs;
|
|
@@ -43,6 +45,8 @@ export class Sandbox {
|
|
|
43
45
|
this.dataPlane = dataPlane;
|
|
44
46
|
this.files = new Filesystem(dataPlane);
|
|
45
47
|
this.commands = new Commands(dataPlane, this.config, this.envs);
|
|
48
|
+
this.pty = new Pty(dataPlane, this.config);
|
|
49
|
+
this.git = new Git(dataPlane);
|
|
46
50
|
}
|
|
47
51
|
/** Create a sandbox and return it only after the API supplies a data-plane session. */
|
|
48
52
|
static async create(templateOrOpts, opts = {}) {
|
|
@@ -110,6 +114,8 @@ export class Sandbox {
|
|
|
110
114
|
this.dataPlane = dataPlane;
|
|
111
115
|
this.files = new Filesystem(dataPlane);
|
|
112
116
|
this.commands = new Commands(dataPlane, this.config, this.envs);
|
|
117
|
+
this.pty = new Pty(dataPlane, this.config);
|
|
118
|
+
this.git = new Git(dataPlane);
|
|
113
119
|
return this;
|
|
114
120
|
}
|
|
115
121
|
/** Destroy a sandbox by id. */
|
|
@@ -133,7 +139,7 @@ export class Sandbox {
|
|
|
133
139
|
/** Create a Watasu checkpoint using snapshot naming. */
|
|
134
140
|
static async createSnapshot(sandboxId, opts = {}) {
|
|
135
141
|
const control = new ControlClient(new ConnectionConfig(opts));
|
|
136
|
-
const payload = await control.post(`/sandboxes/${sandboxId}/
|
|
142
|
+
const payload = await control.post(`/sandboxes/${sandboxId}/snapshots`, {
|
|
137
143
|
json: snapshotPayload(opts),
|
|
138
144
|
requestTimeoutMs: opts.requestTimeoutMs,
|
|
139
145
|
});
|
|
@@ -150,9 +156,20 @@ export class Sandbox {
|
|
|
150
156
|
return snapshots.map((item) => snapshotInfo(record(item)));
|
|
151
157
|
});
|
|
152
158
|
}
|
|
153
|
-
/**
|
|
154
|
-
static deleteSnapshot(
|
|
155
|
-
|
|
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
|
+
}
|
|
156
173
|
}
|
|
157
174
|
/** Destroy this sandbox. */
|
|
158
175
|
async kill() {
|
|
@@ -206,6 +223,10 @@ export class Sandbox {
|
|
|
206
223
|
async createSnapshot(opts = {}) {
|
|
207
224
|
return Sandbox.createSnapshot(this.sandboxId, { ...this.configOptions(), ...opts });
|
|
208
225
|
}
|
|
226
|
+
/** Delete a snapshot by id. */
|
|
227
|
+
async deleteSnapshot(snapshotId, opts = {}) {
|
|
228
|
+
return Sandbox.deleteSnapshot(snapshotId, { ...this.configOptions(), ...opts });
|
|
229
|
+
}
|
|
209
230
|
/** Watasu-native alias for `createSnapshot`. */
|
|
210
231
|
async checkpoint(opts = {}) {
|
|
211
232
|
return this.createSnapshot(opts);
|
|
@@ -249,10 +270,40 @@ export class Sandbox {
|
|
|
249
270
|
throw new SandboxError('port response did not include host or url');
|
|
250
271
|
return `p${port}-${routeToken}.sandbox.${this.config.dataPlaneDomain}`;
|
|
251
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
|
+
}
|
|
252
291
|
updateNetwork(..._args) { unsupported('Sandbox.updateNetwork'); }
|
|
253
292
|
pause() { unsupported('Sandbox.pause'); }
|
|
254
293
|
betaPause() { unsupported('Sandbox.betaPause'); }
|
|
255
294
|
resume() { unsupported('Sandbox.resume'); }
|
|
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
|
+
}
|
|
256
307
|
configOptions() {
|
|
257
308
|
return {
|
|
258
309
|
apiKey: this.config.apiKey,
|
|
@@ -273,6 +324,18 @@ function dataPlaneFromSession(session, config) {
|
|
|
273
324
|
}
|
|
274
325
|
return new DataPlaneClient(url, token, config);
|
|
275
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
|
+
}
|
|
276
339
|
function sessionOperationRequestTimeout(config, opts) {
|
|
277
340
|
if (opts.requestTimeoutMs !== undefined)
|
|
278
341
|
return opts.requestTimeoutMs;
|