@hatchet-dev/sandbox 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Eac
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.5] - 2026-11-11
10
+
11
+ ### Added
12
+
13
+ - `@hatchet-dev/sandbox/wire` exports the Zod schemas and types of the sandbox task protocol, `turnInput`, `turnResult`, `commandOutput`, `sandboxInfo`, `resources` and `metadata`. It imports nothing but `zod`, so code that spawns the sandbox task itself, such as a serverless durable task on an edge runtime where the main entry cannot load, can share the wire types instead of writing its own.
14
+
15
+ ### Changed
16
+
17
+ - Every option the SDK accepts is now checked with Zod before a task is sent. Unknown option keys, an empty sandbox ID, path, or command, a non-positive `idleTimeoutMs`, `timeoutMs`, or `depth`, a sandbox `id` outside lowercase letters, digits, and hyphens, `inheritParentMetadata` without `from`, and a `download` or `upload` URL that is not https are rejected with a `ZodError`. The `resources` and `delete` checks that already existed throw a `ZodError` instead of a plain `Error`. Provider responses are checked the same way, so a malformed result fails the call instead of surfacing later as a missing field. `zod` 4 is a dependency of the package.
18
+
19
+ ## [0.1.4] - 2026-11-09
20
+
21
+ ### Fixed
22
+
23
+ - `resources.memory` and `resources.disk` strings without a unit were read as MiB, so a byte count such as `'1073741824'` asked for a billion MiB. A string now has to carry a unit (`Ki`, `Mi`, `Gi`, `Ti`, `K`, `M`, `G`, `T`) and is rejected otherwise. Pass a number for MiB.
24
+
9
25
  ## [0.1.3] - 2026-11-09
10
26
 
11
27
  ### Added
package/README.md CHANGED
@@ -86,16 +86,16 @@ const sandbox = await hatchet.sandbox.create({ provider: 'e2b' });
86
86
 
87
87
  Options for `create`:
88
88
 
89
- | Option | What it does |
90
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
- | `provider` | Which sandbox provider to create on, by its name. Needed when the tenant has several; a fork always uses its snapshot's. |
92
- | `id` | Pick your own sandbox ID. Lowercase letters, digits, and hyphens. It must not exist yet. |
93
- | `template` | The image or template, in the provider's own terms. Defaults to the provider's default. |
94
- | `from` | Start from this snapshot. Create two sandboxes from the same snapshot and you have a fork. |
95
- | `metadata` | Tags you can search for with `list`. |
96
- | `inheritParentMetadata` | With `from`, copy the tags of the sandbox that made the snapshot onto the new one. Tags given in `metadata` take precedence. |
97
- | `idleTimeoutMs` | Stop the sandbox after this long with no calls. The provider sets the maximum. |
98
- | `resources` | Machine size as `{ vcpu, memory, disk }`. `memory` and `disk` take MiB or a quantity like `'4Gi'`. Providers that cannot honour a field fail the create with their own message. |
89
+ | Option | What it does |
90
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `provider` | Which sandbox provider to create on, by its name. Needed when the tenant has several; a fork always uses its snapshot's. |
92
+ | `id` | Pick your own sandbox ID. Lowercase letters, digits, and hyphens. It must not exist yet. |
93
+ | `template` | The image or template, in the provider's own terms. Defaults to the provider's default. |
94
+ | `from` | Start from this snapshot. Create two sandboxes from the same snapshot and you have a fork. |
95
+ | `metadata` | Tags you can search for with `list`. |
96
+ | `inheritParentMetadata` | With `from`, copy the tags of the sandbox that made the snapshot onto the new one. Tags given in `metadata` take precedence. |
97
+ | `idleTimeoutMs` | Stop the sandbox after this long with no calls. The provider sets the maximum. |
98
+ | `resources` | Machine size as `{ vcpu, memory, disk }`. `memory` and `disk` take a number of MiB or a quantity with a unit like `'4Gi'`. Providers that cannot honour a field fail the create with their own message. |
99
99
 
100
100
  ### `Sandbox`
101
101
 
package/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var v1 = require('@hatchet-dev/typescript-sdk/v1');
4
+ var zod = require('zod');
4
5
  var fs = require('fs/promises');
5
6
 
6
7
  function _interopNamespace(e) {
@@ -32,10 +33,40 @@ var FilesystemError = class extends Error {
32
33
  }
33
34
  code;
34
35
  };
