@sandbar-sdk/cli 0.0.0 → 0.4.0

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.
Files changed (3) hide show
  1. package/README.md +145 -2
  2. package/dist/main.js +2663 -0
  3. package/package.json +38 -7
package/dist/main.js ADDED
@@ -0,0 +1,2663 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+ // package.json
6
+ var package_default = {
7
+ name: "@sandbar-sdk/cli",
8
+ version: "0.4.0",
9
+ private: false,
10
+ type: "module",
11
+ description: "Agent-first command line for creating and managing Sandbar sandboxes.",
12
+ bin: {
13
+ sandbar: "./dist/main.js"
14
+ },
15
+ files: [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ engines: {
21
+ node: ">=20"
22
+ },
23
+ scripts: {
24
+ check: "tsc -p tsconfig.json",
25
+ build: "rm -rf dist && bun build src/main.ts --target node --format esm --outdir dist --external sandbar-sdk --external @sandbar-sdk/adapter --external zod"
26
+ },
27
+ dependencies: {
28
+ "@sandbar-sdk/adapter": "workspace:*",
29
+ "sandbar-sdk": "workspace:*",
30
+ zod: "4.6.5"
31
+ },
32
+ devDependencies: {
33
+ "@sandbar-sdk/boxd": "workspace:*",
34
+ "@sandbar-sdk/daytona": "workspace:*",
35
+ "@sandbar-sdk/e2b": "workspace:*",
36
+ "@sandbar-sdk/modal": "workspace:*",
37
+ "@sandbar-sdk/vercel": "workspace:*",
38
+ "@sandbar/provider-fake": "workspace:*"
39
+ },
40
+ license: "Apache-2.0",
41
+ repository: {
42
+ type: "git",
43
+ url: "git+https://github.com/pandemicsyn/sandbar.git",
44
+ directory: "packages/cli"
45
+ },
46
+ bugs: {
47
+ url: "https://github.com/pandemicsyn/sandbar/issues"
48
+ },
49
+ publishConfig: {
50
+ access: "public"
51
+ }
52
+ };
53
+
54
+ // src/commands.ts
55
+ import { readFile as readFile4, stat as stat2, writeFile as writeFile2 } from "node:fs/promises";
56
+ import { basename, join as join4, resolve as resolve3 } from "node:path";
57
+ import {
58
+ AdapterSandbox,
59
+ Image as Image2,
60
+ NonzeroExitError,
61
+ ResourceReference as ResourceReference2,
62
+ SandbarError as SandbarError3
63
+ } from "sandbar-sdk";
64
+ import { z as z4 } from "zod";
65
+
66
+ // src/config.ts
67
+ import { readFile } from "node:fs/promises";
68
+ import { homedir } from "node:os";
69
+ import { dirname, join, resolve } from "node:path";
70
+ import { z } from "zod";
71
+
72
+ // src/output.ts
73
+ import {
74
+ OutcomeUnknownError,
75
+ SandbarError
76
+ } from "sandbar-sdk";
77
+
78
+ class CliError extends Error {
79
+ code;
80
+ hint;
81
+ known;
82
+ outcome;
83
+ details;
84
+ constructor(code, message, hint, known, outcome = code === "OUTCOME_UNKNOWN" ? "unknown" : "rejected", details) {
85
+ super(message);
86
+ this.code = code;
87
+ this.hint = hint;
88
+ this.known = known;
89
+ this.outcome = outcome;
90
+ this.details = details;
91
+ this.name = "CliError";
92
+ }
93
+ }
94
+ function usage(message, hint) {
95
+ return new CliError("USAGE", message, hint);
96
+ }
97
+ var retryableCodes = new Set(["RATE_LIMIT", "CAPACITY", "UNAVAILABLE"]);
98
+ var uncertainEffects = new Set(["possible", "unknown", "partial", "applied"]);
99
+ function describeError(cause) {
100
+ if (cause instanceof CliError)
101
+ return {
102
+ code: cause.code,
103
+ message: cause.message,
104
+ retryable: false,
105
+ outcome: cause.outcome,
106
+ hint: cause.hint,
107
+ known: cause.known,
108
+ details: cause.details
109
+ };
110
+ if (cause instanceof SandbarError) {
111
+ const partial = cause.outcome?.status === "partial";
112
+ let known;
113
+ if (cause.outcome && "sandbox" in cause.outcome && cause.outcome.sandbox)
114
+ known = { reference: cause.outcome.sandbox };
115
+ if (cause.reference)
116
+ known = { ...known, recovery: cause.reference };
117
+ const outcome = partial ? "partial" : cause instanceof OutcomeUnknownError || uncertainEffects.has(cause.effect) ? "unknown" : "rejected";
118
+ return {
119
+ code: partial ? "PARTIAL" : cause.code,
120
+ message: cause.message,
121
+ retryable: outcome === "rejected" && retryableCodes.has(cause.code),
122
+ outcome,
123
+ known
124
+ };
125
+ }
126
+ const local = cause instanceof Error && "syscall" in cause && "code" in cause;
127
+ if (local)
128
+ return {
129
+ code: "LOCAL_IO",
130
+ message: cause.message,
131
+ retryable: false,
132
+ outcome: "rejected"
133
+ };
134
+ return {
135
+ code: "INTERNAL",
136
+ message: cause instanceof Error ? cause.message : String(cause),
137
+ retryable: false,
138
+ outcome: "unknown"
139
+ };
140
+ }
141
+ var managementExitCodes = new Map([
142
+ ["USAGE", 2],
143
+ ["LOCAL_IO", 2],
144
+ ["INVALID_ARGUMENT", 2],
145
+ ["NOT_FOUND", 3],
146
+ ["UNSUPPORTED", 4],
147
+ ["ADAPTER_NOT_INSTALLED", 4],
148
+ ["ADAPTER_LOAD_FAILED", 4],
149
+ ["LIMIT_REACHED", 4],
150
+ ["SECRET_TOOL_MISSING", 4],
151
+ ["SECRET_UNAVAILABLE", 5],
152
+ ["UNAUTHENTICATED", 5],
153
+ ["FORBIDDEN", 5]
154
+ ]);
155
+ function managementExitCode(report) {
156
+ if (report.outcome === "partial")
157
+ return 8;
158
+ if (report.outcome === "unknown")
159
+ return report.code === "INTERNAL" ? 1 : 6;
160
+ if (report.retryable)
161
+ return 7;
162
+ return managementExitCodes.get(report.code) ?? 1;
163
+ }
164
+ function commandExitCode() {
165
+ return 125;
166
+ }
167
+
168
+ class Output {
169
+ json;
170
+ streams;
171
+ constructor(json, streams) {
172
+ this.json = json;
173
+ this.streams = streams;
174
+ }
175
+ success(data, human) {
176
+ if (this.json)
177
+ this.streams.stdout(`${JSON.stringify({ ok: true, data })}
178
+ `);
179
+ else if (human)
180
+ this.streams.stdout(human.endsWith(`
181
+ `) ? human : `${human}
182
+ `);
183
+ }
184
+ failure(report) {
185
+ if (this.json) {
186
+ this.streams.stdout(`${JSON.stringify({ ok: false, error: report })}
187
+ `);
188
+ return;
189
+ }
190
+ this.streams.stderr(`sandbar: ${report.message}
191
+ `);
192
+ if (report.hint)
193
+ this.streams.stderr(`hint: ${report.hint}
194
+ `);
195
+ if (report.outcome === "unknown" && report.code !== "INTERNAL")
196
+ this.streams.stderr(`The operation may have taken effect; inspect before retrying (sandbar ls, sandbar inspect).
197
+ `);
198
+ if (report.known?.reference)
199
+ this.streams.stderr(`known sandbox reference: ${JSON.stringify(report.known.reference)}
200
+ `);
201
+ else if (report.known?.recovery)
202
+ this.streams.stderr(`operation recovery reference: ${JSON.stringify(report.known.recovery)}
203
+ `);
204
+ for (const item of report.details?.removed ?? [])
205
+ this.streams.stdout(`${item.name}
206
+ `);
207
+ for (const item of report.details?.skipped ?? [])
208
+ this.streams.stderr(`skipped ${item.name}: ${item.reason ?? item.result}
209
+ `);
210
+ for (const item of report.details?.failed ?? [])
211
+ this.streams.stderr(`failed ${item.name}: ${item.code} ${item.reason ?? ""}
212
+ `);
213
+ }
214
+ note(text) {
215
+ this.streams.stderr(`${text}
216
+ `);
217
+ }
218
+ }
219
+
220
+ // src/config.ts
221
+ var ProfileConfig = z.strictObject({
222
+ provider: z.string().regex(/^[a-z0-9-]{1,64}$/).optional(),
223
+ module: z.string().min(1).max(1024).optional(),
224
+ export: z.string().min(1).max(128).optional(),
225
+ config: z.record(z.string(), z.json()).optional(),
226
+ credentials: z.record(z.string(), z.string()).optional()
227
+ }).refine((profile) => profile.provider || profile.module, {
228
+ message: "A profile needs a provider or a module"
229
+ });
230
+ var CliConfig = z.strictObject({
231
+ default: z.string().min(1).optional(),
232
+ profiles: z.record(z.string(), ProfileConfig).optional(),
233
+ state: z.strictObject({ kind: z.literal("fs"), dir: z.string().min(1).optional() }).optional(),
234
+ limits: z.strictObject({ maxSandboxes: z.number().int().positive().nullable().optional() }).optional(),
235
+ images: z.strictObject({ autoBuild: z.boolean().optional() }).optional(),
236
+ adapters: z.strictObject({ searchProject: z.boolean().optional() }).optional()
237
+ });
238
+ function configHome(env) {
239
+ return join(env.XDG_CONFIG_HOME || join(env.HOME || homedir(), ".config"), "sandbar");
240
+ }
241
+ function expandHome(path, env) {
242
+ return path === "~" || path.startsWith("~/") ? join(env.HOME || homedir(), path.slice(1)) : path;
243
+ }
244
+ async function loadConfig(explicitPath, env) {
245
+ const chosen = explicitPath ?? env.SANDBAR_CONFIG;
246
+ const path = resolve(expandHome(chosen ?? join(configHome(env), "sandbar.json"), env));
247
+ let text;
248
+ try {
249
+ text = await readFile(path, "utf8");
250
+ } catch (cause) {
251
+ if (!chosen && cause instanceof Error && "code" in cause && cause.code === "ENOENT")
252
+ return { path, dir: dirname(path), exists: false, value: {} };
253
+ throw usage(`Cannot read config ${path}: ${cause instanceof Error ? cause.message : cause}`);
254
+ }
255
+ let json;
256
+ try {
257
+ json = z.json().parse(JSON.parse(text));
258
+ } catch {
259
+ throw usage(`Config ${path} is not valid JSON`);
260
+ }
261
+ const parsed = CliConfig.safeParse(json);
262
+ if (!parsed.success)
263
+ throw usage(`Config ${path} is invalid: ${z.prettifyError(parsed.error).replaceAll(`
264
+ `, "; ")}`);
265
+ return { path, dir: dirname(path), exists: true, value: parsed.data };
266
+ }
267
+ function stateDir(flag, config, env, cwd) {
268
+ const given = flag ?? env.SANDBAR_STATE_DIR;
269
+ if (given)
270
+ return resolve(cwd, expandHome(given, env));
271
+ const configured = config.value.state?.dir;
272
+ if (configured)
273
+ return resolve(config.dir, expandHome(configured, env));
274
+ return join(configHome(env), "state");
275
+ }
276
+ function searchProjectAdapters(config, env) {
277
+ const fromEnv = env.SANDBAR_PROJECT_ADAPTERS;
278
+ if (fromEnv !== undefined && fromEnv !== "")
279
+ return fromEnv === "1" || fromEnv === "true";
280
+ return config.value.adapters?.searchProject ?? false;
281
+ }
282
+ function maxSandboxes(config, env) {
283
+ const fromEnv = env.SANDBAR_MAX_SANDBOXES;
284
+ if (fromEnv !== undefined && fromEnv !== "") {
285
+ const parsed = z.coerce.number().int().positive().safeParse(fromEnv);
286
+ if (!parsed.success)
287
+ throw usage("SANDBAR_MAX_SANDBOXES must be a positive integer");
288
+ return parsed.data;
289
+ }
290
+ return config.value.limits?.maxSandboxes ?? null;
291
+ }
292
+ var EnvReference = z.string().regex(/^env:[A-Za-z_][A-Za-z0-9_]*$/);
293
+ function valueAvailable(value, env) {
294
+ const reference = EnvReference.safeParse(value);
295
+ return !reference.success || Boolean(env[reference.data.slice(4)]);
296
+ }
297
+ function resolveEnvReferences(value, env, where) {
298
+ const reference = EnvReference.safeParse(value);
299
+ if (reference.success) {
300
+ const name = reference.data.slice(4);
301
+ const found = env[name];
302
+ if (found === undefined || found === "")
303
+ throw new CliError("USAGE", `${where} needs environment variable ${name}`);
304
+ return found;
305
+ }
306
+ if (Array.isArray(value))
307
+ return value.map((item) => resolveEnvReferences(item, env, where));
308
+ const record = z.record(z.string(), z.json()).safeParse(value);
309
+ if (record.success)
310
+ return Object.fromEntries(Object.entries(record.data).map(([key, item]) => [
311
+ key,
312
+ resolveEnvReferences(item, env, `${where}.${key}`)
313
+ ]));
314
+ return value;
315
+ }
316
+
317
+ // src/images.ts
318
+ import { randomBytes as randomBytes2 } from "node:crypto";
319
+ import {
320
+ Image,
321
+ SandbarError as SandbarError2
322
+ } from "sandbar-sdk";
323
+
324
+ // src/store.ts
325
+ import { createHash, randomBytes } from "node:crypto";
326
+ import { link, mkdir, readFile as readFile2, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
327
+ import { join as join2 } from "node:path";
328
+ import { ResourceReference } from "sandbar-sdk";
329
+ import { z as z2 } from "zod";
330
+ var RecordName = z2.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
331
+ var SandboxRecord = z2.strictObject({
332
+ format: z2.literal(1),
333
+ kind: z2.literal("sandbox"),
334
+ name: RecordName,
335
+ status: z2.enum(["creating", "created", "unknown"]),
336
+ profile: z2.string().min(1),
337
+ provider: z2.string().min(1),
338
+ createdAt: z2.iso.datetime({ offset: true }),
339
+ labels: z2.record(z2.string(), z2.string()),
340
+ image: z2.string().optional(),
341
+ reference: ResourceReference.extend({ kind: z2.literal("sandbox") }).nullable(),
342
+ recovery: z2.custom((value) => z2.record(z2.string(), z2.unknown()).safeParse(value).success).optional()
343
+ });
344
+ var RetainedResource = z2.strictObject({
345
+ kind: z2.string().min(1),
346
+ id: z2.string().min(1),
347
+ ownership: z2.enum(["verified", "unknown"]),
348
+ cleanup: z2.enum(["manual", "provider_expiry", "none_known"])
349
+ });
350
+ var ImageRecord = z2.strictObject({
351
+ format: z2.literal(1),
352
+ kind: z2.literal("image"),
353
+ name: RecordName,
354
+ status: z2.enum(["building", "built", "unknown"]),
355
+ profile: z2.string().min(1),
356
+ provider: z2.string().min(1),
357
+ createdAt: z2.iso.datetime({ offset: true }),
358
+ source: z2.string().min(1),
359
+ prepared: z2.strictObject({
360
+ kind: z2.literal("prepared"),
361
+ value: z2.string().min(1),
362
+ provider: z2.string().min(1),
363
+ scope: ResourceReference.shape.scope
364
+ }).nullable(),
365
+ retainedResources: z2.array(RetainedResource),
366
+ recovery: z2.custom((value) => z2.record(z2.string(), z2.unknown()).safeParse(value).success).optional()
367
+ });
368
+ var schemas = { sandbox: SandboxRecord, image: ImageRecord };
369
+ var folders = { sandbox: "sandboxes", image: "images" };
370
+ function errorCode(cause) {
371
+ return cause instanceof Error && "code" in cause ? String(cause.code) : undefined;
372
+ }
373
+ function versionOf(text) {
374
+ return createHash("sha256").update(text).digest("hex");
375
+ }
376
+ function encode(kind, record) {
377
+ return `${JSON.stringify(schemas[kind].parse(record), null, 2)}
378
+ `;
379
+ }
380
+ var STALE_LOCK_MS = 30000;
381
+ var sleep = (ms) => new Promise((done) => setTimeout(done, ms));
382
+
383
+ class FsStateStore {
384
+ dir;
385
+ constructor(dir) {
386
+ this.dir = dir;
387
+ }
388
+ folder(kind) {
389
+ return join2(this.dir, folders[kind]);
390
+ }
391
+ path(kind, name) {
392
+ return join2(this.folder(kind), `${RecordName.parse(name)}.json`);
393
+ }
394
+ async temporary(kind, name, text) {
395
+ await mkdir(this.folder(kind), { recursive: true });
396
+ const path = `${this.path(kind, name)}.${randomBytes(6).toString("hex")}.tmp`;
397
+ await writeFile(path, text, { flag: "wx" });
398
+ return path;
399
+ }
400
+ async locked(kind, name, action) {
401
+ const lock = `${this.path(kind, name)}.lock`;
402
+ await mkdir(this.folder(kind), { recursive: true });
403
+ for (let attempt = 0;; attempt++) {
404
+ try {
405
+ await writeFile(lock, String(process.pid), { flag: "wx" });
406
+ break;
407
+ } catch (cause) {
408
+ if (errorCode(cause) !== "EEXIST")
409
+ throw cause;
410
+ const held = await stat(lock).catch(() => null);
411
+ if (held && Date.now() - held.mtimeMs > STALE_LOCK_MS)
412
+ await unlink(lock).catch(() => {});
413
+ else if (attempt >= 100)
414
+ throw new CliError("CONFLICT", `Store record ${name} is locked by another process`);
415
+ else
416
+ await sleep(20);
417
+ }
418
+ }
419
+ try {
420
+ return await action();
421
+ } finally {
422
+ await unlink(lock).catch(() => {});
423
+ }
424
+ }
425
+ async get(kind, name) {
426
+ let text;
427
+ try {
428
+ text = await readFile2(this.path(kind, name), "utf8");
429
+ } catch (cause) {
430
+ if (errorCode(cause) === "ENOENT")
431
+ return null;
432
+ throw cause;
433
+ }
434
+ let json;
435
+ try {
436
+ json = z2.json().parse(JSON.parse(text));
437
+ } catch {
438
+ throw new CliError("STORE_INVALID", `Store record ${this.path(kind, name)} is not valid JSON`);
439
+ }
440
+ const parsed = schemas[kind].safeParse(json);
441
+ if (!parsed.success)
442
+ throw new CliError("STORE_INVALID", `Store record ${this.path(kind, name)} is invalid`);
443
+ return { record: parsed.data, version: versionOf(text) };
444
+ }
445
+ async create(kind, name, record) {
446
+ const text = encode(kind, record);
447
+ const temporary = await this.temporary(kind, name, text);
448
+ try {
449
+ await link(temporary, this.path(kind, name));
450
+ } catch (cause) {
451
+ if (errorCode(cause) === "EEXIST")
452
+ throw new CliError("CONFLICT", `Name ${name} is already in use`);
453
+ throw cause;
454
+ } finally {
455
+ await unlink(temporary).catch(() => {});
456
+ }
457
+ return { version: versionOf(text) };
458
+ }
459
+ async update(kind, name, record, version) {
460
+ return this.locked(kind, name, async () => {
461
+ const current = await this.get(kind, name);
462
+ if (!current || current.version !== version)
463
+ throw new CliError("CONFLICT", `Store record ${name} changed concurrently`);
464
+ const text = encode(kind, record);
465
+ await rename(await this.temporary(kind, name, text), this.path(kind, name));
466
+ return { version: versionOf(text) };
467
+ });
468
+ }
469
+ async remove(kind, name, version) {
470
+ await this.locked(kind, name, async () => {
471
+ if (version !== undefined) {
472
+ const current = await this.get(kind, name);
473
+ if (current && current.version !== version)
474
+ throw new CliError("CONFLICT", `Store record ${name} changed concurrently`);
475
+ }
476
+ try {
477
+ await unlink(this.path(kind, name));
478
+ } catch (cause) {
479
+ if (errorCode(cause) !== "ENOENT")
480
+ throw cause;
481
+ }
482
+ });
483
+ }
484
+ async list(kind, cursor, limit = 1000) {
485
+ let files;
486
+ try {
487
+ files = await readdir(this.folder(kind));
488
+ } catch (cause) {
489
+ if (errorCode(cause) === "ENOENT")
490
+ return { names: [] };
491
+ throw cause;
492
+ }
493
+ const names = files.filter((file) => file.endsWith(".json")).map((file) => file.slice(0, -5)).filter((name) => RecordName.safeParse(name).success && (!cursor || name > cursor)).sort();
494
+ const page = names.slice(0, limit);
495
+ return { names: page, cursor: names.length > limit ? page.at(-1) : undefined };
496
+ }
497
+ }
498
+ async function listAll(store, kind, onInvalid) {
499
+ const found = [];
500
+ let cursor;
501
+ do {
502
+ const page = await store.list(kind, cursor);
503
+ for (const name of page.names) {
504
+ try {
505
+ const entry = await store.get(kind, name);
506
+ if (entry)
507
+ found.push(entry);
508
+ } catch (cause) {
509
+ if (!(cause instanceof CliError) || cause.code !== "STORE_INVALID")
510
+ throw cause;
511
+ onInvalid?.(`warning: ${cause.message}; skipped`);
512
+ }
513
+ }
514
+ cursor = page.cursor;
515
+ } while (cursor);
516
+ return found;
517
+ }
518
+ function checkName(name) {
519
+ const parsed = RecordName.safeParse(name);
520
+ if (!parsed.success)
521
+ throw usage(`Invalid name ${name}`, "use 1-63 lowercase letters, digits and dashes, starting with a letter or digit");
522
+ return parsed.data;
523
+ }
524
+ function generatedName() {
525
+ return `sb-${randomBytes(3).toString("hex").slice(0, 5)}`;
526
+ }
527
+
528
+ // src/images.ts
529
+ var BuildResult = ImageRecord.pick({ prepared: true, retainedResources: true }).extend({
530
+ prepared: ImageRecord.shape.prepared.unwrap()
531
+ });
532
+ function derivedName(source) {
533
+ const base = (source.split("/").at(-1) ?? source).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 57).replace(/^-+|-+$/g, "");
534
+ return RecordName.safeParse(base).success ? base : "image";
535
+ }
536
+ async function findBuild(store, profile, source) {
537
+ const entries = await listAll(store, "image");
538
+ return entries.find(({ record }) => record.profile === profile && record.source === source && record.prepared);
539
+ }
540
+ function mergeRetained(previous, next) {
541
+ const seen = new Set(previous.map((item) => `${item.kind}/${item.id}`));
542
+ return [...previous, ...next.filter((item) => !seen.has(`${item.kind}/${item.id}`))];
543
+ }
544
+ async function claim(store, record, requested) {
545
+ const base = requested ? checkName(requested) : derivedName(record.source);
546
+ for (let attempt = 0;; attempt++) {
547
+ const name = attempt === 0 ? base : `${base}-${randomBytes2(2).toString("hex")}`;
548
+ const full = { ...record, name };
549
+ try {
550
+ const { version } = await store.create("image", name, full);
551
+ return { record: full, version };
552
+ } catch (cause) {
553
+ if (!(cause instanceof CliError) || cause.code !== "CONFLICT" || attempt >= 8)
554
+ throw cause;
555
+ if (requested)
556
+ throw new CliError("CONFLICT", `Image name ${name} is already in use`, "choose another --name, or `sandbar image forget` the old record");
557
+ }
558
+ }
559
+ }
560
+ async function buildImage(session, client, profile, source, options) {
561
+ const store = await session.store();
562
+ const existing = await findBuild(store, profile.name, source);
563
+ if (existing && options.name && existing.record.name !== checkName(options.name))
564
+ throw new CliError("CONFLICT", `${source} is already built for profile ${profile.name} as ${existing.record.name}`, "use that name, or `sandbar image forget` it first");
565
+ if (existing && !options.refresh)
566
+ return { entry: existing, built: false };
567
+ if (!(await client.capabilities()).imageBuild)
568
+ throw new CliError("UNSUPPORTED", `${profile.provider} does not build images`, "providers that boot OCI images directly take --image without a build");
569
+ const claimed = existing ?? await claim(store, {
570
+ format: 1,
571
+ kind: "image",
572
+ status: "building",
573
+ profile: profile.name,
574
+ provider: profile.provider,
575
+ createdAt: new Date().toISOString(),
576
+ source,
577
+ prepared: null,
578
+ retainedResources: []
579
+ }, options.name);
580
+ session.ctx.out.note(`building ${source} on ${profile.provider}; this can take a few minutes`);
581
+ let result;
582
+ try {
583
+ result = await client.images.build({ source: { kind: "oci", value: source } }, { signal: session.ctx.signal });
584
+ } catch (cause) {
585
+ const report = describeError(cause);
586
+ if (report.outcome === "rejected") {
587
+ if (!existing)
588
+ await store.remove("image", claimed.record.name, claimed.version);
589
+ throw cause;
590
+ }
591
+ const recovery = report.known?.recovery;
592
+ let kept = `the store keeps ${claimed.record.name}; \`sandbar image inspect ${claimed.record.name}\` checks the build again`;
593
+ if (existing)
594
+ kept = `${existing.record.name} still holds the previous build; the new one may exist on the provider`;
595
+ else
596
+ try {
597
+ await store.update("image", claimed.record.name, { ...claimed.record, status: "unknown", recovery }, claimed.version);
598
+ } catch (storeCause) {
599
+ kept = `the store could not record it (${describeError(storeCause).message}); keep the reference below`;
600
+ }
601
+ throw new CliError("OUTCOME_UNKNOWN", report.message, kept, {
602
+ image: claimed.record.name,
603
+ recovery
604
+ });
605
+ }
606
+ const { recovery: _recovery, ...rest } = claimed.record;
607
+ const record = {
608
+ ...rest,
609
+ status: "built",
610
+ createdAt: new Date().toISOString(),
611
+ prepared: result.prepared,
612
+ retainedResources: mergeRetained(claimed.record.retainedResources, result.retainedResources)
613
+ };
614
+ try {
615
+ const { version } = await store.update("image", record.name, record, claimed.version);
616
+ return { entry: { record, version }, built: true };
617
+ } catch (cause) {
618
+ throw new CliError("STORE_WRITE_FAILED", `Built ${source} as ${result.prepared.value} but could not record it: ${describeError(cause).message}`, "the provider keeps the image; pass it with --prepared, or rebuild once the store is writable", { image: record.name }, "partial");
619
+ }
620
+ }
621
+ async function imageEnvironment(session, client, profile, source, build) {
622
+ if (!(await client.capabilities()).imageBuild)
623
+ return Image.oci(source);
624
+ const store = await session.store();
625
+ const found = await findBuild(store, profile.name, source);
626
+ if (found?.record.prepared)
627
+ return Image.prepared(found.record.prepared);
628
+ const autoBuild = (await session.config()).value.images?.autoBuild === true;
629
+ if (!build && !autoBuild)
630
+ throw new CliError("UNSUPPORTED", `image ${source} is not built for profile ${profile.name}; run: sandbar image build ${source}`, `pass --build to build it during this command, or set images.autoBuild in sandbar.json`);
631
+ const { entry } = await buildImage(session, client, profile, source, {});
632
+ if (!entry.record.prepared)
633
+ throw new Error(`Image record ${entry.record.name} has no build`);
634
+ return Image.prepared(entry.record.prepared);
635
+ }
636
+ function summary(record) {
637
+ return {
638
+ name: record.name,
639
+ source: record.source,
640
+ profile: record.profile,
641
+ provider: record.provider,
642
+ status: record.status,
643
+ createdAt: record.createdAt,
644
+ prepared: record.prepared?.value ?? null,
645
+ retainedResources: record.retainedResources
646
+ };
647
+ }
648
+ function retainedLines(record) {
649
+ return record.retainedResources.map((item) => ` ${item.kind} ${item.id} (cleanup ${item.cleanup}, ownership ${item.ownership})`);
650
+ }
651
+ async function findImage(store, name) {
652
+ const found = await store.get("image", checkName(name));
653
+ if (!found)
654
+ throw new CliError("NOT_FOUND", `No image record named ${name}`, "list them with `sandbar image ls`");
655
+ return found;
656
+ }
657
+ async function recoverBuild(session, store, entry) {
658
+ const { record } = entry;
659
+ if (record.status !== "unknown" || record.recovery?.kind !== "image_build")
660
+ return entry;
661
+ const client = await session.connect(await session.profileNamed(record.profile));
662
+ let value;
663
+ try {
664
+ value = await (await client.recover(record.recovery)).observe();
665
+ } catch (cause) {
666
+ if (cause instanceof SandbarError2 && cause.effect !== "none")
667
+ return entry;
668
+ throw cause;
669
+ }
670
+ const parsed = BuildResult.safeParse(value);
671
+ if (!parsed.success)
672
+ return entry;
673
+ const { recovery: _recovery, ...rest } = record;
674
+ const recovered = { ...rest, status: "built", ...parsed.data };
675
+ const { version } = await store.update("image", record.name, recovered, entry.version);
676
+ session.ctx.out.note(`${record.name}: recovered the uncertain build as ${parsed.data.prepared.value}`);
677
+ return { record: recovered, version };
678
+ }
679
+ async function buildCommand(session, flags, args) {
680
+ if (args.length !== 1)
681
+ throw usage("Expected exactly one OCI image reference", "e.g. sandbar image build python:3.12");
682
+ const profile = await session.profile();
683
+ const client = await session.connect(profile);
684
+ const { entry, built } = await buildImage(session, client, profile, args[0], {
685
+ name: flags.string("name"),
686
+ refresh: flags.bool("refresh")
687
+ });
688
+ const { record } = entry;
689
+ session.ctx.out.success({ ...summary(record), built }, built ? record.name : `${record.name} (already built; pass --refresh to build ${record.source} again)`);
690
+ return 0;
691
+ }
692
+ async function lsImages(session) {
693
+ const store = await session.store();
694
+ const entries = await listAll(store, "image", (message) => session.ctx.out.note(message));
695
+ const rows = entries.map(({ record }) => summary(record));
696
+ const lines = rows.map((row) => [row.name.padEnd(20), row.profile.padEnd(12), row.status.padEnd(9), row.source].join(" "));
697
+ session.ctx.out.success({ images: rows }, rows.length ? [
698
+ `${"NAME".padEnd(20)} ${"PROFILE".padEnd(12)} ${"STATUS".padEnd(9)} SOURCE`,
699
+ ...lines
700
+ ].join(`
701
+ `) : "no image records; build one with `sandbar image build <oci-ref>`");
702
+ return 0;
703
+ }
704
+ async function inspectImage(session, args) {
705
+ if (args.length !== 1)
706
+ throw usage("Expected exactly one image name");
707
+ const store = await session.store();
708
+ const { record } = await recoverBuild(session, store, await findImage(store, args[0]));
709
+ const lines = [
710
+ `name ${record.name}`,
711
+ `source ${record.source}`,
712
+ `provider ${record.provider} (profile ${record.profile})`,
713
+ `status ${record.status}`,
714
+ `prepared ${record.prepared?.value ?? "none"}`,
715
+ `built ${record.createdAt}`,
716
+ ...record.retainedResources.length ? ["retained", ...retainedLines(record)] : []
717
+ ];
718
+ session.ctx.out.success({ ...summary(record), preparedImage: record.prepared, recovery: record.recovery ?? null }, lines.join(`
719
+ `));
720
+ return 0;
721
+ }
722
+ async function forgetImage(session, args) {
723
+ if (args.length !== 1)
724
+ throw usage("Expected exactly one image name");
725
+ const store = await session.store();
726
+ const { record, version } = await findImage(store, args[0]);
727
+ await store.remove("image", record.name, version);
728
+ const lines = [`forgot ${record.name}`];
729
+ if (record.retainedResources.length)
730
+ lines.push(`${record.provider} still holds these; delete them with the provider's own tools if you no longer need them:`, ...retainedLines(record));
731
+ session.ctx.out.success({ name: record.name, result: "forgotten", retainedResources: record.retainedResources }, lines.join(`
732
+ `));
733
+ return 0;
734
+ }
735
+ var imageActions = ["build", "ls", "inspect", "forget"];
736
+ async function imageCommand(session, flags) {
737
+ const [action, ...args] = flags.positionals;
738
+ if ((flags.string("name") || flags.bool("refresh")) && action !== "build")
739
+ throw usage("--name and --refresh only apply to `sandbar image build`");
740
+ switch (action) {
741
+ case "build":
742
+ return buildCommand(session, flags, args);
743
+ case "ls":
744
+ if (args.length)
745
+ throw usage("image ls takes no arguments");
746
+ return lsImages(session);
747
+ case "inspect":
748
+ return inspectImage(session, args);
749
+ case "forget":
750
+ return forgetImage(session, args);
751
+ default:
752
+ throw usage(action ? `Unknown image command ${action}` : "Expected an image command", `use one of: ${imageActions.join(", ")}`);
753
+ }
754
+ }
755
+
756
+ // src/providers.ts
757
+ import { access, readFile as readFile3 } from "node:fs/promises";
758
+ import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "node:path";
759
+ import { fileURLToPath, pathToFileURL } from "node:url";
760
+ import {
761
+ bindAdapter
762
+ } from "@sandbar-sdk/adapter";
763
+ import { Sandbar } from "sandbar-sdk";
764
+ import { z as z3 } from "zod";
765
+ var providers = [
766
+ {
767
+ id: "daytona",
768
+ module: "@sandbar-sdk/daytona",
769
+ export: "daytonaAdapter",
770
+ experimental: false,
771
+ credentials: { apiKey: "DAYTONA_API_KEY" },
772
+ config: { target: { env: "DAYTONA_TARGET", required: true } }
773
+ },
774
+ {
775
+ id: "e2b",
776
+ module: "@sandbar-sdk/e2b",
777
+ export: "e2bAdapter",
778
+ experimental: false,
779
+ credentials: { apiKey: "E2B_API_KEY" },
780
+ config: {}
781
+ },
782
+ {
783
+ id: "boxd",
784
+ module: "@sandbar-sdk/boxd",
785
+ export: "boxdAdapter",
786
+ experimental: true,
787
+ credentials: { apiKey: "BOXD_API_KEY" },
788
+ config: {
789
+ org: { env: "BOXD_ORG", required: true },
790
+ network: { env: "BOXD_NETWORK", required: true }
791
+ }
792
+ },
793
+ {
794
+ id: "vercel",
795
+ module: "@sandbar-sdk/vercel",
796
+ export: "vercel",
797
+ experimental: false,
798
+ credentials: { token: "VERCEL_TOKEN" },
799
+ config: {
800
+ teamId: { env: "VERCEL_TEAM_ID", required: true },
801
+ projectId: { env: "VERCEL_PROJECT_ID", required: true }
802
+ }
803
+ },
804
+ {
805
+ id: "modal",
806
+ module: "@sandbar-sdk/modal",
807
+ export: "modalAdapter",
808
+ experimental: true,
809
+ credentials: { tokenId: "MODAL_TOKEN_ID", tokenSecret: "MODAL_TOKEN_SECRET" },
810
+ config: {
811
+ appName: { env: "MODAL_APP_NAME", required: true },
812
+ environment: { env: "MODAL_ENVIRONMENT", required: true }
813
+ }
814
+ }
815
+ ];
816
+ function descriptor(id) {
817
+ return providers.find((provider) => provider.id === id);
818
+ }
819
+ function missingEnv(provider, env, covered = new Set) {
820
+ const needed = [
821
+ ...Object.entries(provider.credentials),
822
+ ...Object.entries(provider.config).flatMap(([key, entry]) => entry.required ? [[key, entry.env]] : [])
823
+ ];
824
+ return needed.flatMap(([key, name]) => covered.has(key) || env[name] ? [] : [name]);
825
+ }
826
+ function resolveProfile(flags, loaded, env) {
827
+ const profiles = loaded.value.profiles ?? {};
828
+ const profileName = flags.profile ?? (flags.provider ? undefined : env.SANDBAR_PROFILE);
829
+ if (profileName) {
830
+ const profile = profiles[profileName];
831
+ if (!profile)
832
+ throw usage(`Profile ${profileName} is not defined in ${loaded.path}`, Object.keys(profiles).length ? `known profiles: ${Object.keys(profiles).join(", ")}` : 'add it under "profiles" in sandbar.json');
833
+ return fromProfile(profileName, profile, loaded, env);
834
+ }
835
+ const providerId = flags.provider ?? env.SANDBAR_PROVIDER;
836
+ if (providerId)
837
+ return fromProfile(providerId, { provider: providerId }, loaded, env);
838
+ const fallback = loaded.value.default;
839
+ if (fallback) {
840
+ const profile = profiles[fallback];
841
+ if (!profile)
842
+ throw usage(`Default profile ${fallback} is not defined in ${loaded.path}`);
843
+ return fromProfile(fallback, profile, loaded, env);
844
+ }
845
+ const ready = providers.filter((provider) => !missingEnv(provider, env).length);
846
+ throw usage("No provider selected", ready.length ? `credentials found for: ${ready.map((provider) => provider.id).join(", ")}; set SANDBAR_PROVIDER or pass --provider` : "set SANDBAR_PROVIDER and the provider's credentials (see `sandbar providers`), or add a profile to sandbar.json");
847
+ }
848
+ function fromProfile(name, profile, loaded, env) {
849
+ const official = profile.provider ? descriptor(profile.provider) : undefined;
850
+ if (profile.provider && !official && !profile.module)
851
+ throw usage(`Unknown provider ${profile.provider}`, `official providers: ${providers.map((provider) => provider.id).join(", ")}; use "module" for other adapters`);
852
+ const where = `profile ${name}`;
853
+ const credentials = Object.fromEntries([
854
+ ...Object.entries(official?.credentials ?? {}).flatMap(([key, variable]) => {
855
+ const value = env[variable];
856
+ return value ? [[key, value]] : [];
857
+ }),
858
+ ...Object.entries(profile.credentials ?? {}).map(([key, value]) => [
859
+ key,
860
+ z3.string().parse(resolveEnvReferences(value, env, `${where}.credentials.${key}`))
861
+ ])
862
+ ]);
863
+ const config = Object.fromEntries([
864
+ ...Object.entries(official?.config ?? {}).flatMap(([key, entry]) => {
865
+ const value = env[entry.env];
866
+ return value ? [[key, value]] : [];
867
+ }),
868
+ ...Object.entries(profile.config ?? {}).map(([key, value]) => [key, resolveEnvReferences(value, env, `${where}.config.${key}`)])
869
+ ]);
870
+ if (official) {
871
+ const missing = [
872
+ ...Object.entries(official.credentials).filter(([key]) => !credentials[key]).map(([, variable]) => variable),
873
+ ...Object.entries(official.config).filter(([key, entry]) => entry.required && config[key] === undefined).map(([, entry]) => entry.env)
874
+ ];
875
+ if (missing.length)
876
+ throw usage(`${where} (${official.id}) is missing ${missing.join(", ")}`, "set the environment variables, or put the values in the profile in sandbar.json");
877
+ }
878
+ return {
879
+ name,
880
+ provider: official?.id ?? profile.provider ?? name,
881
+ module: profile.module ?? official?.module ?? "",
882
+ export: profile.export ?? official?.export ?? "default",
883
+ baseDir: loaded.dir,
884
+ searchProject: searchProjectAdapters(loaded, env),
885
+ config,
886
+ credentials
887
+ };
888
+ }
889
+ var PackageManifest = z3.object({
890
+ main: z3.string().optional(),
891
+ exports: z3.json().optional()
892
+ });
893
+ async function exists(path) {
894
+ try {
895
+ await access(path);
896
+ return true;
897
+ } catch {
898
+ return false;
899
+ }
900
+ }
901
+ var importConditions = new Set(["import", "node", "default"]);
902
+ function exportTarget(value) {
903
+ const plain = z3.string().safeParse(value);
904
+ if (plain.success)
905
+ return plain.data;
906
+ const list = z3.array(z3.json()).safeParse(value);
907
+ if (list.success) {
908
+ for (const item of list.data) {
909
+ const target = exportTarget(item);
910
+ if (target)
911
+ return target;
912
+ }
913
+ return null;
914
+ }
915
+ const conditions = z3.record(z3.string(), z3.json()).safeParse(value);
916
+ if (!conditions.success)
917
+ return null;
918
+ for (const [condition, nested] of Object.entries(conditions.data)) {
919
+ if (!importConditions.has(condition))
920
+ continue;
921
+ const target = exportTarget(nested);
922
+ if (target)
923
+ return target;
924
+ }
925
+ return null;
926
+ }
927
+ function rootEntry(manifest) {
928
+ const exports = manifest.exports;
929
+ if (exports === undefined || exports === null)
930
+ return manifest.main || "index.js";
931
+ const subpaths = z3.record(z3.string(), z3.json()).safeParse(exports);
932
+ const root = subpaths.success && Object.keys(subpaths.data).some((key) => key.startsWith(".")) ? subpaths.data["."] : exports;
933
+ return (root === undefined ? null : exportTarget(root)) ?? manifest.main ?? "index.js";
934
+ }
935
+ async function entryFor(packageDir) {
936
+ const manifestPath = join3(packageDir, "package.json");
937
+ if (!await exists(manifestPath))
938
+ return null;
939
+ const manifest = PackageManifest.safeParse(JSON.parse(await readFile3(manifestPath, "utf8")));
940
+ if (!manifest.success)
941
+ return null;
942
+ const entry = resolve2(packageDir, rootEntry(manifest.data));
943
+ const inside = relative(packageDir, entry);
944
+ if (!inside || inside.startsWith("..") || isAbsolute(inside))
945
+ throw new CliError("ADAPTER_NOT_INSTALLED", `${manifestPath} points its entry outside the package (${rootEntry(manifest.data)})`);
946
+ return entry;
947
+ }
948
+ var PackageName = /^(@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
949
+ async function resolveAdapterModule(profile, cwd, env) {
950
+ const spec = profile.module;
951
+ if (spec.startsWith(".") || isAbsolute(spec))
952
+ return pathToFileURL(resolve2(profile.baseDir, spec)).href;
953
+ if (!PackageName.test(spec))
954
+ throw usage(`Adapter module ${spec} is not a package name or a path`);
955
+ const roots = [join3(configHome(env), "adapters", "node_modules")];
956
+ if (profile.searchProject)
957
+ for (let dir = resolve2(cwd);; dir = dirname2(dir)) {
958
+ roots.push(join3(dir, "node_modules"));
959
+ if (dirname2(dir) === dir)
960
+ break;
961
+ }
962
+ for (const root of roots) {
963
+ const entry = await entryFor(join3(root, spec));
964
+ if (entry)
965
+ return pathToFileURL(entry).href;
966
+ }
967
+ return spec;
968
+ }
969
+ function isAdapter(value) {
970
+ return value instanceof Object && "name" in value && "connect" in value && "config" in value;
971
+ }
972
+ var AdapterExport = z3.custom(isAdapter);
973
+ async function adapterMissing(target, spec, message) {
974
+ if (target.startsWith("file:"))
975
+ return !await exists(fileURLToPath(target));
976
+ return message.includes(`'${spec}'`) || message.includes(`"${spec}"`);
977
+ }
978
+ async function loadAdapter(profile, cwd, env, secrets) {
979
+ const target = await resolveAdapterModule(profile, cwd, env);
980
+ const schema = z3.object({ [profile.export]: AdapterExport });
981
+ let parsed;
982
+ try {
983
+ parsed = schema.safeParse(await import(target));
984
+ } catch (cause) {
985
+ const message = cause instanceof Error ? cause.message : String(cause);
986
+ const official = descriptor(profile.provider);
987
+ if (await adapterMissing(target, profile.module, message))
988
+ throw new CliError("ADAPTER_NOT_INSTALLED", `Cannot load adapter ${profile.module} for profile ${profile.name}: ${message}`, official ? `install it into ${join3(configHome(env), "adapters")} (cd there && npm install ${official.module}); project installs need adapters.searchProject or SANDBAR_PROJECT_ADAPTERS=1` : "check the profile's module path");
989
+ throw new CliError("ADAPTER_LOAD_FAILED", `Adapter ${profile.module} for profile ${profile.name} is installed but failed to load: ${message}`, "reinstall the adapter and its dependencies; the message above comes from the adapter package");
990
+ }
991
+ if (!parsed.success)
992
+ throw new CliError("ADAPTER_NOT_INSTALLED", `${profile.module} has no adapter export named ${profile.export}`);
993
+ const exported = parsed.data[profile.export];
994
+ if (!exported)
995
+ throw new CliError("ADAPTER_NOT_INSTALLED", `${profile.module} export is empty`);
996
+ if ("bound" in exported)
997
+ return exported;
998
+ const where = `profile ${profile.name}`;
999
+ const config = await secrets.resolve(profile.config, `${where}.config`);
1000
+ const credentials = z3.record(z3.string(), z3.string()).parse(await secrets.resolve(profile.credentials, `${where}.credentials`));
1001
+ return bindAdapter(exported, config, credentials);
1002
+ }
1003
+ async function connect(profile, cwd, env, secrets) {
1004
+ return Sandbar.connect(await loadAdapter(profile, cwd, env, secrets));
1005
+ }
1006
+
1007
+ // src/units.ts
1008
+ var durationUnits = new Map([
1009
+ ["", 1],
1010
+ ["s", 1],
1011
+ ["m", 60],
1012
+ ["h", 3600]
1013
+ ]);
1014
+ function parseDuration(value, flag) {
1015
+ const match = /^(\d+)(s|m|h)?$/.exec(value.trim());
1016
+ const seconds = match ? Number(match[1]) * (durationUnits.get(match[2] ?? "") ?? 1) : NaN;
1017
+ if (!Number.isSafeInteger(seconds) || seconds <= 0)
1018
+ throw usage(`--${flag} must be a duration like 90s, 30m or 2h`);
1019
+ return seconds;
1020
+ }
1021
+ var sizeUnits = new Map([
1022
+ ["", 1],
1023
+ ["b", 1],
1024
+ ["k", 1024],
1025
+ ["m", 1024 ** 2],
1026
+ ["g", 1024 ** 3]
1027
+ ]);
1028
+ function parseSize(value, flag) {
1029
+ const match = /^(\d+)(b|k|m|g)?$/i.exec(value.trim());
1030
+ const bytes = match ? Number(match[1]) * (sizeUnits.get((match[2] ?? "").toLowerCase()) ?? 1) : NaN;
1031
+ if (!Number.isSafeInteger(bytes) || bytes <= 0)
1032
+ throw usage(`--${flag} must be a size like 4096, 512k or 1m`);
1033
+ return bytes;
1034
+ }
1035
+ function parsePairs(values, flag) {
1036
+ const pairs = values.map((value) => {
1037
+ const index = value.indexOf("=");
1038
+ if (index <= 0)
1039
+ throw usage(`--${flag} expects KEY=VALUE, got ${value}`);
1040
+ return [value.slice(0, index), value.slice(index + 1)];
1041
+ });
1042
+ return Object.fromEntries(pairs);
1043
+ }
1044
+
1045
+ // src/commands.ts
1046
+ class Flags {
1047
+ values;
1048
+ positionals;
1049
+ rest;
1050
+ constructor(values, positionals, rest) {
1051
+ this.values = values;
1052
+ this.positionals = positionals;
1053
+ this.rest = rest;
1054
+ }
1055
+ string(name) {
1056
+ return z4.string().optional().parse(this.values[name]);
1057
+ }
1058
+ strings(name) {
1059
+ return z4.array(z4.string()).optional().parse(this.values[name]) ?? [];
1060
+ }
1061
+ bool(name) {
1062
+ return z4.boolean().optional().parse(this.values[name]) ?? false;
1063
+ }
1064
+ }
1065
+ function createRequest(flags) {
1066
+ if (flags.string("image") && flags.string("prepared"))
1067
+ throw usage("Pass either --image or --prepared, not both");
1068
+ if (flags.bool("build") && !flags.string("image"))
1069
+ throw usage("--build needs --image");
1070
+ return {
1071
+ build: flags.bool("build"),
1072
+ name: flags.string("name"),
1073
+ ifNotExists: flags.bool("if-not-exists"),
1074
+ image: flags.string("image"),
1075
+ prepared: flags.string("prepared"),
1076
+ network: flags.string("network"),
1077
+ labels: parsePairs(flags.strings("label"), "label")
1078
+ };
1079
+ }
1080
+ function createInput(request) {
1081
+ return {
1082
+ environment: request.prepared ? Image2.prepared(request.prepared) : undefined,
1083
+ network: request.network,
1084
+ labels: Object.keys(request.labels).length ? request.labels : undefined
1085
+ };
1086
+ }
1087
+ async function claimName(store, record, requested, ifNotExists = false) {
1088
+ for (let attempt = 0;; attempt++) {
1089
+ const name = requested ? checkName(requested) : generatedName();
1090
+ const full = { ...record, name };
1091
+ try {
1092
+ const { version } = await store.create("sandbox", name, full);
1093
+ return { record: full, version };
1094
+ } catch (cause) {
1095
+ if (!(cause instanceof CliError) || cause.code !== "CONFLICT" || attempt >= 8)
1096
+ throw cause;
1097
+ if (requested)
1098
+ throw new CliError("CONFLICT", cause.message, ifNotExists ? "its record is not a confirmed sandbox (still creating, or outcome unknown); check `sandbar inspect`, or drop it with `sandbar rm --force`" : "pass --if-not-exists to reuse it, or choose another --name");
1099
+ }
1100
+ }
1101
+ }
1102
+ async function liveCount(store) {
1103
+ return (await listAll(store, "sandbox")).length;
1104
+ }
1105
+ async function createSandbox(session, request) {
1106
+ const store = await session.store();
1107
+ const profile = await session.profile();
1108
+ if (request.name && request.ifNotExists) {
1109
+ const current = await store.get("sandbox", checkName(request.name));
1110
+ if (current?.record.status === "created" && current.record.reference) {
1111
+ const client2 = await session.connect(await session.profileNamed(current.record.profile));
1112
+ try {
1113
+ const box2 = await session.reopen(current.record, client2);
1114
+ return { client: client2, box: box2, ...current, existing: true };
1115
+ } catch (cause) {
1116
+ if (!(cause instanceof SandbarError3) || cause.code !== "NOT_FOUND")
1117
+ throw cause;
1118
+ await store.remove("sandbox", current.record.name, current.version);
1119
+ }
1120
+ }
1121
+ }
1122
+ const limit = await session.limit();
1123
+ if (limit !== null && await liveCount(store) >= limit)
1124
+ throw new CliError("LIMIT_REACHED", `The store already holds ${limit} sandboxes (limits.maxSandboxes)`, "remove some with `sandbar rm`, or raise limits.maxSandboxes in sandbar.json");
1125
+ const claimed = await claimName(store, {
1126
+ format: 1,
1127
+ kind: "sandbox",
1128
+ status: "creating",
1129
+ profile: profile.name,
1130
+ provider: profile.provider,
1131
+ createdAt: new Date().toISOString(),
1132
+ labels: request.labels,
1133
+ image: request.image ?? request.prepared,
1134
+ reference: null
1135
+ }, request.name, request.ifNotExists);
1136
+ let client;
1137
+ const input = createInput(request);
1138
+ try {
1139
+ client = await session.connect(profile);
1140
+ if (request.image)
1141
+ input.environment = await imageEnvironment(session, client, profile, request.image, request.build);
1142
+ if (input.environment) {
1143
+ const plan = await client.sandboxes.checkCreate(input);
1144
+ if (plan.status === "unsupported" || plan.status === "unavailable")
1145
+ throw new CliError("UNSUPPORTED", `${profile.provider} cannot create this sandbox: ${plan.reason}`, request.image ? "use --prepared with a provider image instead" : undefined);
1146
+ }
1147
+ } catch (cause) {
1148
+ await store.remove("sandbox", claimed.record.name, claimed.version);
1149
+ throw cause;
1150
+ }
1151
+ let box;
1152
+ try {
1153
+ box = await client.sandboxes.create(input, { signal: session.ctx.signal });
1154
+ } catch (cause) {
1155
+ const report = describeError(cause);
1156
+ if (report.outcome === "rejected") {
1157
+ await store.remove("sandbox", claimed.record.name, claimed.version);
1158
+ if (report.code === "INVALID_ARGUMENT" && !input.environment)
1159
+ throw new CliError("INVALID_ARGUMENT", `${profile.provider} needs an image for this sandbox: ${report.message}`, "pass --prepared <id> (a provider template or snapshot) or --image <oci-ref>");
1160
+ throw cause;
1161
+ }
1162
+ const reference = report.known?.reference ?? null;
1163
+ const recovery = report.known?.recovery;
1164
+ let kept = `the store keeps ${claimed.record.name} so it can be inspected or removed`;
1165
+ try {
1166
+ await store.update("sandbox", claimed.record.name, { ...claimed.record, status: "unknown", reference, recovery }, claimed.version);
1167
+ } catch (storeCause) {
1168
+ kept = `the store could not record it (${describeError(storeCause).message}); keep the reference below`;
1169
+ }
1170
+ throw new CliError(report.outcome === "partial" ? "PARTIAL" : "OUTCOME_UNKNOWN", report.message, kept, { sandbox: claimed.record.name, reference, recovery }, report.outcome);
1171
+ }
1172
+ const record = { ...claimed.record, status: "created", reference: box.reference };
1173
+ if (!box.reference)
1174
+ session.ctx.out.note(`warning: ${profile.provider} returned no reopenable reference; later commands cannot reach ${record.name}`);
1175
+ try {
1176
+ await store.update("sandbox", record.name, record, claimed.version);
1177
+ } catch (cause) {
1178
+ const unrecorded = cause instanceof Error ? cause : new Error(String(cause));
1179
+ return { client, box, record, existing: false, unrecorded };
1180
+ }
1181
+ return { client, box, record, existing: false };
1182
+ }
1183
+ function unrecordedError(created) {
1184
+ return new CliError("STORE_WRITE_FAILED", `Created ${created.record.name} but could not record it in the store: ${created.unrecorded?.message}`, "save the reference below to a file and use `sandbar rm @<file>` to destroy it", { sandbox: created.record.name, reference: created.record.reference }, "partial");
1185
+ }
1186
+ function sandboxSummary(record) {
1187
+ return {
1188
+ name: record.name,
1189
+ id: record.reference?.nativeId ?? null,
1190
+ provider: record.provider,
1191
+ profile: record.profile,
1192
+ status: record.status,
1193
+ createdAt: record.createdAt,
1194
+ labels: record.labels,
1195
+ image: record.image ?? null
1196
+ };
1197
+ }
1198
+ async function findTarget(session, target) {
1199
+ const store = await session.store();
1200
+ if (target.startsWith("@")) {
1201
+ const path = resolve3(session.ctx.cwd, target.slice(1));
1202
+ const parsed = ResourceReference2.safeParse(JSON.parse(await readFile4(path, "utf8")));
1203
+ if (!parsed.success || parsed.data.kind !== "sandbox")
1204
+ throw usage(`${path} does not hold a sandbox reference`);
1205
+ const profile = await session.profile();
1206
+ return {
1207
+ record: {
1208
+ format: 1,
1209
+ kind: "sandbox",
1210
+ name: "from-file",
1211
+ status: "created",
1212
+ profile: profile.name,
1213
+ provider: parsed.data.provider,
1214
+ createdAt: new Date().toISOString(),
1215
+ labels: {},
1216
+ reference: { ...parsed.data, kind: "sandbox" }
1217
+ },
1218
+ version: "",
1219
+ fromFile: true
1220
+ };
1221
+ }
1222
+ if (RecordName.safeParse(target).success) {
1223
+ const entry = await store.get("sandbox", target);
1224
+ if (entry)
1225
+ return entry;
1226
+ }
1227
+ const byId = (await listAll(store, "sandbox")).find((entry) => entry.record.reference?.nativeId === target);
1228
+ if (byId)
1229
+ return byId;
1230
+ throw new CliError("NOT_FOUND", `No sandbox named ${target} in the store`, "list known sandboxes with `sandbar ls`");
1231
+ }
1232
+ async function createCommand(session, flags) {
1233
+ const created = await createSandbox(session, createRequest(flags));
1234
+ if (created.unrecorded)
1235
+ throw unrecordedError(created);
1236
+ session.ctx.out.success({
1237
+ ...sandboxSummary(created.record),
1238
+ existing: created.existing,
1239
+ reference: created.record.reference
1240
+ }, created.record.name);
1241
+ return 0;
1242
+ }
1243
+ async function stateOf(session, record, client) {
1244
+ if (!record.reference || !client)
1245
+ return record.status === "created" ? "unreachable" : record.status;
1246
+ try {
1247
+ const box = await session.reopen(record, client);
1248
+ return (await box.inspect({ signal: session.ctx.signal })).state;
1249
+ } catch (cause) {
1250
+ if (cause instanceof SandbarError3 && cause.code === "NOT_FOUND")
1251
+ return "gone";
1252
+ if (cause instanceof SandbarError3 && cause.code === "UNSUPPORTED")
1253
+ return "unknown";
1254
+ return "unreachable";
1255
+ }
1256
+ }
1257
+ async function lsCommand(session, flags) {
1258
+ const entries = await listAll(await session.store(), "sandbox", (warning) => session.ctx.out.note(warning));
1259
+ const offline = flags.bool("offline");
1260
+ const clients = new Map;
1261
+ if (!offline)
1262
+ for (const profile of new Set(entries.map((entry) => entry.record.profile))) {
1263
+ try {
1264
+ clients.set(profile, await session.connect(await session.profileNamed(profile)));
1265
+ } catch {
1266
+ clients.set(profile, null);
1267
+ }
1268
+ }
1269
+ const rows = await Promise.all(entries.map(async ({ record }) => ({
1270
+ ...sandboxSummary(record),
1271
+ state: offline ? null : await stateOf(session, record, clients.get(record.profile) ?? null)
1272
+ })));
1273
+ const table = [
1274
+ ["NAME", "STATE", "PROVIDER", "PROFILE", "ID", "CREATED"],
1275
+ ...rows.map((row) => [
1276
+ row.name,
1277
+ row.state ?? row.status,
1278
+ row.provider,
1279
+ row.profile,
1280
+ row.id ?? "-",
1281
+ row.createdAt
1282
+ ])
1283
+ ];
1284
+ const widths = table[0].map((_, column) => Math.max(...table.map((row) => row[column].length)));
1285
+ session.ctx.out.success({ sandboxes: rows }, rows.length ? table.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()).join(`
1286
+ `) : "");
1287
+ return 0;
1288
+ }
1289
+ function onePositional(flags, what) {
1290
+ if (flags.positionals.length !== 1)
1291
+ throw usage(`Expected exactly one ${what}`);
1292
+ return flags.positionals[0];
1293
+ }
1294
+ async function inspectCommand(session, flags) {
1295
+ const { record } = await recoverCreate(session, await session.store(), await findTarget(session, onePositional(flags, "sandbox")));
1296
+ const box = await session.reopen(record);
1297
+ const info = await box.inspect({ signal: session.ctx.signal });
1298
+ const lines = [
1299
+ `name ${record.name}`,
1300
+ `id ${box.id}`,
1301
+ `provider ${record.provider} (profile ${record.profile})`,
1302
+ `state ${info.state}${info.nativeState ? ` (${info.nativeState})` : ""}`,
1303
+ `expires ${info.expires.status === "known" ? `${info.expires.at} (${info.expires.action})` : info.expires.status}`,
1304
+ `observed ${info.observedAt}`
1305
+ ];
1306
+ session.ctx.out.success({
1307
+ ...sandboxSummary(record),
1308
+ info: { ...info, reference: undefined },
1309
+ reference: record.reference
1310
+ }, lines.join(`
1311
+ `));
1312
+ return 0;
1313
+ }
1314
+ function refusal(record) {
1315
+ if (!record.reference)
1316
+ return new CliError("UNSUPPORTED", `${record.name} has no reopenable reference (status ${record.status}); it cannot be destroyed from here`, "check the provider for it, then `sandbar rm --force` to drop the record", { sandbox: record.name });
1317
+ if (record.reference.ownership !== "verified-created")
1318
+ return new CliError("FORBIDDEN", `${record.name} was not created by this CLI (ownership ${record.reference.ownership})`, "pass --force to destroy it anyway", { sandbox: record.name });
1319
+ return null;
1320
+ }
1321
+ function sameSandbox(a, b) {
1322
+ if (a.createdAt !== b.createdAt || a.provider !== b.provider || a.profile !== b.profile)
1323
+ return false;
1324
+ return !a.reference || !b.reference || a.reference.nativeId === b.reference.nativeId;
1325
+ }
1326
+ async function forget(store, target) {
1327
+ if (target.fromFile)
1328
+ return;
1329
+ for (let attempt = 0;; attempt++) {
1330
+ const current = await store.get("sandbox", target.record.name);
1331
+ if (!current || !sameSandbox(current.record, target.record))
1332
+ return;
1333
+ try {
1334
+ await store.remove("sandbox", target.record.name, current.version);
1335
+ return;
1336
+ } catch (cause) {
1337
+ if (!(cause instanceof CliError) || cause.code !== "CONFLICT" || attempt === 2)
1338
+ throw cause;
1339
+ }
1340
+ }
1341
+ }
1342
+ async function recoverCreate(session, store, target) {
1343
+ const { record } = target;
1344
+ if (target.fromFile || record.status !== "unknown" || record.reference || !record.recovery)
1345
+ return target;
1346
+ if (record.recovery.kind !== "create")
1347
+ return target;
1348
+ const client = await session.connect(await session.profileNamed(record.profile));
1349
+ let value;
1350
+ try {
1351
+ value = await (await client.recover(record.recovery)).observe();
1352
+ } catch (cause) {
1353
+ if (cause instanceof SandbarError3 && cause.effect !== "none")
1354
+ return target;
1355
+ throw cause;
1356
+ }
1357
+ if (!(value instanceof AdapterSandbox))
1358
+ return target;
1359
+ const { recovery: _recovery, ...rest } = record;
1360
+ const recovered = { ...rest, status: "created", reference: value.reference };
1361
+ const { version } = await store.update("sandbox", record.name, recovered, target.version);
1362
+ session.ctx.out.note(`${record.name}: recovered the uncertain create as ${value.id}`);
1363
+ return { record: recovered, version };
1364
+ }
1365
+ async function removeOne(session, store, found, force, all) {
1366
+ const target = await recoverCreate(session, store, found).catch((cause) => {
1367
+ if (force)
1368
+ return found;
1369
+ throw cause;
1370
+ });
1371
+ const { record } = target;
1372
+ const refused = refusal(record);
1373
+ if (refused && !force) {
1374
+ if (all)
1375
+ return {
1376
+ kind: "skipped",
1377
+ item: { name: record.name, result: "skipped", reason: refused.message }
1378
+ };
1379
+ throw refused;
1380
+ }
1381
+ if (!record.reference) {
1382
+ await forget(store, target);
1383
+ return {
1384
+ kind: "removed",
1385
+ item: { name: record.name, result: "forgotten", retainedResources: [] }
1386
+ };
1387
+ }
1388
+ let box;
1389
+ try {
1390
+ box = await session.reopen(record);
1391
+ } catch (cause) {
1392
+ if (!(cause instanceof SandbarError3) || cause.code !== "NOT_FOUND")
1393
+ throw cause;
1394
+ await forget(store, target);
1395
+ return { kind: "removed", item: { name: record.name, result: "gone", retainedResources: [] } };
1396
+ }
1397
+ const destroyed = await box.destroy({ signal: session.ctx.signal });
1398
+ await forget(store, target);
1399
+ return {
1400
+ kind: "removed",
1401
+ item: {
1402
+ name: record.name,
1403
+ result: "destroyed",
1404
+ retainedResources: destroyed.retainedResources
1405
+ }
1406
+ };
1407
+ }
1408
+ async function rmCommand(session, flags) {
1409
+ const store = await session.store();
1410
+ const all = flags.bool("all");
1411
+ const force = flags.bool("force");
1412
+ const { out } = session.ctx;
1413
+ if (all === Boolean(flags.positionals.length))
1414
+ throw usage("Name sandboxes to remove, or pass --all");
1415
+ const targets = all ? (await listAll(store, "sandbox", (warning) => out.note(warning))).map((entry) => ({
1416
+ name: entry.record.name,
1417
+ find: async () => entry
1418
+ })) : flags.positionals.map((target) => ({
1419
+ name: target,
1420
+ find: () => findTarget(session, target)
1421
+ }));
1422
+ const details = {
1423
+ removed: [],
1424
+ skipped: [],
1425
+ failed: []
1426
+ };
1427
+ let lastCause;
1428
+ for (const target of targets) {
1429
+ try {
1430
+ const result = await removeOne(session, store, await target.find(), force, all);
1431
+ if (result.kind === "skipped") {
1432
+ details.skipped.push(result.item);
1433
+ continue;
1434
+ }
1435
+ details.removed.push(result.item);
1436
+ if (result.item.result === "gone")
1437
+ out.note(`${result.item.name}: already gone`);
1438
+ if (result.item.retainedResources.length)
1439
+ out.note(`${result.item.name}: retained ${result.item.retainedResources.join(", ")}`);
1440
+ } catch (cause) {
1441
+ const report = describeError(cause);
1442
+ lastCause = cause;
1443
+ details.failed.push({
1444
+ name: target.name,
1445
+ result: "failed",
1446
+ code: report.code,
1447
+ reason: report.message
1448
+ });
1449
+ }
1450
+ }
1451
+ if (details.failed.length === 1 && targets.length === 1)
1452
+ throw lastCause;
1453
+ if (details.failed.length)
1454
+ throw new CliError("PARTIAL", `Removed ${details.removed.length} of ${targets.length} sandboxes; ${details.failed.length} failed`, "see error.details; retry the failed names once the cause is fixed", undefined, "partial", details);
1455
+ for (const item of details.skipped)
1456
+ out.note(`skipped ${item.name}: ${item.reason}`);
1457
+ out.success({ removed: details.removed, skipped: details.skipped }, details.removed.map((entry) => entry.name).join(`
1458
+ `));
1459
+ return 0;
1460
+ }
1461
+ async function execInput(session, flags, command) {
1462
+ if (flags.bool("stdin") && flags.string("stdin-file"))
1463
+ throw usage("Pass either --stdin or --stdin-file");
1464
+ const stdinFile = flags.string("stdin-file");
1465
+ const stdin = flags.bool("stdin") ? await session.ctx.readStdin() : stdinFile ? new Uint8Array(await readFile4(resolve3(session.ctx.cwd, stdinFile))) : undefined;
1466
+ const timeout = flags.string("timeout");
1467
+ const maxOutput = flags.string("max-output");
1468
+ const env = parsePairs(flags.strings("env"), "env");
1469
+ return {
1470
+ command,
1471
+ cwd: flags.string("cwd"),
1472
+ env: Object.keys(env).length ? env : undefined,
1473
+ stdin,
1474
+ deadlineSeconds: timeout ? parseDuration(timeout, "timeout") : undefined,
1475
+ maxOutputBytes: maxOutput ? parseSize(maxOutput, "max-output") : undefined
1476
+ };
1477
+ }
1478
+ function argvCommand(flags) {
1479
+ if (!flags.rest?.length)
1480
+ throw usage("Give the command after --, e.g. sandbar exec web -- ls -la");
1481
+ return { kind: "argv", argv: [...flags.rest] };
1482
+ }
1483
+ async function execute(session, box, input) {
1484
+ try {
1485
+ return await box.exec(input, { signal: session.ctx.signal });
1486
+ } catch (cause) {
1487
+ if (!(cause instanceof NonzeroExitError))
1488
+ throw cause;
1489
+ return cause.result;
1490
+ }
1491
+ }
1492
+ function execSummary(result) {
1493
+ return {
1494
+ exitCode: result.exitCode,
1495
+ stdout: result.stdoutText({ full: true }),
1496
+ stderr: result.stderrText({ full: true }),
1497
+ truncated: result.truncated
1498
+ };
1499
+ }
1500
+ function printStreams(out, result) {
1501
+ if (out.json)
1502
+ return;
1503
+ if (result.stdout.byteLength)
1504
+ out.streams.stdout(result.stdout);
1505
+ if (result.stderr.byteLength)
1506
+ out.streams.stderr(result.stderr);
1507
+ if (result.truncated)
1508
+ out.note("sandbar: output was truncated; raise --max-output to capture more");
1509
+ }
1510
+ function finishExec(out, result, sandbox) {
1511
+ printStreams(out, result);
1512
+ if (result.exitCode === null)
1513
+ throw new CliError("OUTCOME_UNKNOWN", "The command ran but the provider reported no exit status", "the captured output is in error.known.exec", { exec: execSummary(result) });
1514
+ if (out.json)
1515
+ out.success({ ...execSummary(result), sandbox }, "");
1516
+ return result.exitCode;
1517
+ }
1518
+ async function execCommand(session, flags) {
1519
+ const { record } = await findTarget(session, onePositional(flags, "sandbox"));
1520
+ const input = await execInput(session, flags, argvCommand(flags));
1521
+ const box = await session.reopen(record);
1522
+ return finishExec(session.ctx.out, await execute(session, box, input));
1523
+ }
1524
+ function shScript(flags) {
1525
+ const [target, inline] = flags.positionals;
1526
+ if (flags.rest && flags.positionals.length === 1 && flags.rest.length === 1)
1527
+ return { target, script: flags.rest[0] };
1528
+ if (!flags.rest && flags.positionals.length === 2)
1529
+ return { target, script: inline };
1530
+ throw usage("Usage: sandbar sh <sandbox> '<script>' (or put the script after --, as one argument)");
1531
+ }
1532
+ async function shCommand(session, flags) {
1533
+ const { target, script } = shScript(flags);
1534
+ const { record } = await findTarget(session, target);
1535
+ const input = await execInput(session, flags, { kind: "shell", script });
1536
+ const box = await session.reopen(record);
1537
+ return finishExec(session.ctx.out, await execute(session, box, input));
1538
+ }
1539
+ async function runCommand(session, flags) {
1540
+ if (flags.positionals.length)
1541
+ throw usage("run takes no positional arguments; put the command after --");
1542
+ const command = argvCommand(flags);
1543
+ const request = createRequest(flags);
1544
+ const input = await execInput(session, flags, command);
1545
+ const keep = flags.bool("keep");
1546
+ const created = await createSandbox(session, { ...request, ifNotExists: false });
1547
+ const store = await session.store();
1548
+ const { out, signal } = session.ctx;
1549
+ let result;
1550
+ let failure;
1551
+ const sandbox = (kept) => ({
1552
+ name: created.record.name,
1553
+ id: created.record.reference?.nativeId ?? null,
1554
+ kept
1555
+ });
1556
+ if (created.unrecorded)
1557
+ failure = unrecordedError(created);
1558
+ else
1559
+ try {
1560
+ result = await execute(session, created.box, input);
1561
+ } catch (cause) {
1562
+ failure = cause;
1563
+ }
1564
+ if (keep) {
1565
+ out.note(`kept sandbox ${created.record.name}`);
1566
+ if (failure)
1567
+ throw failure;
1568
+ return finishExec(out, result, sandbox(true));
1569
+ }
1570
+ try {
1571
+ await created.box.destroy();
1572
+ await store.remove("sandbox", created.record.name);
1573
+ } catch (cause) {
1574
+ const report = describeError(cause);
1575
+ if (result)
1576
+ printStreams(out, result);
1577
+ throw new CliError("CLEANUP_FAILED", `Cleanup of ${created.record.name} failed: ${report.message}${failure ? `; the command also failed: ${describeError(failure).message}` : ""}`, `remove it with \`sandbar rm ${created.record.name}\``, {
1578
+ sandbox: created.record.name,
1579
+ reference: created.record.reference,
1580
+ exec: result ? execSummary(result) : undefined
1581
+ }, "partial");
1582
+ }
1583
+ if (failure && signal.aborted)
1584
+ throw new CliError("INTERRUPTED", `Interrupted; sandbox ${created.record.name} was destroyed`, "the command may have partly run before the sandbox was destroyed", { sandbox: created.record.name, reference: created.record.reference }, "partial");
1585
+ if (failure) {
1586
+ if (failure instanceof CliError && failure.code === "STORE_WRITE_FAILED")
1587
+ throw new CliError("STORE_WRITE_FAILED", `${failure.message}; the sandbox was destroyed, so nothing was run`);
1588
+ throw failure;
1589
+ }
1590
+ return finishExec(out, result, sandbox(false));
1591
+ }
1592
+ var RemotePath = /^([a-z0-9][a-z0-9-]{0,62}):(\/.*)$/;
1593
+ function remote(spec) {
1594
+ const match = RemotePath.exec(spec);
1595
+ return match ? { target: match[1], path: match[2] } : null;
1596
+ }
1597
+ async function cpCommand(session, flags) {
1598
+ if (flags.positionals.length !== 2)
1599
+ throw usage("Usage: sandbar cp <src> <dst> (one side is <sandbox>:/path)");
1600
+ if (flags.bool("recursive"))
1601
+ throw usage("Recursive copy is not available yet; copy files one at a time");
1602
+ const [source, destination] = [flags.positionals[0], flags.positionals[1]];
1603
+ const from = remote(source);
1604
+ const to = remote(destination);
1605
+ if (Boolean(from) === Boolean(to))
1606
+ throw usage("Exactly one side of cp must be <sandbox>:/path");
1607
+ if (to) {
1608
+ const local2 = resolve3(session.ctx.cwd, source);
1609
+ if ((await stat2(local2)).isDirectory())
1610
+ throw usage(`${source} is a directory; recursive copy is not available yet`);
1611
+ const bytes2 = new Uint8Array(await readFile4(local2));
1612
+ const { record: record2 } = await findTarget(session, to.target);
1613
+ const box2 = await session.reopen(record2);
1614
+ await box2.writeFile(to.path, bytes2, {
1615
+ overwrite: !flags.bool("no-clobber"),
1616
+ signal: session.ctx.signal
1617
+ });
1618
+ session.ctx.out.success({ sandbox: record2.name, path: to.path, bytes: bytes2.byteLength }, "");
1619
+ return 0;
1620
+ }
1621
+ const { record } = await findTarget(session, from.target);
1622
+ const box = await session.reopen(record);
1623
+ const bytes = await box.readFile(from.path, { signal: session.ctx.signal });
1624
+ let local = resolve3(session.ctx.cwd, destination);
1625
+ try {
1626
+ if ((await stat2(local)).isDirectory())
1627
+ local = join4(local, basename(from.path));
1628
+ } catch {}
1629
+ try {
1630
+ await writeFile2(local, bytes, { flag: flags.bool("no-clobber") ? "wx" : "w" });
1631
+ } catch (cause) {
1632
+ if (cause instanceof Error && "code" in cause && cause.code === "EEXIST")
1633
+ throw new CliError("CONFLICT", `${local} already exists`, "drop --no-clobber to overwrite it");
1634
+ throw cause;
1635
+ }
1636
+ session.ctx.out.success({ sandbox: record.name, path: local, bytes: bytes.byteLength }, "");
1637
+ return 0;
1638
+ }
1639
+ async function catCommand(session, flags) {
1640
+ const location = remote(onePositional(flags, "<sandbox>:/path"));
1641
+ if (!location)
1642
+ throw usage("Usage: sandbar cat <sandbox>:/path");
1643
+ const { record } = await findTarget(session, location.target);
1644
+ const box = await session.reopen(record);
1645
+ const bytes = await box.readFile(location.path, { signal: session.ctx.signal });
1646
+ const { out } = session.ctx;
1647
+ if (!out.json) {
1648
+ out.streams.stdout(bytes);
1649
+ return 0;
1650
+ }
1651
+ let text = null;
1652
+ try {
1653
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1654
+ } catch {}
1655
+ out.success(text === null ? {
1656
+ path: location.path,
1657
+ bytes: bytes.byteLength,
1658
+ base64: Buffer.from(bytes).toString("base64")
1659
+ } : { path: location.path, bytes: bytes.byteLength, text }, "");
1660
+ return 0;
1661
+ }
1662
+ async function doctorCommand(session) {
1663
+ const loaded = await session.config();
1664
+ const profile = await session.profile();
1665
+ const client = await session.connect(profile);
1666
+ const capabilities = await client.capabilities();
1667
+ let plan;
1668
+ try {
1669
+ plan = await client.sandboxes.checkCreate();
1670
+ } catch (cause) {
1671
+ plan = { status: "unavailable", reason: describeError(cause).message };
1672
+ }
1673
+ const store = await session.store();
1674
+ const count = (await listAll(store, "sandbox")).length;
1675
+ session.ctx.out.success({
1676
+ config: { path: loaded.path, exists: loaded.exists },
1677
+ profile: profile.name,
1678
+ provider: profile.provider,
1679
+ connected: true,
1680
+ create: plan,
1681
+ sandboxesInStore: count,
1682
+ capabilities
1683
+ }, [
1684
+ `config ${loaded.path}${loaded.exists ? "" : " (not found, using defaults)"}`,
1685
+ `profile ${profile.name} (${profile.provider})`,
1686
+ "connect ok",
1687
+ `create ${plan.status}${plan.status === "supported" ? "" : `: ${plan.reason}`}`,
1688
+ `store ${count} sandbox record(s)`
1689
+ ].join(`
1690
+ `));
1691
+ return 0;
1692
+ }
1693
+ function profileKeys(loaded, provider, env) {
1694
+ return new Set(Object.values(loaded.value.profiles ?? {}).filter((profile) => profile.provider === provider).flatMap((profile) => [
1695
+ ...Object.entries(profile.config ?? {}),
1696
+ ...Object.entries(profile.credentials ?? {})
1697
+ ].flatMap(([key, value]) => valueAvailable(value, env) ? [key] : [])));
1698
+ }
1699
+ async function providersCommand(session) {
1700
+ const { env, cwd } = session.ctx;
1701
+ const loaded = await session.config();
1702
+ const rows = await Promise.all(providers.map(async (provider) => {
1703
+ const resolved = await resolveAdapterModule({
1704
+ name: provider.id,
1705
+ provider: provider.id,
1706
+ module: provider.module,
1707
+ export: provider.export,
1708
+ baseDir: cwd,
1709
+ searchProject: searchProjectAdapters(loaded, env),
1710
+ config: {},
1711
+ credentials: {}
1712
+ }, cwd, env);
1713
+ let installed = resolved !== provider.module;
1714
+ if (!installed)
1715
+ try {
1716
+ import.meta.resolve(provider.module);
1717
+ installed = true;
1718
+ } catch {
1719
+ installed = false;
1720
+ }
1721
+ return {
1722
+ id: provider.id,
1723
+ module: provider.module,
1724
+ experimental: provider.experimental,
1725
+ installed,
1726
+ missingEnv: missingEnv(provider, env, profileKeys(loaded, provider.id, env))
1727
+ };
1728
+ }));
1729
+ session.ctx.out.success({ providers: rows }, rows.map((row) => `${row.id.padEnd(8)} ${row.installed ? "installed" : "not installed"} ${row.missingEnv.length ? `needs ${row.missingEnv.join(", ")}` : "credentials set"}${row.experimental ? " (experimental)" : ""}`).join(`
1730
+ `));
1731
+ return 0;
1732
+ }
1733
+
1734
+ // src/secrets.ts
1735
+ import { execFile } from "node:child_process";
1736
+ import { z as z5 } from "zod";
1737
+ var BwsSecret = z5.object({ value: z5.string() });
1738
+ var backends = [
1739
+ {
1740
+ name: "1Password",
1741
+ pattern: /^op:\/\/[^/\s]+\/[^/\s]+(\/[^/\s]+){1,2}(\?\S*)?$/,
1742
+ command: "op",
1743
+ args: (reference) => ["read", "--no-newline", reference],
1744
+ value: (stdout) => stdout,
1745
+ install: "install the 1Password CLI (https://developer.1password.com/docs/cli) and sign in",
1746
+ check: "check `op whoami` and that the vault, item and field exist"
1747
+ },
1748
+ {
1749
+ name: "Bitwarden Secrets Manager",
1750
+ pattern: /^bws:\/\/[0-9a-fA-F-]{36}$/,
1751
+ command: "bws",
1752
+ args: (reference) => ["secret", "get", reference.slice("bws://".length), "--output", "json"],
1753
+ value: (stdout) => BwsSecret.parse(JSON.parse(stdout)).value,
1754
+ install: "install the Bitwarden Secrets Manager CLI (https://bitwarden.com/help/secrets-manager-cli/) and set BWS_ACCESS_TOKEN",
1755
+ check: "check BWS_ACCESS_TOKEN and that the machine account can read the secret"
1756
+ },
1757
+ {
1758
+ name: "pass",
1759
+ pattern: /^pass:\/\/(?!.*(^|\/)\.\.(\/|$))[\w@+.-]+(\/[\w@+.-]+)*$/,
1760
+ command: "pass",
1761
+ args: (reference) => ["show", reference.slice("pass://".length)],
1762
+ value: (stdout) => stdout.split(`
1763
+ `)[0] ?? "",
1764
+ install: "install pass (https://www.passwordstore.org) and initialise your store",
1765
+ check: "check that `pass show <path>` works, including the GPG agent"
1766
+ }
1767
+ ];
1768
+ function backendFor(value) {
1769
+ return backends.find((backend) => backend.pattern.test(value));
1770
+ }
1771
+ function run(command, args, env, signal) {
1772
+ return new Promise((done) => {
1773
+ execFile(command, args, { env: { ...env }, signal, timeout: 60000, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => done({
1774
+ code: error ? error.code ?? "FAILED" : 0,
1775
+ stdout: String(stdout),
1776
+ stderr: String(stderr)
1777
+ }));
1778
+ });
1779
+ }
1780
+
1781
+ class SecretReader {
1782
+ env;
1783
+ signal;
1784
+ cache = new Map;
1785
+ constructor(env, signal) {
1786
+ this.env = env;
1787
+ this.signal = signal;
1788
+ }
1789
+ read(backend, reference, where) {
1790
+ let pending = this.cache.get(reference);
1791
+ if (!pending) {
1792
+ pending = this.fetch(backend, reference, where);
1793
+ this.cache.set(reference, pending);
1794
+ }
1795
+ return pending;
1796
+ }
1797
+ async fetch(backend, reference, where) {
1798
+ const result = await run(backend.command, backend.args(reference), this.env, this.signal);
1799
+ if (result.code === "ENOENT")
1800
+ throw new CliError("SECRET_TOOL_MISSING", `${where} is a ${backend.name} reference, but \`${backend.command}\` is not installed`, backend.install);
1801
+ if (result.code !== 0)
1802
+ throw new CliError("SECRET_UNAVAILABLE", `Cannot read ${where} from ${backend.name} (${reference}): ${result.stderr.trim() || `${backend.command} exited with ${result.code}`}`, backend.check);
1803
+ try {
1804
+ return backend.value(result.stdout);
1805
+ } catch {
1806
+ throw new CliError("SECRET_UNAVAILABLE", `${backend.name} returned an unreadable answer for ${where} (${reference})`, backend.check);
1807
+ }
1808
+ }
1809
+ async resolve(value, where) {
1810
+ const text = z5.string().safeParse(value);
1811
+ if (text.success) {
1812
+ const backend = backendFor(text.data);
1813
+ return backend ? this.read(backend, text.data, where) : value;
1814
+ }
1815
+ if (Array.isArray(value))
1816
+ return Promise.all(value.map((item, index) => this.resolve(item, `${where}[${index}]`)));
1817
+ const record = z5.record(z5.string(), z5.json()).safeParse(value);
1818
+ if (record.success)
1819
+ return Object.fromEntries(await Promise.all(Object.entries(record.data).map(async ([key, item]) => [key, await this.resolve(item, `${where}.${key}`)])));
1820
+ return value;
1821
+ }
1822
+ }
1823
+
1824
+ // src/session.ts
1825
+ class Session {
1826
+ ctx;
1827
+ loaded;
1828
+ opened = [];
1829
+ secrets;
1830
+ constructor(ctx) {
1831
+ this.ctx = ctx;
1832
+ this.secrets = new SecretReader(ctx.env, ctx.signal);
1833
+ }
1834
+ async config() {
1835
+ this.loaded ??= await loadConfig(this.ctx.flags.config, this.ctx.env);
1836
+ return this.loaded;
1837
+ }
1838
+ async store() {
1839
+ const { flags, env, cwd } = this.ctx;
1840
+ return new FsStateStore(stateDir(flags.stateDir, await this.config(), env, cwd));
1841
+ }
1842
+ async limit() {
1843
+ return maxSandboxes(await this.config(), this.ctx.env);
1844
+ }
1845
+ async profile() {
1846
+ return resolveProfile(this.ctx.flags, await this.config(), this.ctx.env);
1847
+ }
1848
+ async profileNamed(name) {
1849
+ const loaded = await this.config();
1850
+ if (loaded.value.profiles?.[name])
1851
+ return resolveProfile({ profile: name }, loaded, this.ctx.env);
1852
+ if (descriptor(name))
1853
+ return resolveProfile({ provider: name }, loaded, this.ctx.env);
1854
+ throw new CliError("USAGE", `Profile ${name} is no longer defined`, `restore it in ${loaded.path}; the sandbox was created with it`);
1855
+ }
1856
+ async connect(profile) {
1857
+ const client = await connect(profile, this.ctx.cwd, this.ctx.env, this.secrets);
1858
+ this.opened.push(client);
1859
+ return client;
1860
+ }
1861
+ async reopen(record, client) {
1862
+ if (!record.reference)
1863
+ throw new CliError("UNSUPPORTED", `Sandbox ${record.name} has no reopenable reference (status ${record.status})`, "inspect it with the provider's own tools; `sandbar rm --force` drops the record");
1864
+ const connected = client ?? await this.connect(await this.profileNamed(record.profile));
1865
+ return connected.sandboxes.get(record.reference, { signal: this.ctx.signal });
1866
+ }
1867
+ async close() {
1868
+ await Promise.allSettled(this.opened.splice(0).map((client) => client.close()));
1869
+ }
1870
+ }
1871
+
1872
+ // src/skill.ts
1873
+ import { access as access2, mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
1874
+ import { homedir as homedir2 } from "node:os";
1875
+ import { dirname as dirname3, join as join5 } from "node:path";
1876
+
1877
+ // ../../skills/sandbar-cli/SKILL.md
1878
+ var SKILL_default = "---\nname: sandbar-cli\ndescription: Create, use and clean up cloud sandboxes from the shell with the `sandbar` CLI (Daytona, E2B, boxd, Vercel, Modal). Use when you need an isolated machine to run untrusted or generated code, try a build or test in a clean environment, or manage sandboxes someone created with sandbar; prefer it over provider-specific CLIs.\n---\n\n# Use sandboxes with the sandbar CLI\n\n`sandbar` creates and manages sandboxes on any Sandbar provider. It is built for agents: it never prompts, every command has a stable `--json` form, and exit codes tell you what to do next.\n\n## Check the setup first\n\n```sh\nsandbar doctor --json # config, credentials, provider connection; creates nothing\nsandbar providers --json # which adapters are installed and which credentials are missing\n```\n\n- If `sandbar` is not on PATH, stop and tell the user; do not install anything yourself.\n- The provider comes from `--profile`, `SANDBAR_PROFILE`, `--provider`, `SANDBAR_PROVIDER`, then `default` in `~/.config/sandbar/sandbar.json`. Do not edit that file or credentials unless the user asks.\n- A missing adapter (exit 4, `ADAPTER_NOT_INSTALLED`), an installed adapter that fails to load (exit 4, `ADAPTER_LOAD_FAILED`) or a missing credential (exit 2) is the user's to fix. Report the `hint` from the error.\n\n**Sandboxes cost money.** Create only what the task needs and destroy it when done.\n\n## Always use JSON\n\nPass `--json` (or set `SANDBAR_JSON=1`). stdout then holds exactly one document:\n\n```json\n{\"ok\": true, \"data\": {...}}\n{\"ok\": false, \"error\": {\"code\": \"...\", \"message\": \"...\", \"retryable\": false, \"outcome\": \"rejected\", \"hint\": \"...\", \"known\": {...}}}\n```\n\nDiagnostics go to stderr. Read `error.hint` before deciding what to do.\n\n## One-shot work: `run`\n\n`run` creates a sandbox, runs one command and always destroys it. Prefer it whenever you do not need the sandbox afterwards.\n\n```sh\nsandbar run --json -- python3 -c 'print(2 + 2)'\nsandbar run --json --prepared base --timeout 5m -- sh -c 'pip install -q requests && python3 app.py'\n```\n\nPass the command after `--` as separate arguments. Its data is `{exitCode, stdout, stderr, truncated, sandbox}`, where `sandbox` names the sandbox it created and destroyed.\n\n## Longer work: create, use, remove\n\n```sh\nsandbar create --json --name job-1 --prepared base # prints the name; reuse it everywhere\nsandbar cp ./app.py job-1:/home/user/app.py\nsandbar exec --json job-1 --cwd /home/user -- python3 app.py\nsandbar sh --json job-1 'cd /home/user && ls -la | head'\nsandbar cat job-1:/home/user/out.txt\nsandbar rm --json job-1\n```\n\n- Give sandboxes a `--name` so you can find them again; names are lowercase letters, digits and dashes. `--if-not-exists` reuses a live sandbox with that name instead of failing.\n- `exec` takes an argv after `--`; no shell runs. Use `sh` only when you need pipes, redirects or `&&`. For a script that starts with `-`, put it after `--`: `sandbar sh job-1 -- '-n ...'`.\n- `--env K=V` (repeatable), `--cwd`, `--stdin` or `--stdin-file <path>`, `--timeout 30s|5m|1h`, `--max-output 1m`.\n- `cp` copies one file; one side is `<sandbox>:/absolute/path`. `--no-clobber` refuses to overwrite.\n- `--image <oci-ref>` boots an OCI image where the provider supports it; `--prepared <id>` uses a provider template or snapshot. On providers that need a build first (E2B, Daytona), `--image` fails with exit 4 and `UNSUPPORTED` before creating anything until `sandbar image build <oci-ref>` has run once for that profile; later creates reuse the build. Build once rather than passing `--build` in a loop: a build is slow, may cost money and leaves a provider-side resource. `sandbar image ls` shows the builds; `sandbar image forget <name>` drops a record and lists what the provider still holds.\n- `sandbar ls --json` lists known sandboxes with their live state (`running`, `gone`, `unreachable`, ...). `sandbar inspect --json <name>` shows state, expiry and the saved reference.\n\nAlways `rm` what you created before finishing, even after failures. `rm` only destroys sandboxes this CLI created; never pass `--force` or `rm --all` unless the user asked.\n\n## Exit codes\n\n`exec`, `sh` and `run` exit with the **remote command's** exit code. Their own failures always exit **125**, with the error in the JSON document. A command that ran but reported no exit status is 125 with `OUTCOME_UNKNOWN`; its output is in `error.known.exec`. It is never success.\n\nOther commands:\n\n| Code | Meaning | What to do |\n| ---- | ----------------------------------------------------------------- | ---------------------------------------------------- |\n| 0 | success | |\n| 1 | other failure, including a name already taken | read the message |\n| 2 | usage or local error | fix the command; `sandbar <command> --help` |\n| 3 | not found | `sandbar ls` |\n| 4 | unsupported, adapter or tool not installed, limit reached | tell the user; do not work around it |\n| 5 | authentication, permission, or unreadable password-manager secret | tell the user |\n| 6 | **outcome unknown**: the effect may have happened | **do not retry**; run `sandbar ls` / `inspect` first |\n| 7 | retryable (rate limit, capacity, unavailable) before any effect | wait, then retry once or twice |\n| 8 | partial: some steps happened | read `error.known` and `error.details` |\n\n## When things go wrong\n\n- **Never repeat a create after exit 6 or `outcome: \"unknown\"`.** The store keeps the name in status `unknown`; check `sandbar ls --json`. `sandbar rm <name>` first asks the provider how that create ended (never resending it) and destroys the sandbox if one exists. If it still exits 4 because the outcome is unknown, and the user agrees it is gone, `sandbar rm --force <name>` drops the record.\n- **`run` exits 125 with `CLEANUP_FAILED`**: the command result is in `error.known.exec`, but the sandbox is still running. Tell the user and run the `rm` from the hint.\n- **`rm` exits 8**: `error.details` lists what was `removed`, `skipped` and `failed`. Retry only the failed names once the cause is fixed.\n- **`STORE_WRITE_FAILED`**: the sandbox exists but was not recorded. The reference is in `error.known.reference`; save it to a file and destroy with `sandbar rm @file.json`.\n- Interrupting `run` (Ctrl-C, SIGTERM) still destroys its sandbox and exits 130 (143 for SIGTERM) with `INTERRUPTED`. A second signal exits immediately and may leave it running.\n\n## Discover more\n\n`sandbar schema` prints every command, flag and exit code as JSON. `sandbar <command> --help` shows usage and examples.\n";
1879
+
1880
+ // src/skill.ts
1881
+ var skillName = "sandbar-cli";
1882
+ var marker = "<!-- Installed by `sandbar skill install`; rerun it to update this file. -->";
1883
+ async function exists2(path) {
1884
+ try {
1885
+ await access2(path);
1886
+ return true;
1887
+ } catch {
1888
+ return false;
1889
+ }
1890
+ }
1891
+ async function skillRoots(base) {
1892
+ const roots = [join5(base, ".agents", "skills")];
1893
+ if (await exists2(join5(base, ".claude")))
1894
+ roots.push(join5(base, ".claude", "skills"));
1895
+ return roots;
1896
+ }
1897
+ async function installAt(root, content) {
1898
+ const path = join5(root, skillName, "SKILL.md");
1899
+ let current = null;
1900
+ try {
1901
+ current = await readFile5(path, "utf8");
1902
+ } catch (cause) {
1903
+ if (!(cause instanceof Error && ("code" in cause) && cause.code === "ENOENT"))
1904
+ throw cause;
1905
+ }
1906
+ if (current === content)
1907
+ return { path, result: "unchanged" };
1908
+ if (current !== null && !current.includes(marker))
1909
+ return {
1910
+ path,
1911
+ result: "skipped",
1912
+ reason: "a copy installed another way is already there; remove it to let sandbar manage it"
1913
+ };
1914
+ await mkdir2(dirname3(path), { recursive: true });
1915
+ await writeFile3(path, content);
1916
+ return { path, result: current === null ? "written" : "updated" };
1917
+ }
1918
+ async function skillCommand(session, flags) {
1919
+ const [action, ...extra] = flags.positionals;
1920
+ const { out, env, cwd } = session.ctx;
1921
+ if (extra.length || action !== undefined && action !== "install")
1922
+ throw usage("Usage: sandbar skill [install [--project]]");
1923
+ if (action === undefined) {
1924
+ if (flags.bool("project"))
1925
+ throw usage("--project only applies to `sandbar skill install`");
1926
+ if (out.json)
1927
+ out.success({ name: skillName, content: SKILL_default }, "");
1928
+ else
1929
+ out.streams.stdout(SKILL_default);
1930
+ return 0;
1931
+ }
1932
+ const content = `${SKILL_default.trimEnd()}
1933
+
1934
+ ${marker}
1935
+ `;
1936
+ const base = flags.bool("project") ? cwd : env.HOME || homedir2();
1937
+ const results = await Promise.all((await skillRoots(base)).map((root) => installAt(root, content)));
1938
+ if (results.every((entry) => entry.result === "skipped"))
1939
+ throw new CliError("CONFLICT", `The ${skillName} skill is already installed another way: ${results.map((entry) => entry.path).join(", ")}`, "remove those files and rerun, or update them with the tool that installed them");
1940
+ for (const entry of results)
1941
+ if (entry.reason)
1942
+ out.note(`skipped ${entry.path}: ${entry.reason}`);
1943
+ out.success({ name: skillName, installed: results }, results.filter((entry) => entry.result !== "skipped").map((entry) => `${entry.result.padEnd(9)} ${entry.path}`).join(`
1944
+ `));
1945
+ return 0;
1946
+ }
1947
+
1948
+ // src/completion.ts
1949
+ var NoSpace = 1;
1950
+ var Files = 2;
1951
+ var fileFlags = new Set(["config", "state-dir", "stdin-file"]);
1952
+ var sandboxArguments = new Map([
1953
+ ["inspect", 1],
1954
+ ["exec", 1],
1955
+ ["sh", 1],
1956
+ ["rm", Infinity]
1957
+ ]);
1958
+ async function sandboxNames(session) {
1959
+ const entries = await listAll(await session.store(), "sandbox");
1960
+ return entries.map(({ record }) => ({
1961
+ value: record.name,
1962
+ description: `${record.provider}, ${record.status}`
1963
+ }));
1964
+ }
1965
+ async function profileNames(session) {
1966
+ const loaded = await session.config();
1967
+ return Object.keys(loaded.value.profiles ?? {}).map((name) => ({ value: name }));
1968
+ }
1969
+ function flagCandidates(flags, globals) {
1970
+ return Object.entries({ ...globals, ...flags }).filter(([name]) => name !== "help").map(([name, spec]) => ({ value: `--${name}`, description: spec.description }));
1971
+ }
1972
+ async function complete(words, commands, globals, session) {
1973
+ const current = words.at(-1) ?? "";
1974
+ const before = words.slice(0, -1);
1975
+ const none = { candidates: [], directive: 0 };
1976
+ if (before.includes("--"))
1977
+ return { candidates: [], directive: Files };
1978
+ let command;
1979
+ const positionals = [];
1980
+ for (let index = 0;index < before.length; index++) {
1981
+ const word = before[index];
1982
+ if (word.startsWith("--")) {
1983
+ const spec = { ...globals, ...command?.flags }[word.slice(2)];
1984
+ if (spec?.type === "string" && !word.includes("="))
1985
+ index++;
1986
+ continue;
1987
+ }
1988
+ if (!command)
1989
+ command = commands.find((candidate) => candidate.name === word);
1990
+ else
1991
+ positionals.push(word);
1992
+ }
1993
+ const previous = before.at(-1);
1994
+ const valueOf = previous?.startsWith("--") ? { ...globals, ...command?.flags }[previous.slice(2)] : undefined;
1995
+ if (previous && valueOf?.type === "string") {
1996
+ const flag = previous.slice(2);
1997
+ if (fileFlags.has(flag))
1998
+ return { candidates: [], directive: Files };
1999
+ if (flag === "profile")
2000
+ return { candidates: await profileNames(session), directive: 0 };
2001
+ if (flag === "provider")
2002
+ return {
2003
+ candidates: providers.map((provider) => ({ value: provider.id })),
2004
+ directive: 0
2005
+ };
2006
+ return none;
2007
+ }
2008
+ if (current.startsWith("-"))
2009
+ return { candidates: flagCandidates(command?.flags ?? {}, globals), directive: 0 };
2010
+ if (!command)
2011
+ return {
2012
+ candidates: commands.map((candidate) => ({
2013
+ value: candidate.name,
2014
+ description: candidate.summary
2015
+ })),
2016
+ directive: 0
2017
+ };
2018
+ const limit = sandboxArguments.get(command.name);
2019
+ if (limit !== undefined)
2020
+ return positionals.length < limit ? { candidates: await sandboxNames(session), directive: 0 } : none;
2021
+ if (command.name === "cp" || command.name === "cat") {
2022
+ if (current.includes(":"))
2023
+ return none;
2024
+ const remote2 = (await sandboxNames(session)).map((entry) => ({
2025
+ ...entry,
2026
+ value: `${entry.value}:`
2027
+ }));
2028
+ return { candidates: remote2, directive: NoSpace | (command.name === "cp" ? Files : 0) };
2029
+ }
2030
+ if (command.name === "skill" && positionals.length === 0)
2031
+ return {
2032
+ candidates: [{ value: "install", description: "Install the skill for your agents" }],
2033
+ directive: 0
2034
+ };
2035
+ if (command.name === "image") {
2036
+ if (positionals.length === 0)
2037
+ return { candidates: imageActions.map((action) => ({ value: action })), directive: 0 };
2038
+ if (positionals.length === 1 && (positionals[0] === "inspect" || positionals[0] === "forget")) {
2039
+ const entries = await listAll(await session.store(), "image");
2040
+ return {
2041
+ candidates: entries.map(({ record }) => ({
2042
+ value: record.name,
2043
+ description: `${record.source} on ${record.profile}`
2044
+ })),
2045
+ directive: 0
2046
+ };
2047
+ }
2048
+ return none;
2049
+ }
2050
+ if (command.name === "completion" && positionals.length === 0)
2051
+ return { candidates: shells.map((shell) => ({ value: shell })), directive: 0 };
2052
+ return none;
2053
+ }
2054
+ function formatCompletion(completion) {
2055
+ const lines = completion.candidates.map((candidate) => candidate.description ? `${candidate.value} ${candidate.description.replaceAll(/\s+/g, " ")}` : candidate.value);
2056
+ return `${[...lines, `:${completion.directive}`].join(`
2057
+ `)}
2058
+ `;
2059
+ }
2060
+ var zsh = `#compdef sandbar
2061
+ # sandbar completion for zsh. Enable with: source <(sandbar completion zsh)
2062
+
2063
+ _sandbar() {
2064
+ local -a lines described plain
2065
+ local line value description directive
2066
+ lines=("\${(@f)$(command sandbar __complete -- "\${(@)words[2,CURRENT]}" 2>/dev/null)}")
2067
+ directive=\${lines[-1]#:}
2068
+ lines=("\${(@)lines[1,-2]}")
2069
+
2070
+ for line in "\${lines[@]}"; do
2071
+ [[ -z $line ]] && continue
2072
+ value=\${line%%$'\\t'*}
2073
+ if [[ $line == *$'\\t'* ]]; then
2074
+ description=\${line#*$'\\t'}
2075
+ described+=("\${value//:/\\\\:}:$description")
2076
+ else
2077
+ plain+=("$value")
2078
+ fi
2079
+ done
2080
+
2081
+ local -a suffix
2082
+ (( directive & 1 )) && suffix=(-S '')
2083
+ (( \${#described} )) && _describe -t sandbar sandbar described "\${suffix[@]}"
2084
+ (( \${#plain} )) && compadd "\${suffix[@]}" -a plain
2085
+ (( directive & 2 )) && _files
2086
+ return 0
2087
+ }
2088
+
2089
+ if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
2090
+ _sandbar "$@"
2091
+ else
2092
+ compdef _sandbar sandbar
2093
+ fi
2094
+ `;
2095
+ var bash = `# sandbar completion for bash. Enable with: source <(sandbar completion bash)
2096
+
2097
+ _sandbar() {
2098
+ local line current output directive
2099
+ local -a words candidates
2100
+ # Split the line ourselves so "name:/path" stays one word whatever COMP_WORDBREAKS says.
2101
+ line=\${COMP_LINE:0:COMP_POINT}
2102
+ read -ra words <<< "$line"
2103
+ [[ $line == *" " ]] && words+=("")
2104
+ current=\${words[-1]}
2105
+ output=$(command sandbar __complete -- "\${words[@]:1}" 2>/dev/null) || return 0
2106
+ directive=\${output##*:}
2107
+ output=\${output%:*}
2108
+
2109
+ while IFS= read -r candidate; do
2110
+ [[ -z $candidate ]] && continue
2111
+ candidate=\${candidate%%$'\\t'*}
2112
+ [[ $candidate == "$current"* ]] && candidates+=("$candidate")
2113
+ done <<< "$output"
2114
+
2115
+ if (( directive & 2 )); then
2116
+ local file
2117
+ while IFS= read -r file; do candidates+=("$file"); done < <(compgen -f -- "$current")
2118
+ fi
2119
+
2120
+ # bash replaces only the text after the last ':' when ':' is a word break.
2121
+ if [[ $current == *:* && $COMP_WORDBREAKS == *:* ]]; then
2122
+ local prefix=\${current%:*}:
2123
+ candidates=("\${candidates[@]#"$prefix"}")
2124
+ fi
2125
+
2126
+ COMPREPLY=("\${candidates[@]}")
2127
+ (( directive & 1 )) && compopt -o nospace 2>/dev/null
2128
+ return 0
2129
+ }
2130
+
2131
+ complete -F _sandbar sandbar
2132
+ `;
2133
+ var fish = `# sandbar completion for fish. Enable with: sandbar completion fish | source
2134
+
2135
+ function __sandbar_complete
2136
+ set -l words (commandline -opc)[2..-1] (commandline -ct)
2137
+ set -l output (command sandbar __complete -- $words 2>/dev/null)
2138
+ set -l directive (string replace ':' '' -- $output[-1])
2139
+ string join \\n -- $output[1..-2]
2140
+ if test (math "bitand($directive, 2)") -ne 0
2141
+ __fish_complete_path (commandline -ct)
2142
+ end
2143
+ end
2144
+
2145
+ complete -c sandbar -f -a '(__sandbar_complete)'
2146
+ `;
2147
+ var scripts = { zsh, bash, fish };
2148
+ var shells = Object.keys(scripts);
2149
+ async function completionCommand(session, flags) {
2150
+ const shell = flags.positionals[0];
2151
+ const script = Object.entries(scripts).find(([name]) => name === shell)?.[1];
2152
+ if (flags.positionals.length !== 1 || !script)
2153
+ throw usage(`Usage: sandbar completion <${shells.join("|")}>`, "e.g. source <(sandbar completion zsh)");
2154
+ session.ctx.out.streams.stdout(script);
2155
+ return 0;
2156
+ }
2157
+
2158
+ // src/cli.ts
2159
+ var globalFlags = {
2160
+ json: { type: "boolean", description: "Print one JSON document on stdout (also SANDBAR_JSON=1)" },
2161
+ profile: {
2162
+ type: "string",
2163
+ value: "name",
2164
+ description: "Profile from sandbar.json (also SANDBAR_PROFILE)"
2165
+ },
2166
+ provider: {
2167
+ type: "string",
2168
+ value: "id",
2169
+ description: "Official provider from environment credentials (also SANDBAR_PROVIDER)"
2170
+ },
2171
+ config: {
2172
+ type: "string",
2173
+ value: "path",
2174
+ description: "Config file (default ~/.config/sandbar/sandbar.json, also SANDBAR_CONFIG)"
2175
+ },
2176
+ "state-dir": {
2177
+ type: "string",
2178
+ value: "dir",
2179
+ description: "State store folder (default ~/.config/sandbar/state, also SANDBAR_STATE_DIR)"
2180
+ },
2181
+ help: { type: "boolean", short: "h", description: "Show help" },
2182
+ version: { type: "boolean", short: "V", description: "Print the CLI version" }
2183
+ };
2184
+ var createFlags = {
2185
+ name: {
2186
+ type: "string",
2187
+ value: "name",
2188
+ description: "Store name (default: generated, like sb-1a2b3)"
2189
+ },
2190
+ image: {
2191
+ type: "string",
2192
+ value: "oci-ref",
2193
+ description: "OCI image, e.g. python:3.12; default is the profile's image"
2194
+ },
2195
+ prepared: {
2196
+ type: "string",
2197
+ value: "id",
2198
+ description: "Provider-prepared image id (template, snapshot name)"
2199
+ },
2200
+ network: {
2201
+ type: "string",
2202
+ value: "policy",
2203
+ description: "blocked or internet; default is the profile's policy"
2204
+ },
2205
+ label: { type: "string", multiple: true, value: "k=v", description: "Label to set (repeatable)" },
2206
+ build: {
2207
+ type: "boolean",
2208
+ description: "Build --image now if this provider needs a build and none is recorded"
2209
+ }
2210
+ };
2211
+ var execFlags = {
2212
+ cwd: { type: "string", value: "dir", description: "Working directory in the sandbox" },
2213
+ env: {
2214
+ type: "string",
2215
+ multiple: true,
2216
+ value: "K=V",
2217
+ description: "Environment variable (repeatable)"
2218
+ },
2219
+ stdin: { type: "boolean", description: "Send this process's stdin to the command" },
2220
+ "stdin-file": {
2221
+ type: "string",
2222
+ value: "path",
2223
+ description: "Send a local file as the command's stdin"
2224
+ },
2225
+ timeout: {
2226
+ type: "string",
2227
+ value: "duration",
2228
+ description: "Execution deadline, e.g. 90s or 10m (default 300s)"
2229
+ },
2230
+ "max-output": { type: "string", value: "size", description: "Captured output limit, e.g. 1m" }
2231
+ };
2232
+ var commands = [
2233
+ {
2234
+ name: "create",
2235
+ summary: "Create a sandbox and print its name",
2236
+ usage: "sandbar create [--name <name>] [--image <oci-ref> | --prepared <id>] [--network <policy>]",
2237
+ flags: {
2238
+ ...createFlags,
2239
+ "if-not-exists": {
2240
+ type: "boolean",
2241
+ description: "Return the named sandbox if it is still alive instead of failing"
2242
+ }
2243
+ },
2244
+ passthrough: false,
2245
+ remoteExit: false,
2246
+ examples: [
2247
+ "sandbar create --name web --network internet",
2248
+ "sandbar create --name job-42 --if-not-exists --json"
2249
+ ],
2250
+ run: createCommand
2251
+ },
2252
+ {
2253
+ name: "ls",
2254
+ summary: "List sandboxes in the store with their current state",
2255
+ usage: "sandbar ls [--offline]",
2256
+ flags: {
2257
+ offline: { type: "boolean", description: "Show stored records without asking providers" }
2258
+ },
2259
+ passthrough: false,
2260
+ remoteExit: false,
2261
+ examples: ["sandbar ls", "sandbar ls --json"],
2262
+ run: lsCommand
2263
+ },
2264
+ {
2265
+ name: "inspect",
2266
+ summary: "Show a sandbox's state, expiry and reference",
2267
+ usage: "sandbar inspect <sandbox>",
2268
+ flags: {},
2269
+ passthrough: false,
2270
+ remoteExit: false,
2271
+ examples: ["sandbar inspect web --json"],
2272
+ run: inspectCommand
2273
+ },
2274
+ {
2275
+ name: "rm",
2276
+ summary: "Destroy sandboxes and remove their records",
2277
+ usage: "sandbar rm <sandbox>... | --all [--force]",
2278
+ flags: {
2279
+ all: { type: "boolean", description: "Every sandbox in this store" },
2280
+ force: {
2281
+ type: "boolean",
2282
+ description: "Also destroy sandboxes this CLI did not create, or drop unreachable records"
2283
+ }
2284
+ },
2285
+ passthrough: false,
2286
+ remoteExit: false,
2287
+ examples: ["sandbar rm web", "sandbar rm --all"],
2288
+ run: rmCommand
2289
+ },
2290
+ {
2291
+ name: "exec",
2292
+ summary: "Run a command in a sandbox; exits with the command's exit code",
2293
+ usage: "sandbar exec <sandbox> [--cwd <dir>] [--env K=V]... [--stdin] -- <command> [args...]",
2294
+ flags: execFlags,
2295
+ passthrough: true,
2296
+ remoteExit: true,
2297
+ examples: [
2298
+ "sandbar exec web -- ls -la /",
2299
+ "sandbar exec web --cwd /work --env CI=1 -- npm test"
2300
+ ],
2301
+ run: execCommand
2302
+ },
2303
+ {
2304
+ name: "sh",
2305
+ summary: "Run a shell script in a sandbox; exits with its exit code",
2306
+ usage: "sandbar sh <sandbox> '<script>' | sandbar sh <sandbox> -- '<script>'",
2307
+ flags: execFlags,
2308
+ passthrough: true,
2309
+ remoteExit: true,
2310
+ examples: ["sandbar sh web 'cd /work && make test'", "sandbar sh web -- '-n is fine here'"],
2311
+ run: shCommand
2312
+ },
2313
+ {
2314
+ name: "run",
2315
+ summary: "Create a sandbox, run one command, and always destroy it",
2316
+ usage: "sandbar run [create flags] [exec flags] [--keep] -- <command> [args...]",
2317
+ flags: {
2318
+ ...createFlags,
2319
+ ...execFlags,
2320
+ keep: { type: "boolean", description: "Keep the sandbox and print its name on stderr" }
2321
+ },
2322
+ passthrough: true,
2323
+ remoteExit: true,
2324
+ examples: [
2325
+ "sandbar run -- python3 -c 'print(2 + 2)'",
2326
+ "sandbar run --network internet -- curl -sI https://example.com"
2327
+ ],
2328
+ run: runCommand
2329
+ },
2330
+ {
2331
+ name: "image",
2332
+ summary: "Build OCI images for providers that need it, and list or forget the builds",
2333
+ usage: "sandbar image build <oci-ref> [--name <name>] [--refresh] | ls | inspect <name> | forget <name>",
2334
+ flags: {
2335
+ name: {
2336
+ type: "string",
2337
+ value: "name",
2338
+ description: "Record name for a build (default: from the reference, like python-3-12)"
2339
+ },
2340
+ refresh: { type: "boolean", description: "Build again even if this reference is recorded" }
2341
+ },
2342
+ passthrough: false,
2343
+ remoteExit: false,
2344
+ examples: [
2345
+ "sandbar image build python:3.12 --provider e2b",
2346
+ "sandbar create --image python:3.12 --provider e2b",
2347
+ "sandbar image ls",
2348
+ "sandbar image forget python-3-12"
2349
+ ],
2350
+ run: imageCommand
2351
+ },
2352
+ {
2353
+ name: "cp",
2354
+ summary: "Copy one file to or from a sandbox",
2355
+ usage: "sandbar cp <src> <dst> (one side is <sandbox>:/path)",
2356
+ flags: {
2357
+ "no-clobber": {
2358
+ type: "boolean",
2359
+ description: "Fail instead of overwriting a file in the sandbox"
2360
+ },
2361
+ recursive: {
2362
+ type: "boolean",
2363
+ short: "r",
2364
+ description: "Copy directories (not available yet)"
2365
+ }
2366
+ },
2367
+ passthrough: false,
2368
+ remoteExit: false,
2369
+ examples: ["sandbar cp ./app.py web:/work/app.py", "sandbar cp web:/work/out.json ./out.json"],
2370
+ run: cpCommand
2371
+ },
2372
+ {
2373
+ name: "cat",
2374
+ summary: "Print a file from a sandbox",
2375
+ usage: "sandbar cat <sandbox>:/path",
2376
+ flags: {},
2377
+ passthrough: false,
2378
+ remoteExit: false,
2379
+ examples: ["sandbar cat web:/etc/os-release"],
2380
+ run: catCommand
2381
+ },
2382
+ {
2383
+ name: "doctor",
2384
+ summary: "Check config, credentials and the provider connection without creating anything",
2385
+ usage: "sandbar doctor",
2386
+ flags: {},
2387
+ passthrough: false,
2388
+ remoteExit: false,
2389
+ examples: ["sandbar doctor --provider e2b"],
2390
+ run: doctorCommand
2391
+ },
2392
+ {
2393
+ name: "providers",
2394
+ summary: "List official providers, whether their adapter is installed and their credentials set",
2395
+ usage: "sandbar providers",
2396
+ flags: {},
2397
+ passthrough: false,
2398
+ remoteExit: false,
2399
+ examples: ["sandbar providers --json"],
2400
+ run: providersCommand
2401
+ },
2402
+ {
2403
+ name: "skill",
2404
+ summary: "Print the agent skill for this CLI, or install it for your coding agents",
2405
+ usage: "sandbar skill [install [--project]]",
2406
+ flags: {
2407
+ project: {
2408
+ type: "boolean",
2409
+ description: "Install into this folder's .agents/skills (and .claude/skills) instead of your home"
2410
+ }
2411
+ },
2412
+ passthrough: false,
2413
+ remoteExit: false,
2414
+ examples: ["sandbar skill", "sandbar skill install", "sandbar skill install --project"],
2415
+ run: skillCommand
2416
+ },
2417
+ {
2418
+ name: "completion",
2419
+ summary: "Print a shell completion script for zsh, bash or fish",
2420
+ usage: "sandbar completion <zsh|bash|fish>",
2421
+ flags: {},
2422
+ passthrough: false,
2423
+ remoteExit: false,
2424
+ examples: [
2425
+ "source <(sandbar completion zsh) # in ~/.zshrc",
2426
+ "source <(sandbar completion bash) # in ~/.bashrc",
2427
+ "sandbar completion fish > ~/.config/fish/completions/sandbar.fish"
2428
+ ],
2429
+ run: completionCommand
2430
+ }
2431
+ ];
2432
+ var exitCodes = [
2433
+ { code: 0, meaning: "success" },
2434
+ { code: 1, meaning: "other failure" },
2435
+ { code: 2, meaning: "usage error; fix the command" },
2436
+ { code: 3, meaning: "not found" },
2437
+ {
2438
+ code: 4,
2439
+ meaning: "unsupported by this provider or profile, adapter missing or broken, password-manager CLI not installed, or store limit reached"
2440
+ },
2441
+ {
2442
+ code: 5,
2443
+ meaning: "authentication or permission failure, or an unreadable password-manager secret"
2444
+ },
2445
+ { code: 6, meaning: "outcome unknown: an effect may have happened, inspect before retrying" },
2446
+ { code: 7, meaning: "retryable: rate limit, capacity or unavailable before any effect" },
2447
+ { code: 8, meaning: "partial: some steps were confirmed; read error.known" },
2448
+ { code: 125, meaning: "exec/run: the CLI or provider failed; details in the error" },
2449
+ {
2450
+ code: 130,
2451
+ meaning: "interrupted by SIGINT (129 for SIGHUP, 143 for SIGTERM); an interrupted run still destroys its sandbox"
2452
+ }
2453
+ ];
2454
+ function schema() {
2455
+ return {
2456
+ version: 1,
2457
+ cliVersion: package_default.version,
2458
+ globalFlags,
2459
+ commands: commands.map(({ run: _run, ...spec }) => spec),
2460
+ exitCodes,
2461
+ output: {
2462
+ json: "With --json, stdout holds exactly one document: {ok:true,data} or {ok:false,error:{code,message,retryable,outcome,hint?,known?,details?}}",
2463
+ exec: "exec, sh and run exit with the remote command's exit code; their JSON data is {exitCode,stdout,stderr,truncated}, and run adds sandbox {name,id,kept}"
2464
+ }
2465
+ };
2466
+ }
2467
+ function flagLines(flags) {
2468
+ return Object.entries(flags).map(([name, spec]) => {
2469
+ const label = `${spec.short ? `-${spec.short}, ` : ""}--${name}${spec.value ? ` <${spec.value}>` : ""}`;
2470
+ return ` ${label.padEnd(26)} ${spec.description}`;
2471
+ });
2472
+ }
2473
+ function topHelp() {
2474
+ return [
2475
+ "sandbar: create and manage sandboxes on any Sandbar provider",
2476
+ "",
2477
+ "Usage: sandbar <command> [flags]",
2478
+ "",
2479
+ "Commands:",
2480
+ ...commands.map((command) => ` ${command.name.padEnd(10)} ${command.summary}`),
2481
+ " schema Print a machine-readable description of every command",
2482
+ "",
2483
+ "Global flags:",
2484
+ ...flagLines(globalFlags),
2485
+ "",
2486
+ "Quick start:",
2487
+ " export SANDBAR_PROVIDER=e2b E2B_API_KEY=...",
2488
+ " sandbar create --name web",
2489
+ " sandbar exec web -- uname -a",
2490
+ " sandbar rm web",
2491
+ " sandbar run -- python3 -c 'print(2 + 2)' # create, run, destroy",
2492
+ "",
2493
+ "Run `sandbar <command> --help` for details, `sandbar schema` for JSON."
2494
+ ].join(`
2495
+ `);
2496
+ }
2497
+ function commandHelp(command) {
2498
+ return [
2499
+ command.summary,
2500
+ "",
2501
+ `Usage: ${command.usage}`,
2502
+ ...Object.keys(command.flags).length ? ["", "Flags:", ...flagLines(command.flags)] : [],
2503
+ "",
2504
+ "Examples:",
2505
+ ...command.examples.map((example) => ` ${example}`)
2506
+ ].join(`
2507
+ `);
2508
+ }
2509
+ function parseOption(spec) {
2510
+ const option = { type: spec.type };
2511
+ if (spec.multiple)
2512
+ option.multiple = true;
2513
+ if (spec.short)
2514
+ option.short = spec.short;
2515
+ return option;
2516
+ }
2517
+ function commandIndex(head) {
2518
+ for (let index = 0;index < head.length; index++) {
2519
+ const arg = head[index];
2520
+ if (!arg.startsWith("-"))
2521
+ return index;
2522
+ const spec = Object.entries(globalFlags).find(([flag]) => flag === arg.slice(2))?.[1];
2523
+ if (spec?.type === "string" && !arg.includes("="))
2524
+ index++;
2525
+ }
2526
+ return -1;
2527
+ }
2528
+ async function completeWords(words, io) {
2529
+ const flagValue = (name) => {
2530
+ const index = words.indexOf(`--${name}`);
2531
+ return index === -1 ? undefined : words[index + 1];
2532
+ };
2533
+ const session = new Session({
2534
+ env: io.env,
2535
+ cwd: io.cwd,
2536
+ out: new Output(false, io.streams),
2537
+ signal: io.signal,
2538
+ readStdin: io.readStdin,
2539
+ flags: { json: false, config: flagValue("config"), stateDir: flagValue("state-dir") }
2540
+ });
2541
+ try {
2542
+ io.streams.stdout(formatCompletion(await complete(words, commands, globalFlags, session)));
2543
+ } catch {
2544
+ io.streams.stdout(`:0
2545
+ `);
2546
+ }
2547
+ return 0;
2548
+ }
2549
+ async function main(argv, io) {
2550
+ if (argv[0] === "__complete")
2551
+ return completeWords(argv.slice(argv[1] === "--" ? 2 : 1), io);
2552
+ const separator = argv.indexOf("--");
2553
+ const head = separator === -1 ? argv : argv.slice(0, separator);
2554
+ const rest = separator === -1 ? null : argv.slice(separator + 1);
2555
+ const nameIndex = commandIndex(head);
2556
+ const name = nameIndex === -1 ? undefined : head[nameIndex];
2557
+ const jsonFromEnv = io.env.SANDBAR_JSON === "1" || io.env.SANDBAR_JSON === "true";
2558
+ const out = new Output(jsonFromEnv || head.includes("--json"), io.streams);
2559
+ const command = commands.find((candidate) => candidate.name === name);
2560
+ try {
2561
+ if (!name && (head.includes("--version") || head.includes("-V"))) {
2562
+ out.success({ version: package_default.version }, package_default.version);
2563
+ return 0;
2564
+ }
2565
+ if (!name) {
2566
+ out.streams.stdout(`${topHelp()}
2567
+ `);
2568
+ return head.includes("--help") || head.includes("-h") ? 0 : 2;
2569
+ }
2570
+ if (name === "schema") {
2571
+ out.streams.stdout(`${JSON.stringify(schema(), null, 2)}
2572
+ `);
2573
+ return 0;
2574
+ }
2575
+ if (name === "help") {
2576
+ const topic = commands.find((candidate) => candidate.name === head[1]);
2577
+ out.streams.stdout(`${topic ? commandHelp(topic) : topHelp()}
2578
+ `);
2579
+ return 0;
2580
+ }
2581
+ if (!command) {
2582
+ const close = commands.find((candidate) => candidate.name.startsWith(name.slice(0, 2)));
2583
+ throw usage(`Unknown command ${name}`, close ? `did you mean \`sandbar ${close.name}\`?` : "run `sandbar --help`");
2584
+ }
2585
+ if (rest && !command.passthrough)
2586
+ throw usage(`${command.name} does not take a command after --`);
2587
+ const options = { ...globalFlags, ...command.flags };
2588
+ let parsed;
2589
+ try {
2590
+ parsed = parseArgs({
2591
+ args: head.filter((_, index) => index !== nameIndex),
2592
+ options: Object.fromEntries(Object.entries(options).map(([flag, spec]) => [flag, parseOption(spec)])),
2593
+ allowPositionals: true,
2594
+ strict: true
2595
+ });
2596
+ } catch (cause) {
2597
+ const known = Object.keys(options).map((flag) => `--${flag}`);
2598
+ throw usage(cause instanceof Error ? cause.message.split(`
2599
+ `)[0] : String(cause), `${command.name} accepts: ${known.join(" ")}`);
2600
+ }
2601
+ const flags = new Flags(parsed.values, parsed.positionals, rest);
2602
+ if (flags.bool("help")) {
2603
+ out.streams.stdout(`${commandHelp(command)}
2604
+ `);
2605
+ return 0;
2606
+ }
2607
+ const context = {
2608
+ env: io.env,
2609
+ cwd: io.cwd,
2610
+ out,
2611
+ signal: io.signal,
2612
+ readStdin: io.readStdin,
2613
+ flags: {
2614
+ json: out.json,
2615
+ profile: flags.string("profile"),
2616
+ provider: flags.string("provider"),
2617
+ config: flags.string("config"),
2618
+ stateDir: flags.string("state-dir")
2619
+ }
2620
+ };
2621
+ const session = new Session(context);
2622
+ try {
2623
+ return await command.run(session, flags);
2624
+ } finally {
2625
+ await session.close();
2626
+ }
2627
+ } catch (cause) {
2628
+ const report = describeError(cause);
2629
+ out.failure(report);
2630
+ return command?.remoteExit ? commandExitCode() : managementExitCode(report);
2631
+ }
2632
+ }
2633
+
2634
+ // src/main.ts
2635
+ var controller = new AbortController;
2636
+ var interrupted = false;
2637
+ var signalExitCodes = { SIGINT: 130, SIGHUP: 129, SIGTERM: 143 };
2638
+ var exitCode = 130;
2639
+ for (const [name, code] of Object.entries(signalExitCodes))
2640
+ process.on(name, () => {
2641
+ if (interrupted)
2642
+ process.exit(code);
2643
+ interrupted = true;
2644
+ exitCode = code;
2645
+ controller.abort(new Error(`Interrupted by ${name}`));
2646
+ });
2647
+ async function readStdin() {
2648
+ const chunks = [];
2649
+ for await (const chunk of process.stdin)
2650
+ chunks.push(new Uint8Array(chunk));
2651
+ return new Uint8Array(Buffer.concat(chunks));
2652
+ }
2653
+ var code = await main(process.argv.slice(2), {
2654
+ env: process.env,
2655
+ cwd: process.cwd(),
2656
+ signal: controller.signal,
2657
+ readStdin,
2658
+ streams: {
2659
+ stdout: (text) => process.stdout.write(text),
2660
+ stderr: (text) => process.stderr.write(text)
2661
+ }
2662
+ });
2663
+ process.exitCode = interrupted ? exitCode : code;