@sandbar-sdk/cli 0.4.0 → 0.6.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.
- package/README.md +5 -5
- package/dist/main.js +471 -51
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ The adapter package must be installed where the CLI can find it: in `~/.config/s
|
|
|
30
30
|
| `sh <sandbox> '<script>'` (or `-- '<script>'`) | Run a shell script; same flags as exec |
|
|
31
31
|
| `run [create and exec flags] [--keep] -- cmd...` | Create, run one command, destroy |
|
|
32
32
|
| `image build <oci-ref> [--name] [--refresh]` | Build an OCI image on a provider that needs a build first, and record it |
|
|
33
|
-
| `image ls`, `image inspect <name>`, `image forget <name>` | List, show or drop image records; `
|
|
33
|
+
| `image ls [--remote]`, `image inspect <name>`, `image rm <name> [--force]`, `image forget <name>` | List, show, delete or drop image records; `ls --remote` lists the provider's images, `rm` deletes a built image on the provider (only builds this CLI created unless `--force`), `forget` only drops the record |
|
|
34
34
|
| `cp <src> <dst>` | Copy one file; one side is `<sandbox>:/path` |
|
|
35
35
|
| `cat <sandbox>:/path` | Print a file |
|
|
36
36
|
| `doctor` | Check config, credentials and the connection; creates nothing |
|
|
@@ -69,7 +69,7 @@ source <(sandbar completion bash) # add to ~/.bashrc
|
|
|
69
69
|
sandbar completion fish > ~/.config/fish/completions/sandbar.fish
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
Completion covers commands, flags, `--provider` ids, `--profile` names from `sandbar.json`, sandbox names for `exec`, `sh`, `inspect`, `rm`, `cp` and `cat`, and image record names for `image inspect` and `image forget`. Sandbox names come from the local store, so pressing tab never calls a provider.
|
|
72
|
+
Completion covers commands, flags, `--provider` ids, `--profile` names from `sandbar.json`, sandbox names for `exec`, `sh`, `inspect`, `rm`, `cp` and `cat`, and image record names for `image inspect`, `image rm` and `image forget`. Sandbox names come from the local store, so pressing tab never calls a provider.
|
|
73
73
|
|
|
74
74
|
## Agent skill
|
|
75
75
|
|
|
@@ -93,7 +93,7 @@ The CLI carries an agent skill that matches its own version. `sandbar skill` pri
|
|
|
93
93
|
|
|
94
94
|
- Profile selection: `--profile`, `SANDBAR_PROFILE`, `--provider`, `SANDBAR_PROVIDER`, then `default`.
|
|
95
95
|
- `provider` names an official adapter (`@sandbar-sdk/<provider>`); `config` and `credentials` are that adapter's own settings. `module` and `export` load any adapter, either a definition or a bound adapter.
|
|
96
|
-
- Credentials can be written in the profile directly, as `env:NAME` references, or as password-manager references (below). Keep the file private (`chmod 600`) if it holds keys. Env-only providers read `DAYTONA_API_KEY` and `DAYTONA_TARGET`; `E2B_API_KEY`; `BOXD_API_KEY`, `BOXD_ORG` and `BOXD_NETWORK=internet` (boxd always allows egress, so its adapter requires choosing it); `VERCEL_TOKEN`, `VERCEL_TEAM_ID` and `VERCEL_PROJECT_ID`; `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, `MODAL_APP_NAME` and `MODAL_ENVIRONMENT`.
|
|
96
|
+
- Credentials can be written in the profile directly, as `env:NAME` references, or as password-manager references (below). Keep the file private (`chmod 600`) if it holds keys. Env-only providers read `DAYTONA_API_KEY` and `DAYTONA_TARGET`; `E2B_API_KEY`; `BOXD_API_KEY`, `BOXD_ORG` and `BOXD_NETWORK=internet` (boxd always allows egress, so its adapter requires choosing it); `BOAT_API_KEY` and `BOAT_NETWORK=internet` (likewise for Boat); `VERCEL_TOKEN`, `VERCEL_TEAM_ID` and `VERCEL_PROJECT_ID`; `MODAL_TOKEN_ID`, `MODAL_TOKEN_SECRET`, `MODAL_APP_NAME` and `MODAL_ENVIRONMENT`.
|
|
97
97
|
- `limits.maxSandboxes` (or `SANDBAR_MAX_SANDBOXES`) caps how many sandboxes the store may hold. Unset means uncapped.
|
|
98
98
|
|
|
99
99
|
## Password managers
|
|
@@ -127,7 +127,7 @@ If the tool is not installed the command exits 4 (`SECRET_TOOL_MISSING`); if it
|
|
|
127
127
|
Pass `--build` to build a missing image during `create` or `run`, or set `"images": { "autoBuild": true }` in `sandbar.json` to always do so. Each reference is built once per profile either way.
|
|
128
128
|
- Vercel takes neither and rejects `--image`.
|
|
129
129
|
|
|
130
|
-
`sandbar image build` records the reference as typed, the profile, the prepared image (bound to the provider account that built it) and the provider-side resources it left. Running it again for a recorded reference does nothing unless you pass `--refresh`, which builds again, for example after a mutable tag such as `python:3.12` moved; pin a digest for an exact image. `image forget` removes the record and prints the resources the provider still holds
|
|
130
|
+
`sandbar image build` records the reference as typed, the profile, the prepared image (bound to the provider account that built it) and the provider-side resources it left. Running it again for a recorded reference does nothing unless you pass `--refresh`, which builds again, for example after a mutable tag such as `python:3.12` moved; pin a digest for an exact image. `image rm <name>` deletes a built image on the provider through `images.delete` and then drops the record; it works where the provider supports image deletion (E2B and Daytona) and exits 4 with `UNSUPPORTED` elsewhere. Without `--force` it only deletes builds this CLI verifiably created, so a build recovered from an older record or of unknown ownership needs `--force`. It refuses, with no effect, a record without a confirmed build, a record that shares the same prepared image, and an image the provider reports as public, shared or still in use. An image that is already gone counts as deleted. If the delete's outcome is unknown, `rm` exits 6, keeps the record in a `deleting` state with its recovery reference, and the next `image rm` asks the provider how the original request ended instead of sending it again. If the provider gives no operation to check, the record stays built and `rm` says so; check the provider before running it again, which sends a new delete. If that check itself fails (for example the profile now reaches another account), the record stays `deleting` with its recovery reference, so a later `image rm` from the original account can still check it. `image forget` removes the record without deleting anything and prints the resources the provider still holds. `image ls --remote` lists the images the provider reports for the profile (up to 1,000), marks the ones with a local record, and says how complete the provider's listing is; a listed image is not proof that you own it. A build whose outcome is unknown (exit 6) keeps its record, and `image inspect` asks the provider again how it ended. Providers do not report a resolved digest, so records do not have one.
|
|
131
131
|
|
|
132
132
|
## State store
|
|
133
133
|
|
|
@@ -135,7 +135,7 @@ Sandbox references are kept as one JSON file per sandbox in `~/.config/sandbar/s
|
|
|
135
135
|
|
|
136
136
|
## Not yet
|
|
137
137
|
|
|
138
|
-
Planned for later steps: `--lifetime`/`--idle`/`--port` create flags, recursive `cp`, detached processes and logs, preview URLs, suspend/resume/renew, `gc`,
|
|
138
|
+
Planned for later steps: `--lifetime`/`--idle`/`--port` create flags, recursive `cp`, detached processes and logs, preview URLs, suspend/resume/renew, `gc`, snapshots and volumes, `sandbar adapter add`, an S3-compatible store, KeePassXC and system-keyring references, and MCP mode.
|
|
139
139
|
|
|
140
140
|
## Development
|
|
141
141
|
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
// package.json
|
|
6
6
|
var package_default = {
|
|
7
7
|
name: "@sandbar-sdk/cli",
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.6.0",
|
|
9
9
|
private: false,
|
|
10
10
|
type: "module",
|
|
11
11
|
description: "Agent-first command line for creating and managing Sandbar sandboxes.",
|
|
@@ -30,6 +30,7 @@ var package_default = {
|
|
|
30
30
|
zod: "4.6.5"
|
|
31
31
|
},
|
|
32
32
|
devDependencies: {
|
|
33
|
+
"@sandbar-sdk/boat": "workspace:*",
|
|
33
34
|
"@sandbar-sdk/boxd": "workspace:*",
|
|
34
35
|
"@sandbar-sdk/daytona": "workspace:*",
|
|
35
36
|
"@sandbar-sdk/e2b": "workspace:*",
|
|
@@ -59,9 +60,10 @@ import {
|
|
|
59
60
|
Image as Image2,
|
|
60
61
|
NonzeroExitError,
|
|
61
62
|
ResourceReference as ResourceReference2,
|
|
62
|
-
SandbarError as SandbarError3
|
|
63
|
+
SandbarError as SandbarError3,
|
|
64
|
+
assertResourceScope
|
|
63
65
|
} from "sandbar-sdk";
|
|
64
|
-
import { z as
|
|
66
|
+
import { z as z5 } from "zod";
|
|
65
67
|
|
|
66
68
|
// src/config.ts
|
|
67
69
|
import { readFile } from "node:fs/promises";
|
|
@@ -316,6 +318,7 @@ function resolveEnvReferences(value, env, where) {
|
|
|
316
318
|
|
|
317
319
|
// src/images.ts
|
|
318
320
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
321
|
+
import { z as z3 } from "zod";
|
|
319
322
|
import {
|
|
320
323
|
Image,
|
|
321
324
|
SandbarError as SandbarError2
|
|
@@ -339,6 +342,7 @@ var SandboxRecord = z2.strictObject({
|
|
|
339
342
|
labels: z2.record(z2.string(), z2.string()),
|
|
340
343
|
image: z2.string().optional(),
|
|
341
344
|
reference: ResourceReference.extend({ kind: z2.literal("sandbox") }).nullable(),
|
|
345
|
+
adopted: z2.literal(true).optional(),
|
|
342
346
|
recovery: z2.custom((value) => z2.record(z2.string(), z2.unknown()).safeParse(value).success).optional()
|
|
343
347
|
});
|
|
344
348
|
var RetainedResource = z2.strictObject({
|
|
@@ -351,7 +355,7 @@ var ImageRecord = z2.strictObject({
|
|
|
351
355
|
format: z2.literal(1),
|
|
352
356
|
kind: z2.literal("image"),
|
|
353
357
|
name: RecordName,
|
|
354
|
-
status: z2.enum(["building", "built", "unknown"]),
|
|
358
|
+
status: z2.enum(["building", "built", "unknown", "deleting"]),
|
|
355
359
|
profile: z2.string().min(1),
|
|
356
360
|
provider: z2.string().min(1),
|
|
357
361
|
createdAt: z2.iso.datetime({ offset: true }),
|
|
@@ -363,6 +367,7 @@ var ImageRecord = z2.strictObject({
|
|
|
363
367
|
scope: ResourceReference.shape.scope
|
|
364
368
|
}).nullable(),
|
|
365
369
|
retainedResources: z2.array(RetainedResource),
|
|
370
|
+
reference: ResourceReference.extend({ kind: z2.literal("image") }).optional(),
|
|
366
371
|
recovery: z2.custom((value) => z2.record(z2.string(), z2.unknown()).safeParse(value).success).optional()
|
|
367
372
|
});
|
|
368
373
|
var schemas = { sandbox: SandboxRecord, image: ImageRecord };
|
|
@@ -526,16 +531,21 @@ function generatedName() {
|
|
|
526
531
|
}
|
|
527
532
|
|
|
528
533
|
// src/images.ts
|
|
529
|
-
var BuildResult = ImageRecord.pick({
|
|
534
|
+
var BuildResult = ImageRecord.pick({
|
|
535
|
+
prepared: true,
|
|
536
|
+
retainedResources: true,
|
|
537
|
+
reference: true
|
|
538
|
+
}).extend({
|
|
530
539
|
prepared: ImageRecord.shape.prepared.unwrap()
|
|
531
540
|
});
|
|
541
|
+
var DeleteResult = z3.looseObject({ deleted: z3.literal(true) });
|
|
532
542
|
function derivedName(source) {
|
|
533
543
|
const base = (source.split("/").at(-1) ?? source).toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 57).replace(/^-+|-+$/g, "");
|
|
534
544
|
return RecordName.safeParse(base).success ? base : "image";
|
|
535
545
|
}
|
|
536
546
|
async function findBuild(store, profile, source) {
|
|
537
547
|
const entries = await listAll(store, "image");
|
|
538
|
-
return entries.find(({ record }) => record.profile === profile && record.source === source && record.prepared);
|
|
548
|
+
return entries.find(({ record }) => record.profile === profile && record.source === source && record.prepared && record.status !== "deleting");
|
|
539
549
|
}
|
|
540
550
|
function mergeRetained(previous, next) {
|
|
541
551
|
const seen = new Set(previous.map((item) => `${item.kind}/${item.id}`));
|
|
@@ -609,7 +619,8 @@ async function buildImage(session, client, profile, source, options) {
|
|
|
609
619
|
status: "built",
|
|
610
620
|
createdAt: new Date().toISOString(),
|
|
611
621
|
prepared: result.prepared,
|
|
612
|
-
retainedResources: mergeRetained(claimed.record.retainedResources, result.retainedResources)
|
|
622
|
+
retainedResources: mergeRetained(claimed.record.retainedResources, result.retainedResources),
|
|
623
|
+
reference: result.reference
|
|
613
624
|
};
|
|
614
625
|
try {
|
|
615
626
|
const { version } = await store.update("image", record.name, record, claimed.version);
|
|
@@ -715,7 +726,12 @@ async function inspectImage(session, args) {
|
|
|
715
726
|
`built ${record.createdAt}`,
|
|
716
727
|
...record.retainedResources.length ? ["retained", ...retainedLines(record)] : []
|
|
717
728
|
];
|
|
718
|
-
session.ctx.out.success({
|
|
729
|
+
session.ctx.out.success({
|
|
730
|
+
...summary(record),
|
|
731
|
+
preparedImage: record.prepared,
|
|
732
|
+
reference: record.reference ?? null,
|
|
733
|
+
recovery: record.recovery ?? null
|
|
734
|
+
}, lines.join(`
|
|
719
735
|
`));
|
|
720
736
|
return 0;
|
|
721
737
|
}
|
|
@@ -727,25 +743,171 @@ async function forgetImage(session, args) {
|
|
|
727
743
|
await store.remove("image", record.name, version);
|
|
728
744
|
const lines = [`forgot ${record.name}`];
|
|
729
745
|
if (record.retainedResources.length)
|
|
730
|
-
lines.push(`${record.provider} still holds these;
|
|
746
|
+
lines.push(`${record.provider} still holds these; forget never deletes them (\`sandbar image rm\` does, where the provider supports it):`, ...retainedLines(record));
|
|
731
747
|
session.ctx.out.success({ name: record.name, result: "forgotten", retainedResources: record.retainedResources }, lines.join(`
|
|
732
748
|
`));
|
|
733
749
|
return 0;
|
|
734
750
|
}
|
|
735
|
-
|
|
751
|
+
function recordReference(record) {
|
|
752
|
+
if (record.reference)
|
|
753
|
+
return record.reference;
|
|
754
|
+
if (!record.prepared)
|
|
755
|
+
return null;
|
|
756
|
+
return {
|
|
757
|
+
version: 1,
|
|
758
|
+
kind: "image",
|
|
759
|
+
provider: record.prepared.provider,
|
|
760
|
+
scope: structuredClone(record.prepared.scope),
|
|
761
|
+
nativeId: record.prepared.value,
|
|
762
|
+
ownership: "unknown"
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
async function forgetDeleted(store, entry) {
|
|
766
|
+
const current = await store.get("image", entry.record.name);
|
|
767
|
+
if (current?.version === entry.version)
|
|
768
|
+
await store.remove("image", entry.record.name, entry.version);
|
|
769
|
+
}
|
|
770
|
+
function deletedOutput(session, record, result) {
|
|
771
|
+
const prepared = record.prepared?.value ?? null;
|
|
772
|
+
const others = record.retainedResources.filter((item) => item.id !== prepared);
|
|
773
|
+
const lines = [result === "gone" ? `${record.name} was already gone` : `deleted ${record.name}`];
|
|
774
|
+
if (others.length)
|
|
775
|
+
lines.push(`${record.provider} still holds these from earlier builds:`, ...retainedLines({ ...record, retainedResources: others }));
|
|
776
|
+
session.ctx.out.success({ name: record.name, result, prepared, retainedResources: others }, lines.join(`
|
|
777
|
+
`));
|
|
778
|
+
return 0;
|
|
779
|
+
}
|
|
780
|
+
async function rmImage(session, args, force) {
|
|
781
|
+
if (args.length !== 1)
|
|
782
|
+
throw usage("Expected exactly one image name");
|
|
783
|
+
const store = await session.store();
|
|
784
|
+
const entry = await recoverBuild(session, store, await findImage(store, args[0]));
|
|
785
|
+
const { record } = entry;
|
|
786
|
+
const client = await session.connect(await session.profileNamed(record.profile));
|
|
787
|
+
if (record.status === "deleting") {
|
|
788
|
+
if (record.recovery?.kind !== "image_delete") {
|
|
789
|
+
const { recovery: _recovery, ...rest } = record;
|
|
790
|
+
await store.update("image", record.name, { ...rest, status: "built" }, entry.version);
|
|
791
|
+
throw new CliError("OUTCOME_UNKNOWN", `${record.name} has no saved delete to check`, `the store keeps ${record.name} as built; check the provider (\`sandbar image ls --remote\`) before running \`sandbar image rm ${record.name}\` again, which sends a new delete`, { image: record.name });
|
|
792
|
+
}
|
|
793
|
+
let value;
|
|
794
|
+
try {
|
|
795
|
+
value = await (await client.recover(record.recovery)).observe();
|
|
796
|
+
} catch (cause) {
|
|
797
|
+
if (!(cause instanceof SandbarError2))
|
|
798
|
+
throw cause;
|
|
799
|
+
if (cause.effect === "none")
|
|
800
|
+
throw new CliError(cause.code, cause.message, `the store keeps ${record.name} deleting; run \`sandbar image rm ${record.name}\` again with profile ${record.profile} connected to the account the delete was sent from`, { image: record.name, recovery: record.recovery });
|
|
801
|
+
}
|
|
802
|
+
if (!DeleteResult.safeParse(value).success)
|
|
803
|
+
throw new CliError("OUTCOME_UNKNOWN", `The delete of ${record.name} is still unconfirmed`, `the store keeps ${record.name}; run \`sandbar image rm ${record.name}\` again later`, { image: record.name, recovery: record.recovery });
|
|
804
|
+
await forgetDeleted(store, entry);
|
|
805
|
+
return deletedOutput(session, record, "deleted");
|
|
806
|
+
}
|
|
807
|
+
const reference = recordReference(record);
|
|
808
|
+
if (record.status !== "built" || !reference)
|
|
809
|
+
throw new CliError("CONFLICT", `${record.name} has no confirmed build to delete`, "`sandbar image inspect` checks an uncertain build; `sandbar image forget` drops the record");
|
|
810
|
+
if (reference.ownership !== "verified-created" && !force)
|
|
811
|
+
throw new CliError("FORBIDDEN", `${record.name} was not created by this CLI (ownership ${reference.ownership})`, "pass --force to delete it anyway", { image: record.name });
|
|
812
|
+
const capabilities = await client.capabilities();
|
|
813
|
+
if (capabilities.imageManagement.delete.status !== "supported")
|
|
814
|
+
throw new CliError("UNSUPPORTED", `${record.provider} cannot delete images through Sandbar`, "`sandbar image forget` drops the record; delete the image with the provider's own tools");
|
|
815
|
+
const shared = (await listAll(store, "image")).find((other) => other.record.name !== record.name && other.record.profile === record.profile && other.record.prepared?.value === reference.nativeId);
|
|
816
|
+
if (shared)
|
|
817
|
+
throw new CliError("CONFLICT", `${record.name} and ${shared.record.name} record the same provider image`, "forget one record first, so the other is not left pointing at a deleted image");
|
|
818
|
+
try {
|
|
819
|
+
await client.images.delete(reference, { signal: session.ctx.signal });
|
|
820
|
+
} catch (cause) {
|
|
821
|
+
const report = describeError(cause);
|
|
822
|
+
if (report.outcome === "rejected" && report.code === "NOT_FOUND") {
|
|
823
|
+
await forgetDeleted(store, entry);
|
|
824
|
+
return deletedOutput(session, record, "gone");
|
|
825
|
+
}
|
|
826
|
+
if (report.outcome === "rejected")
|
|
827
|
+
throw cause;
|
|
828
|
+
const recovery = report.known?.recovery;
|
|
829
|
+
if (!recovery)
|
|
830
|
+
throw new CliError("OUTCOME_UNKNOWN", report.message, `the store keeps ${record.name} as built and there is no saved delete to check; check the provider (\`sandbar image ls --remote\`) before running \`sandbar image rm ${record.name}\` again, which sends a new delete`, { image: record.name });
|
|
831
|
+
let kept = `the store keeps ${record.name}; run \`sandbar image rm ${record.name}\` again to check`;
|
|
832
|
+
try {
|
|
833
|
+
await store.update("image", record.name, { ...record, status: "deleting", recovery }, entry.version);
|
|
834
|
+
} catch (storeCause) {
|
|
835
|
+
kept = `the store could not record it (${describeError(storeCause).message}); keep the reference below`;
|
|
836
|
+
}
|
|
837
|
+
throw new CliError("OUTCOME_UNKNOWN", report.message, kept, {
|
|
838
|
+
image: record.name,
|
|
839
|
+
recovery
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
await forgetDeleted(store, entry);
|
|
843
|
+
return deletedOutput(session, record, "deleted");
|
|
844
|
+
}
|
|
845
|
+
function remoteRow(info, records) {
|
|
846
|
+
const recorded = records.find(({ record }) => record.prepared?.value === info.reference.nativeId)?.record.name;
|
|
847
|
+
return {
|
|
848
|
+
id: info.reference.nativeId,
|
|
849
|
+
state: info.state,
|
|
850
|
+
visibility: info.visibility,
|
|
851
|
+
source: info.source?.value ?? null,
|
|
852
|
+
createdAt: info.createdAt,
|
|
853
|
+
record: recorded ?? null,
|
|
854
|
+
reference: info.reference
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
async function lsRemote(session) {
|
|
858
|
+
const profile = await session.profile();
|
|
859
|
+
const client = await session.connect(profile);
|
|
860
|
+
const capabilities = await client.capabilities();
|
|
861
|
+
if (capabilities.imageManagement.list.status !== "supported")
|
|
862
|
+
throw new CliError("UNSUPPORTED", `${profile.provider} cannot list images through Sandbar`, "`sandbar image ls` shows the builds this store recorded");
|
|
863
|
+
const records = (await listAll(await session.store(), "image")).filter(({ record }) => record.profile === profile.name);
|
|
864
|
+
const rows = [];
|
|
865
|
+
let cursor;
|
|
866
|
+
let coverage = capabilities.imageManagement.list.value.coverage;
|
|
867
|
+
for (let page = 0;page < 10; page++) {
|
|
868
|
+
const result = await client.images.list({ limit: 100, cursor });
|
|
869
|
+
coverage = result.coverage;
|
|
870
|
+
rows.push(...result.items.map((info) => remoteRow(info, records)));
|
|
871
|
+
cursor = result.nextCursor;
|
|
872
|
+
if (!cursor)
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
if (cursor)
|
|
876
|
+
session.ctx.out.note(`showing the first ${rows.length} images; more exist`);
|
|
877
|
+
const lines = rows.map((row) => [
|
|
878
|
+
row.id.padEnd(28),
|
|
879
|
+
row.state.padEnd(9),
|
|
880
|
+
row.visibility.padEnd(10),
|
|
881
|
+
(row.record ?? "-").padEnd(20),
|
|
882
|
+
row.source ?? "-"
|
|
883
|
+
].join(" "));
|
|
884
|
+
session.ctx.out.success({ profile: profile.name, coverage, images: rows, truncated: !!cursor }, rows.length ? [
|
|
885
|
+
`${"ID".padEnd(28)} ${"STATE".padEnd(9)} ${"VISIBILITY".padEnd(10)} ${"RECORD".padEnd(20)} SOURCE`,
|
|
886
|
+
...lines
|
|
887
|
+
].join(`
|
|
888
|
+
`) : `no images on ${profile.provider} for profile ${profile.name}`);
|
|
889
|
+
return 0;
|
|
890
|
+
}
|
|
891
|
+
var imageActions = ["build", "ls", "inspect", "rm", "forget"];
|
|
736
892
|
async function imageCommand(session, flags) {
|
|
737
893
|
const [action, ...args] = flags.positionals;
|
|
738
894
|
if ((flags.string("name") || flags.bool("refresh")) && action !== "build")
|
|
739
895
|
throw usage("--name and --refresh only apply to `sandbar image build`");
|
|
896
|
+
if (flags.bool("remote") && action !== "ls")
|
|
897
|
+
throw usage("--remote only applies to `sandbar image ls`");
|
|
898
|
+
if (flags.bool("force") && action !== "rm")
|
|
899
|
+
throw usage("--force only applies to `sandbar image rm`");
|
|
740
900
|
switch (action) {
|
|
741
901
|
case "build":
|
|
742
902
|
return buildCommand(session, flags, args);
|
|
743
903
|
case "ls":
|
|
744
904
|
if (args.length)
|
|
745
905
|
throw usage("image ls takes no arguments");
|
|
746
|
-
return lsImages(session);
|
|
906
|
+
return flags.bool("remote") ? lsRemote(session) : lsImages(session);
|
|
747
907
|
case "inspect":
|
|
748
908
|
return inspectImage(session, args);
|
|
909
|
+
case "rm":
|
|
910
|
+
return rmImage(session, args, flags.bool("force"));
|
|
749
911
|
case "forget":
|
|
750
912
|
return forgetImage(session, args);
|
|
751
913
|
default:
|
|
@@ -761,7 +923,7 @@ import {
|
|
|
761
923
|
bindAdapter
|
|
762
924
|
} from "@sandbar-sdk/adapter";
|
|
763
925
|
import { Sandbar } from "sandbar-sdk";
|
|
764
|
-
import { z as
|
|
926
|
+
import { z as z4 } from "zod";
|
|
765
927
|
var providers = [
|
|
766
928
|
{
|
|
767
929
|
id: "daytona",
|
|
@@ -790,6 +952,16 @@ var providers = [
|
|
|
790
952
|
network: { env: "BOXD_NETWORK", required: true }
|
|
791
953
|
}
|
|
792
954
|
},
|
|
955
|
+
{
|
|
956
|
+
id: "boat",
|
|
957
|
+
module: "@sandbar-sdk/boat",
|
|
958
|
+
export: "boatAdapter",
|
|
959
|
+
experimental: true,
|
|
960
|
+
credentials: { apiKey: "BOAT_API_KEY" },
|
|
961
|
+
config: {
|
|
962
|
+
network: { env: "BOAT_NETWORK", required: true }
|
|
963
|
+
}
|
|
964
|
+
},
|
|
793
965
|
{
|
|
794
966
|
id: "vercel",
|
|
795
967
|
module: "@sandbar-sdk/vercel",
|
|
@@ -857,7 +1029,7 @@ function fromProfile(name, profile, loaded, env) {
|
|
|
857
1029
|
}),
|
|
858
1030
|
...Object.entries(profile.credentials ?? {}).map(([key, value]) => [
|
|
859
1031
|
key,
|
|
860
|
-
|
|
1032
|
+
z4.string().parse(resolveEnvReferences(value, env, `${where}.credentials.${key}`))
|
|
861
1033
|
])
|
|
862
1034
|
]);
|
|
863
1035
|
const config = Object.fromEntries([
|
|
@@ -886,9 +1058,9 @@ function fromProfile(name, profile, loaded, env) {
|
|
|
886
1058
|
credentials
|
|
887
1059
|
};
|
|
888
1060
|
}
|
|
889
|
-
var PackageManifest =
|
|
890
|
-
main:
|
|
891
|
-
exports:
|
|
1061
|
+
var PackageManifest = z4.object({
|
|
1062
|
+
main: z4.string().optional(),
|
|
1063
|
+
exports: z4.json().optional()
|
|
892
1064
|
});
|
|
893
1065
|
async function exists(path) {
|
|
894
1066
|
try {
|
|
@@ -900,10 +1072,10 @@ async function exists(path) {
|
|
|
900
1072
|
}
|
|
901
1073
|
var importConditions = new Set(["import", "node", "default"]);
|
|
902
1074
|
function exportTarget(value) {
|
|
903
|
-
const plain =
|
|
1075
|
+
const plain = z4.string().safeParse(value);
|
|
904
1076
|
if (plain.success)
|
|
905
1077
|
return plain.data;
|
|
906
|
-
const list =
|
|
1078
|
+
const list = z4.array(z4.json()).safeParse(value);
|
|
907
1079
|
if (list.success) {
|
|
908
1080
|
for (const item of list.data) {
|
|
909
1081
|
const target = exportTarget(item);
|
|
@@ -912,7 +1084,7 @@ function exportTarget(value) {
|
|
|
912
1084
|
}
|
|
913
1085
|
return null;
|
|
914
1086
|
}
|
|
915
|
-
const conditions =
|
|
1087
|
+
const conditions = z4.record(z4.string(), z4.json()).safeParse(value);
|
|
916
1088
|
if (!conditions.success)
|
|
917
1089
|
return null;
|
|
918
1090
|
for (const [condition, nested] of Object.entries(conditions.data)) {
|
|
@@ -928,7 +1100,7 @@ function rootEntry(manifest) {
|
|
|
928
1100
|
const exports = manifest.exports;
|
|
929
1101
|
if (exports === undefined || exports === null)
|
|
930
1102
|
return manifest.main || "index.js";
|
|
931
|
-
const subpaths =
|
|
1103
|
+
const subpaths = z4.record(z4.string(), z4.json()).safeParse(exports);
|
|
932
1104
|
const root = subpaths.success && Object.keys(subpaths.data).some((key) => key.startsWith(".")) ? subpaths.data["."] : exports;
|
|
933
1105
|
return (root === undefined ? null : exportTarget(root)) ?? manifest.main ?? "index.js";
|
|
934
1106
|
}
|
|
@@ -969,7 +1141,7 @@ async function resolveAdapterModule(profile, cwd, env) {
|
|
|
969
1141
|
function isAdapter(value) {
|
|
970
1142
|
return value instanceof Object && "name" in value && "connect" in value && "config" in value;
|
|
971
1143
|
}
|
|
972
|
-
var AdapterExport =
|
|
1144
|
+
var AdapterExport = z4.custom(isAdapter);
|
|
973
1145
|
async function adapterMissing(target, spec, message) {
|
|
974
1146
|
if (target.startsWith("file:"))
|
|
975
1147
|
return !await exists(fileURLToPath(target));
|
|
@@ -977,7 +1149,7 @@ async function adapterMissing(target, spec, message) {
|
|
|
977
1149
|
}
|
|
978
1150
|
async function loadAdapter(profile, cwd, env, secrets) {
|
|
979
1151
|
const target = await resolveAdapterModule(profile, cwd, env);
|
|
980
|
-
const schema =
|
|
1152
|
+
const schema = z4.object({ [profile.export]: AdapterExport });
|
|
981
1153
|
let parsed;
|
|
982
1154
|
try {
|
|
983
1155
|
parsed = schema.safeParse(await import(target));
|
|
@@ -997,7 +1169,7 @@ async function loadAdapter(profile, cwd, env, secrets) {
|
|
|
997
1169
|
return exported;
|
|
998
1170
|
const where = `profile ${profile.name}`;
|
|
999
1171
|
const config = await secrets.resolve(profile.config, `${where}.config`);
|
|
1000
|
-
const credentials =
|
|
1172
|
+
const credentials = z4.record(z4.string(), z4.string()).parse(await secrets.resolve(profile.credentials, `${where}.credentials`));
|
|
1001
1173
|
return bindAdapter(exported, config, credentials);
|
|
1002
1174
|
}
|
|
1003
1175
|
async function connect(profile, cwd, env, secrets) {
|
|
@@ -1053,16 +1225,16 @@ class Flags {
|
|
|
1053
1225
|
this.rest = rest;
|
|
1054
1226
|
}
|
|
1055
1227
|
string(name) {
|
|
1056
|
-
return
|
|
1228
|
+
return z5.string().optional().parse(this.values[name]);
|
|
1057
1229
|
}
|
|
1058
1230
|
strings(name) {
|
|
1059
|
-
return
|
|
1231
|
+
return z5.array(z5.string()).optional().parse(this.values[name]) ?? [];
|
|
1060
1232
|
}
|
|
1061
1233
|
bool(name) {
|
|
1062
|
-
return
|
|
1234
|
+
return z5.boolean().optional().parse(this.values[name]) ?? false;
|
|
1063
1235
|
}
|
|
1064
1236
|
}
|
|
1065
|
-
function createRequest(flags) {
|
|
1237
|
+
function createRequest(flags, env = {}) {
|
|
1066
1238
|
if (flags.string("image") && flags.string("prepared"))
|
|
1067
1239
|
throw usage("Pass either --image or --prepared, not both");
|
|
1068
1240
|
if (flags.bool("build") && !flags.string("image"))
|
|
@@ -1074,14 +1246,89 @@ function createRequest(flags) {
|
|
|
1074
1246
|
image: flags.string("image"),
|
|
1075
1247
|
prepared: flags.string("prepared"),
|
|
1076
1248
|
network: flags.string("network"),
|
|
1077
|
-
labels: parsePairs(flags.strings("label"), "label")
|
|
1249
|
+
labels: parsePairs(flags.strings("label"), "label"),
|
|
1250
|
+
env
|
|
1078
1251
|
};
|
|
1079
1252
|
}
|
|
1253
|
+
var EnvName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1254
|
+
var escapes = new Map([
|
|
1255
|
+
["n", `
|
|
1256
|
+
`],
|
|
1257
|
+
['"', '"'],
|
|
1258
|
+
["\\", "\\"]
|
|
1259
|
+
]);
|
|
1260
|
+
function envFileValue(text) {
|
|
1261
|
+
const value = text.trimStart();
|
|
1262
|
+
const quote = value[0];
|
|
1263
|
+
if (quote !== '"' && quote !== "'")
|
|
1264
|
+
return { value: text.replace(/\s#.*$/s, "").trim() };
|
|
1265
|
+
let parsed = "";
|
|
1266
|
+
let end = -1;
|
|
1267
|
+
for (let at = 1;at < value.length; at++) {
|
|
1268
|
+
const char = value[at];
|
|
1269
|
+
if (char === quote) {
|
|
1270
|
+
end = at;
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
if (quote === '"' && char === "\\" && at + 1 < value.length) {
|
|
1274
|
+
const next = value[++at];
|
|
1275
|
+
parsed += escapes.get(next) ?? `\\${next}`;
|
|
1276
|
+
} else
|
|
1277
|
+
parsed += char;
|
|
1278
|
+
}
|
|
1279
|
+
if (end < 0)
|
|
1280
|
+
return { error: "has an unterminated quoted value; multi-line values are not supported" };
|
|
1281
|
+
if (!/^\s*(#.*)?$/s.test(value.slice(end + 1)))
|
|
1282
|
+
return { error: "has text after the closing quote" };
|
|
1283
|
+
return { value: parsed };
|
|
1284
|
+
}
|
|
1285
|
+
async function envFlags(session, flags) {
|
|
1286
|
+
const env = {};
|
|
1287
|
+
for (const file of flags.strings("env-file")) {
|
|
1288
|
+
const path = resolve3(session.ctx.cwd, file);
|
|
1289
|
+
let text;
|
|
1290
|
+
try {
|
|
1291
|
+
text = await readFile4(path, "utf8");
|
|
1292
|
+
} catch {
|
|
1293
|
+
throw usage(`--env-file ${file} could not be read`);
|
|
1294
|
+
}
|
|
1295
|
+
for (const [index, raw] of text.split(/\r?\n/).entries()) {
|
|
1296
|
+
const line = raw.trim();
|
|
1297
|
+
if (!line || line.startsWith("#"))
|
|
1298
|
+
continue;
|
|
1299
|
+
const assignment = line.startsWith("export ") ? line.slice(7).trimStart() : line;
|
|
1300
|
+
const split = assignment.indexOf("=");
|
|
1301
|
+
const name = split > 0 ? assignment.slice(0, split).trim() : "";
|
|
1302
|
+
if (!EnvName.test(name))
|
|
1303
|
+
throw usage(`--env-file ${file}:${index + 1} must be NAME=VALUE with a valid NAME`);
|
|
1304
|
+
const parsed = envFileValue(assignment.slice(split + 1));
|
|
1305
|
+
if ("error" in parsed)
|
|
1306
|
+
throw usage(`--env-file ${file}:${index + 1} ${parsed.error}`);
|
|
1307
|
+
env[name] = parsed.value;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
for (const entry of flags.strings("env")) {
|
|
1311
|
+
const split = entry.indexOf("=");
|
|
1312
|
+
const name = split < 0 ? entry : entry.slice(0, split);
|
|
1313
|
+
if (!EnvName.test(name))
|
|
1314
|
+
throw usage("--env expects NAME=VALUE or NAME, with NAME like [A-Za-z_][A-Za-z0-9_]*");
|
|
1315
|
+
if (split >= 0)
|
|
1316
|
+
env[name] = entry.slice(split + 1);
|
|
1317
|
+
else {
|
|
1318
|
+
const inherited = session.ctx.env[name];
|
|
1319
|
+
if (inherited === undefined)
|
|
1320
|
+
throw usage(`--env ${name} is not set in this environment`);
|
|
1321
|
+
env[name] = inherited;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return env;
|
|
1325
|
+
}
|
|
1080
1326
|
function createInput(request) {
|
|
1081
1327
|
return {
|
|
1082
1328
|
environment: request.prepared ? Image2.prepared(request.prepared) : undefined,
|
|
1083
1329
|
network: request.network,
|
|
1084
|
-
labels: Object.keys(request.labels).length ? request.labels : undefined
|
|
1330
|
+
labels: Object.keys(request.labels).length ? request.labels : undefined,
|
|
1331
|
+
env: Object.keys(request.env).length ? request.env : undefined
|
|
1085
1332
|
};
|
|
1086
1333
|
}
|
|
1087
1334
|
async function claimName(store, record, requested, ifNotExists = false) {
|
|
@@ -1192,7 +1439,8 @@ function sandboxSummary(record) {
|
|
|
1192
1439
|
status: record.status,
|
|
1193
1440
|
createdAt: record.createdAt,
|
|
1194
1441
|
labels: record.labels,
|
|
1195
|
-
image: record.image ?? null
|
|
1442
|
+
image: record.image ?? null,
|
|
1443
|
+
adopted: record.adopted ?? false
|
|
1196
1444
|
};
|
|
1197
1445
|
}
|
|
1198
1446
|
async function findTarget(session, target) {
|
|
@@ -1224,13 +1472,15 @@ async function findTarget(session, target) {
|
|
|
1224
1472
|
if (entry)
|
|
1225
1473
|
return entry;
|
|
1226
1474
|
}
|
|
1227
|
-
const byId = (await listAll(store, "sandbox")).
|
|
1228
|
-
if (byId)
|
|
1229
|
-
|
|
1475
|
+
const byId = (await listAll(store, "sandbox")).filter((entry) => entry.record.reference?.nativeId === target);
|
|
1476
|
+
if (byId.length > 1)
|
|
1477
|
+
throw new CliError("CONFLICT", `${target} names more than one stored sandbox: ${byId.map((entry) => entry.record.name).join(", ")}`, "use the store name instead of the id");
|
|
1478
|
+
if (byId[0])
|
|
1479
|
+
return byId[0];
|
|
1230
1480
|
throw new CliError("NOT_FOUND", `No sandbox named ${target} in the store`, "list known sandboxes with `sandbar ls`");
|
|
1231
1481
|
}
|
|
1232
1482
|
async function createCommand(session, flags) {
|
|
1233
|
-
const created = await createSandbox(session, createRequest(flags));
|
|
1483
|
+
const created = await createSandbox(session, createRequest(flags, await envFlags(session, flags)));
|
|
1234
1484
|
if (created.unrecorded)
|
|
1235
1485
|
throw unrecordedError(created);
|
|
1236
1486
|
session.ctx.out.success({
|
|
@@ -1254,7 +1504,108 @@ async function stateOf(session, record, client) {
|
|
|
1254
1504
|
return "unreachable";
|
|
1255
1505
|
}
|
|
1256
1506
|
}
|
|
1507
|
+
var REMOTE_PAGES = 20;
|
|
1508
|
+
async function remoteSandboxes(client, until) {
|
|
1509
|
+
const items = [];
|
|
1510
|
+
let cursor;
|
|
1511
|
+
let coverage = "provider-scope";
|
|
1512
|
+
for (let page = 0;page < REMOTE_PAGES; page++) {
|
|
1513
|
+
const result = await client.sandboxes.list({ cursor, limit: 100 });
|
|
1514
|
+
coverage = result.coverage;
|
|
1515
|
+
items.push(...result.items);
|
|
1516
|
+
cursor = result.nextCursor;
|
|
1517
|
+
if (!cursor || until && result.items.some(until))
|
|
1518
|
+
return { items, coverage, complete: true };
|
|
1519
|
+
}
|
|
1520
|
+
return { items, coverage, complete: false };
|
|
1521
|
+
}
|
|
1522
|
+
var coverageNote = {
|
|
1523
|
+
"provider-scope": "every sandbox this profile's account and scope can see",
|
|
1524
|
+
"sandbar-managed": "only sandboxes created through Sandbar in this profile's scope"
|
|
1525
|
+
};
|
|
1526
|
+
function renderTable(rows) {
|
|
1527
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
1528
|
+
return rows.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()).join(`
|
|
1529
|
+
`);
|
|
1530
|
+
}
|
|
1531
|
+
async function remoteLs(session) {
|
|
1532
|
+
const { out } = session.ctx;
|
|
1533
|
+
const profile = await session.profile();
|
|
1534
|
+
const entries = await listAll(await session.store(), "sandbox", (warning) => out.note(warning));
|
|
1535
|
+
const client = await session.connect(profile);
|
|
1536
|
+
const listing = await remoteSandboxes(client);
|
|
1537
|
+
const nameOf = (item) => entries.find(({ record }) => storesSandbox(record, profile, client, item))?.record.name ?? null;
|
|
1538
|
+
const rows = listing.items.map((item) => ({
|
|
1539
|
+
id: item.id,
|
|
1540
|
+
state: item.state,
|
|
1541
|
+
name: nameOf(item),
|
|
1542
|
+
reopenable: item.reference !== null,
|
|
1543
|
+
ownership: item.reference?.ownership ?? null,
|
|
1544
|
+
reference: item.reference
|
|
1545
|
+
}));
|
|
1546
|
+
out.note(`${profile.provider} lists ${coverageNote[listing.coverage]}`);
|
|
1547
|
+
if (!listing.complete)
|
|
1548
|
+
out.note(`warning: showing the first ${rows.length} sandboxes; the provider has more`);
|
|
1549
|
+
out.success({
|
|
1550
|
+
provider: profile.provider,
|
|
1551
|
+
profile: profile.name,
|
|
1552
|
+
coverage: listing.coverage,
|
|
1553
|
+
complete: listing.complete,
|
|
1554
|
+
sandboxes: rows
|
|
1555
|
+
}, rows.length ? renderTable([
|
|
1556
|
+
["ID", "STATE", "NAME", "OWNERSHIP"],
|
|
1557
|
+
...rows.map((row) => [
|
|
1558
|
+
row.id,
|
|
1559
|
+
row.state,
|
|
1560
|
+
row.name ?? "-",
|
|
1561
|
+
row.ownership ?? "not reopenable"
|
|
1562
|
+
])
|
|
1563
|
+
]) : "");
|
|
1564
|
+
return 0;
|
|
1565
|
+
}
|
|
1566
|
+
async function adoptCommand(session, flags) {
|
|
1567
|
+
const id = onePositional(flags, "sandbox id");
|
|
1568
|
+
const requested = flags.string("name");
|
|
1569
|
+
const store = await session.store();
|
|
1570
|
+
const profile = await session.profile();
|
|
1571
|
+
const client = await session.connect(profile);
|
|
1572
|
+
const listing = await remoteSandboxes(client, (item) => item.id === id);
|
|
1573
|
+
const listed = listing.items.find((item) => item.id === id);
|
|
1574
|
+
const known = listed && (await listAll(store, "sandbox")).find(({ record: record2 }) => storesSandbox(record2, profile, client, listed));
|
|
1575
|
+
if (known)
|
|
1576
|
+
throw new CliError("CONFLICT", `${id} is already in the store as ${known.record.name}`, `use \`sandbar inspect ${known.record.name}\``);
|
|
1577
|
+
if (!listed)
|
|
1578
|
+
throw new CliError("NOT_FOUND", `${profile.provider} does not list a sandbox ${id} for profile ${profile.name}`, listing.complete ? "list what it does show with `sandbar ls --remote`" : "the listing was cut short; check the provider's own tools");
|
|
1579
|
+
if (!listed.reference)
|
|
1580
|
+
throw new CliError("UNSUPPORTED", `${profile.provider} lists ${id} without a reopenable reference`, "this provider cannot verify that sandbox's identity, so the CLI cannot reach it");
|
|
1581
|
+
const box = await client.sandboxes.get(listed.reference, { signal: session.ctx.signal });
|
|
1582
|
+
const record = {
|
|
1583
|
+
format: 1,
|
|
1584
|
+
kind: "sandbox",
|
|
1585
|
+
status: "created",
|
|
1586
|
+
profile: profile.name,
|
|
1587
|
+
provider: profile.provider,
|
|
1588
|
+
createdAt: new Date().toISOString(),
|
|
1589
|
+
labels: {},
|
|
1590
|
+
reference: box.reference ?? listed.reference,
|
|
1591
|
+
adopted: true
|
|
1592
|
+
};
|
|
1593
|
+
let claimed;
|
|
1594
|
+
try {
|
|
1595
|
+
claimed = await claimName(store, record, requested);
|
|
1596
|
+
} catch (cause) {
|
|
1597
|
+
if (cause instanceof CliError && cause.code === "CONFLICT")
|
|
1598
|
+
throw new CliError("CONFLICT", cause.message, "choose another --name");
|
|
1599
|
+
throw cause;
|
|
1600
|
+
}
|
|
1601
|
+
session.ctx.out.success({ ...sandboxSummary(claimed.record), id: box.id, reference: claimed.record.reference }, claimed.record.name);
|
|
1602
|
+
return 0;
|
|
1603
|
+
}
|
|
1257
1604
|
async function lsCommand(session, flags) {
|
|
1605
|
+
if (flags.bool("remote") && flags.bool("offline"))
|
|
1606
|
+
throw usage("Pass either --remote or --offline, not both");
|
|
1607
|
+
if (flags.bool("remote"))
|
|
1608
|
+
return remoteLs(session);
|
|
1258
1609
|
const entries = await listAll(await session.store(), "sandbox", (warning) => session.ctx.out.note(warning));
|
|
1259
1610
|
const offline = flags.bool("offline");
|
|
1260
1611
|
const clients = new Map;
|
|
@@ -1314,10 +1665,30 @@ async function inspectCommand(session, flags) {
|
|
|
1314
1665
|
function refusal(record) {
|
|
1315
1666
|
if (!record.reference)
|
|
1316
1667
|
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 });
|
|
1668
|
+
if (record.adopted)
|
|
1669
|
+
return new CliError("FORBIDDEN", `${record.name} was adopted from a provider listing, not created by this CLI`, "pass --force to destroy it anyway", { sandbox: record.name });
|
|
1317
1670
|
if (record.reference.ownership !== "verified-created")
|
|
1318
1671
|
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
1672
|
return null;
|
|
1320
1673
|
}
|
|
1674
|
+
function storesSandbox(record, profile, client, item) {
|
|
1675
|
+
const stored = record.reference;
|
|
1676
|
+
if (!stored || record.provider !== profile.provider || stored.nativeId !== item.id)
|
|
1677
|
+
return false;
|
|
1678
|
+
if (item.reference?.generation !== undefined && stored.generation !== undefined && stored.generation !== item.reference.generation)
|
|
1679
|
+
return false;
|
|
1680
|
+
try {
|
|
1681
|
+
assertResourceScope(stored, {
|
|
1682
|
+
provider: client.provider,
|
|
1683
|
+
scope: item.reference?.scope ?? client.scope
|
|
1684
|
+
});
|
|
1685
|
+
return true;
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
if (error instanceof Error && "code" in error && error.code === "CONFLICT")
|
|
1688
|
+
return false;
|
|
1689
|
+
throw error;
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1321
1692
|
function sameSandbox(a, b) {
|
|
1322
1693
|
if (a.createdAt !== b.createdAt || a.provider !== b.provider || a.profile !== b.profile)
|
|
1323
1694
|
return false;
|
|
@@ -1465,7 +1836,7 @@ async function execInput(session, flags, command) {
|
|
|
1465
1836
|
const stdin = flags.bool("stdin") ? await session.ctx.readStdin() : stdinFile ? new Uint8Array(await readFile4(resolve3(session.ctx.cwd, stdinFile))) : undefined;
|
|
1466
1837
|
const timeout = flags.string("timeout");
|
|
1467
1838
|
const maxOutput = flags.string("max-output");
|
|
1468
|
-
const env =
|
|
1839
|
+
const env = await envFlags(session, flags);
|
|
1469
1840
|
return {
|
|
1470
1841
|
command,
|
|
1471
1842
|
cwd: flags.string("cwd"),
|
|
@@ -1733,8 +2104,8 @@ async function providersCommand(session) {
|
|
|
1733
2104
|
|
|
1734
2105
|
// src/secrets.ts
|
|
1735
2106
|
import { execFile } from "node:child_process";
|
|
1736
|
-
import { z as
|
|
1737
|
-
var BwsSecret =
|
|
2107
|
+
import { z as z6 } from "zod";
|
|
2108
|
+
var BwsSecret = z6.object({ value: z6.string() });
|
|
1738
2109
|
var backends = [
|
|
1739
2110
|
{
|
|
1740
2111
|
name: "1Password",
|
|
@@ -1807,14 +2178,14 @@ class SecretReader {
|
|
|
1807
2178
|
}
|
|
1808
2179
|
}
|
|
1809
2180
|
async resolve(value, where) {
|
|
1810
|
-
const text =
|
|
2181
|
+
const text = z6.string().safeParse(value);
|
|
1811
2182
|
if (text.success) {
|
|
1812
2183
|
const backend = backendFor(text.data);
|
|
1813
2184
|
return backend ? this.read(backend, text.data, where) : value;
|
|
1814
2185
|
}
|
|
1815
2186
|
if (Array.isArray(value))
|
|
1816
2187
|
return Promise.all(value.map((item, index) => this.resolve(item, `${where}[${index}]`)));
|
|
1817
|
-
const record =
|
|
2188
|
+
const record = z6.record(z6.string(), z6.json()).safeParse(value);
|
|
1818
2189
|
if (record.success)
|
|
1819
2190
|
return Object.fromEntries(await Promise.all(Object.entries(record.data).map(async ([key, item]) => [key, await this.resolve(item, `${where}.${key}`)])));
|
|
1820
2191
|
return value;
|
|
@@ -1875,7 +2246,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
1875
2246
|
import { dirname as dirname3, join as join5 } from "node:path";
|
|
1876
2247
|
|
|
1877
2248
|
// ../../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
|
|
2249
|
+
var SKILL_default = "---\nname: sandbar-cli\ndescription: Create, use and clean up cloud sandboxes from the shell with the `sandbar` CLI (Daytona, E2B, boxd, Boat, 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), `--env-file <path>`, `--cwd`, `--stdin` or `--stdin-file <path>`, `--timeout 30s|5m|1h`, `--max-output 1m`.\n- On `create`, `--env K=V` and `--env-file <path>` set variables for every later `exec`/`sh` in that sandbox; a command's own `--env` wins. For a secret, pass `--env NAME` (no value) to copy it from your environment instead of typing it. Values are never stored or printed. Providers that cannot keep sandbox variables (boxd) fail with exit 4 and `UNSUPPORTED` before creating anything; pass `--env` on each `exec` there. On `run`, `--env` applies to its one command and works everywhere.\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 (`--remote` lists the provider's images instead); `sandbar image rm <name>` deletes a build on the provider and drops its record; `sandbar image forget <name>` only drops the record. Only `image rm` images you built for this task; it refuses builds this CLI did not verifiably create, and never pass `--force` unless the user asked. Exit 6 from `image rm` means the outcome is unknown: run `sandbar image rm <name>` again later to check, which never resends the delete.\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- `sandbar ls --remote --json` asks the provider instead of the store. `data.coverage` says whether it shows every sandbox in the account scope (`provider-scope`) or only Sandbar-created ones (`sandbar-managed`); `name` is the store name, or null for sandboxes this store does not know. `sandbar adopt <id> [--name <name>]` records a listed sandbox so `exec`, `cp` and `inspect` can reach it. Adopted sandboxes are not the CLI's own: `rm --all` skips them and `rm <name>` needs `--force`. Do not adopt or remove sandboxes the user did not ask about.\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
2250
|
|
|
1880
2251
|
// src/skill.ts
|
|
1881
2252
|
var skillName = "sandbar-cli";
|
|
@@ -1948,7 +2319,7 @@ ${marker}
|
|
|
1948
2319
|
// src/completion.ts
|
|
1949
2320
|
var NoSpace = 1;
|
|
1950
2321
|
var Files = 2;
|
|
1951
|
-
var fileFlags = new Set(["config", "state-dir", "stdin-file"]);
|
|
2322
|
+
var fileFlags = new Set(["config", "state-dir", "stdin-file", "env-file"]);
|
|
1952
2323
|
var sandboxArguments = new Map([
|
|
1953
2324
|
["inspect", 1],
|
|
1954
2325
|
["exec", 1],
|
|
@@ -2035,7 +2406,7 @@ async function complete(words, commands, globals, session) {
|
|
|
2035
2406
|
if (command.name === "image") {
|
|
2036
2407
|
if (positionals.length === 0)
|
|
2037
2408
|
return { candidates: imageActions.map((action) => ({ value: action })), directive: 0 };
|
|
2038
|
-
if (positionals.length === 1 &&
|
|
2409
|
+
if (positionals.length === 1 && ["inspect", "forget", "rm"].includes(positionals[0])) {
|
|
2039
2410
|
const entries = await listAll(await session.store(), "image");
|
|
2040
2411
|
return {
|
|
2041
2412
|
candidates: entries.map(({ record }) => ({
|
|
@@ -2203,6 +2574,18 @@ var createFlags = {
|
|
|
2203
2574
|
description: "blocked or internet; default is the profile's policy"
|
|
2204
2575
|
},
|
|
2205
2576
|
label: { type: "string", multiple: true, value: "k=v", description: "Label to set (repeatable)" },
|
|
2577
|
+
env: {
|
|
2578
|
+
type: "string",
|
|
2579
|
+
multiple: true,
|
|
2580
|
+
value: "K=V",
|
|
2581
|
+
description: "Environment variable for every command in the sandbox (repeatable; K alone copies it from here)"
|
|
2582
|
+
},
|
|
2583
|
+
"env-file": {
|
|
2584
|
+
type: "string",
|
|
2585
|
+
multiple: true,
|
|
2586
|
+
value: "path",
|
|
2587
|
+
description: "Read K=V lines into the sandbox environment (repeatable; --env wins)"
|
|
2588
|
+
},
|
|
2206
2589
|
build: {
|
|
2207
2590
|
type: "boolean",
|
|
2208
2591
|
description: "Build --image now if this provider needs a build and none is recorded"
|
|
@@ -2214,7 +2597,13 @@ var execFlags = {
|
|
|
2214
2597
|
type: "string",
|
|
2215
2598
|
multiple: true,
|
|
2216
2599
|
value: "K=V",
|
|
2217
|
-
description: "Environment variable (repeatable)"
|
|
2600
|
+
description: "Environment variable for this command (repeatable; K alone copies it from here)"
|
|
2601
|
+
},
|
|
2602
|
+
"env-file": {
|
|
2603
|
+
type: "string",
|
|
2604
|
+
multiple: true,
|
|
2605
|
+
value: "path",
|
|
2606
|
+
description: "Read K=V lines into this command's environment (repeatable; --env wins)"
|
|
2218
2607
|
},
|
|
2219
2608
|
stdin: { type: "boolean", description: "Send this process's stdin to the command" },
|
|
2220
2609
|
"stdin-file": {
|
|
@@ -2233,7 +2622,7 @@ var commands = [
|
|
|
2233
2622
|
{
|
|
2234
2623
|
name: "create",
|
|
2235
2624
|
summary: "Create a sandbox and print its name",
|
|
2236
|
-
usage: "sandbar create [--name <name>] [--image <oci-ref> | --prepared <id>] [--network <policy>]",
|
|
2625
|
+
usage: "sandbar create [--name <name>] [--image <oci-ref> | --prepared <id>] [--network <policy>] [--env K=V]...",
|
|
2237
2626
|
flags: {
|
|
2238
2627
|
...createFlags,
|
|
2239
2628
|
"if-not-exists": {
|
|
@@ -2245,22 +2634,43 @@ var commands = [
|
|
|
2245
2634
|
remoteExit: false,
|
|
2246
2635
|
examples: [
|
|
2247
2636
|
"sandbar create --name web --network internet",
|
|
2248
|
-
"sandbar create --name job-42 --if-not-exists --json"
|
|
2637
|
+
"sandbar create --name job-42 --if-not-exists --json",
|
|
2638
|
+
"sandbar create --name ci --env-file .env --env API_TOKEN"
|
|
2249
2639
|
],
|
|
2250
2640
|
run: createCommand
|
|
2251
2641
|
},
|
|
2252
2642
|
{
|
|
2253
2643
|
name: "ls",
|
|
2254
2644
|
summary: "List sandboxes in the store with their current state",
|
|
2255
|
-
usage: "sandbar ls [--offline]",
|
|
2645
|
+
usage: "sandbar ls [--offline | --remote]",
|
|
2256
2646
|
flags: {
|
|
2257
|
-
offline: { type: "boolean", description: "Show stored records without asking providers" }
|
|
2647
|
+
offline: { type: "boolean", description: "Show stored records without asking providers" },
|
|
2648
|
+
remote: {
|
|
2649
|
+
type: "boolean",
|
|
2650
|
+
description: "List the profile's sandboxes from the provider instead of the store"
|
|
2651
|
+
}
|
|
2258
2652
|
},
|
|
2259
2653
|
passthrough: false,
|
|
2260
2654
|
remoteExit: false,
|
|
2261
|
-
examples: ["sandbar ls", "sandbar ls --json"],
|
|
2655
|
+
examples: ["sandbar ls", "sandbar ls --json", "sandbar ls --remote --provider daytona"],
|
|
2262
2656
|
run: lsCommand
|
|
2263
2657
|
},
|
|
2658
|
+
{
|
|
2659
|
+
name: "adopt",
|
|
2660
|
+
summary: "Add a sandbox from the provider's listing to the store so other commands reach it",
|
|
2661
|
+
usage: "sandbar adopt <id> [--name <name>]",
|
|
2662
|
+
flags: {
|
|
2663
|
+
name: {
|
|
2664
|
+
type: "string",
|
|
2665
|
+
value: "name",
|
|
2666
|
+
description: "Store name (default: generated, like sb-1a2b3)"
|
|
2667
|
+
}
|
|
2668
|
+
},
|
|
2669
|
+
passthrough: false,
|
|
2670
|
+
remoteExit: false,
|
|
2671
|
+
examples: ["sandbar ls --remote", "sandbar adopt 4f1c2a --name worker"],
|
|
2672
|
+
run: adoptCommand
|
|
2673
|
+
},
|
|
2264
2674
|
{
|
|
2265
2675
|
name: "inspect",
|
|
2266
2676
|
summary: "Show a sandbox's state, expiry and reference",
|
|
@@ -2329,15 +2739,23 @@ var commands = [
|
|
|
2329
2739
|
},
|
|
2330
2740
|
{
|
|
2331
2741
|
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>",
|
|
2742
|
+
summary: "Build OCI images for providers that need it, and list, delete or forget the builds",
|
|
2743
|
+
usage: "sandbar image build <oci-ref> [--name <name>] [--refresh] | ls [--remote] | inspect <name> | rm <name> [--force] | forget <name>",
|
|
2334
2744
|
flags: {
|
|
2335
2745
|
name: {
|
|
2336
2746
|
type: "string",
|
|
2337
2747
|
value: "name",
|
|
2338
2748
|
description: "Record name for a build (default: from the reference, like python-3-12)"
|
|
2339
2749
|
},
|
|
2340
|
-
refresh: { type: "boolean", description: "Build again even if this reference is recorded" }
|
|
2750
|
+
refresh: { type: "boolean", description: "Build again even if this reference is recorded" },
|
|
2751
|
+
remote: {
|
|
2752
|
+
type: "boolean",
|
|
2753
|
+
description: "List the images on the provider for this profile, not the local records"
|
|
2754
|
+
},
|
|
2755
|
+
force: {
|
|
2756
|
+
type: "boolean",
|
|
2757
|
+
description: "Delete a recorded image this CLI did not verifiably create"
|
|
2758
|
+
}
|
|
2341
2759
|
},
|
|
2342
2760
|
passthrough: false,
|
|
2343
2761
|
remoteExit: false,
|
|
@@ -2345,6 +2763,8 @@ var commands = [
|
|
|
2345
2763
|
"sandbar image build python:3.12 --provider e2b",
|
|
2346
2764
|
"sandbar create --image python:3.12 --provider e2b",
|
|
2347
2765
|
"sandbar image ls",
|
|
2766
|
+
"sandbar image ls --remote --provider e2b",
|
|
2767
|
+
"sandbar image rm python-3-12",
|
|
2348
2768
|
"sandbar image forget python-3-12"
|
|
2349
2769
|
],
|
|
2350
2770
|
run: imageCommand
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sandbar-sdk/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Agent-first command line for creating and managing Sandbar sandboxes.",
|
|
@@ -20,16 +20,17 @@
|
|
|
20
20
|
"build": "rm -rf dist && bun build src/main.ts --target node --format esm --outdir dist --external sandbar-sdk --external @sandbar-sdk/adapter --external zod"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@sandbar-sdk/adapter": "0.
|
|
24
|
-
"sandbar-sdk": "0.
|
|
23
|
+
"@sandbar-sdk/adapter": "0.6.0",
|
|
24
|
+
"sandbar-sdk": "0.6.0",
|
|
25
25
|
"zod": "4.6.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@sandbar-sdk/
|
|
29
|
-
"@sandbar-sdk/
|
|
30
|
-
"@sandbar-sdk/
|
|
28
|
+
"@sandbar-sdk/boat": "0.6.0",
|
|
29
|
+
"@sandbar-sdk/boxd": "0.6.0",
|
|
30
|
+
"@sandbar-sdk/daytona": "0.6.0",
|
|
31
|
+
"@sandbar-sdk/e2b": "0.6.0",
|
|
31
32
|
"@sandbar-sdk/modal": "0.0.0",
|
|
32
|
-
"@sandbar-sdk/vercel": "0.
|
|
33
|
+
"@sandbar-sdk/vercel": "0.6.0",
|
|
33
34
|
"@sandbar/provider-fake": "0.0.0"
|
|
34
35
|
},
|
|
35
36
|
"license": "Apache-2.0",
|