@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 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.31",
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",