sandbox 3.0.0-beta.22 → 3.0.0-beta.23

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.
@@ -15,10 +15,11 @@ import childProcess, { execFile } from "node:child_process";
15
15
  import fs, { constants, writeFile } from "node:fs/promises";
16
16
  import fs$1, { createWriteStream } from "node:fs";
17
17
  import * as Auth from "@vercel/sandbox/dist/auth/index.js";
18
- import { OAuth, getAuth, inferScope, pollForToken, updateAuthConfig } from "@vercel/sandbox/dist/auth/index.js";
18
+ import { NotOk, OAuth, getAuth, inferScope, pollForToken, updateAuthConfig } from "@vercel/sandbox/dist/auth/index.js";
19
19
  import { EOL } from "os";
20
20
  import { z } from "zod/v4";
21
21
  import { z as z$1 } from "zod";
22
+ import retry from "async-retry";
22
23
  import { APIError, Sandbox, Snapshot } from "@vercel/sandbox";
23
24
  import readline from "node:readline";
24
25
  import { randomUUID } from "crypto";
@@ -1997,7 +1998,7 @@ var require_parser$1 = /* @__PURE__ */ __commonJS$1({ "../../node_modules/.pnpm/
1997
1998
  };
1998
1999
  Object.defineProperty(exports, "__esModule", { value: true });
1999
2000
  exports.parse = parse$5;
2000
- const debug$6 = (0, __importDefault$1(__require$1("debug")).default)("cmd-ts:parser");
2001
+ const debug$7 = (0, __importDefault$1(__require$1("debug")).default)("cmd-ts:parser");
2001
2002
  /**
2002
2003
  * Create an AST from a token list
2003
2004
  *
@@ -2005,14 +2006,14 @@ var require_parser$1 = /* @__PURE__ */ __commonJS$1({ "../../node_modules/.pnpm/
2005
2006
  * @param forceFlag Keys to force as flag. {@see ForceFlag} to read more about it.
2006
2007
  */
2007
2008
  function parse$5(tokens, forceFlag) {
2008
- if (debug$6.enabled) {
2009
+ if (debug$7.enabled) {
2009
2010
  const registered = {
2010
2011
  shortFlags: [...forceFlag.forceFlagShortNames],
2011
2012
  longFlags: [...forceFlag.forceFlagLongNames],
2012
2013
  shortOptions: [...forceFlag.forceOptionShortNames],
2013
2014
  longOptions: [...forceFlag.forceOptionLongNames]
2014
2015
  };
2015
- debug$6("Registered:", JSON.stringify(registered));
2016
+ debug$7("Registered:", JSON.stringify(registered));
2016
2017
  }
2017
2018
  const nodes = [];
2018
2019
  let index = 0;
@@ -2143,9 +2144,9 @@ var require_parser$1 = /* @__PURE__ */ __commonJS$1({ "../../node_modules/.pnpm/
2143
2144
  }
2144
2145
  index++;
2145
2146
  }
2146
- if (debug$6.enabled) {
2147
+ if (debug$7.enabled) {
2147
2148
  const objectNodes = nodes.map((node) => ({ [node.type]: node.raw }));
2148
- debug$6("Parsed items:", JSON.stringify(objectNodes));
2149
+ debug$7("Parsed items:", JSON.stringify(objectNodes));
2149
2150
  }
2150
2151
  return nodes;
2151
2152
  }
@@ -6755,7 +6756,7 @@ function _usingCtx() {
6755
6756
  //#region src/commands/login.ts
6756
6757
  var import_cjs$23 = /* @__PURE__ */ __toESM(require_cjs());
6757
6758
  init_source();
6758
- const debug$5 = createDebugger("sandbox:login");
6759
+ const debug$6 = createDebugger("sandbox:login");
6759
6760
  const login = import_cjs$23.command({
6760
6761
  name: "login",
6761
6762
  description: "Log in to the Sandbox CLI",
@@ -6797,13 +6798,13 @@ const login = import_cjs$23.command({
6797
6798
  oauth
6798
6799
  })) switch (event._tag) {
6799
6800
  case "Timeout":
6800
- debug$5(`Connection timeout. Slowing down, polling every ${event.newInterval / 1e3}s...`);
6801
+ debug$6(`Connection timeout. Slowing down, polling every ${event.newInterval / 1e3}s...`);
6801
6802
  break;
6802
6803
  case "SlowDown":
6803
- debug$5(`Authorization server requests to slow down. Polling every ${event.newInterval / 1e3}s...`);
6804
+ debug$6(`Authorization server requests to slow down. Polling every ${event.newInterval / 1e3}s...`);
6804
6805
  break;
6805
6806
  case "Response":
6806
- debug$5("Device Access Token response:", await event.response.text());
6807
+ debug$6("Device Access Token response:", await event.response.text());
6807
6808
  break;
6808
6809
  case "Error":
6809
6810
  error = event.error;
@@ -6977,7 +6978,20 @@ var require_dist = /* @__PURE__ */ __commonJS$1({ "../../node_modules/.pnpm/@ver
6977
6978
  var import_cjs$22 = /* @__PURE__ */ __toESM(require_cjs());
6978
6979
  init_source();
6979
6980
  var import_dist = require_dist();
6980
- const debug$4 = createDebugger("sandbox:args:auth");
6981
+ const debug$5 = createDebugger("sandbox:args:auth");
6982
+ /**
6983
+ * Timestamp (ms epoch) of the most recent auto-login. Used to identify
6984
+ * tokens so that the first 401/403 against a freshly-issued token can be
6985
+ * retried instead of surfaced to the user.
6986
+ */
6987
+ let freshTokenAcquiredAt;
6988
+ const FRESH_TOKEN_WINDOW_MS = 1e4;
6989
+ function isTokenFresh() {
6990
+ return freshTokenAcquiredAt !== void 0 && Date.now() - freshTokenAcquiredAt < FRESH_TOKEN_WINDOW_MS;
6991
+ }
6992
+ function markTokenAsFresh() {
6993
+ freshTokenAcquiredAt = Date.now();
6994
+ }
6981
6995
  const token = import_cjs$22.option({
6982
6996
  long: "token",
6983
6997
  description: "A Vercel authentication token. If not provided, will use the token stored in your system from `VERCEL_AUTH_TOKEN` or will start a log in process.",
@@ -6990,18 +7004,20 @@ const token = import_cjs$22.option({
6990
7004
  if (process.env.VERCEL_OIDC_TOKEN) try {
6991
7005
  return await (0, import_dist.getVercelOidcToken)();
6992
7006
  } catch (cause) {
6993
- debug$4(`Failed to get or refresh OIDC token: ${getMessage(cause)}`);
7007
+ debug$5(`Failed to get or refresh OIDC token: ${getMessage(cause)}`);
6994
7008
  console.warn(source_default.yellow(`${source_default.bold("warn:")} failed to get or refresh OIDC token, using personal token authentication.`));
6995
7009
  }
6996
7010
  try {
6997
7011
  return await (0, import_dist.getVercelToken)();
6998
7012
  } catch (error) {
6999
7013
  if (error instanceof import_dist.AccessTokenMissingError || error instanceof import_dist.RefreshAccessTokenFailedError) {
7000
- debug$4(`CLI token unavailable (${error.name}), prompting for login...`);
7014
+ debug$5(`CLI token unavailable (${error.name}), prompting for login...`);
7001
7015
  console.warn(source_default.yellow(`${source_default.bold("notice:")} Your session has expired. Please log in again.`));
7002
7016
  await login.handler({});
7003
7017
  try {
7004
- return await (0, import_dist.getVercelToken)();
7018
+ const refreshed = await (0, import_dist.getVercelToken)();
7019
+ markTokenAsFresh();
7020
+ return refreshed;
7005
7021
  } catch (retryError) {
7006
7022
  throw new Error([
7007
7023
  `Failed to retrieve authentication token.`,
@@ -7062,18 +7078,18 @@ function readProjectConfiguration(cwd) {
7062
7078
 
7063
7079
  //#endregion
7064
7080
  //#region src/util/infer-scope.ts
7065
- const debug$3 = createDebugger("sandbox:scope");
7081
+ const debug$4 = createDebugger("sandbox:scope");
7066
7082
  async function inferScope$1({ token: token$1, team: team$1 }) {
7067
7083
  const jwt = z.jwt().safeParse(token$1);
7068
7084
  if (jwt.success) {
7069
- debug$3("trying to infer scope from OIDC JWT");
7085
+ debug$4("trying to infer scope from OIDC JWT");
7070
7086
  const data = await inferFromJwt(jwt.data);
7071
- debug$3("Using scope from OIDC JWT", data);
7087
+ debug$4("Using scope from OIDC JWT", data);
7072
7088
  return data;
7073
7089
  }
7074
7090
  const projectJson = readProjectConfiguration(process.cwd());
7075
7091
  if (projectJson) {
7076
- debug$3("Using scope from project configuration", {
7092
+ debug$4("Using scope from project configuration", {
7077
7093
  owner: projectJson.orgId,
7078
7094
  project: projectJson.projectId
7079
7095
  });
@@ -7082,12 +7098,12 @@ async function inferScope$1({ token: token$1, team: team$1 }) {
7082
7098
  project: projectJson.projectId
7083
7099
  };
7084
7100
  }
7085
- debug$3("trying to infer scope from API token", {
7101
+ debug$4("trying to infer scope from API token", {
7086
7102
  token: token$1,
7087
7103
  team: team$1
7088
7104
  });
7089
7105
  const fromToken = await inferFromToken(token$1, team$1);
7090
- debug$3("Using scope from API token", fromToken);
7106
+ debug$4("Using scope from API token", fromToken);
7091
7107
  return fromToken;
7092
7108
  }
7093
7109
  const JwtSchema = z.object({
@@ -7123,6 +7139,44 @@ async function inferFromToken(token$1, requestedTeam) {
7123
7139
  };
7124
7140
  }
7125
7141
 
7142
+ //#endregion
7143
+ //#region src/util/fresh-auth-retry.ts
7144
+ const debug$3 = createDebugger("sandbox:fresh-auth-retry");
7145
+ /**
7146
+ * Run an async operation, transparently retrying on 401/403 when the auth
7147
+ * token was just acquired via auto-login.
7148
+ */
7149
+ async function withFreshAuthRetry(factory) {
7150
+ return retry(async (bail, attempt) => {
7151
+ try {
7152
+ return await factory();
7153
+ } catch (error) {
7154
+ const status = getAuthFailureStatus(error);
7155
+ if (status !== void 0 && isTokenFresh()) {
7156
+ debug$3(`fresh-auth retry attempt ${attempt} (status ${status})`);
7157
+ throw error;
7158
+ }
7159
+ bail(error);
7160
+ return;
7161
+ }
7162
+ }, {
7163
+ retries: 3,
7164
+ minTimeout: 250,
7165
+ factor: 2,
7166
+ maxRetryTime: 3e3
7167
+ });
7168
+ }
7169
+ /**
7170
+ * Returns the HTTP status if the error represents a 401 or 403.
7171
+ * Returns undefined for any other error.
7172
+ */
7173
+ function getAuthFailureStatus(error) {
7174
+ let status;
7175
+ if (error instanceof APIError) status = error.response.status;
7176
+ else if (error instanceof NotOk) status = error.response.statusCode;
7177
+ return status === 401 || status === 403 ? status : void 0;
7178
+ }
7179
+
7126
7180
  //#endregion
7127
7181
  //#region src/args/scope.ts
7128
7182
  var import_cjs$21 = /* @__PURE__ */ __toESM(require_cjs());
@@ -7190,10 +7244,10 @@ const scope = {
7190
7244
  let projectSlug;
7191
7245
  let teamSlug;
7192
7246
  if (typeof projectId.value === "undefined" || typeof teamId.value === "undefined") try {
7193
- const scope$1 = await inferScope$1({
7247
+ const scope$1 = await withFreshAuthRetry(() => inferScope$1({
7194
7248
  token: t.value,
7195
7249
  team: teamId.value
7196
- });
7250
+ }));
7197
7251
  projectId.value ?? (projectId.value = scope$1.project);
7198
7252
  teamId.value ?? (teamId.value = scope$1.owner);
7199
7253
  projectSlug = scope$1.projectSlug;
@@ -7236,7 +7290,7 @@ const scope = {
7236
7290
 
7237
7291
  //#endregion
7238
7292
  //#region package.json
7239
- var version = "3.0.0-beta.22";
7293
+ var version = "3.0.0-beta.23";
7240
7294
 
7241
7295
  //#endregion
7242
7296
  //#region src/error.ts
@@ -7255,27 +7309,27 @@ init_source();
7255
7309
  * A {@link Sandbox} wrapper that adds user-agent headers and error handling.
7256
7310
  */
7257
7311
  const sandboxClient = {
7258
- get: (params) => withErrorHandling(Sandbox.get({
7312
+ get: (params) => withErrorHandling(() => Sandbox.get({
7259
7313
  fetch: fetchWithUserAgent,
7260
7314
  resume: false,
7261
7315
  ...params
7262
7316
  })),
7263
- create: (params) => withErrorHandling(Sandbox.create({
7317
+ create: (params) => withErrorHandling(() => Sandbox.create({
7264
7318
  fetch: fetchWithUserAgent,
7265
7319
  ...params
7266
7320
  })),
7267
- list: (params) => withErrorHandling(Sandbox.list({
7321
+ list: (params) => withErrorHandling(() => Sandbox.list({
7268
7322
  fetch: fetchWithUserAgent,
7269
7323
  ...params
7270
7324
  }))
7271
7325
  };
7272
7326
  const snapshotClient = {
7273
- list: (params) => withErrorHandling(Snapshot.list({
7327
+ list: (params) => withErrorHandling(() => Snapshot.list({
7274
7328
  fetch: fetchWithUserAgent,
7275
7329
  ...params
7276
7330
  })),
7277
- get: (params) => withErrorHandling(Snapshot.get({ ...params })),
7278
- fromSandbox: (name, opts) => withErrorHandling(Snapshot.fromSandbox(name, {
7331
+ get: (params) => withErrorHandling(() => Snapshot.get({ ...params })),
7332
+ fromSandbox: (name, opts) => withErrorHandling(() => Snapshot.fromSandbox(name, {
7279
7333
  fetch: fetchWithUserAgent,
7280
7334
  ...opts
7281
7335
  }))
@@ -7291,9 +7345,9 @@ const fetchWithUserAgent = (input, init$1) => {
7291
7345
  headers
7292
7346
  });
7293
7347
  };
7294
- async function withErrorHandling(promise) {
7348
+ async function withErrorHandling(factory) {
7295
7349
  try {
7296
- return await promise;
7350
+ return await withFreshAuthRetry(factory);
7297
7351
  } catch (error) {
7298
7352
  if (error instanceof APIError) return await handleApiError(error);
7299
7353
  throw error;
@@ -11621,6 +11675,30 @@ const args$2 = {
11621
11675
  type: import_cjs$14.optional(SnapshotExpiration),
11622
11676
  description: "Default snapshot expiration. Use \"none\" or 0 for no expiration. Example: 7d, 30d"
11623
11677
  }),
11678
+ keepLastSnapshots: import_cjs$14.option({
11679
+ long: "keep-last-snapshots",
11680
+ type: import_cjs$14.optional(import_cjs$14.extendType(import_cjs$14.number, {
11681
+ displayName: "COUNT",
11682
+ async from(n) {
11683
+ if (!Number.isInteger(n) || n < 1 || n > 10) throw new Error(`Invalid --keep-last-snapshots value: ${n}. Must be an integer between 1 and 10.`);
11684
+ return n;
11685
+ }
11686
+ })),
11687
+ description: "Keep only the N most recent snapshots of this sandbox (1-10)."
11688
+ }),
11689
+ keepLastSnapshotsFor: import_cjs$14.option({
11690
+ long: "keep-last-snapshots-for",
11691
+ type: import_cjs$14.optional(SnapshotExpiration),
11692
+ description: "Expiration applied to kept snapshots. Use \"none\" or 0 for no expiration. Example: 7d, 30d"
11693
+ }),
11694
+ deleteEvictedSnapshots: import_cjs$14.option({
11695
+ long: "delete-evicted-snapshots",
11696
+ type: import_cjs$14.optional({
11697
+ ...import_cjs$14.oneOf(["true", "false"]),
11698
+ displayName: "true|false"
11699
+ }),
11700
+ description: "When \"true\" (the default), evicted snapshots are deleted immediately; when \"false\", they keep the default expiration."
11701
+ }),
11624
11702
  ...networkPolicyArgs,
11625
11703
  scope
11626
11704
  };
@@ -11632,7 +11710,7 @@ const create = import_cjs$14.command({
11632
11710
  description: "Create and connect to a sandbox without a network access",
11633
11711
  command: `sandbox run --network-policy=none --connect`
11634
11712
  }],
11635
- async handler({ name, nonPersistent, ports, scope: scope$1, runtime: runtime$1, timeout: timeout$1, vcpus: vcpus$1, silent, snapshot: snapshot$1, sandboxSnapshot, connect: connect$2, envVars, tags, snapshotExpiration, networkPolicy: networkPolicyMode$1, allowedDomains: allowedDomains$1, allowedCIDRs: allowedCIDRs$1, deniedCIDRs: deniedCIDRs$1 }) {
11713
+ async handler({ name, nonPersistent, ports, scope: scope$1, runtime: runtime$1, timeout: timeout$1, vcpus: vcpus$1, silent, snapshot: snapshot$1, sandboxSnapshot, connect: connect$2, envVars, tags, snapshotExpiration, keepLastSnapshots, keepLastSnapshotsFor, deleteEvictedSnapshots, networkPolicy: networkPolicyMode$1, allowedDomains: allowedDomains$1, allowedCIDRs: allowedCIDRs$1, deniedCIDRs: deniedCIDRs$1 }) {
11636
11714
  if (snapshot$1 && sandboxSnapshot) throw new Error([`Cannot use --snapshot and --sandbox-snapshot together.`, `${source_default.bold("hint:")} Pick one source for the new sandbox.`].join("\n"));
11637
11715
  const networkPolicy$1 = buildNetworkPolicy({
11638
11716
  networkPolicy: networkPolicyMode$1,
@@ -11640,6 +11718,12 @@ const create = import_cjs$14.command({
11640
11718
  allowedCIDRs: allowedCIDRs$1,
11641
11719
  deniedCIDRs: deniedCIDRs$1
11642
11720
  });
11721
+ if (keepLastSnapshots === void 0 && (keepLastSnapshotsFor !== void 0 || deleteEvictedSnapshots !== void 0)) throw new Error(["--keep-last-snapshots-for and --delete-evicted-snapshots require --keep-last-snapshots.", `${source_default.bold("hint:")} Pass --keep-last-snapshots <count> to enable the retention policy.`].join("\n"));
11722
+ const keepLastSnapshotsPayload = keepLastSnapshots !== void 0 ? {
11723
+ count: keepLastSnapshots,
11724
+ expiration: keepLastSnapshotsFor !== void 0 ? (0, import_ms$2.default)(keepLastSnapshotsFor) : void 0,
11725
+ deleteEvicted: deleteEvictedSnapshots !== void 0 ? deleteEvictedSnapshots === "true" : void 0
11726
+ } : void 0;
11643
11727
  const persistent = !nonPersistent;
11644
11728
  const resources = vcpus$1 ? { vcpus: vcpus$1 } : void 0;
11645
11729
  const tagsObj = Object.keys(tags).length > 0 ? tags : void 0;
@@ -11666,6 +11750,7 @@ const create = import_cjs$14.command({
11666
11750
  tags: tagsObj,
11667
11751
  persistent,
11668
11752
  snapshotExpiration: snapshotExpiration ? (0, import_ms$2.default)(snapshotExpiration) : void 0,
11753
+ keepLastSnapshots: keepLastSnapshotsPayload,
11669
11754
  __interactive: true
11670
11755
  }) : await sandboxClient.create({
11671
11756
  name,
@@ -11681,6 +11766,7 @@ const create = import_cjs$14.command({
11681
11766
  tags: tagsObj,
11682
11767
  persistent,
11683
11768
  snapshotExpiration: snapshotExpiration ? (0, import_ms$2.default)(snapshotExpiration) : void 0,
11769
+ keepLastSnapshots: keepLastSnapshotsPayload,
11684
11770
  __interactive: true
11685
11771
  });
11686
11772
  spinner?.stop();
@@ -14910,6 +14996,136 @@ const snapshotExpirationCommand = import_cjs$1.command({
14910
14996
  }
14911
14997
  }
14912
14998
  });
14999
+ const keepLastCountType = import_cjs$1.extendType(import_cjs$1.number, {
15000
+ displayName: "COUNT",
15001
+ async from(n) {
15002
+ if (!Number.isInteger(n) || n < 0 || n > 10) throw new Error(`Invalid count: ${n}. Must be an integer between 0 and 10 (0 removes the policy).`);
15003
+ return n;
15004
+ }
15005
+ });
15006
+ const keepLastSnapshotsCommand = import_cjs$1.command({
15007
+ name: "keep-last-snapshots",
15008
+ description: "Update the snapshot retention policy (keep only the N most recent snapshots) of a sandbox",
15009
+ args: {
15010
+ sandbox: import_cjs$1.positional({
15011
+ type: sandboxName,
15012
+ description: "Sandbox name to update"
15013
+ }),
15014
+ count: import_cjs$1.positional({
15015
+ type: keepLastCountType,
15016
+ description: "Number of most recent snapshots to keep (1-10). Pass 0 to remove the policy."
15017
+ }),
15018
+ scope
15019
+ },
15020
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandbox: name, count }) {
15021
+ const sandbox = await sandboxClient.get({
15022
+ name,
15023
+ projectId: project$1,
15024
+ teamId: team$1,
15025
+ token: token$1
15026
+ });
15027
+ const spinner = ora("Updating sandbox configuration...").start();
15028
+ try {
15029
+ if (count === 0) await sandbox.update({ keepLastSnapshots: null });
15030
+ else {
15031
+ const current = sandbox.keepLastSnapshots;
15032
+ await sandbox.update({ keepLastSnapshots: {
15033
+ count,
15034
+ expiration: current?.expiration,
15035
+ deleteEvicted: current?.deleteEvicted
15036
+ } });
15037
+ }
15038
+ spinner.stop();
15039
+ process.stderr.write("✅ Configuration updated for sandbox " + source_default.cyan(name) + "\n");
15040
+ process.stderr.write(source_default.dim(" ╰ ") + "keep-last-snapshots: " + source_default.cyan(count === 0 ? "cleared" : `count=${count}`) + "\n");
15041
+ } catch (error) {
15042
+ spinner.stop();
15043
+ throw error;
15044
+ }
15045
+ }
15046
+ });
15047
+ const keepLastSnapshotsForCommand = import_cjs$1.command({
15048
+ name: "keep-last-snapshots-for",
15049
+ description: "Update the expiration applied to snapshots kept by the retention policy",
15050
+ args: {
15051
+ sandbox: import_cjs$1.positional({
15052
+ type: sandboxName,
15053
+ description: "Sandbox name to update"
15054
+ }),
15055
+ duration: import_cjs$1.positional({
15056
+ type: SnapshotExpiration,
15057
+ description: "Expiration for kept snapshots. Use \"none\" or 0 for no expiration. Example: 7d, 30d"
15058
+ }),
15059
+ scope
15060
+ },
15061
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandbox: name, duration }) {
15062
+ const sandbox = await sandboxClient.get({
15063
+ name,
15064
+ projectId: project$1,
15065
+ teamId: team$1,
15066
+ token: token$1
15067
+ });
15068
+ const current = sandbox.keepLastSnapshots;
15069
+ if (!current) throw new Error(["No keep-last-snapshots policy is set on this sandbox.", `${source_default.bold("hint:")} Set a count first with: sandbox config keep-last-snapshots ${name} <count>`].join("\n"));
15070
+ const spinner = ora("Updating sandbox configuration...").start();
15071
+ try {
15072
+ await sandbox.update({ keepLastSnapshots: {
15073
+ count: current.count,
15074
+ expiration: (0, import_ms.default)(duration),
15075
+ deleteEvicted: current.deleteEvicted
15076
+ } });
15077
+ spinner.stop();
15078
+ const display = (0, import_ms.default)(duration) === 0 ? "none" : duration;
15079
+ process.stderr.write("✅ Configuration updated for sandbox " + source_default.cyan(name) + "\n");
15080
+ process.stderr.write(source_default.dim(" ╰ ") + "keep-last-snapshots-for: " + source_default.cyan(display) + "\n");
15081
+ } catch (error) {
15082
+ spinner.stop();
15083
+ throw error;
15084
+ }
15085
+ }
15086
+ });
15087
+ const deleteEvictedSnapshotsCommand = import_cjs$1.command({
15088
+ name: "delete-evicted-snapshots",
15089
+ description: "When \"true\" (the default), snapshots evicted by the keep-last-snapshots policy are deleted immediately; when \"false\", they keep the default expiration.",
15090
+ args: {
15091
+ sandbox: import_cjs$1.positional({
15092
+ type: sandboxName,
15093
+ description: "Sandbox name to update"
15094
+ }),
15095
+ value: import_cjs$1.positional({
15096
+ type: {
15097
+ ...import_cjs$1.oneOf(["true", "false"]),
15098
+ displayName: "true|false"
15099
+ },
15100
+ description: "Whether to delete evicted snapshots immediately (\"true\") or let them keep the default expiration (\"false\")."
15101
+ }),
15102
+ scope
15103
+ },
15104
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandbox: name, value }) {
15105
+ const sandbox = await sandboxClient.get({
15106
+ name,
15107
+ projectId: project$1,
15108
+ teamId: team$1,
15109
+ token: token$1
15110
+ });
15111
+ const current = sandbox.keepLastSnapshots;
15112
+ if (!current) throw new Error(["No keep-last-snapshots policy is set on this sandbox.", `${source_default.bold("hint:")} Set a count first with: sandbox config keep-last-snapshots ${name} <count>`].join("\n"));
15113
+ const spinner = ora("Updating sandbox configuration...").start();
15114
+ try {
15115
+ await sandbox.update({ keepLastSnapshots: {
15116
+ count: current.count,
15117
+ expiration: current.expiration,
15118
+ deleteEvicted: value === "true"
15119
+ } });
15120
+ spinner.stop();
15121
+ process.stderr.write("✅ Configuration updated for sandbox " + source_default.cyan(name) + "\n");
15122
+ process.stderr.write(source_default.dim(" ╰ ") + "delete-evicted-snapshots: " + source_default.cyan(value) + "\n");
15123
+ } catch (error) {
15124
+ spinner.stop();
15125
+ throw error;
15126
+ }
15127
+ }
15128
+ });
14913
15129
  const currentSnapshotCommand = import_cjs$1.command({
14914
15130
  name: "current-snapshot",
14915
15131
  description: "Update the current snapshot of a sandbox",
@@ -14994,6 +15210,10 @@ const listCommand = import_cjs$1.command({
14994
15210
  field: "Snapshot expiration",
14995
15211
  value: sandbox.snapshotExpiration != null && sandbox.snapshotExpiration > 0 ? (0, import_ms.default)(sandbox.snapshotExpiration, { long: true }) : sandbox.snapshotExpiration === 0 ? "none" : "-"
14996
15212
  },
15213
+ {
15214
+ field: "Keep last snapshots",
15215
+ value: formatKeepLastSnapshots(sandbox.keepLastSnapshots)
15216
+ },
14997
15217
  {
14998
15218
  field: "Current snapshot",
14999
15219
  value: sandbox.currentSnapshotId ?? "-"
@@ -15098,6 +15318,13 @@ const tagsCommand = import_cjs$1.command({
15098
15318
  }
15099
15319
  }
15100
15320
  });
15321
+ function formatKeepLastSnapshots(keepLastSnapshots) {
15322
+ if (!keepLastSnapshots) return "-";
15323
+ const parts = [`count=${keepLastSnapshots.count}`];
15324
+ if (keepLastSnapshots.expiration !== void 0) parts.push(`for=${keepLastSnapshots.expiration === 0 ? "none" : (0, import_ms.default)(keepLastSnapshots.expiration, { long: true })}`);
15325
+ if (keepLastSnapshots.deleteEvicted !== void 0) parts.push(`delete-evicted-snapshots=${keepLastSnapshots.deleteEvicted}`);
15326
+ return parts.join(", ");
15327
+ }
15101
15328
  const config = import_cjs$1.subcommands({
15102
15329
  name: "config",
15103
15330
  description: "View and update sandbox configuration",
@@ -15108,6 +15335,9 @@ const config = import_cjs$1.subcommands({
15108
15335
  persistent: persistentCommand,
15109
15336
  "network-policy": networkPolicyCommand,
15110
15337
  "snapshot-expiration": snapshotExpirationCommand,
15338
+ "keep-last-snapshots": keepLastSnapshotsCommand,
15339
+ "keep-last-snapshots-for": keepLastSnapshotsForCommand,
15340
+ "delete-evicted-snapshots": deleteEvictedSnapshotsCommand,
15111
15341
  "current-snapshot": currentSnapshotCommand,
15112
15342
  tags: tagsCommand
15113
15343
  }
@@ -15156,4 +15386,4 @@ const app = (opts) => (0, import_cjs.subcommands)({
15156
15386
 
15157
15387
  //#endregion
15158
15388
  export { source_exports as a, init_source as i, StyledError as n, require_cjs as r, app as t };
15159
- //# sourceMappingURL=app-BvadkI4m.mjs.map
15389
+ //# sourceMappingURL=app-BjVyEIk1.mjs.map