sandbox 3.0.0-beta.22 → 3.0.0-beta.24

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.24";
7240
7294
 
7241
7295
  //#endregion
7242
7296
  //#region src/error.ts
@@ -7255,29 +7309,33 @@ 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
7335
+ })),
7336
+ tree: (params) => withErrorHandling(() => Snapshot.tree({
7337
+ fetch: fetchWithUserAgent,
7338
+ ...params
7281
7339
  }))
7282
7340
  };
7283
7341
  const fetchWithUserAgent = (input, init$1) => {
@@ -7291,9 +7349,9 @@ const fetchWithUserAgent = (input, init$1) => {
7291
7349
  headers
7292
7350
  });
7293
7351
  };
7294
- async function withErrorHandling(promise) {
7352
+ async function withErrorHandling(factory) {
7295
7353
  try {
7296
- return await promise;
7354
+ return await withFreshAuthRetry(factory);
7297
7355
  } catch (error) {
7298
7356
  if (error instanceof APIError) return await handleApiError(error);
7299
7357
  throw error;
@@ -11621,6 +11679,30 @@ const args$2 = {
11621
11679
  type: import_cjs$14.optional(SnapshotExpiration),
11622
11680
  description: "Default snapshot expiration. Use \"none\" or 0 for no expiration. Example: 7d, 30d"
11623
11681
  }),
11682
+ keepLastSnapshots: import_cjs$14.option({
11683
+ long: "keep-last-snapshots",
11684
+ type: import_cjs$14.optional(import_cjs$14.extendType(import_cjs$14.number, {
11685
+ displayName: "COUNT",
11686
+ async from(n) {
11687
+ 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.`);
11688
+ return n;
11689
+ }
11690
+ })),
11691
+ description: "Keep only the N most recent snapshots of this sandbox (1-10)."
11692
+ }),
11693
+ keepLastSnapshotsFor: import_cjs$14.option({
11694
+ long: "keep-last-snapshots-for",
11695
+ type: import_cjs$14.optional(SnapshotExpiration),
11696
+ description: "Expiration applied to kept snapshots. Use \"none\" or 0 for no expiration. Example: 7d, 30d"
11697
+ }),
11698
+ deleteEvictedSnapshots: import_cjs$14.option({
11699
+ long: "delete-evicted-snapshots",
11700
+ type: import_cjs$14.optional({
11701
+ ...import_cjs$14.oneOf(["true", "false"]),
11702
+ displayName: "true|false"
11703
+ }),
11704
+ description: "When \"true\" (the default), evicted snapshots are deleted immediately; when \"false\", they keep the default expiration."
11705
+ }),
11624
11706
  ...networkPolicyArgs,
11625
11707
  scope
11626
11708
  };
@@ -11632,7 +11714,7 @@ const create = import_cjs$14.command({
11632
11714
  description: "Create and connect to a sandbox without a network access",
11633
11715
  command: `sandbox run --network-policy=none --connect`
11634
11716
  }],
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 }) {
11717
+ 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
11718
  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
11719
  const networkPolicy$1 = buildNetworkPolicy({
11638
11720
  networkPolicy: networkPolicyMode$1,
@@ -11640,6 +11722,12 @@ const create = import_cjs$14.command({
11640
11722
  allowedCIDRs: allowedCIDRs$1,
11641
11723
  deniedCIDRs: deniedCIDRs$1
11642
11724
  });
11725
+ 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"));
11726
+ const keepLastSnapshotsPayload = keepLastSnapshots !== void 0 ? {
11727
+ count: keepLastSnapshots,
11728
+ expiration: keepLastSnapshotsFor !== void 0 ? (0, import_ms$2.default)(keepLastSnapshotsFor) : void 0,
11729
+ deleteEvicted: deleteEvictedSnapshots !== void 0 ? deleteEvictedSnapshots === "true" : void 0
11730
+ } : void 0;
11643
11731
  const persistent = !nonPersistent;
11644
11732
  const resources = vcpus$1 ? { vcpus: vcpus$1 } : void 0;
11645
11733
  const tagsObj = Object.keys(tags).length > 0 ? tags : void 0;
@@ -11666,6 +11754,7 @@ const create = import_cjs$14.command({
11666
11754
  tags: tagsObj,
11667
11755
  persistent,
11668
11756
  snapshotExpiration: snapshotExpiration ? (0, import_ms$2.default)(snapshotExpiration) : void 0,
11757
+ keepLastSnapshots: keepLastSnapshotsPayload,
11669
11758
  __interactive: true
11670
11759
  }) : await sandboxClient.create({
11671
11760
  name,
@@ -11681,6 +11770,7 @@ const create = import_cjs$14.command({
11681
11770
  tags: tagsObj,
11682
11771
  persistent,
11683
11772
  snapshotExpiration: snapshotExpiration ? (0, import_ms$2.default)(snapshotExpiration) : void 0,
11773
+ keepLastSnapshots: keepLastSnapshotsPayload,
11684
11774
  __interactive: true
11685
11775
  });
11686
11776
  spinner?.stop();
@@ -14501,6 +14591,75 @@ const snapshot = import_cjs$6.command({
14501
14591
  }
14502
14592
  });
14503
14593
 
14594
+ //#endregion
14595
+ //#region src/util/snapshot-tree.ts
14596
+ init_source();
14597
+ function formatExpires(expiresAt) {
14598
+ if (expiresAt === void 0) return source_default.gray("never");
14599
+ const ms$4 = expiresAt - Date.now();
14600
+ if (ms$4 <= 0) return source_default.red("expired");
14601
+ const formatted = timeAgo(expiresAt);
14602
+ return ms$4 <= 3600 * 1e3 ? source_default.red(formatted) : source_default.green(formatted);
14603
+ }
14604
+ function renderNode(id, expiresAt, isCurrent) {
14605
+ const bullet = isCurrent ? source_default.magenta.bold("●") : source_default.magenta("●");
14606
+ const suffix = isCurrent ? ` ${source_default.green("◂ current")}` : "";
14607
+ return `${bullet} ${source_default.yellow(id)} expires: ${formatExpires(expiresAt)}${suffix}`;
14608
+ }
14609
+ function renderSiblings(siblings, count) {
14610
+ const lines = [];
14611
+ const shown = siblings.slice(0, 5);
14612
+ const totalCount = parseInt(count, 10);
14613
+ const hasPlus = count.endsWith("+");
14614
+ const remaining = totalCount - 1 - shown.length;
14615
+ for (let i = 0; i < shown.length; i++) {
14616
+ const connector = i === shown.length - 1 && remaining <= 0 ? "╰──" : "├──";
14617
+ lines.push(`│ ${connector} ${source_default.gray(shown[i].sourceSessionId)}`);
14618
+ }
14619
+ if (remaining > 0) {
14620
+ const suffix = hasPlus ? "+" : "";
14621
+ lines.push(`│ ╰── ${source_default.gray(`+${remaining}${suffix} more sandboxes`)}`);
14622
+ }
14623
+ return lines;
14624
+ }
14625
+ function renderSnapshotTree(params) {
14626
+ const { ancestors, descendants } = params;
14627
+ const hideCurrent = params.hideCurrent === true;
14628
+ const currentSnapshotId = hideCurrent ? void 0 : params.currentSnapshotId;
14629
+ const lines = [];
14630
+ const pushNode = (node, isCurrent) => {
14631
+ lines.push("│");
14632
+ lines.push(renderNode(node.snapshot.id, node.snapshot.expiresAt, isCurrent));
14633
+ if (node.siblings.length > 0) lines.push(...renderSiblings(node.siblings, node.count));
14634
+ };
14635
+ const descendantNodes = descendants.snapshots.filter((n) => n.snapshot.id !== currentSnapshotId);
14636
+ if (descendantNodes.length > 0) for (const node of [...descendantNodes].reverse()) pushNode(node, false);
14637
+ if (!hideCurrent) {
14638
+ const { currentSnapshotId: id, currentSnapshotExpiresAt } = params;
14639
+ pushNode(ancestors.snapshots.find((n) => n.snapshot.id === id) ?? descendants.snapshots.find((n) => n.snapshot.id === id) ?? {
14640
+ snapshot: {
14641
+ id,
14642
+ sourceSessionId: "",
14643
+ expiresAt: currentSnapshotExpiresAt
14644
+ },
14645
+ siblings: [],
14646
+ count: "1"
14647
+ }, true);
14648
+ }
14649
+ const ancestorNodes = ancestors.snapshots.filter((n) => n.snapshot.id !== currentSnapshotId);
14650
+ for (const node of ancestorNodes) pushNode(node, false);
14651
+ if (!hideCurrent) {
14652
+ const lastAncestor = ancestorNodes[ancestorNodes.length - 1];
14653
+ if (ancestors.pagination.next === null) {
14654
+ if (lastAncestor && !lastAncestor.snapshot.parentId || ancestorNodes.length === 0) {
14655
+ lines.push("│");
14656
+ lines.push(`${source_default.white("○")} ${source_default.gray("(root)")}`);
14657
+ }
14658
+ }
14659
+ }
14660
+ return lines.join("\n");
14661
+ }
14662
+
14504
14663
  //#endregion
14505
14664
  //#region src/commands/snapshots.ts
14506
14665
  var import_cjs$4 = /* @__PURE__ */ __toESM(require_cjs());
@@ -14651,12 +14810,154 @@ const remove$1 = import_cjs$4.command({
14651
14810
  }
14652
14811
  }
14653
14812
  });
14813
+ const tree = import_cjs$4.command({
14814
+ name: "tree",
14815
+ description: "Show the snapshot ancestry tree for a sandbox.",
14816
+ args: {
14817
+ scope,
14818
+ sandboxName: import_cjs$4.positional({
14819
+ type: sandboxName,
14820
+ description: "Sandbox name"
14821
+ }),
14822
+ sortOrder: import_cjs$4.option({
14823
+ long: "sort-order",
14824
+ description: "Sort order. Options: asc, desc (default). 'desc' walks ancestors, 'asc' walks descendants. Requires --cursor.",
14825
+ type: import_cjs$4.optional(import_cjs$4.oneOf(["asc", "desc"]))
14826
+ }),
14827
+ limit: import_cjs$4.option({
14828
+ long: "limit",
14829
+ description: "Maximum number of snapshots per page (1–10, default 10).",
14830
+ type: import_cjs$4.optional(import_cjs$4.number)
14831
+ }),
14832
+ cursor: import_cjs$4.option({
14833
+ long: "cursor",
14834
+ description: "Pagination cursor from a previous 'More ancestors' or 'More descendants' hint.",
14835
+ type: import_cjs$4.optional(import_cjs$4.string)
14836
+ })
14837
+ },
14838
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandboxName: name, sortOrder, limit, cursor }) {
14839
+ if (limit !== void 0 && (limit < 1 || limit > 10)) {
14840
+ console.error(source_default.red("Error: --limit must be between 1 and 10."));
14841
+ process.exitCode = 1;
14842
+ return;
14843
+ }
14844
+ if (sortOrder !== void 0 && !cursor) {
14845
+ console.error(source_default.red("Error: --sort-order requires --cursor."));
14846
+ process.exitCode = 1;
14847
+ return;
14848
+ }
14849
+ const pageLimit = limit ?? 10;
14850
+ if (cursor) {
14851
+ const effectiveSortOrder = sortOrder ?? "desc";
14852
+ const page = await (async () => {
14853
+ try {
14854
+ var _usingCtx4 = _usingCtx();
14855
+ const _spinner$1 = _usingCtx4.u(acquireRelease(() => ora("Fetching snapshot tree...").start(), (s$1) => s$1.stop()));
14856
+ return snapshotClient.tree({
14857
+ snapshotId: cursor,
14858
+ sortOrder: effectiveSortOrder,
14859
+ limit: pageLimit,
14860
+ token: token$1,
14861
+ teamId: team$1,
14862
+ projectId: project$1
14863
+ });
14864
+ } catch (_) {
14865
+ _usingCtx4.e = _;
14866
+ } finally {
14867
+ _usingCtx4.d();
14868
+ }
14869
+ })();
14870
+ const ancestors = effectiveSortOrder === "desc" ? page : {
14871
+ snapshots: [],
14872
+ pagination: {
14873
+ count: 0,
14874
+ next: null
14875
+ }
14876
+ };
14877
+ const descendants = effectiveSortOrder === "asc" ? page : {
14878
+ snapshots: [],
14879
+ pagination: {
14880
+ count: 0,
14881
+ next: null
14882
+ }
14883
+ };
14884
+ console.log(renderSnapshotTree({
14885
+ hideCurrent: true,
14886
+ ancestors,
14887
+ descendants
14888
+ }));
14889
+ if (page.pagination.next !== null) console.log(formatNextCursorHint(page.pagination.next));
14890
+ return;
14891
+ }
14892
+ const result = await (async () => {
14893
+ try {
14894
+ var _usingCtx5 = _usingCtx();
14895
+ const _spinner$1 = _usingCtx5.u(acquireRelease(() => ora("Fetching snapshot tree...").start(), (s$1) => s$1.stop()));
14896
+ const currentSnapshotId = (await sandboxClient.get({
14897
+ name,
14898
+ token: token$1,
14899
+ teamId: team$1,
14900
+ projectId: project$1
14901
+ })).currentSnapshotId;
14902
+ if (!currentSnapshotId) return null;
14903
+ const [currentSnap, ancestors, descendants] = await Promise.all([
14904
+ snapshotClient.get({
14905
+ snapshotId: currentSnapshotId,
14906
+ token: token$1,
14907
+ teamId: team$1,
14908
+ projectId: project$1
14909
+ }),
14910
+ snapshotClient.tree({
14911
+ snapshotId: currentSnapshotId,
14912
+ sortOrder: "desc",
14913
+ limit: pageLimit,
14914
+ token: token$1,
14915
+ teamId: team$1,
14916
+ projectId: project$1
14917
+ }),
14918
+ snapshotClient.tree({
14919
+ snapshotId: currentSnapshotId,
14920
+ sortOrder: "asc",
14921
+ limit: pageLimit,
14922
+ token: token$1,
14923
+ teamId: team$1,
14924
+ projectId: project$1
14925
+ })
14926
+ ]);
14927
+ return {
14928
+ currentSnap,
14929
+ currentSnapshotId,
14930
+ ancestors,
14931
+ descendants
14932
+ };
14933
+ } catch (_) {
14934
+ _usingCtx5.e = _;
14935
+ } finally {
14936
+ _usingCtx5.d();
14937
+ }
14938
+ })();
14939
+ if (!result) {
14940
+ console.log(source_default.yellow("No snapshots found for this sandbox."));
14941
+ return;
14942
+ }
14943
+ console.log(renderSnapshotTree({
14944
+ currentSnapshotId: result.currentSnapshotId,
14945
+ currentSnapshotExpiresAt: result.currentSnap.expiresAt?.getTime(),
14946
+ ancestors: result.ancestors,
14947
+ descendants: result.descendants
14948
+ }));
14949
+ const limitArg = limit !== void 0 ? ` --limit ${limit}` : "";
14950
+ if (result.ancestors.pagination.next !== null) console.log(`\nMore ancestors: sandbox snapshots tree ${name} --sort-order desc${limitArg} --cursor ${result.ancestors.pagination.next}`);
14951
+ if (result.descendants.pagination.next !== null) console.log(`More descendants: sandbox snapshots tree ${name} --sort-order asc${limitArg} --cursor ${result.descendants.pagination.next}`);
14952
+ }
14953
+ });
14654
14954
  const snapshots = (0, import_cjs$5.subcommands)({
14655
14955
  name: "snapshots",
14656
14956
  description: "Manage sandbox snapshots",
14657
14957
  cmds: {
14658
14958
  list: list$2,
14659
14959
  get,
14960
+ tree,
14660
14961
  delete: remove$1
14661
14962
  }
14662
14963
  });
@@ -14910,6 +15211,136 @@ const snapshotExpirationCommand = import_cjs$1.command({
14910
15211
  }
14911
15212
  }
14912
15213
  });
15214
+ const keepLastCountType = import_cjs$1.extendType(import_cjs$1.number, {
15215
+ displayName: "COUNT",
15216
+ async from(n) {
15217
+ 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).`);
15218
+ return n;
15219
+ }
15220
+ });
15221
+ const keepLastSnapshotsCommand = import_cjs$1.command({
15222
+ name: "keep-last-snapshots",
15223
+ description: "Update the snapshot retention policy (keep only the N most recent snapshots) of a sandbox",
15224
+ args: {
15225
+ sandbox: import_cjs$1.positional({
15226
+ type: sandboxName,
15227
+ description: "Sandbox name to update"
15228
+ }),
15229
+ count: import_cjs$1.positional({
15230
+ type: keepLastCountType,
15231
+ description: "Number of most recent snapshots to keep (1-10). Pass 0 to remove the policy."
15232
+ }),
15233
+ scope
15234
+ },
15235
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandbox: name, count }) {
15236
+ const sandbox = await sandboxClient.get({
15237
+ name,
15238
+ projectId: project$1,
15239
+ teamId: team$1,
15240
+ token: token$1
15241
+ });
15242
+ const spinner = ora("Updating sandbox configuration...").start();
15243
+ try {
15244
+ if (count === 0) await sandbox.update({ keepLastSnapshots: null });
15245
+ else {
15246
+ const current = sandbox.keepLastSnapshots;
15247
+ await sandbox.update({ keepLastSnapshots: {
15248
+ count,
15249
+ expiration: current?.expiration,
15250
+ deleteEvicted: current?.deleteEvicted
15251
+ } });
15252
+ }
15253
+ spinner.stop();
15254
+ process.stderr.write("✅ Configuration updated for sandbox " + source_default.cyan(name) + "\n");
15255
+ process.stderr.write(source_default.dim(" ╰ ") + "keep-last-snapshots: " + source_default.cyan(count === 0 ? "cleared" : `count=${count}`) + "\n");
15256
+ } catch (error) {
15257
+ spinner.stop();
15258
+ throw error;
15259
+ }
15260
+ }
15261
+ });
15262
+ const keepLastSnapshotsForCommand = import_cjs$1.command({
15263
+ name: "keep-last-snapshots-for",
15264
+ description: "Update the expiration applied to snapshots kept by the retention policy",
15265
+ args: {
15266
+ sandbox: import_cjs$1.positional({
15267
+ type: sandboxName,
15268
+ description: "Sandbox name to update"
15269
+ }),
15270
+ duration: import_cjs$1.positional({
15271
+ type: SnapshotExpiration,
15272
+ description: "Expiration for kept snapshots. Use \"none\" or 0 for no expiration. Example: 7d, 30d"
15273
+ }),
15274
+ scope
15275
+ },
15276
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandbox: name, duration }) {
15277
+ const sandbox = await sandboxClient.get({
15278
+ name,
15279
+ projectId: project$1,
15280
+ teamId: team$1,
15281
+ token: token$1
15282
+ });
15283
+ const current = sandbox.keepLastSnapshots;
15284
+ 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"));
15285
+ const spinner = ora("Updating sandbox configuration...").start();
15286
+ try {
15287
+ await sandbox.update({ keepLastSnapshots: {
15288
+ count: current.count,
15289
+ expiration: (0, import_ms.default)(duration),
15290
+ deleteEvicted: current.deleteEvicted
15291
+ } });
15292
+ spinner.stop();
15293
+ const display = (0, import_ms.default)(duration) === 0 ? "none" : duration;
15294
+ process.stderr.write("✅ Configuration updated for sandbox " + source_default.cyan(name) + "\n");
15295
+ process.stderr.write(source_default.dim(" ╰ ") + "keep-last-snapshots-for: " + source_default.cyan(display) + "\n");
15296
+ } catch (error) {
15297
+ spinner.stop();
15298
+ throw error;
15299
+ }
15300
+ }
15301
+ });
15302
+ const deleteEvictedSnapshotsCommand = import_cjs$1.command({
15303
+ name: "delete-evicted-snapshots",
15304
+ description: "When \"true\" (the default), snapshots evicted by the keep-last-snapshots policy are deleted immediately; when \"false\", they keep the default expiration.",
15305
+ args: {
15306
+ sandbox: import_cjs$1.positional({
15307
+ type: sandboxName,
15308
+ description: "Sandbox name to update"
15309
+ }),
15310
+ value: import_cjs$1.positional({
15311
+ type: {
15312
+ ...import_cjs$1.oneOf(["true", "false"]),
15313
+ displayName: "true|false"
15314
+ },
15315
+ description: "Whether to delete evicted snapshots immediately (\"true\") or let them keep the default expiration (\"false\")."
15316
+ }),
15317
+ scope
15318
+ },
15319
+ async handler({ scope: { token: token$1, team: team$1, project: project$1 }, sandbox: name, value }) {
15320
+ const sandbox = await sandboxClient.get({
15321
+ name,
15322
+ projectId: project$1,
15323
+ teamId: team$1,
15324
+ token: token$1
15325
+ });
15326
+ const current = sandbox.keepLastSnapshots;
15327
+ 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"));
15328
+ const spinner = ora("Updating sandbox configuration...").start();
15329
+ try {
15330
+ await sandbox.update({ keepLastSnapshots: {
15331
+ count: current.count,
15332
+ expiration: current.expiration,
15333
+ deleteEvicted: value === "true"
15334
+ } });
15335
+ spinner.stop();
15336
+ process.stderr.write("✅ Configuration updated for sandbox " + source_default.cyan(name) + "\n");
15337
+ process.stderr.write(source_default.dim(" ╰ ") + "delete-evicted-snapshots: " + source_default.cyan(value) + "\n");
15338
+ } catch (error) {
15339
+ spinner.stop();
15340
+ throw error;
15341
+ }
15342
+ }
15343
+ });
14913
15344
  const currentSnapshotCommand = import_cjs$1.command({
14914
15345
  name: "current-snapshot",
14915
15346
  description: "Update the current snapshot of a sandbox",
@@ -14994,6 +15425,10 @@ const listCommand = import_cjs$1.command({
14994
15425
  field: "Snapshot expiration",
14995
15426
  value: sandbox.snapshotExpiration != null && sandbox.snapshotExpiration > 0 ? (0, import_ms.default)(sandbox.snapshotExpiration, { long: true }) : sandbox.snapshotExpiration === 0 ? "none" : "-"
14996
15427
  },
15428
+ {
15429
+ field: "Keep last snapshots",
15430
+ value: formatKeepLastSnapshots(sandbox.keepLastSnapshots)
15431
+ },
14997
15432
  {
14998
15433
  field: "Current snapshot",
14999
15434
  value: sandbox.currentSnapshotId ?? "-"
@@ -15098,6 +15533,13 @@ const tagsCommand = import_cjs$1.command({
15098
15533
  }
15099
15534
  }
15100
15535
  });
15536
+ function formatKeepLastSnapshots(keepLastSnapshots) {
15537
+ if (!keepLastSnapshots) return "-";
15538
+ const parts = [`count=${keepLastSnapshots.count}`];
15539
+ if (keepLastSnapshots.expiration !== void 0) parts.push(`for=${keepLastSnapshots.expiration === 0 ? "none" : (0, import_ms.default)(keepLastSnapshots.expiration, { long: true })}`);
15540
+ if (keepLastSnapshots.deleteEvicted !== void 0) parts.push(`delete-evicted-snapshots=${keepLastSnapshots.deleteEvicted}`);
15541
+ return parts.join(", ");
15542
+ }
15101
15543
  const config = import_cjs$1.subcommands({
15102
15544
  name: "config",
15103
15545
  description: "View and update sandbox configuration",
@@ -15108,6 +15550,9 @@ const config = import_cjs$1.subcommands({
15108
15550
  persistent: persistentCommand,
15109
15551
  "network-policy": networkPolicyCommand,
15110
15552
  "snapshot-expiration": snapshotExpirationCommand,
15553
+ "keep-last-snapshots": keepLastSnapshotsCommand,
15554
+ "keep-last-snapshots-for": keepLastSnapshotsForCommand,
15555
+ "delete-evicted-snapshots": deleteEvictedSnapshotsCommand,
15111
15556
  "current-snapshot": currentSnapshotCommand,
15112
15557
  tags: tagsCommand
15113
15558
  }
@@ -15156,4 +15601,4 @@ const app = (opts) => (0, import_cjs.subcommands)({
15156
15601
 
15157
15602
  //#endregion
15158
15603
  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
15604
+ //# sourceMappingURL=app-CpryW0ai.mjs.map