@lizard-build/sdk 0.1.29 → 0.1.31

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
@@ -70,8 +70,12 @@ interface FsOpts {
70
70
  interface FileInfo {
71
71
  name: string;
72
72
  path: string;
73
- type: 'file' | 'dir';
73
+ type: 'file' | 'dir' | 'symlink';
74
74
  size: number;
75
+ /** Permission bits as a string, e.g. `-rw-r--r--`. Absent on older sandboxes. */
76
+ mode?: string;
77
+ /** Last modification time, unix milliseconds. Absent on older sandboxes. */
78
+ modTime?: number;
75
79
  }
76
80
  /**
77
81
  * Read and write files inside a Lizard sandbox microVM.
@@ -126,6 +130,35 @@ declare class Fs {
126
130
  * Create a directory (and any missing parents) inside the microVM.
127
131
  */
128
132
  makeDir(path: string, opts?: FsOpts): Promise<void>;
133
+ /**
134
+ * Metadata for a single path — size, type, permissions, modification time.
135
+ *
136
+ * Saves listing a parent directory and filtering it just to answer "does this
137
+ * exist, and how big is it".
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const info = await sandbox.fs.stat('/app/out.bin')
142
+ * console.log(info.size, info.type)
143
+ * ```
144
+ *
145
+ * Throws `NotFoundError` if the path does not exist. Sandboxes created before
146
+ * this shipped run a guest agent without it and throw `LizardError` (501) —
147
+ * recreate the sandbox to use it.
148
+ */
149
+ stat(path: string, opts?: FsOpts): Promise<FileInfo>;
150
+ /**
151
+ * Move or rename a path. Creates the destination's parent directories.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
156
+ * ```
157
+ *
158
+ * Sandboxes created before this shipped run a guest agent without it and throw
159
+ * `LizardError` (501) — recreate the sandbox to use it.
160
+ */
161
+ move(from: string, to: string): Promise<void>;
129
162
  private execInternal;
130
163
  }
131
164
 
@@ -150,7 +183,14 @@ interface SandboxOpts extends ConnectionOpts {
150
183
  metadata?: Record<string, string>;
151
184
  envs?: Record<string, string>;
152
185
  timeoutMs?: number;
186
+ /** Attach a persistent volume by id, mounted at `/data`. */
153
187
  volumeId?: string;
188
+ /**
189
+ * Attach a persistent volume by name, mounted at `/data`. A volume's name is its
190
+ * key inside a project, so this is usually what you want — see {@link Volume}.
191
+ * Requires the sandbox's project to be given as an exact `projectId`.
192
+ */
193
+ volumeName?: string;
154
194
  }
155
195
  /**
156
196
  * Low-level HTTP client for the Lizard sandbox API.
@@ -293,9 +333,16 @@ declare class Sandbox extends SandboxClient {
293
333
  * nothing is written to disk, so a paused sandbox does not survive a host
294
334
  * failure. Resume with `sandbox.resume()` or `Sandbox.connect(id)`.
295
335
  *
296
- * Pausing does not stop the timeout: a paused sandbox is still deleted at
297
- * its original expiry (default 5 minutes). Pass `timeoutMs: 0` at create
298
- * time to opt out of expiry.
336
+ * Pausing does not buy you time past the original deadline. Resuming pushes the
337
+ * expiry forward by however long the sandbox was paused — so a pause/resume cycle
338
+ * costs no runtime — but that adjustment only happens on `resume()`. A sandbox left
339
+ * paused past its original `expiresAt` is deleted there, within a minute, and
340
+ * `resume()` then returns `false`.
341
+ *
342
+ * In other words: pause and come back *before* the original deadline and you lose
343
+ * nothing; leave it paused across the deadline and it is gone. Pass `timeoutMs: 0`
344
+ * at create time to opt out of expiry entirely, which is the only way to park a
345
+ * sandbox indefinitely.
299
346
  *
300
347
  * @returns `true` if paused successfully.
301
348
  */
