Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"light-results-change",
"open-planets-joke",
"public-ears-heal",
"rich-areas-pick",
"seven-olives-mate",
"slick-colts-learn",
"some-numbers-arrive",
Expand Down
6 changes: 6 additions & 0 deletions .changeset/rich-areas-pick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@vercel/sandbox": minor
"sandbox": minor
---

Support new keepLastSnapshots parameter for CLI and SDK
11 changes: 11 additions & 0 deletions packages/sandbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# sandbox

## 3.0.0-beta.18

### Minor Changes

- Support new keepLastSnapshots parameter for CLI and SDK

### Patch Changes

- Updated dependencies []:
- @vercel/sandbox@2.0.0-beta.16

## 3.0.0-beta.17

### Minor Changes
Expand Down
63 changes: 35 additions & 28 deletions packages/sandbox/docs/index.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## `sandbox --help`

```
sandbox 3.0.0-beta.17
sandbox 3.0.0-beta.18

▲ sandbox [options] <command>

Expand Down Expand Up @@ -78,23 +78,26 @@ Create and run a command in a sandbox

Options:

--name <str> A user-chosen name for the sandbox. It must be unique per project. [optional]
--runtime <runtime> One of 'node22', 'node24', 'python3.13' [default: node24]
--timeout <num UNIT> The maximum duration a sandbox can run for. Example: 5m, 30m [default: 5 minutes]
--vcpus <COUNT> Number of vCPUs to allocate (each vCPU includes 2048 MB of memory) [optional]
--publish-port <PORT>, -p=<PORT> Publish sandbox port(s) to DOMAIN.vercel.run
--snapshot, -s <snapshot_id> Start the sandbox from a snapshot ID [optional]
--env <key=value>, -e=<key=value> Environment variables to set for the command
--tag <key=value>, -t=<key=value> Key-value tags to associate with the sandbox (e.g. --tag env=staging)
--snapshot-expiration <DURATION|none> Default snapshot expiration. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--network-policy <MODE> Network policy mode: "allow-all" or "deny-all"
--name <str> A user-chosen name for the sandbox. It must be unique per project. [optional]
--runtime <runtime> One of 'node22', 'node24', 'python3.13' [default: node24]
--timeout <num UNIT> The maximum duration a sandbox can run for. Example: 5m, 30m [default: 5 minutes]
--vcpus <COUNT> Number of vCPUs to allocate (each vCPU includes 2048 MB of memory) [optional]
--publish-port <PORT>, -p=<PORT> Publish sandbox port(s) to DOMAIN.vercel.run
--snapshot, -s <snapshot_id> Start the sandbox from a snapshot ID [optional]
--env <key=value>, -e=<key=value> Environment variables to set for the command
--tag <key=value>, -t=<key=value> Key-value tags to associate with the sandbox (e.g. --tag env=staging)
--snapshot-expiration <DURATION|none> Default snapshot expiration. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--keep-last-snapshots <COUNT> Keep only the N most recent snapshots of this sandbox (1-10). [optional]
--keep-last-snapshots-for <DURATION|none> Expiration applied to kept snapshots. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--delete-evicted-snapshots <true|false> When "true" (the default), evicted snapshots are deleted immediately; when "false", they keep the default expiration. [optional]
--network-policy <MODE> Network policy mode: "allow-all" or "deny-all"
- allow-all: sandbox can access any website/domain
- deny-all: sandbox has no network access
Omit this option and use --allowed-domain / --allowed-cidr / --denied-cidr for custom policies. [optional]
--allowed-domain <str> Domain to allow traffic to (creates a custom network policy). Supports "*" for wildcards for a segment (e.g. '*.vercel.com', 'www.*.com'). If used as the first segment, will match any subdomain.
--allowed-cidr <str> CIDR to allow traffic to (creates a custom network policy). Takes precedence over 'allowed-domain'.
--denied-cidr <str> CIDR to deny traffic to (creates a custom network policy). Takes precedence over allowed domains/CIDRs.
--workdir, -w <str> The working directory to run the command in [optional]
--allowed-domain <str> Domain to allow traffic to (creates a custom network policy). Supports "*" for wildcards for a segment (e.g. '*.vercel.com', 'www.*.com'). If used as the first segment, will match any subdomain.
--allowed-cidr <str> CIDR to allow traffic to (creates a custom network policy). Takes precedence over 'allowed-domain'.
--denied-cidr <str> CIDR to deny traffic to (creates a custom network policy). Takes precedence over allowed domains/CIDRs.
--workdir, -w <str> The working directory to run the command in [optional]

Flags:

Expand Down Expand Up @@ -132,22 +135,25 @@ Create a sandbox in the specified account and project.

Options:

--name <str> A user-chosen name for the sandbox. It must be unique per project. [optional]
--runtime <runtime> One of 'node22', 'node24', 'python3.13' [default: node24]
--timeout <num UNIT> The maximum duration a sandbox can run for. Example: 5m, 30m [default: 5 minutes]
--vcpus <COUNT> Number of vCPUs to allocate (each vCPU includes 2048 MB of memory) [optional]
--publish-port <PORT>, -p=<PORT> Publish sandbox port(s) to DOMAIN.vercel.run
--snapshot, -s <snapshot_id> Start the sandbox from a snapshot ID [optional]
--env <key=value>, -e=<key=value> Default environment variables for sandbox commands
--tag <key=value>, -t=<key=value> Key-value tags to associate with the sandbox (e.g. --tag env=staging)
--snapshot-expiration <DURATION|none> Default snapshot expiration. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--network-policy <MODE> Network policy mode: "allow-all" or "deny-all"
--name <str> A user-chosen name for the sandbox. It must be unique per project. [optional]
--runtime <runtime> One of 'node22', 'node24', 'python3.13' [default: node24]
--timeout <num UNIT> The maximum duration a sandbox can run for. Example: 5m, 30m [default: 5 minutes]
--vcpus <COUNT> Number of vCPUs to allocate (each vCPU includes 2048 MB of memory) [optional]
--publish-port <PORT>, -p=<PORT> Publish sandbox port(s) to DOMAIN.vercel.run
--snapshot, -s <snapshot_id> Start the sandbox from a snapshot ID [optional]
--env <key=value>, -e=<key=value> Default environment variables for sandbox commands
--tag <key=value>, -t=<key=value> Key-value tags to associate with the sandbox (e.g. --tag env=staging)
--snapshot-expiration <DURATION|none> Default snapshot expiration. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--keep-last-snapshots <COUNT> Keep only the N most recent snapshots of this sandbox (1-10). [optional]
--keep-last-snapshots-for <DURATION|none> Expiration applied to kept snapshots. Use "none" or 0 for no expiration. Example: 7d, 30d [optional]
--delete-evicted-snapshots <true|false> When "true" (the default), evicted snapshots are deleted immediately; when "false", they keep the default expiration. [optional]
--network-policy <MODE> Network policy mode: "allow-all" or "deny-all"
- allow-all: sandbox can access any website/domain
- deny-all: sandbox has no network access
Omit this option and use --allowed-domain / --allowed-cidr / --denied-cidr for custom policies. [optional]
--allowed-domain <str> Domain to allow traffic to (creates a custom network policy). Supports "*" for wildcards for a segment (e.g. '*.vercel.com', 'www.*.com'). If used as the first segment, will match any subdomain.
--allowed-cidr <str> CIDR to allow traffic to (creates a custom network policy). Takes precedence over 'allowed-domain'.
--denied-cidr <str> CIDR to deny traffic to (creates a custom network policy). Takes precedence over allowed domains/CIDRs.
--allowed-domain <str> Domain to allow traffic to (creates a custom network policy). Supports "*" for wildcards for a segment (e.g. '*.vercel.com', 'www.*.com'). If used as the first segment, will match any subdomain.
--allowed-cidr <str> CIDR to allow traffic to (creates a custom network policy). Takes precedence over 'allowed-domain'.
--denied-cidr <str> CIDR to deny traffic to (creates a custom network policy). Takes precedence over allowed domains/CIDRs.

Flags:

Expand Down Expand Up @@ -348,6 +354,7 @@ Commands:
persistent <name> <true|false> Enable or disable automatic restore of the filesystem between sessions
network-policy <name> Update the network policy of a sandbox
snapshot-expiration <name> <DURATION|none> Update the default snapshot expiration of a sandbox
keep-last-snapshots <name> [COUNT] Update the snapshot retention policy (keep only the N most recent snapshots) of a sandbox
current-snapshot <name> <snapshot_id> Update the current snapshot of a sandbox
tags <name> Update the tags of a sandbox. Replaces all existing tags with the provided tags.
```
Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sandbox",
"description": "Command line interface for Vercel Sandbox",
"version": "3.0.0-beta.17",
"version": "3.0.0-beta.18",
"scripts": {
"clean": "rm -rf node_modules dist",
"sandbox": "ts-node ./src/sandbox.ts",
Expand Down
161 changes: 161 additions & 0 deletions packages/sandbox/src/commands/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,147 @@ const snapshotExpirationCommand = cmd.command({
},
});