35
- var toEntry = (e) => ({
36
- ...e,
37
- modifiedTime: e.modifiedTime ? new Date(e.modifiedTime) : void 0
36
+ var path = zod.z.string().min(1);
37
+ var positiveInt = zod.z.int().positive();
38
+ var httpsUrl = zod.z.url({ protocol: /^https$/ });
39
+ var fileData = zod.z.union([
40
+ zod.z.string(),
41
+ zod.z.instanceof(Uint8Array),
42
+ zod.z.instanceof(ArrayBuffer),
43
+ zod.z.instanceof(Blob)
44
+ ]);
45
+ var writeEntries = zod.z.strictObject({ path, data: fileData }).array();
46
+ var readOpts = zod.z.strictObject({ format: zod.z.enum(["text", "bytes", "blob"]).optional() });
47
+ var listOpts = zod.z.strictObject({ depth: positiveInt.optional() });
48
+ var transferOpts = zod.z.strictObject({ timeoutMs: positiveInt.optional() });
49
+ var entry = zod.z.looseObject({
50
+ name: zod.z.string(),
51
+ path: zod.z.string(),
52
+ type: zod.z.enum(["file", "dir"]),
53
+ size: zod.z.number(),
54
+ mode: zod.z.number(),
55
+ permissions: zod.z.string(),
56
+ modifiedTime: zod.z.coerce.date().optional()
38
57
  });
58
+ var fsOutput = zod.z.looseObject({
59
+ content: zod.z.string().optional(),
60
+ stagedKey: zod.z.string().optional(),
61
+ url: zod.z.string().optional(),
62
+ putUrl: zod.z.string().optional(),
63
+ getUrl: zod.z.string().optional(),
64
+ entry: entry.nullish(),
65
+ entries: entry.array().optional(),
66
+ created: zod.z.boolean().optional(),
67
+ error: zod.z.object({ code: zod.z.string(), message: zod.z.string() }).optional()
68
+ });
69
+ var fsOutputWithEntry = fsOutput.extend({ entry });
39
70
  var STAGE_OVER_BYTES = 2 << 20;
40
71
  async function toBytes(data) {
41
72
  if (typeof data === "string") return Buffer.from(data, "utf8");
@@ -71,16 +102,18 @@ var Files = class {
71
102
  this.turn = turn;
72
103
  }
73
104
  turn;
74
- async call(fn, input, fresh = false) {
75
- const out = await this.turn(`$fs.${fn}`, input, fresh);
76
- if (out.error) throw new FilesystemError(out.error.code, out.error.message);
77
- return out;
78
- }
79
- async read(path, opts = {}) {
80
- const out = await this.call("read", { path });
105
+ async call(fn, input, fresh = false, schema = fsOutput) {
106
+ const out = fsOutput.parse(await this.turn(`$fs.${fn}`, input, fresh));
107
+ if (out.error)
108
+ throw new FilesystemError(out.error.code, out.error.message);
109
+ return schema.parse(out);
110
+ }
111
+ async read(p, opts = {}) {
112
+ const { format } = readOpts.parse(opts);
113
+ const out = await this.call("read", { path: path.parse(p) });
81
114
  const bytes = out.url ? await this.readStaged(out.url, out.stagedKey) : Buffer.from(out.content ?? "", "base64");
82
- if (opts.format === "bytes") return new Uint8Array(bytes);
83
- if (opts.format === "blob") return new Blob([new Uint8Array(bytes)]);
115
+ if (format === "bytes") return new Uint8Array(bytes);
116
+ if (format === "blob") return new Blob([new Uint8Array(bytes)]);
84
117
  return bytes.toString("utf8");
85
118
  }
86
119
  /** A replayed durable read may hold a URL that has expired; the provider signs the same staged file again. The renewal is a plain run, not a durable child, so the replayed child sequence stays aligned. */
@@ -95,11 +128,12 @@ var Files = class {
95
128
  }
96
129
  async write(pathOrFiles, data) {
97
130
  if (typeof pathOrFiles !== "string") return this.writeFiles(pathOrFiles);
98
- const [entry] = await this.writeFiles([{ path: pathOrFiles, data: data ?? "" }]);
99
- return entry;
131
+ const [entry2] = await this.writeFiles([{ path: pathOrFiles, data: data ?? "" }]);
132
+ return entry2;
100
133
  }
101
134
  /** Writes several files in one task. */
102
135
  async writeFiles(files) {
136
+ writeEntries.parse(files);
103
137
  const bytes = await Promise.all(files.map((f) => toBytes(f.data)));
104
138
  const encoded = [];
105
139
  for (const [i, f] of files.entries()) {
@@ -119,58 +153,70 @@ var Files = class {
119
153
  }
120
154
  }
121
155
  const { entries = [] } = await this.call("write", { files: encoded });
122
- return entries.map(toEntry);
156
+ return entries;
123
157
  }
124
158
  /** Lists a directory. `depth` 1 (default) is the directory itself; higher values recurse. No snapshot is made. */
125
- async list(path = ".", opts = {}) {
126
- const { entries = [] } = await this.call("list", { path, depth: opts.depth ?? 1 });
127
- return entries.map(toEntry);
159
+ async list(p = ".", opts = {}) {
160
+ const { depth = 1 } = listOpts.parse(opts);
161
+ const { entries = [] } = await this.call("list", { path: path.parse(p), depth });
162
+ return entries;
128
163
  }
129
164
  /** True if the path exists. No snapshot is made. */
130
- async exists(path) {
131
- const { entry } = await this.call("stat", { path });
132
- return entry != null;
165
+ async exists(p) {
166
+ const { entry: entry2 } = await this.call("stat", { path: path.parse(p) });
167
+ return entry2 != null;
133
168
  }
134
169
  /** Metadata of a file or directory. Throws `FilesystemError` with code `not_found` if absent. */
135
- async getInfo(path) {
136
- const { entry } = await this.call("stat", { path });
137
- if (!entry) throw new FilesystemError("not_found", `${path} does not exist`);
138
- return toEntry(entry);
170
+ async getInfo(p) {
171
+ const { entry: entry2 } = await this.call("stat", { path: path.parse(p) });
172
+ if (!entry2) throw new FilesystemError("not_found", `${p} does not exist`);
173
+ return entry2;
139
174
  }
140
175
  /** Creates a directory and its parents. Returns false if it already existed. */
141
- async makeDir(path) {
142
- const { created } = await this.call("mkdir", { path });
176
+ async makeDir(p) {
177
+ const { created } = await this.call("mkdir", { path: path.parse(p) });
143
178
  return created === true;
144
179
  }
145
180
  /** Renames or moves a file or directory. */
146
181
  async rename(oldPath, newPath) {
147
- const { entry } = await this.call("rename", { path: oldPath, newPath });
148
- return toEntry(entry);
182
+ const { entry: entry2 } = await this.call(
183
+ "rename",
184
+ { path: path.parse(oldPath), newPath: path.parse(newPath) },
185
+ false,
186
+ fsOutputWithEntry
187
+ );
188
+ return entry2;
149
189
  }
150
190
  /** Copies a file or directory tree. */
151
191
  async copy(srcPath, dstPath) {
152
- const { entry } = await this.call("copy", { path: srcPath, newPath: dstPath });
153
- return toEntry(entry);
192
+ const { entry: entry2 } = await this.call(
193
+ "copy",
194
+ { path: path.parse(srcPath), newPath: path.parse(dstPath) },
195
+ false,
196
+ fsOutputWithEntry
197
+ );
198
+ return entry2;
154
199
  }
155
200
  /** Deletes a file or directory tree. */
156
- async remove(path) {
157
- await this.call("remove", { path });
201
+ async remove(p) {
202
+ await this.call("remove", { path: path.parse(p) });
158
203
  }
159
204
  /** The provider fetches an https URL into the workspace. The sandbox needs no network. Private and loopback addresses are refused. */
160
- async download(url, path, opts = {}) {
161
- const { entry } = await this.call("download", {
162
- url,
163
- path,
164
- ...opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
165
- });
166
- return toEntry(entry);
205
+ async download(url, p, opts = {}) {
206
+ const { entry: entry2 } = await this.call(
207
+ "download",
208
+ { url: httpsUrl.parse(url), path: path.parse(p), ...transferOpts.parse(opts) },
209
+ false,
210
+ fsOutputWithEntry
211
+ );
212
+ return entry2;
167
213
  }
168
214
  /** The provider sends a workspace file to an https URL with HTTP PUT, for example a presigned URL. */
169
- async upload(path, url, opts = {}) {
215
+ async upload(p, url, opts = {}) {
170
216
  await this.call("upload", {
171
- path,
172
- url,
173
- ...opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
217
+ path: path.parse(p),
218
+ url: httpsUrl.parse(url),
219
+ ...transferOpts.parse(opts)
174
220
  });
175
221
  }
176
222
  };
@@ -192,6 +238,43 @@ async function setupTelemetry() {
192
238
  console.error(`sandbox: opentelemetry disabled: ${err.message}`);
193
239
  }
194
240
  }
241
+ var positiveInt2 = zod.z.int().positive();
242
+ var metadata = zod.z.record(zod.z.string(), zod.z.string());
243
+ var resources = zod.z.strictObject({
244
+ vcpu: positiveInt2.optional(),
245
+ memoryMib: positiveInt2.optional(),
246
+ diskMib: positiveInt2.optional()
247
+ });
248
+ var turnInput = zod.z.strictObject({
249
+ fn: zod.z.string().min(1),
250
+ input: zod.z.record(zod.z.string(), zod.z.unknown()),
251
+ sandboxId: zod.z.string().min(1).optional(),
252
+ image: zod.z.string().min(1).optional(),
253
+ snapshotId: zod.z.string().min(1).optional(),
254
+ metadata: metadata.optional(),
255
+ idleTtlMs: positiveInt2.optional(),
256
+ inheritMetadata: zod.z.boolean().optional(),
257
+ resources: resources.optional()
258
+ });
259
+ var turnResult = zod.z.looseObject({
260
+ output: zod.z.looseObject({}),
261
+ sandboxId: zod.z.string(),
262
+ snapshotId: zod.z.string().optional(),
263
+ timings: zod.z.record(zod.z.string(), zod.z.number()).optional()
264
+ });
265
+ var commandOutput = zod.z.looseObject({
266
+ stdout: zod.z.string(),
267
+ stderr: zod.z.string(),
268
+ exitCode: zod.z.int()
269
+ });
270
+ var sandboxInfo = zod.z.looseObject({
271
+ sandboxId: zod.z.string(),
272
+ metadata: metadata.optional(),
273
+ live: zod.z.boolean(),
274
+ latestSnapshot: zod.z.string().optional(),
275
+ createdAt: zod.z.coerce.date(),
276
+ lastUsed: zod.z.coerce.date().optional()
277
+ });
195
278
 
196
279
  // src/sandbox.ts
197
280
  var TASK_NAMES = ["hatchet-sandbox", "brickie-sandbox"];
@@ -229,6 +312,11 @@ function resolveTaskName(client) {
229
312
  }
230
313
  return pending;
231
314
  }
315
+ var positiveInt3 = zod.z.int().positive();
316
+ var nonEmpty = zod.z.string().min(1);
317
+ var commandResult = commandOutput.extend(
318
+ turnResult.pick({ snapshotId: true, timings: true }).shape
319
+ );
232
320
  var UNITS = {
233
321
  K: 1e3,
234
322
  M: 1e6,
@@ -239,38 +327,55 @@ var UNITS = {
239
327
  Gi: 2 ** 30,
240
328
  Ti: 2 ** 40
241
329
  };
242
- function toMib(value, field) {
243
- if (typeof value === "number") {
244
- if (!Number.isInteger(value) || value <= 0)
245
- throw new Error(`resources.${field}: expected a positive whole number of MiB`);
246
- return value;
247
- }
248
- const m = /^(\d+(?:\.\d+)?)\s*([KMGT]i?)?B?$/.exec(value.trim());
249
- if (!m)
250
- throw new Error(
251
- `resources.${field}: expected a quantity like '512Mi', '4Gi' or '8G', got ${JSON.stringify(value)}`
252
- );
253
- const bytes = Number(m[1]) * (m[2] ? UNITS[m[2]] : UNITS.Mi);
254
- const mib = Math.ceil(bytes / UNITS.Mi);
255
- if (mib <= 0) throw new Error(`resources.${field}: must be at least 1Mi`);
256
- return mib;
257
- }
258
- function wireResources(r) {
259
- const out = {};
260
- if (r.vcpu !== void 0) {
261
- if (!Number.isInteger(r.vcpu) || r.vcpu <= 0)
262
- throw new Error("resources.vcpu: expected a positive whole number of cores");
263
- out.vcpu = r.vcpu;
264
- }
265
- if (r.memory !== void 0) out.memoryMib = toMib(r.memory, "memory");
266
- if (r.disk !== void 0) out.diskMib = toMib(r.disk, "disk");
267
- if (Object.keys(out).length === 0)
268
- throw new Error("resources: set at least one of vcpu, memory, disk");
269
- return out;
270
- }
330
+ var QUANTITY = /^(\d+(?:\.\d+)?)\s*([KMGT]i?)B?$/;
331
+ var mib = zod.z.union(
332
+ [
333
+ positiveInt3,
334
+ zod.z.string().trim().regex(QUANTITY).transform((s) => {
335
+ const [, n, unit] = QUANTITY.exec(s);
336
+ return Math.ceil(Number(n) * UNITS[unit] / UNITS.Mi);
337
+ }).pipe(zod.z.int().min(1))
338
+ ],
339
+ {
340
+ error: "expected a positive whole number of MiB or a quantity with a unit like '512Mi', '4Gi' or '8G'"
341
+ }
342
+ );
343
+ var resources2 = zod.z.strictObject({
344
+ vcpu: positiveInt3.optional(),
345
+ memory: mib.optional(),
346
+ disk: mib.optional()
347
+ }).refine(
348
+ (r) => r.vcpu !== void 0 || r.memory !== void 0 || r.disk !== void 0,
349
+ "set at least one of vcpu, memory, disk"
350
+ ).transform(({ vcpu, memory, disk }) => ({ vcpu, memoryMib: memory, diskMib: disk }));
351
+ var sandboxOpts = zod.z.strictObject({
352
+ id: zod.z.string().regex(/^[a-z0-9-]+$/, "lowercase letters, digits, and hyphens only").optional(),
353
+ template: nonEmpty.optional(),
354
+ from: nonEmpty.optional(),
355
+ metadata: metadata.optional(),
356
+ idleTimeoutMs: positiveInt3.optional(),
357
+ inheritParentMetadata: zod.z.boolean().optional(),
358
+ provider: nonEmpty.optional(),
359
+ resources: resources2.optional()
360
+ }).refine((o) => !o.inheritParentMetadata || o.from !== void 0, {
361
+ error: "inheritParentMetadata needs from",
362
+ path: ["inheritParentMetadata"]
363
+ });
364
+ var execOpts = zod.z.strictObject({ timeoutMs: positiveInt3.optional() });
365
+ var connectOpts = zod.z.strictObject({ template: nonEmpty.optional() });
366
+ var listOpts2 = zod.z.strictObject({
367
+ sandboxId: nonEmpty.optional(),
368
+ metadata: metadata.optional()
369
+ });
370
+ var deleteOpts = listOpts2.refine(
371
+ (o) => o.sandboxId !== void 0 || Object.keys(o.metadata ?? {}).length > 0,
372
+ "delete needs a sandboxId or a metadata filter"
373
+ );
271
374
  function turnRunner(client) {
272
375
  const resolved = resolveTaskName(client);
273
- return async (input, opts) => {
376
+ return async (raw, opts) => {
377
+ const input = turnInput.parse(raw);
378
+ const payload = input;
274
379
  const name = await resolved;
275
380
  const additionalMetadata = {
276
381
  "hatchet.sandbox.fn": input.fn,
@@ -278,10 +383,10 @@ function turnRunner(client) {
278
383
  ...input.image ? { "hatchet.sandbox.image": input.image } : {},
279
384
  ...input.snapshotId ? { "hatchet.sandbox.from": input.snapshotId } : {}
280
385
  };
281
- const result = opts?.fresh ? await (await client.admin.runWorkflow(name, input, {
386
+ const result = opts?.fresh ? await (await client.admin.runWorkflow(name, payload, {
282
387
  additionalMetadata
283
- })).result() : await client.workflow({ name }).run(input, { additionalMetadata });
284
- return result[name] ?? result;
388
+ })).result() : await client.workflow({ name }).run(payload, { additionalMetadata });
389
+ return turnResult.parse(result[name] ?? result);
285
390
  };
286
391
  }
287
392
  var Sandbox = class _Sandbox {
@@ -298,54 +403,40 @@ var Sandbox = class _Sandbox {
298
403
  files;
299
404
  /** Starts a new sandbox. Prefer `hatchet.sandbox.create`. */
300
405
  static async create(client, opts = {}) {
406
+ const o = sandboxOpts.parse(opts);
301
407
  await setupTelemetry();
302
408
  const run = turnRunner(client);
303
- const metadata = opts.provider ? { ...opts.metadata, provider: opts.provider } : opts.metadata;
304
409
  const created = await run({
305
410
  fn: "$create",
306
411
  input: {},
307
- ...opts.id ? { sandboxId: opts.id } : {},
308
- ...opts.template ? { image: opts.template } : {},
309
- ...opts.from ? { snapshotId: opts.from } : {},
310
- ...metadata ? { metadata } : {},
311
- ...opts.idleTimeoutMs ? { idleTtlMs: opts.idleTimeoutMs } : {},
312
- ...opts.inheritParentMetadata ? { inheritMetadata: true } : {},
313
- ...opts.resources ? { resources: wireResources(opts.resources) } : {}
412
+ sandboxId: o.id,
413
+ image: o.template,
414
+ snapshotId: o.from,
415
+ metadata: o.provider ? { ...o.metadata, provider: o.provider } : o.metadata,
416
+ idleTtlMs: o.idleTimeoutMs,
417
+ inheritMetadata: o.inheritParentMetadata,
418
+ resources: o.resources
314
419
  });
315
- return new _Sandbox(run, created.sandboxId, opts.template);
420
+ return new _Sandbox(run, created.sandboxId, o.template);
316
421
  }
317
422
  /** Attaches to a sandbox by ID. Prefer `hatchet.sandbox.connect`. */
318
423
  static async connect(client, sandboxId, opts = {}) {
424
+ const id = nonEmpty.parse(sandboxId);
425
+ const o = connectOpts.parse(opts);
319
426
  await setupTelemetry();
320
427
  const run = turnRunner(client);
321
- await run({
322
- fn: "$connect",
323
- input: {},
324
- sandboxId,
325
- ...opts.template ? { image: opts.template } : {}
326
- });
327
- return new _Sandbox(run, sandboxId, opts.template);
428
+ await run({ fn: "$connect", input: {}, sandboxId: id, image: o.template });
429
+ return new _Sandbox(run, id, o.template);
328
430
  }
329
431
  /** Lists sandboxes on the provider. Prefer `hatchet.sandbox.list`. */
330
432
  static async list(client, opts = {}) {
331
- const result = await turnRunner(client)({
332
- fn: "$list",
333
- input: {
334
- ...opts.sandboxId ? { sandboxId: opts.sandboxId } : {},
335
- ...opts.metadata ? { metadata: opts.metadata } : {}
336
- }
337
- });
338
- const raw = result.output.sandboxes ?? [];
339
- return raw.map((s) => ({
340
- ...s,
341
- createdAt: new Date(s.createdAt),
342
- lastUsed: s.lastUsed ? new Date(s.lastUsed) : void 0
343
- }));
433
+ const o = listOpts2.parse(opts);
434
+ const result = await turnRunner(client)({ fn: "$list", input: o });
435
+ return sandboxInfo.array().parse(result.output.sandboxes ?? []);
344
436
  }
345
437
  /** Deletes every sandbox matching the filter, with all their snapshots and files. Prefer `hatchet.sandbox.delete`. */
346
438
  static async delete(client, opts) {
347
- if (!opts.sandboxId && Object.keys(opts.metadata ?? {}).length === 0)
348
- throw new Error("delete needs a sandboxId or a metadata filter");
439
+ deleteOpts.parse(opts);
349
440
  const run = turnRunner(client);
350
441
  const ids = [];
351
442
  for (const { sandboxId } of await _Sandbox.list(client, opts)) {
@@ -356,10 +447,8 @@ var Sandbox = class _Sandbox {
356
447
  }
357
448
  /** Runs a shell command with `sh -c` in the workspace and makes a snapshot. A timeout gives `exitCode` 124, not an error. */
358
449
  async exec(cmd, opts = {}) {
359
- return this.turn("$command", {
360
- cmd,
361
- ...opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
362
- });
450
+ const input = { cmd: nonEmpty.parse(cmd), ...execOpts.parse(opts) };
451
+ return commandResult.parse(await this.turn("$command", input));
363
452
  }
364
453
  /** Removes the sandbox. Its snapshots stay; `connect` can bring it back. */
365
454
  async kill() {
@@ -371,12 +460,7 @@ var Sandbox = class _Sandbox {
371
460
  }
372
461
  async turn(fn, input, fresh = false) {
373
462
  const result = await this.run(
374
- {
375
- sandboxId: this.sandboxId,
376
- ...this.template ? { image: this.template } : {},
377
- fn,
378
- input
379
- },
463
+ { sandboxId: this.sandboxId, image: this.template, fn, input },
380
464
  { fresh }
381
465
  );
382
466
  return {