@@ -303,7 +350,12 @@ declare class Sandbox extends SandboxClient {
303
350
  /**
304
351
  * Resume a paused sandbox by unfreezing its vCPUs.
305
352
  *
306
- * @returns `true` if resumed successfully.
353
+ * The expiry is pushed forward by the time spent paused, so the timeout measures
354
+ * running time rather than wall-clock — provided the original deadline had not
355
+ * already passed while paused. See {@link pause}.
356
+ *
357
+ * @returns `true` if resumed successfully, `false` if the sandbox no longer exists
358
+ * (including because it expired while paused).
307
359
  */
308
360
  resume(opts?: ConnectionOpts): Promise<boolean>;
309
361
  /**
@@ -780,6 +832,14 @@ declare class AuthenticationError extends LizardError {
780
832
  declare class NotFoundError extends LizardError {
781
833
  constructor(message?: string);
782
834
  }
835
+ /**
836
+ * The resource already exists — most often a volume whose name is already taken in
837
+ * the project. Volume names are the key inside a project, so `Volume.create` refuses
838
+ * to make a second one; catch this, or call `Volume.getOrCreate` instead.
839
+ */
840
+ declare class ConflictError extends LizardError {
841
+ constructor(message?: string);
842
+ }
783
843
  declare class TimeoutError extends LizardError {
784
844
  constructor(message?: string);
785
845
  }
@@ -950,19 +1010,48 @@ interface CreateVolumeOpts extends ConnectionOpts {
950
1010
  }
951
1011
  /**
952
1012
  * A persistent volume that outlives sandboxes.
953
- * Mount it to a sandbox via `Sandbox.create({ volumeId: volume.volumeId })`.
1013
+ *
1014
+ * A volume's **name** is its key inside a project: names are unique per project, and
1015
+ * every method here accepts either a name or the generated id wherever a volume is
1016
+ * addressed. Prefer the name — it is the thing you chose and can reconstruct, while
1017
+ * the id only exists after the first create.
1018
+ *
1019
+ * Names are slugs: lowercase letters, digits and dashes, starting and ending with a
1020
+ * letter or digit, up to 64 characters.
1021
+ *
1022
+ * Mount one to a sandbox via `Sandbox.create({ volumeName: 'my-data' })`.
954
1023
  */
955
1024
  declare class Volume {
1025
+ /** The volume's generated id. Stable, but you rarely need it — address by name. */
956
1026
  readonly volumeId: string;
1027
+ /** The volume's name: unique within its project, and usable anywhere `volumeId` is. */
1028
+ readonly name?: string;
957
1029
  private readonly config;
958
1030
  constructor(opts: {
959
1031
  volumeId: string;
1032
+ name?: string;
960
1033
  } & ConnectionOpts);
1034
+ /**
1035
+ * Create a volume. Throws `ConflictError` (409) if the project already has one with
1036
+ * this name — use {@link getOrCreate} when you want "make sure this exists" instead.
1037
+ */
961
1038
  static create(projectId: string, name: string, opts?: CreateVolumeOpts): Promise<Volume>;
962
- static get(projectId: string, volumeId: string, opts?: ConnectionOpts): Promise<Volume>;
1039
+ /**
1040
+ * Return the project's volume with this name, creating it first if it does not
1041
+ * exist yet. This is the reason a volume's name is its key: an agent that wants
1042
+ * "the scratch disk for this task" no longer has to store an id between runs.
1043
+ *
1044
+ * An existing volume is returned as-is — `sizeGb` applies only to a fresh create
1045
+ * and never resizes one that is already there.
1046
+ */
1047
+ static getOrCreate(projectId: string, name: string, opts?: CreateVolumeOpts): Promise<Volume>;
1048
+ /** Look a volume up by name or by id. */
1049
+ static get(projectId: string, nameOrId: string, opts?: ConnectionOpts): Promise<Volume>;
963
1050
  static list(projectId: string, opts?: ConnectionOpts): Promise<VolumeInfo[]>;
1051
+ /** Delete a volume by name or by id, without constructing one first. */
1052
+ static delete(projectId: string, nameOrId: string, opts?: ConnectionOpts): Promise<void>;
964
1053
  getInfo(projectId: string): Promise<VolumeInfo>;
965
1054
  delete(projectId: string): Promise<void>;
966
1055
  }
967
1056
 
968
- export { type Addon, type AddonType, AuthenticationError, type CodeContext, CodeSandbox, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type DeployEvent, DeployHandle, type DomainInfo, Execution, ExecutionError, type FileInfo, type FsOpts, Lizard, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, NotFoundError, type ProcessOpts, type ProcessResult, type Project, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type ScaleOpts, type Secret, type Service, type ServiceMetrics, type SetSecretOpts, TimeoutError, Volume, type VolumeInfo, resolveProjectId };
1057
+ export { type Addon, type AddonType, AuthenticationError, type CodeContext, CodeSandbox, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type DeployEvent, DeployHandle, type DomainInfo, Execution, ExecutionError, type FileInfo, type FsOpts, Lizard, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, NotFoundError, type ProcessOpts, type ProcessResult, type Project, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type ScaleOpts, type Secret, type Service, type ServiceMetrics, type SetSecretOpts, TimeoutError, Volume, type VolumeInfo, resolveProjectId };
package/dist/index.d.ts CHANGED
@@ -70,8 +70,12 @@ interface FsOpts {
70
70
  interface FileInfo {
71
71
  name: string;
72
72
  path: string;
73
- type: 'file' | 'dir';
73
+ type: 'file' | 'dir' | 'symlink';
74
74
  size: number;
75
+ /** Permission bits as a string, e.g. `-rw-r--r--`. Absent on older sandboxes. */
76
+ mode?: string;
77
+ /** Last modification time, unix milliseconds. Absent on older sandboxes. */
78
+ modTime?: number;
75
79
  }
76
80
  /**
77
81
  * Read and write files inside a Lizard sandbox microVM.
@@ -126,6 +130,35 @@ declare class Fs {
126
130
  * Create a directory (and any missing parents) inside the microVM.
127
131
  */
128
132
  makeDir(path: string, opts?: FsOpts): Promise<void>;
133
+ /**
134
+ * Metadata for a single path — size, type, permissions, modification time.
135
+ *
136
+ * Saves listing a parent directory and filtering it just to answer "does this
137
+ * exist, and how big is it".
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * const info = await sandbox.fs.stat('/app/out.bin')
142
+ * console.log(info.size, info.type)
143
+ * ```
144
+ *
145
+ * Throws `NotFoundError` if the path does not exist. Sandboxes created before
146
+ * this shipped run a guest agent without it and throw `LizardError` (501) —
147
+ * recreate the sandbox to use it.
148
+ */
149
+ stat(path: string, opts?: FsOpts): Promise<FileInfo>;
150
+ /**
151
+ * Move or rename a path. Creates the destination's parent directories.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
156
+ * ```
157
+ *
158
+ * Sandboxes created before this shipped run a guest agent without it and throw
159
+ * `LizardError` (501) — recreate the sandbox to use it.
160
+ */
161
+ move(from: string, to: string): Promise<void>;
129
162
  private execInternal;
130
163
  }
131
164
 
@@ -150,7 +183,14 @@ interface SandboxOpts extends ConnectionOpts {
150
183
  metadata?: Record<string, string>;
151
184
  envs?: Record<string, string>;
152
185
  timeoutMs?: number;
186
+ /** Attach a persistent volume by id, mounted at `/data`. */
153
187
  volumeId?: string;
188
+ /**
189
+ * Attach a persistent volume by name, mounted at `/data`. A volume's name is its
190
+ * key inside a project, so this is usually what you want — see {@link Volume}.
191
+ * Requires the sandbox's project to be given as an exact `projectId`.
192
+ */
193
+ volumeName?: string;
154
194
  }
155
195
  /**
156
196
  * Low-level HTTP client for the Lizard sandbox API.
@@ -293,9 +333,16 @@ declare class Sandbox extends SandboxClient {
293
333
  * nothing is written to disk, so a paused sandbox does not survive a host
294
334
  * failure. Resume with `sandbox.resume()` or `Sandbox.connect(id)`.
295
335
  *
296
- * Pausing does not stop the timeout: a paused sandbox is still deleted at
297
- * its original expiry (default 5 minutes). Pass `timeoutMs: 0` at create
298
- * time to opt out of expiry.
336
+ * Pausing does not buy you time past the original deadline. Resuming pushes the
337
+ * expiry forward by however long the sandbox was paused — so a pause/resume cycle
338
+ * costs no runtime — but that adjustment only happens on `resume()`. A sandbox left
339
+ * paused past its original `expiresAt` is deleted there, within a minute, and
340
+ * `resume()` then returns `false`.
341
+ *
342
+ * In other words: pause and come back *before* the original deadline and you lose
343
+ * nothing; leave it paused across the deadline and it is gone. Pass `timeoutMs: 0`
344
+ * at create time to opt out of expiry entirely, which is the only way to park a
345
+ * sandbox indefinitely.
299
346
  *
300
347
  * @returns `true` if paused successfully.
301
348
  */
@@ -303,7 +350,12 @@ declare class Sandbox extends SandboxClient {
303
350
  /**
304
351
  * Resume a paused sandbox by unfreezing its vCPUs.
305
352
  *
306
- * @returns `true` if resumed successfully.
353
+ * The expiry is pushed forward by the time spent paused, so the timeout measures
354
+ * running time rather than wall-clock — provided the original deadline had not
355
+ * already passed while paused. See {@link pause}.
356
+ *
357
+ * @returns `true` if resumed successfully, `false` if the sandbox no longer exists
358
+ * (including because it expired while paused).
307
359
  */
308
360
  resume(opts?: ConnectionOpts): Promise<boolean>;
309
361
  /**
@@ -780,6 +832,14 @@ declare class AuthenticationError extends LizardError {
780
832
  declare class NotFoundError extends LizardError {
781
833
  constructor(message?: string);
782
834
  }
835
+ /**
836
+ * The resource already exists — most often a volume whose name is already taken in
837
+ * the project. Volume names are the key inside a project, so `Volume.create` refuses
838
+ * to make a second one; catch this, or call `Volume.getOrCreate` instead.
839
+ */
840
+ declare class ConflictError extends LizardError {
841
+ constructor(message?: string);
842
+ }
783
843
  declare class TimeoutError extends LizardError {
784
844
  constructor(message?: string);
785
845
  }
@@ -950,19 +1010,48 @@ interface CreateVolumeOpts extends ConnectionOpts {
950
1010
  }
951
1011
  /**
952
1012
  * A persistent volume that outlives sandboxes.
953
- * Mount it to a sandbox via `Sandbox.create({ volumeId: volume.volumeId })`.
1013
+ *
1014
+ * A volume's **name** is its key inside a project: names are unique per project, and
1015
+ * every method here accepts either a name or the generated id wherever a volume is
1016
+ * addressed. Prefer the name — it is the thing you chose and can reconstruct, while
1017
+ * the id only exists after the first create.
1018
+ *
1019
+ * Names are slugs: lowercase letters, digits and dashes, starting and ending with a
1020
+ * letter or digit, up to 64 characters.
1021
+ *
1022
+ * Mount one to a sandbox via `Sandbox.create({ volumeName: 'my-data' })`.
954
1023
  */
955
1024
  declare class Volume {
1025
+ /** The volume's generated id. Stable, but you rarely need it — address by name. */
956
1026
  readonly volumeId: string;
1027
+ /** The volume's name: unique within its project, and usable anywhere `volumeId` is. */
1028
+ readonly name?: string;
957
1029
  private readonly config;
958
1030
  constructor(opts: {
959
1031
  volumeId: string;
1032
+ name?: string;
960
1033
  } & ConnectionOpts);
1034
+ /**
1035
+ * Create a volume. Throws `ConflictError` (409) if the project already has one with
1036
+ * this name — use {@link getOrCreate} when you want "make sure this exists" instead.
1037
+ */
961
1038
  static create(projectId: string, name: string, opts?: CreateVolumeOpts): Promise<Volume>;
962
- static get(projectId: string, volumeId: string, opts?: ConnectionOpts): Promise<Volume>;
1039
+ /**
1040
+ * Return the project's volume with this name, creating it first if it does not
1041
+ * exist yet. This is the reason a volume's name is its key: an agent that wants
1042
+ * "the scratch disk for this task" no longer has to store an id between runs.
1043
+ *
1044
+ * An existing volume is returned as-is — `sizeGb` applies only to a fresh create
1045
+ * and never resizes one that is already there.
1046
+ */
1047
+ static getOrCreate(projectId: string, name: string, opts?: CreateVolumeOpts): Promise<Volume>;
1048
+ /** Look a volume up by name or by id. */
1049
+ static get(projectId: string, nameOrId: string, opts?: ConnectionOpts): Promise<Volume>;
963
1050
  static list(projectId: string, opts?: ConnectionOpts): Promise<VolumeInfo[]>;
1051
+ /** Delete a volume by name or by id, without constructing one first. */
1052
+ static delete(projectId: string, nameOrId: string, opts?: ConnectionOpts): Promise<void>;
964
1053
  getInfo(projectId: string): Promise<VolumeInfo>;
965
1054
  delete(projectId: string): Promise<void>;
966
1055
  }
967
1056
 
968
- export { type Addon, type AddonType, AuthenticationError, type CodeContext, CodeSandbox, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type DeployEvent, DeployHandle, type DomainInfo, Execution, ExecutionError, type FileInfo, type FsOpts, Lizard, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, NotFoundError, type ProcessOpts, type ProcessResult, type Project, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type ScaleOpts, type Secret, type Service, type ServiceMetrics, type SetSecretOpts, TimeoutError, Volume, type VolumeInfo, resolveProjectId };
1057
+ export { type Addon, type AddonType, AuthenticationError, type CodeContext, CodeSandbox, ConflictError, type ConnectionOpts, type CostMetrics, type CreateAddonOpts, type CreateContextOpts, type CreateProjectOpts, type CreateServiceOpts, type CreateVolumeOpts, type DeployEvent, DeployHandle, type DomainInfo, Execution, ExecutionError, type FileInfo, type FsOpts, Lizard, LizardError, type LizardOpts, type LogLine, type MetricPoint, type MetricRange, NotFoundError, type ProcessOpts, type ProcessResult, type Project, type RunCodeLanguage, type RunCodeOpts, Sandbox, type SandboxInfo, type SandboxOpts, type ScaleOpts, type Secret, type Service, type ServiceMetrics, type SetSecretOpts, TimeoutError, Volume, type VolumeInfo, resolveProjectId };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthenticationError: () => AuthenticationError,
24
24
  CodeSandbox: () => CodeSandbox,
25
+ ConflictError: () => ConflictError,
25
26
  DeployHandle: () => DeployHandle,
26
27
  Lizard: () => Lizard,
27
28
  LizardError: () => LizardError,
@@ -74,6 +75,12 @@ var NotFoundError = class extends LizardError {
74
75
  this.name = "NotFoundError";
75
76
  }
76
77
  };
78
+ var ConflictError = class extends LizardError {
79
+ constructor(message = "Resource already exists") {
80
+ super(message);
81
+ this.name = "ConflictError";
82
+ }
83
+ };
77
84
  var TimeoutError = class extends LizardError {
78
85
  constructor(message = "Sandbox operation timed out") {
79
86
  super(message);
@@ -90,6 +97,7 @@ async function handleApiError(res) {
90
97
  }
91
98
  if (res.status === 401 || res.status === 403) throw new AuthenticationError(message);
92
99
  if (res.status === 404) throw new NotFoundError(message);
100
+ if (res.status === 409) throw new ConflictError(message);
93
101
  if (res.status === 408 || res.status === 504) throw new TimeoutError(message);
94
102
  throw new LizardError(`API error ${res.status}: ${message}`);
95
103
  }
@@ -253,6 +261,49 @@ var Fs = class {
253
261
  async makeDir(path, opts) {
254
262
  await this.execInternal(`mkdir -p ${JSON.stringify(path)}`, opts?.user);
255
263
  }
264
+ /**
265
+ * Metadata for a single path — size, type, permissions, modification time.
266
+ *
267
+ * Saves listing a parent directory and filtering it just to answer "does this
268
+ * exist, and how big is it".
269
+ *
270
+ * @example
271
+ * ```ts
272
+ * const info = await sandbox.fs.stat('/app/out.bin')
273
+ * console.log(info.size, info.type)
274
+ * ```
275
+ *
276
+ * Throws `NotFoundError` if the path does not exist. Sandboxes created before
277
+ * this shipped run a guest agent without it and throw `LizardError` (501) —
278
+ * recreate the sandbox to use it.
279
+ */
280
+ async stat(path, opts) {
281
+ const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/stat`);
282
+ url.searchParams.set("path", path);
283
+ if (opts?.user) url.searchParams.set("user", opts.user);
284
+ const res = await fetch(url.toString(), { headers: this.config.headers });
285
+ if (!res.ok) await handleApiError(res);
286
+ return res.json();
287
+ }
288
+ /**
289
+ * Move or rename a path. Creates the destination's parent directories.
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
294
+ * ```
295
+ *
296
+ * Sandboxes created before this shipped run a guest agent without it and throw
297
+ * `LizardError` (501) — recreate the sandbox to use it.
298
+ */
299
+ async move(from, to) {
300
+ const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/move`, {
301
+ method: "POST",
302
+ headers: this.config.headers,
303
+ body: JSON.stringify({ from, to })
304
+ });
305
+ if (!res.ok) await handleApiError(res);
306
+ }
256
307
  async execInternal(cmd, user) {
257
308
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
258
309
  method: "POST",
@@ -278,7 +329,8 @@ var SandboxClient = class {
278
329
  projectId,
279
330
  metadata: opts?.metadata,
280
331
  envs: opts?.envs,
281
- volumeId: opts?.volumeId
332
+ volumeId: opts?.volumeId,
333
+ volumeName: opts?.volumeName
282
334
  })
283
335
  });
284
336
  if (!res.ok) await handleApiError(res);
@@ -407,9 +459,16 @@ var Sandbox = class extends SandboxClient {
407
459
  * nothing is written to disk, so a paused sandbox does not survive a host
408
460
  * failure. Resume with `sandbox.resume()` or `Sandbox.connect(id)`.
409
461
  *
410
- * Pausing does not stop the timeout: a paused sandbox is still deleted at
411
- * its original expiry (default 5 minutes). Pass `timeoutMs: 0` at create
412
- * time to opt out of expiry.
462
+ * Pausing does not buy you time past the original deadline. Resuming pushes the
463
+ * expiry forward by however long the sandbox was paused — so a pause/resume cycle
464
+ * costs no runtime — but that adjustment only happens on `resume()`. A sandbox left
465
+ * paused past its original `expiresAt` is deleted there, within a minute, and
466
+ * `resume()` then returns `false`.
467
+ *
468
+ * In other words: pause and come back *before* the original deadline and you lose
469
+ * nothing; leave it paused across the deadline and it is gone. Pass `timeoutMs: 0`
470
+ * at create time to opt out of expiry entirely, which is the only way to park a
471
+ * sandbox indefinitely.
413
472
  *
414
473
  * @returns `true` if paused successfully.
415
474
  */
@@ -419,7 +478,12 @@ var Sandbox = class extends SandboxClient {
419
478
  /**
420
479
  * Resume a paused sandbox by unfreezing its vCPUs.
421
480
  *
422
- * @returns `true` if resumed successfully.
481
+ * The expiry is pushed forward by the time spent paused, so the timeout measures
482
+ * running time rather than wall-clock — provided the original deadline had not
483
+ * already passed while paused. See {@link pause}.
484
+ *
485
+ * @returns `true` if resumed successfully, `false` if the sandbox no longer exists
486
+ * (including because it expired while paused).
423
487
  */
424
488
  async resume(opts) {
425
489
  return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
@@ -1111,8 +1175,13 @@ async function* readLines(body) {
1111
1175
  var Volume = class _Volume {
1112
1176
  constructor(opts) {
1113
1177
  this.volumeId = opts.volumeId;
1178
+ this.name = opts.name;
1114
1179
  this.config = new ConnectionConfig(opts);
1115
1180
  }
1181
+ /**
1182
+ * Create a volume. Throws `ConflictError` (409) if the project already has one with
1183
+ * this name — use {@link getOrCreate} when you want "make sure this exists" instead.
1184
+ */
1116
1185
  static async create(projectId, name, opts) {
1117
1186
  const config = new ConnectionConfig(opts);
1118
1187
  const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
@@ -1122,15 +1191,36 @@ var Volume = class _Volume {
1122
1191
  });
1123
1192
  if (!res.ok) await handleApiError(res);
1124
1193
  const vol = await res.json();
1125
- return new _Volume({ volumeId: vol.id, ...opts });
1194
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1126
1195
  }
1127
- static async get(projectId, volumeId, opts) {
1196
+ /**
1197
+ * Return the project's volume with this name, creating it first if it does not
1198
+ * exist yet. This is the reason a volume's name is its key: an agent that wants
1199
+ * "the scratch disk for this task" no longer has to store an id between runs.
1200
+ *
1201
+ * An existing volume is returned as-is — `sizeGb` applies only to a fresh create
1202
+ * and never resizes one that is already there.
1203
+ */
1204
+ static async getOrCreate(projectId, name, opts) {
1128
1205
  const config = new ConnectionConfig(opts);
1129
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${volumeId}`, {
1206
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1207
+ method: "POST",
1208
+ headers: config.headers,
1209
+ body: JSON.stringify({ name, sizeGb: opts?.sizeGb ?? 5, getOrCreate: true })
1210
+ });
1211
+ if (!res.ok) await handleApiError(res);
1212
+ const vol = await res.json();
1213
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1214
+ }
1215
+ /** Look a volume up by name or by id. */
1216
+ static async get(projectId, nameOrId, opts) {
1217
+ const config = new ConnectionConfig(opts);
1218
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1130
1219
  headers: config.headers
1131
1220
  });
1132
1221
  if (!res.ok) await handleApiError(res);
1133
- return new _Volume({ volumeId, ...opts });
1222
+ const vol = await res.json();
1223
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1134
1224
  }
1135
1225
  static async list(projectId, opts) {
1136
1226
  const config = new ConnectionConfig(opts);
@@ -1140,6 +1230,15 @@ var Volume = class _Volume {
1140
1230
  if (!res.ok) await handleApiError(res);
1141
1231
  return res.json();
1142
1232
  }
1233
+ /** Delete a volume by name or by id, without constructing one first. */
1234
+ static async delete(projectId, nameOrId, opts) {
1235
+ const config = new ConnectionConfig(opts);
1236
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1237
+ method: "DELETE",
1238
+ headers: config.headers
1239
+ });
1240
+ if (!res.ok) await handleApiError(res);
1241
+ }
1143
1242
  async getInfo(projectId) {
1144
1243
  const res = await fetch(`${this.config.apiUrl}/api/projects/${projectId}/volumes/${this.volumeId}`, {
1145
1244
  headers: this.config.headers
@@ -1159,6 +1258,7 @@ var Volume = class _Volume {
1159
1258
  0 && (module.exports = {
1160
1259
  AuthenticationError,
1161
1260
  CodeSandbox,
1261
+ ConflictError,
1162
1262
  DeployHandle,
1163
1263
  Lizard,
1164
1264
  LizardError,
package/dist/index.mjs CHANGED
@@ -39,6 +39,12 @@ var NotFoundError = class extends LizardError {
39
39
  this.name = "NotFoundError";
40
40
  }
41
41
  };
42
+ var ConflictError = class extends LizardError {
43
+ constructor(message = "Resource already exists") {
44
+ super(message);
45
+ this.name = "ConflictError";
46
+ }
47
+ };
42
48
  var TimeoutError = class extends LizardError {
43
49
  constructor(message = "Sandbox operation timed out") {
44
50
  super(message);
@@ -55,6 +61,7 @@ async function handleApiError(res) {
55
61
  }
56
62
  if (res.status === 401 || res.status === 403) throw new AuthenticationError(message);
57
63
  if (res.status === 404) throw new NotFoundError(message);
64
+ if (res.status === 409) throw new ConflictError(message);
58
65
  if (res.status === 408 || res.status === 504) throw new TimeoutError(message);
59
66
  throw new LizardError(`API error ${res.status}: ${message}`);
60
67
  }
@@ -218,6 +225,49 @@ var Fs = class {
218
225
  async makeDir(path, opts) {
219
226
  await this.execInternal(`mkdir -p ${JSON.stringify(path)}`, opts?.user);
220
227
  }
228
+ /**
229
+ * Metadata for a single path — size, type, permissions, modification time.
230
+ *
231
+ * Saves listing a parent directory and filtering it just to answer "does this
232
+ * exist, and how big is it".
233
+ *
234
+ * @example
235
+ * ```ts
236
+ * const info = await sandbox.fs.stat('/app/out.bin')
237
+ * console.log(info.size, info.type)
238
+ * ```
239
+ *
240
+ * Throws `NotFoundError` if the path does not exist. Sandboxes created before
241
+ * this shipped run a guest agent without it and throw `LizardError` (501) —
242
+ * recreate the sandbox to use it.
243
+ */
244
+ async stat(path, opts) {
245
+ const url = new URL(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/stat`);
246
+ url.searchParams.set("path", path);
247
+ if (opts?.user) url.searchParams.set("user", opts.user);
248
+ const res = await fetch(url.toString(), { headers: this.config.headers });
249
+ if (!res.ok) await handleApiError(res);
250
+ return res.json();
251
+ }
252
+ /**
253
+ * Move or rename a path. Creates the destination's parent directories.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * await sandbox.fs.move('/tmp/build.log', '/app/logs/build.log')
258
+ * ```
259
+ *
260
+ * Sandboxes created before this shipped run a guest agent without it and throw
261
+ * `LizardError` (501) — recreate the sandbox to use it.
262
+ */
263
+ async move(from, to) {
264
+ const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/files/move`, {
265
+ method: "POST",
266
+ headers: this.config.headers,
267
+ body: JSON.stringify({ from, to })
268
+ });
269
+ if (!res.ok) await handleApiError(res);
270
+ }
221
271
  async execInternal(cmd, user) {
222
272
  const res = await fetch(`${this.config.apiUrl}/api/sandboxes/${this.sandboxId}/exec`, {
223
273
  method: "POST",
@@ -243,7 +293,8 @@ var SandboxClient = class {
243
293
  projectId,
244
294
  metadata: opts?.metadata,
245
295
  envs: opts?.envs,
246
- volumeId: opts?.volumeId
296
+ volumeId: opts?.volumeId,
297
+ volumeName: opts?.volumeName
247
298
  })
248
299
  });
249
300
  if (!res.ok) await handleApiError(res);
@@ -372,9 +423,16 @@ var Sandbox = class extends SandboxClient {
372
423
  * nothing is written to disk, so a paused sandbox does not survive a host
373
424
  * failure. Resume with `sandbox.resume()` or `Sandbox.connect(id)`.
374
425
  *
375
- * Pausing does not stop the timeout: a paused sandbox is still deleted at
376
- * its original expiry (default 5 minutes). Pass `timeoutMs: 0` at create
377
- * time to opt out of expiry.
426
+ * Pausing does not buy you time past the original deadline. Resuming pushes the
427
+ * expiry forward by however long the sandbox was paused — so a pause/resume cycle
428
+ * costs no runtime — but that adjustment only happens on `resume()`. A sandbox left
429
+ * paused past its original `expiresAt` is deleted there, within a minute, and
430
+ * `resume()` then returns `false`.
431
+ *
432
+ * In other words: pause and come back *before* the original deadline and you lose
433
+ * nothing; leave it paused across the deadline and it is gone. Pass `timeoutMs: 0`
434
+ * at create time to opt out of expiry entirely, which is the only way to park a
435
+ * sandbox indefinitely.
378
436
  *
379
437
  * @returns `true` if paused successfully.
380
438
  */
@@ -384,7 +442,12 @@ var Sandbox = class extends SandboxClient {
384
442
  /**
385
443
  * Resume a paused sandbox by unfreezing its vCPUs.
386
444
  *
387
- * @returns `true` if resumed successfully.
445
+ * The expiry is pushed forward by the time spent paused, so the timeout measures
446
+ * running time rather than wall-clock — provided the original deadline had not
447
+ * already passed while paused. See {@link pause}.
448
+ *
449
+ * @returns `true` if resumed successfully, `false` if the sandbox no longer exists
450
+ * (including because it expired while paused).
388
451
  */
389
452
  async resume(opts) {
390
453
  return SandboxClient.resumeSandbox(this.sandboxId, this.resolveOpts(opts));
@@ -1076,8 +1139,13 @@ async function* readLines(body) {
1076
1139
  var Volume = class _Volume {
1077
1140
  constructor(opts) {
1078
1141
  this.volumeId = opts.volumeId;
1142
+ this.name = opts.name;
1079
1143
  this.config = new ConnectionConfig(opts);
1080
1144
  }
1145
+ /**
1146
+ * Create a volume. Throws `ConflictError` (409) if the project already has one with
1147
+ * this name — use {@link getOrCreate} when you want "make sure this exists" instead.
1148
+ */
1081
1149
  static async create(projectId, name, opts) {
1082
1150
  const config = new ConnectionConfig(opts);
1083
1151
  const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
@@ -1087,15 +1155,36 @@ var Volume = class _Volume {
1087
1155
  });
1088
1156
  if (!res.ok) await handleApiError(res);
1089
1157
  const vol = await res.json();
1090
- return new _Volume({ volumeId: vol.id, ...opts });
1158
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1091
1159
  }
1092
- static async get(projectId, volumeId, opts) {
1160
+ /**
1161
+ * Return the project's volume with this name, creating it first if it does not
1162
+ * exist yet. This is the reason a volume's name is its key: an agent that wants
1163
+ * "the scratch disk for this task" no longer has to store an id between runs.
1164
+ *
1165
+ * An existing volume is returned as-is — `sizeGb` applies only to a fresh create
1166
+ * and never resizes one that is already there.
1167
+ */
1168
+ static async getOrCreate(projectId, name, opts) {
1093
1169
  const config = new ConnectionConfig(opts);
1094
- const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${volumeId}`, {
1170
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes`, {
1171
+ method: "POST",
1172
+ headers: config.headers,
1173
+ body: JSON.stringify({ name, sizeGb: opts?.sizeGb ?? 5, getOrCreate: true })
1174
+ });
1175
+ if (!res.ok) await handleApiError(res);
1176
+ const vol = await res.json();
1177
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1178
+ }
1179
+ /** Look a volume up by name or by id. */
1180
+ static async get(projectId, nameOrId, opts) {
1181
+ const config = new ConnectionConfig(opts);
1182
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1095
1183
  headers: config.headers
1096
1184
  });
1097
1185
  if (!res.ok) await handleApiError(res);
1098
- return new _Volume({ volumeId, ...opts });
1186
+ const vol = await res.json();
1187
+ return new _Volume({ volumeId: vol.id, name: vol.name, ...opts });
1099
1188
  }
1100
1189
  static async list(projectId, opts) {
1101
1190
  const config = new ConnectionConfig(opts);
@@ -1105,6 +1194,15 @@ var Volume = class _Volume {
1105
1194
  if (!res.ok) await handleApiError(res);
1106
1195
  return res.json();
1107
1196
  }
1197
+ /** Delete a volume by name or by id, without constructing one first. */
1198
+ static async delete(projectId, nameOrId, opts) {
1199
+ const config = new ConnectionConfig(opts);
1200
+ const res = await fetch(`${config.apiUrl}/api/projects/${projectId}/volumes/${encodeURIComponent(nameOrId)}`, {
1201
+ method: "DELETE",
1202
+ headers: config.headers
1203
+ });
1204
+ if (!res.ok) await handleApiError(res);
1205
+ }
1108
1206
  async getInfo(projectId) {
1109
1207
  const res = await fetch(`${this.config.apiUrl}/api/projects/${projectId}/volumes/${this.volumeId}`, {
1110
1208
  headers: this.config.headers
@@ -1123,6 +1221,7 @@ var Volume = class _Volume {
1123
1221
  export {
1124
1222
  AuthenticationError,
1125
1223
  CodeSandbox,
1224
+ ConflictError,
1126
1225
  DeployHandle,
1127
1226
  Lizard,
1128
1227
  LizardError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lizard-build/sdk",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
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",