const keepLastCountType = cmd.extendType(cmd.number, {
displayName: "COUNT",
async from(n) {
if (!Number.isInteger(n) || n < 1 || n > 10) {
throw new Error(
`Invalid count: ${n}. Must be an integer between 1 and 10.`,
);
}
return n;
},
});

const keepLastSnapshotsCommand = cmd.command({
name: "keep-last-snapshots",
description:
"Update the snapshot retention policy (keep only the N most recent snapshots) of a sandbox",
args: {
sandbox: cmd.positional({
type: sandboxName,
description: "Sandbox name to update",
}),
count: cmd.positional({
type: cmd.optional(keepLastCountType),
description:
"Number of most recent snapshots to keep (1-10). Omit with --clear to remove the policy.",
}),
keepLastSnapshotsFor: cmd.option({
long: "keep-last-snapshots-for",
type: cmd.optional(SnapshotExpiration),
description:
'Expiration for kept snapshots. Use "none" or 0 for no expiration. Example: 7d, 30d',
}),
deleteEvictedSnapshots: cmd.option({
long: "delete-evicted-snapshots",
type: cmd.optional({
...cmd.oneOf(["true", "false"]),
displayName: "true|false",
}),
description:
'When "true" (the default), evicted snapshots are deleted immediately; when "false", they keep the default expiration.',
}),
clear: cmd.flag({
long: "clear",
description:
"Remove the snapshot retention policy from this sandbox.",
}),
scope,
},
async handler({
scope: { token, team, project },
sandbox: name,
count,
keepLastSnapshotsFor,
deleteEvictedSnapshots,
clear,
}) {
if (clear && count !== undefined) {
throw new Error(
"Cannot combine --clear with a <count> argument. Pass one or the other.",
);
}
if (!clear && count === undefined) {
throw new Error(
[
"Missing <count> argument.",
`${chalk.bold("hint:")} Pass a count between 1 and 10, or --clear to remove the policy.`,
].join("\n"),
);
}
if (
clear &&
(keepLastSnapshotsFor !== undefined ||
deleteEvictedSnapshots !== undefined)
) {
throw new Error(
"--keep-last-snapshots-for and --delete-evicted-snapshots cannot be combined with --clear.",
);
}

const sandbox = await sandboxClient.get({
name,
projectId: project,
teamId: team,
token,
});

const spinner = ora("Updating sandbox configuration...").start();
try {
if (clear) {
await sandbox.update({ keepLastSnapshots: null });
} else {
await sandbox.update({
keepLastSnapshots: {
count: count!,
expiration:
keepLastSnapshotsFor !== undefined
? ms(keepLastSnapshotsFor)
: undefined,
deleteEvicted:
deleteEvictedSnapshots !== undefined
? deleteEvictedSnapshots === "true"
: undefined,
},
});
}
spinner.stop();

process.stderr.write(
"✅ Configuration updated for sandbox " + chalk.cyan(name) + "\n",
);
if (clear) {
process.stderr.write(
chalk.dim(" ╰ ") +
"keep-last-snapshots: " +
chalk.cyan("cleared") +
"\n",
);
} else {
const parts: string[] = [`count=${count}`];
if (keepLastSnapshotsFor !== undefined) {
const displayExp =
ms(keepLastSnapshotsFor) === 0 ? "none" : keepLastSnapshotsFor;
parts.push(`for=${displayExp}`);
}
if (deleteEvictedSnapshots === "false") {
parts.push("delete-evicted-snapshots=false");
}
process.stderr.write(
chalk.dim(" ╰ ") +
"keep-last-snapshots: " +
chalk.cyan(parts.join(", ")) +
"\n",
);
}
} catch (error) {
spinner.stop();
throw error;
}
},
});

