@lizard-build/sdk 0.1.32 → 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 +96 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +102 -1
- package/dist/index.mjs +102 -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
|
*/
|
|
@@ -29,6 +38,11 @@ interface ProcessOpts {
|
|
|
29
38
|
onStdout?: (data: string) => void;
|
|
30
39
|
/** Called with each stderr line as it is produced, rather than at the end. */
|
|
31
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;
|
|
32
46
|
}
|
|
33
47
|
/**
|
|
34
48
|
* Runs processes inside a Lizard sandbox microVM.
|
|
@@ -69,12 +83,50 @@ declare class Process {
|
|
|
69
83
|
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
70
84
|
* `{exitCode}` at the end.
|
|
71
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>;
|
|
72
116
|
private consumeStream;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
interface FsOpts {
|
|
76
120
|
user?: string;
|
|
77
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
|
+
}
|
|
78
130
|
interface FileInfo {
|
|
79
131
|
name: string;
|
|
80
132
|
path: string;
|
|
@@ -167,8 +219,52 @@ declare class Fs {
|
|
|
167
219
|
* `LizardError` (501) — recreate the sandbox to use it.
|
|
168
220
|
*/
|
|
169
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>;
|
|
170
247
|
private execInternal;
|
|
171
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
|
+
}
|
|
172
268
|
|
|
173
269
|
interface SandboxInfo {
|
|
174
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
|
*/
|
|
@@ -29,6 +38,11 @@ interface ProcessOpts {
|
|
|
29
38
|
onStdout?: (data: string) => void;
|
|
30
39
|
/** Called with each stderr line as it is produced, rather than at the end. */
|
|
31
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;
|
|
32
46
|
}
|
|
33
47
|
/**
|
|
34
48
|
* Runs processes inside a Lizard sandbox microVM.
|
|
@@ -69,12 +83,50 @@ declare class Process {
|
|
|
69
83
|
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
70
84
|
* `{exitCode}` at the end.
|
|
71
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>;
|
|
72
116
|
private consumeStream;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
interface FsOpts {
|
|
76
120
|
user?: string;
|
|
77
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
|
+
}
|
|
78
130
|
interface FileInfo {
|
|
79
131
|
name: string;
|
|
80
132
|
path: string;
|
|
@@ -167,8 +219,52 @@ declare class Fs {
|
|
|
167
219
|
* `LizardError` (501) — recreate the sandbox to use it.
|
|
168
220
|
*/
|
|
169
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>;
|
|
170
247
|
private execInternal;
|
|
171
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
|
+
}
|
|
172
268
|
|
|
173
269
|
interface SandboxInfo {
|
|
174
270
|
sandboxId: string;
|
package/dist/index.js
CHANGED
|
@@ -163,7 +163,7 @@ var Process = class {
|
|
|
163
163
|
* ```
|
|
164
164
|
*/
|
|
165
165
|
async exec(cmd, opts) {
|
|
166
|
-
const streaming = Boolean(opts?.onStdout || opts?.onStderr);
|
|
166
|
+
const streaming = Boolean(opts?.onStdout || opts?.onStderr || opts?.onPid);
|
|
167
167
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
168
168
|
method: "POST",
|
|
169
169
|
headers: streaming ? { ...this.config.headers, Accept: "text/event-stream" } : this.config.headers,
|
|
@@ -185,6 +185,49 @@ var Process = class {
|
|
|
185
185
|
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
186
186
|
* `{exitCode}` at the end.
|
|
187
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);
|
|
206
|
+
return res.json();
|
|
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
|
+
}
|
|
188
231
|
async consumeStream(res, opts) {
|
|
189
232
|
const reader = res.body?.getReader();
|
|
190
233
|
if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
|
|
@@ -210,6 +253,7 @@ var Process = class {
|
|
|
210
253
|
opts.onStdout?.(ev.line);
|
|
211
254
|
}
|
|
212
255
|
}
|
|
256
|
+
if (ev.pid !== void 0) opts.onPid?.(ev.pid);
|
|
213
257
|
if (ev.exitCode !== void 0) exitCode = ev.exitCode;
|
|
214
258
|
};
|
|
215
259
|
for (; ; ) {
|
|
@@ -349,6 +393,38 @@ var Fs = class {
|
|
|
349
393
|
});
|
|
350
394
|
if (!res.ok) await handleApiError(res);
|
|
351
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
|
+
}
|
|
352
428
|
async execInternal(cmd, user) {
|
|
353
429
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
354
430
|
method: "POST",
|
|
@@ -358,6 +434,31 @@ var Fs = class {
|
|
|
358
434
|
if (!res.ok) await handleApiError(res);
|
|
359
435
|
}
|
|
360
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
|
+
};
|
|
361
462
|
|
|
362
463
|
// src/sandbox/client.ts
|
|
363
464
|
var SandboxClient = class {
|
package/dist/index.mjs
CHANGED
|
@@ -127,7 +127,7 @@ var Process = class {
|
|
|
127
127
|
* ```
|
|
128
128
|
*/
|
|
129
129
|
async exec(cmd, opts) {
|
|
130
|
-
const streaming = Boolean(opts?.onStdout || opts?.onStderr);
|
|
130
|
+
const streaming = Boolean(opts?.onStdout || opts?.onStderr || opts?.onPid);
|
|
131
131
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
132
132
|
method: "POST",
|
|
133
133
|
headers: streaming ? { ...this.config.headers, Accept: "text/event-stream" } : this.config.headers,
|
|
@@ -149,6 +149,49 @@ var Process = class {
|
|
|
149
149
|
* still accumulating the full result. Events are `{stream, line}` for output and
|
|
150
150
|
* `{exitCode}` at the end.
|
|
151
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);
|
|
170
|
+
return res.json();
|
|
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
|
+
}
|
|
152
195
|
async consumeStream(res, opts) {
|
|
153
196
|
const reader = res.body?.getReader();
|
|
154
197
|
if (!reader) return { stdout: "", stderr: "", exitCode: 0 };
|
|
@@ -174,6 +217,7 @@ var Process = class {
|
|
|
174
217
|
opts.onStdout?.(ev.line);
|
|
175
218
|
}
|
|
176
219
|
}
|
|
220
|
+
if (ev.pid !== void 0) opts.onPid?.(ev.pid);
|
|
177
221
|
if (ev.exitCode !== void 0) exitCode = ev.exitCode;
|
|
178
222
|
};
|
|
179
223
|
for (; ; ) {
|
|
@@ -313,6 +357,38 @@ var Fs = class {
|
|
|
313
357
|
});
|
|
314
358
|
if (!res.ok) await handleApiError(res);
|
|
315
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
|
+
}
|
|
316
392
|
async execInternal(cmd, user) {
|
|
317
393
|
const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
|
|
318
394
|
method: "POST",
|
|
@@ -322,6 +398,31 @@ var Fs = class {
|
|
|
322
398
|
if (!res.ok) await handleApiError(res);
|
|
323
399
|
}
|
|
324
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
|
+
};
|
|
325
426
|
|
|
326
427
|
// src/sandbox/client.ts
|
|
327
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",
|