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