const currentSnapshotCommand = cmd.command({
name: "current-snapshot",
description: "Update the current snapshot of a sandbox",
Expand Down Expand Up @@ -291,6 +432,7 @@ const listCommand = cmd.command({
{ field: "Persistent", value: String(sandbox.persistent) },
{ field: "Network policy", value: String(networkPolicy) },
{ field: "Snapshot expiration", value: sandbox.snapshotExpiration != null && sandbox.snapshotExpiration > 0 ? ms(sandbox.snapshotExpiration, { long: true }) : sandbox.snapshotExpiration === 0 ? "none" : "-" },
{ field: "Keep last snapshots", value: formatKeepLastSnapshots(sandbox.keepLastSnapshots) },
{ field: "Current snapshot", value: sandbox.currentSnapshotId ?? "-" },
{ field: "Tags", value: tagsDisplay },
];
Expand Down Expand Up @@ -432,6 +574,24 @@ const tagsCommand = cmd.command({
},
});

function formatKeepLastSnapshots(
keepLastSnapshots:
| { count: number; expiration?: number; deleteEvicted: boolean }
| undefined,
): string {
if (!keepLastSnapshots) return "-";
const parts = [`count=${keepLastSnapshots.count}`];
if (keepLastSnapshots.expiration !== undefined) {
parts.push(
`for=${keepLastSnapshots.expiration === 0 ? "none" : ms(keepLastSnapshots.expiration, { long: true })}`,
);
}
if (!keepLastSnapshots.deleteEvicted) {
parts.push("delete-evicted-snapshots=false");
}
return parts.join(", ");
}

export const config = cmd.subcommands({
name: "config",
description: "View and update sandbox configuration",
Expand All @@ -442,6 +602,7 @@ export const config = cmd.subcommands({
persistent: persistentCommand,
"network-policy": networkPolicyCommand,
"snapshot-expiration": snapshotExpirationCommand,
"keep-last-snapshots": keepLastSnapshotsCommand,
"current-snapshot": currentSnapshotCommand,
tags: tagsCommand,
},
Expand Down
Loading
Loading