@lizard-build/sdk 0.1.31 → 0.1.33
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/dist/index.d.mts +104 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +147 -1
- package/dist/index.mjs +147 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -12,6 +12,15 @@ declare class ConnectionConfig {
|
|
|
12
12
|
get headers(): Record<string, string>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** A process this sandbox is currently running. */
|
|
16
|
+
interface ProcessInfo {
|
|
17
|
+
pid: number;
|
|
18
|
+
cmd: string[];
|
|
19
|
+
/** When the process started, unix milliseconds. */
|
|
20
|
+
startedAt: number;
|
|
21
|
+
}
|
|
22
|
+
/** Signals accepted by {@link Process.kill}. */
|
|
23
|
+
type ProcessSignal = 'SIGTERM' | 'SIGKILL' | 'SIGINT' | 'SIGHUP' | 'SIGQUIT' | 'SIGUSR1' | 'SIGUSR2';
|
|
15
24
|
/**
|
|
16
25
|
* The result of a process execution inside a Lizard microVM.
|
|
17
26
|
*/
|
|
@@ -25,8 +34,15 @@ interface ProcessOpts {
|
|
|
25
34
|
envs?: Record<string, string>;
|
|
26
35
|
user?: string;
|
|
27
36
|
workdir?: string;
|
|
37
|
+
/** Called with each stdout line as it is produced, rather than at the end. */
|
|
28
38
|
onStdout?: (data: string) => void;
|
|
39
|
+
/** Called with each stderr line as it is produced, rather than at the end. */
|
|
29
40
|
onStderr?: (data: string) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Called once with the process's pid, before any output. Gives a streaming
|
|
43
|
+
* `exec` something to pass to {@link Process.kill}.
|
|
44
|
+
*/
|
|
45
|
+
onPid?: (pid: number) => void;
|
|
30
46
|
}
|
|
31
47
|
/**
|
|
32
48
|
* Runs processes inside a Lizard sandbox microVM.
|
|
@@ -62,11 +78,55 @@ declare class Process {
|
|
|
62
78
|
* ```
|
|
63
79
|
*/
|
|
64
80
|
exec(cmd: string, opts?: ProcessOpts): Promise<ProcessResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
83
|
+
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
84
|
+
* `{exitCode}` at the end.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* List the processes this sandbox is currently running.
|
|
88
|
+
*
|
|
89
|
+
* Only processes started through `exec` — not every process in the guest.
|
|
90
|
+
* Those are the ones you can act on; the rest are the image's own business.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* for (const p of await sandbox.process.list()) {
|
|
95
|
+
* console.log(p.pid, p.cmd.join(' '))
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
list(): Promise<ProcessInfo[]>;
|
|
100
|
+
/**
|
|
101
|
+
* Signal a running process. Defaults to `SIGTERM`.
|
|
102
|
+
*
|
|
103
|
+
* The signal goes to the process group, so a shell's children die with it —
|
|
104
|
+
* killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
105
|
+
*
|
|
106
|
+
* The pid comes from {@link list}, or from the `pid` event at the start of a
|
|
107
|
+
* streaming `exec`.
|
|
108
|
+
*
|
|
109
|
+
* @example Stop a long build:
|
|
110
|
+
* ```ts
|
|
111
|
+
* const [build] = await sandbox.process.list()
|
|
112
|
+
* await sandbox.process.kill(build.pid)
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
kill(pid: number, signal?: ProcessSignal): Promise<void>;
|
|
116
|
+
private consumeStream;
|
|
65
117
|
}
|
|
66
118
|
|
|
67
119
|
interface FsOpts {
|
|
68
120
|
user?: string;
|
|
69
121
|
}
|
|
122
|
+
/** A filesystem change reported by a {@link Watcher}. */
|
|
123
|
+
interface FsEvent {
|
|
124
|
+
type: 'create' | 'write' | 'remove' | 'rename' | 'chmod';
|
|
125
|
+
/** The entry's name within its directory. */
|
|
126
|
+
name: string;
|
|
127
|
+
/** Full path inside the sandbox. */
|
|
128
|
+
path: string;
|
|
129
|
+
}
|
|
70
130
|
interface FileInfo {
|
|
71
131
|
name: string;
|
|
72
132
|
path: string;
|
|
@@ -159,8 +219,52 @@ declare class Fs {
|
|
|
159
219
|
* `LizardError` (501) — recreate the sandbox to use it.
|
|
160
220
|
*/
|
|
161
221
|
move(from: string, to: string): Promise<void>;
|
|
222
|
+
/**
|
|
223
|
+
* Watch a directory for changes.
|
|
224
|
+
*
|
|
225
|
+
* Polling rather than a push stream: the events cross two proxy hops, and a
|
|
226
|
+
* long-lived stream through both is exactly what breaks first. Call
|
|
227
|
+
* {@link Watcher.getEvents} on whatever interval suits you — each call drains
|
|
228
|
+
* everything queued since the last one, so nothing is missed between polls.
|
|
229
|
+
*
|
|
230
|
+
* Remember to {@link Watcher.close} it; an abandoned watcher keeps queueing
|
|
231
|
+
* events in the guest until the sandbox ends (bounded, but wasted).
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const w = await sandbox.fs.watch('/app', { recursive: true })
|
|
236
|
+
* setInterval(async () => {
|
|
237
|
+
* for (const e of await w.getEvents()) console.log(e.type, e.path)
|
|
238
|
+
* }, 1000)
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* Sandboxes created before this shipped run a guest agent without it and throw
|
|
242
|
+
* `LizardError` (501) — recreate the sandbox to use it.
|
|
243
|
+
*/
|
|
244
|
+
watch(path: string, opts?: {
|
|
245
|
+
recursive?: boolean;
|
|
246
|
+
}): Promise<Watcher>;
|
|
162
247
|
private execInternal;
|
|
163
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* A handle to a directory watch inside a sandbox. Created by {@link Fs.watch}.
|
|
251
|
+
*/
|
|
252
|
+
declare class Watcher {
|
|
253
|
+
private readonly sandboxId;
|
|
254
|
+
private readonly config;
|
|
255
|
+
/** Server-side id for this watch. */
|
|
256
|
+
readonly watcherId: string;
|
|
257
|
+
constructor(sandboxId: string, config: ConnectionConfig,
|
|
258
|
+
/** Server-side id for this watch. */
|
|
259
|
+
watcherId: string);
|
|
260
|
+
/**
|
|
261
|
+
* Drain every change since the last call. Returns an empty array when nothing
|
|
262
|
+
* has happened — that is not an error, just a quiet interval.
|
|
263
|
+
*/
|
|
264
|
+
getEvents(): Promise<FsEvent[]>;
|
|
265
|
+
/** Stop watching and release the guest-side queue. */
|
|
266
|
+
close(): Promise<void>;
|
|
267
|
+
}
|
|
164
268
|
|
|
165
269
|
interface SandboxInfo {
|
|
166
270
|
sandboxId: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ declare class ConnectionConfig {
|
|
|
12
12
|
get headers(): Record<string, string>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** A process this sandbox is currently running. */
|
|
16
|
+
interface ProcessInfo {
|
|
17
|
+
pid: number;
|
|
18
|
+
cmd: string[];
|
|
19
|
+
/** When the process started, unix milliseconds. */
|
|
20
|
+
startedAt: number;
|
|
21
|
+
}
|
|
22
|
+
/** Signals accepted by {@link Process.kill}. */
|
|
23
|
+
type ProcessSignal = 'SIGTERM' | 'SIGKILL' | 'SIGINT' | 'SIGHUP' | 'SIGQUIT' | 'SIGUSR1' | 'SIGUSR2';
|
|
15
24
|
/**
|
|
16
25
|
* The result of a process execution inside a Lizard microVM.
|
|
17
26
|
*/
|
|
@@ -25,8 +34,15 @@ interface ProcessOpts {
|
|
|
25
34
|
envs?: Record<string, string>;
|
|
26
35
|
user?: string;
|
|
27
36
|
workdir?: string;
|
|
37
|
+
/** Called with each stdout line as it is produced, rather than at the end. */
|
|
28
38
|
onStdout?: (data: string) => void;
|
|
39
|
+
/** Called with each stderr line as it is produced, rather than at the end. */
|
|
29
40
|
onStderr?: (data: string) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Called once with the process's pid, before any output. Gives a streaming
|
|
43
|
+
* `exec` something to pass to {@link Process.kill}.
|
|
44
|
+
*/
|
|
45
|
+
onPid?: (pid: number) => void;
|
|
30
46
|
}
|
|
31
47
|
/**
|
|
32
48
|
* Runs processes inside a Lizard sandbox microVM.
|
|
@@ -62,11 +78,55 @@ declare class Process {
|
|
|
62
78
|
* ```
|
|
63
79
|
*/
|
|
64
80
|
exec(cmd: string, opts?: ProcessOpts): Promise<ProcessResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
83
|
+
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
84
|
+
* `{exitCode}` at the end.
|
|
85
|
+
*/
|
|
86
|
+
/**
|
|
87
|
+
* List the processes this sandbox is currently running.
|
|
88
|
+
*
|
|
89
|
+
* Only processes started through `exec` — not every process in the guest.
|
|
90
|
+
* Those are the ones you can act on; the rest are the image's own business.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* for (const p of await sandbox.process.list()) {
|
|
95
|
+
* console.log(p.pid, p.cmd.join(' '))
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
list(): Promise<ProcessInfo[]>;
|
|
100
|
+
/**
|
|
101
|
+
* Signal a running process. Defaults to `SIGTERM`.
|
|
102
|
+
*
|
|
103
|
+
* The signal goes to the process group, so a shell's children die with it —
|
|
104
|
+
* killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
105
|
+
*
|
|
106
|
+
* The pid comes from {@link list}, or from the `pid` event at the start of a
|
|
107
|
+
* streaming `exec`.
|
|
108
|
+
*
|
|
109
|
+
* @example Stop a long build:
|
|
110
|
+
* ```ts
|
|
111
|
+
* const [build] = await sandbox.process.list()
|
|
112
|
+
* await sandbox.process.kill(build.pid)
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
kill(pid: number, signal?: ProcessSignal): Promise<void>;
|
|
116
|
+
private consumeStream;
|
|
65
117
|
}
|
|
66
118
|
|
|
67
119
|
interface FsOpts {
|
|
68
120
|
user?: string;
|
|
69
121
|
}
|
|
122
|
+
/** A filesystem change reported by a {@link Watcher}. */
|
|
123
|
+
interface FsEvent {
|
|
124
|
+
type: 'create' | 'write' | 'remove' | 'rename' | 'chmod';
|
|
125
|
+
/** The entry's name within its directory. */
|
|
126
|
+
name: string;
|
|
127
|
+
/** Full path inside the sandbox. */
|
|
128
|
+
path: string;
|
|
129
|
+
}
|
|
70
130
|
interface FileInfo {
|
|
71
131
|
name: string;
|
|
72
132
|
path: string;
|
|
@@ -159,8 +219,52 @@ declare class Fs {
|
|
|
159
219
|
* `LizardError` (501) — recreate the sandbox to use it.
|
|
160
220
|
*/
|
|
161
221
|
move(from: string, to: string): Promise<void>;
|
|
222
|
+
/**
|
|
223
|
+
* Watch a directory for changes.
|
|
224
|
+
*
|
|
225
|
+
* Polling rather than a push stream: the events cross two proxy hops, and a
|
|
226
|
+
* long-lived stream through both is exactly what breaks first. Call
|
|
227
|
+
* {@link Watcher.getEvents} on whatever interval suits you — each call drains
|
|
228
|
+
* everything queued since the last one, so nothing is missed between polls.
|
|
229
|
+
*
|
|
230
|
+
* Remember to {@link Watcher.close} it; an abandoned watcher keeps queueing
|
|
231
|
+
* events in the guest until the sandbox ends (bounded, but wasted).
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const w = await sandbox.fs.watch('/app', { recursive: true })
|
|
236
|
+
* setInterval(async () => {
|
|
237
|
+
* for (const e of await w.getEvents()) console.log(e.type, e.path)
|
|
238
|
+
* }, 1000)
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* Sandboxes created before this shipped run a guest agent without it and throw
|
|
242
|
+
* `LizardError` (501) — recreate the sandbox to use it.
|
|
243
|
+
*/
|
|
244
|
+
watch(path: string, opts?: {
|
|
245
|
+
recursive?: boolean;
|
|
246
|
+
}): Promise<Watcher>;
|
|
162
247
|
private execInternal;
|
|
163
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* A handle to a directory watch inside a sandbox. Created by {@link Fs.watch}.
|
|
251
|
+
*/
|
|
252
|
+
declare class Watcher {
|
|
253
|
+
private readonly sandboxId;
|
|
254
|
+
private readonly config;
|
|
255
|
+
/** Server-side id for this watch. */
|
|
256
|
+
readonly watcherId: string;
|
|
257
|
+
constructor(sandboxId: string, config: ConnectionConfig,
|
|
258
|
+
/** Server-side id for this watch. */
|
|
259
|
+
watcherId: string);
|
|
260
|
+
/**
|
|
261
|
+
* Drain every change since the last call. Returns an empty array when nothing
|
|
262
|
+
* has happened — that is not an error, just a quiet interval.
|
|
263
|
+
*/
|
|
264
|
+
getEvents(): Promise<FsEvent[]>;
|
|
265
|
+
/** Stop watching and release the guest-side queue. */
|
|
266
|
+
close(): Promise<void>;
|
|
267
|
+
}
|
|
164
268
|
|
|
165
269
|
interface SandboxInfo {
|
|
166
270
|
sandboxId: string;
|
package/dist/index.js
CHANGED
|
@@ -163,9 +163,10 @@ var Process = class {
|
|
|
163
163
|
* ```
|
|
164
164
|
*/
|
|
165
165
|
async exec(cmd, opts) {
|
|
166
|
+
const streaming = Boolean(opts?.onStdout || opts?.onStderr || opts?.onPid);
|
|
166
167
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
167
168
|
method: "POST",
|
|
168
|
-
headers: this.config.headers,
|
|
169
|
+
headers: streaming ? { ...this.config.headers, Accept: "text/event-stream" } : this.config.headers,
|
|
169
170
|
body: JSON.stringify({
|
|
170
171
|
cmd,
|
|
171
172
|
envs: opts?.envs,
|
|
@@ -176,8 +177,96 @@ var Process = class {
|
|
|
176
177
|
signal: AbortSignal.timeout(opts?.timeoutMs ?? 6e4)
|
|
177
178
|
});
|
|
178
179
|
if (!res.ok) await handleApiError(res);
|
|
180
|
+
if (!streaming) return res.json();
|
|
181
|
+
return this.consumeStream(res, opts);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
185
|
+
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
186
|
+
* `{exitCode}` at the end.
|
|
187
|
+
*/
|
|
188
|
+
/**
|
|
189
|
+
* List the processes this sandbox is currently running.
|
|
190
|
+
*
|
|
191
|
+
* Only processes started through `exec` — not every process in the guest.
|
|
192
|
+
* Those are the ones you can act on; the rest are the image's own business.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* for (const p of await sandbox.process.list()) {
|
|
197
|
+
* console.log(p.pid, p.cmd.join(' '))
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
async list() {
|
|
202
|
+
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/processes`, {
|
|
203
|
+
headers: this.config.headers
|
|
204
|
+
});
|
|
205
|
+
if (!res.ok) await handleApiError(res);
|
|
179
206
|
return res.json();
|
|
180
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Signal a running process. Defaults to `SIGTERM`.
|
|
210
|
+
*
|
|
211
|
+
* The signal goes to the process group, so a shell's children die with it —
|
|
212
|
+
* killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
213
|
+
*
|
|
214
|
+
* The pid comes from {@link list}, or from the `pid` event at the start of a
|
|
215
|
+
* streaming `exec`.
|
|
216
|
+
*
|
|
217
|
+
* @example Stop a long build:
|
|
218
|
+
* ```ts
|
|
219
|
+
* const [build] = await sandbox.process.list()
|
|
220
|
+
* await sandbox.process.kill(build.pid)
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
async kill(pid, signal = "SIGTERM") {
|
|
224
|
+
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/processes/signal`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: this.config.headers,
|
|
227
|
+
body: JSON.stringify({ pid, signal })
|
|
228
|
+
});
|
|
229
|
+
if (!res.ok) await handleApiError(res);
|
|
230
|
+
}
|
|
231
|
+
async consumeStream(res, opts) {
|
|
232
|
+
const reader = res.body?.getReader();
|
|
233
|
+
if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
|
|
234
|
+
const decoder = new TextDecoder();
|
|
235
|
+
let buf = "";
|
|
236
|
+
let stdout = "";
|
|
237
|
+
let stderr = "";
|
|
238
|
+
let exitCode = 0;
|
|
239
|
+
const handle = (raw) => {
|
|
240
|
+
if (!raw.startsWith("data:")) return;
|
|
241
|
+
let ev;
|
|
242
|
+
try {
|
|
243
|
+
ev = JSON.parse(raw.slice(5).trim());
|
|
244
|
+
} catch {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (ev.line !== void 0) {
|
|
248
|
+
if (ev.stream === "stderr") {
|
|
249
|
+
stderr += ev.line + "\n";
|
|
250
|
+
opts.onStderr?.(ev.line);
|
|
251
|
+
} else {
|
|
252
|
+
stdout += ev.line + "\n";
|
|
253
|
+
opts.onStdout?.(ev.line);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (ev.pid !== void 0) opts.onPid?.(ev.pid);
|
|
257
|
+
if (ev.exitCode !== void 0) exitCode = ev.exitCode;
|
|
258
|
+
};
|
|
259
|
+
for (; ; ) {
|
|
260
|
+
const { done, value } = await reader.read();
|
|
261
|
+
if (done) break;
|
|
262
|
+
buf += decoder.decode(value, { stream: true });
|
|
263
|
+
const lines = buf.split("\n");
|
|
264
|
+
buf = lines.pop() ?? "";
|
|
265
|
+
for (const l of lines) handle(l);
|
|
266
|
+
}
|
|
267
|
+
if (buf) handle(buf);
|
|
268
|
+
return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), exitCode };
|
|
269
|
+
}
|
|
181
270
|
};
|
|
182
271
|
|
|
183
272
|
// src/sandbox/fs.ts
|
|
@@ -304,6 +393,38 @@ var Fs = class {
|
|
|
304
393
|
});
|
|
305
394
|
if (!res.ok) await handleApiError(res);
|
|
306
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Watch a directory for changes.
|
|
398
|
+
*
|
|
399
|
+
* Polling rather than a push stream: the events cross two proxy hops, and a
|
|
400
|
+
* long-lived stream through both is exactly what breaks first. Call
|
|
401
|
+
* {@link Watcher.getEvents} on whatever interval suits you — each call drains
|
|
402
|
+
* everything queued since the last one, so nothing is missed between polls.
|
|
403
|
+
*
|
|
404
|
+
* Remember to {@link Watcher.close} it; an abandoned watcher keeps queueing
|
|
405
|
+
* events in the guest until the sandbox ends (bounded, but wasted).
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```ts
|
|
409
|
+
* const w = await sandbox.fs.watch('/app', { recursive: true })
|
|
410
|
+
* setInterval(async () => {
|
|
411
|
+
* for (const e of await w.getEvents()) console.log(e.type, e.path)
|
|
412
|
+
* }, 1000)
|
|
413
|
+
* ```
|
|
414
|
+
*
|
|
415
|
+
* Sandboxes created before this shipped run a guest agent without it and throw
|
|
416
|
+
* `LizardError` (501) — recreate the sandbox to use it.
|
|
417
|
+
*/
|
|
418
|
+
async watch(path, opts) {
|
|
419
|
+
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: this.config.headers,
|
|
422
|
+
body: JSON.stringify({ path, recursive: opts?.recursive ?? false })
|
|
423
|
+
});
|
|
424
|
+
if (!res.ok) await handleApiError(res);
|
|
425
|
+
const { watcherId } = await res.json();
|
|
426
|
+
return new Watcher(this.sandboxId, this.config, watcherId);
|
|
427
|
+
}
|
|
307
428
|
async execInternal(cmd, user) {
|
|
308
429
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
309
430
|
method: "POST",
|
|
@@ -313,6 +434,31 @@ var Fs = class {
|
|
|
313
434
|
if (!res.ok) await handleApiError(res);
|
|
314
435
|
}
|
|
315
436
|
};
|
|
437
|
+
var Watcher = class {
|
|
438
|
+
constructor(sandboxId, config, watcherId) {
|
|
439
|
+
this.sandboxId = sandboxId;
|
|
440
|
+
this.config = config;
|
|
441
|
+
this.watcherId = watcherId;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Drain every change since the last call. Returns an empty array when nothing
|
|
445
|
+
* has happened — that is not an error, just a quiet interval.
|
|
446
|
+
*/
|
|
447
|
+
async getEvents() {
|
|
448
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch/events`);
|
|
449
|
+
url.searchParams.set("watcherId", this.watcherId);
|
|
450
|
+
const res = await fetch(url.toString(), { headers: this.config.headers });
|
|
451
|
+
if (!res.ok) await handleApiError(res);
|
|
452
|
+
return res.json();
|
|
453
|
+
}
|
|
454
|
+
/** Stop watching and release the guest-side queue. */
|
|
455
|
+
async close() {
|
|
456
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`);
|
|
457
|
+
url.searchParams.set("watcherId", this.watcherId);
|
|
458
|
+
const res = await fetch(url.toString(), { method: "DELETE", headers: this.config.headers });
|
|
459
|
+
if (!res.ok) await handleApiError(res);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
316
462
|
|
|
317
463
|
// src/sandbox/client.ts
|
|
318
464
|
var SandboxClient = class {
|
package/dist/index.mjs
CHANGED
|
@@ -127,9 +127,10 @@ var Process = class {
|
|
|
127
127
|
* ```
|
|
128
128
|
*/
|
|
129
129
|
async exec(cmd, opts) {
|
|
130
|
+
const streaming = Boolean(opts?.onStdout || opts?.onStderr || opts?.onPid);
|
|
130
131
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
131
132
|
method: "POST",
|
|
132
|
-
headers: this.config.headers,
|
|
133
|
+
headers: streaming ? { ...this.config.headers, Accept: "text/event-stream" } : this.config.headers,
|
|
133
134
|
body: JSON.stringify({
|
|
134
135
|
cmd,
|
|
135
136
|
envs: opts?.envs,
|
|
@@ -140,8 +141,96 @@ var Process = class {
|
|
|
140
141
|
signal: AbortSignal.timeout(opts?.timeoutMs ?? 6e4)
|
|
141
142
|
});
|
|
142
143
|
if (!res.ok) await handleApiError(res);
|
|
144
|
+
if (!streaming) return res.json();
|
|
145
|
+
return this.consumeStream(res, opts);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Read an SSE exec stream, handing each line to the caller as it arrives while
|
|
149
|
+
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
150
|
+
* `{exitCode}` at the end.
|
|
151
|
+
*/
|
|
152
|
+
/**
|
|
153
|
+
* List the processes this sandbox is currently running.
|
|
154
|
+
*
|
|
155
|
+
* Only processes started through `exec` — not every process in the guest.
|
|
156
|
+
* Those are the ones you can act on; the rest are the image's own business.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* for (const p of await sandbox.process.list()) {
|
|
161
|
+
* console.log(p.pid, p.cmd.join(' '))
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
async list() {
|
|
166
|
+
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/processes`, {
|
|
167
|
+
headers: this.config.headers
|
|
168
|
+
});
|
|
169
|
+
if (!res.ok) await handleApiError(res);
|
|
143
170
|
return res.json();
|
|
144
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Signal a running process. Defaults to `SIGTERM`.
|
|
174
|
+
*
|
|
175
|
+
* The signal goes to the process group, so a shell's children die with it —
|
|
176
|
+
* killing `sh -c 'sleep 100'` otherwise leaves the sleep running.
|
|
177
|
+
*
|
|
178
|
+
* The pid comes from {@link list}, or from the `pid` event at the start of a
|
|
179
|
+
* streaming `exec`.
|
|
180
|
+
*
|
|
181
|
+
* @example Stop a long build:
|
|
182
|
+
* ```ts
|
|
183
|
+
* const [build] = await sandbox.process.list()
|
|
184
|
+
* await sandbox.process.kill(build.pid)
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
async kill(pid, signal = "SIGTERM") {
|
|
188
|
+
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/processes/signal`, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: this.config.headers,
|
|
191
|
+
body: JSON.stringify({ pid, signal })
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok) await handleApiError(res);
|
|
194
|
+
}
|
|
195
|
+
async consumeStream(res, opts) {
|
|
196
|
+
const reader = res.body?.getReader();
|
|
197
|
+
if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
|
|
198
|
+
const decoder = new TextDecoder();
|
|
199
|
+
let buf = "";
|
|
200
|
+
let stdout = "";
|
|
201
|
+
let stderr = "";
|
|
202
|
+
let exitCode = 0;
|
|
203
|
+
const handle = (raw) => {
|
|
204
|
+
if (!raw.startsWith("data:")) return;
|
|
205
|
+
let ev;
|
|
206
|
+
try {
|
|
207
|
+
ev = JSON.parse(raw.slice(5).trim());
|
|
208
|
+
} catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (ev.line !== void 0) {
|
|
212
|
+
if (ev.stream === "stderr") {
|
|
213
|
+
stderr += ev.line + "\n";
|
|
214
|
+
opts.onStderr?.(ev.line);
|
|
215
|
+
} else {
|
|
216
|
+
stdout += ev.line + "\n";
|
|
217
|
+
opts.onStdout?.(ev.line);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (ev.pid !== void 0) opts.onPid?.(ev.pid);
|
|
221
|
+
if (ev.exitCode !== void 0) exitCode = ev.exitCode;
|
|
222
|
+
};
|
|
223
|
+
for (; ; ) {
|
|
224
|
+
const { done, value } = await reader.read();
|
|
225
|
+
if (done) break;
|
|
226
|
+
buf += decoder.decode(value, { stream: true });
|
|
227
|
+
const lines = buf.split("\n");
|
|
228
|
+
buf = lines.pop() ?? "";
|
|
229
|
+
for (const l of lines) handle(l);
|
|
230
|
+
}
|
|
231
|
+
if (buf) handle(buf);
|
|
232
|
+
return { stdout: stdout.trimEnd(), stderr: stderr.trimEnd(), exitCode };
|
|
233
|
+
}
|
|
145
234
|
};
|
|
146
235
|
|
|
147
236
|
// src/sandbox/fs.ts
|
|
@@ -268,6 +357,38 @@ var Fs = class {
|
|
|
268
357
|
});
|
|
269
358
|
if (!res.ok) await handleApiError(res);
|
|
270
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Watch a directory for changes.
|
|
362
|
+
*
|
|
363
|
+
* Polling rather than a push stream: the events cross two proxy hops, and a
|
|
364
|
+
* long-lived stream through both is exactly what breaks first. Call
|
|
365
|
+
* {@link Watcher.getEvents} on whatever interval suits you — each call drains
|
|
366
|
+
* everything queued since the last one, so nothing is missed between polls.
|
|
367
|
+
*
|
|
368
|
+
* Remember to {@link Watcher.close} it; an abandoned watcher keeps queueing
|
|
369
|
+
* events in the guest until the sandbox ends (bounded, but wasted).
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```ts
|
|
373
|
+
* const w = await sandbox.fs.watch('/app', { recursive: true })
|
|
374
|
+
* setInterval(async () => {
|
|
375
|
+
* for (const e of await w.getEvents()) console.log(e.type, e.path)
|
|
376
|
+
* }, 1000)
|
|
377
|
+
* ```
|
|
378
|
+
*
|
|
379
|
+
* Sandboxes created before this shipped run a guest agent without it and throw
|
|
380
|
+
* `LizardError` (501) — recreate the sandbox to use it.
|
|
381
|
+
*/
|
|
382
|
+
async watch(path, opts) {
|
|
383
|
+
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`, {
|
|
384
|
+
method: "POST",
|
|
385
|
+
headers: this.config.headers,
|
|
386
|
+
body: JSON.stringify({ path, recursive: opts?.recursive ?? false })
|
|
387
|
+
});
|
|
388
|
+
if (!res.ok) await handleApiError(res);
|
|
389
|
+
const { watcherId } = await res.json();
|
|
390
|
+
return new Watcher(this.sandboxId, this.config, watcherId);
|
|
391
|
+
}
|
|
271
392
|
async execInternal(cmd, user) {
|
|
272
393
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
273
394
|
method: "POST",
|
|
@@ -277,6 +398,31 @@ var Fs = class {
|
|
|
277
398
|
if (!res.ok) await handleApiError(res);
|
|
278
399
|
}
|
|
279
400
|
};
|
|
401
|
+
var Watcher = class {
|
|
402
|
+
constructor(sandboxId, config, watcherId) {
|
|
403
|
+
this.sandboxId = sandboxId;
|
|
404
|
+
this.config = config;
|
|
405
|
+
this.watcherId = watcherId;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Drain every change since the last call. Returns an empty array when nothing
|
|
409
|
+
* has happened — that is not an error, just a quiet interval.
|
|
410
|
+
*/
|
|
411
|
+
async getEvents() {
|
|
412
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch/events`);
|
|
413
|
+
url.searchParams.set("watcherId", this.watcherId);
|
|
414
|
+
const res = await fetch(url.toString(), { headers: this.config.headers });
|
|
415
|
+
if (!res.ok) await handleApiError(res);
|
|
416
|
+
return res.json();
|
|
417
|
+
}
|
|
418
|
+
/** Stop watching and release the guest-side queue. */
|
|
419
|
+
async close() {
|
|
420
|
+
const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/watch`);
|
|
421
|
+
url.searchParams.set("watcherId", this.watcherId);
|
|
422
|
+
const res = await fetch(url.toString(), { method: "DELETE", headers: this.config.headers });
|
|
423
|
+
if (!res.ok) await handleApiError(res);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
280
426
|
|
|
281
427
|
// src/sandbox/client.ts
|
|
282
428
|
var SandboxClient = class {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lizard-build/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Firecracker microVM sandboxes for AI agents — boot Linux in milliseconds, run code, write files, expose ports over HTTPS, then pause a session with memory intact and resume it later. TypeScript SDK for Lizard Sandboxes (lizard.build).",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|