setsuna-microvm 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +32 -32
  2. package/dist/cli.js +236 -314
  3. package/package.json +1 -1
package/LICENSE CHANGED
@@ -1,32 +1,32 @@
1
- SemSwitch Setsuna Research Preview Client License
2
-
3
- Copyright © 2026 SemSwitch, Inc. All rights reserved.
4
-
5
- The Setsuna client distributed in this package is proprietary software owned by SemSwitch, Inc.
6
- ("SemSwitch"). It is provided for authorized use with the Setsuna Research Preview.
7
-
8
- 1. Grant. Subject to the SemSwitch Terms of Service (https://semswitch.com/terms) and any
9
- applicable Order or evaluation agreement, an authorized user receives a limited, non-exclusive
10
- right to install and use the Setsuna client solely to access or evaluate the Setsuna service
11
- that the user is authorized to use.
12
-
13
- 2. Ownership. The Setsuna client is licensed, not sold. SemSwitch retains all ownership,
14
- copyright, and other intellectual property rights in the Setsuna client. This license transfers
15
- no ownership or other intellectual property rights.
16
-
17
- 3. Restrictions. Except where expressly permitted by an applicable agreement with SemSwitch or by
18
- non-waivable law, no right is granted to redistribute, sublicense, sell, rent, publish, modify,
19
- create derivative works from, reverse engineer, decompile, or disassemble the Setsuna client.
20
-
21
- 4. Third-party software. The Setsuna client uses third-party software and components that are not
22
- owned by SemSwitch. They remain governed by their respective licenses and notices, which this
23
- license does not modify or replace.
24
-
25
- 5. Term. The right to use the Setsuna client ends when the applicable Setsuna authorization ends.
26
-
27
- 6. Agreements. If an applicable signed Order or separately negotiated agreement with SemSwitch
28
- expressly conflicts with this license, that Order or agreement controls.
29
-
30
- 7. Warranties, liability, and service terms. Warranties, disclaimers, limitations of liability,
31
- and the terms of the Setsuna service are governed by the applicable agreement and the SemSwitch
32
- Terms of Service rather than by this license.
1
+ SemSwitch Setsuna Research Preview Client License
2
+
3
+ Copyright © 2026 SemSwitch, Inc. All rights reserved.
4
+
5
+ The Setsuna client distributed in this package is proprietary software owned by SemSwitch, Inc.
6
+ ("SemSwitch"). It is provided for authorized use with the Setsuna Research Preview.
7
+
8
+ 1. Grant. Subject to the SemSwitch Terms of Service (https://semswitch.com/terms) and any
9
+ applicable Order or evaluation agreement, an authorized user receives a limited, non-exclusive
10
+ right to install and use the Setsuna client solely to access or evaluate the Setsuna service
11
+ that the user is authorized to use.
12
+
13
+ 2. Ownership. The Setsuna client is licensed, not sold. SemSwitch retains all ownership,
14
+ copyright, and other intellectual property rights in the Setsuna client. This license transfers
15
+ no ownership or other intellectual property rights.
16
+
17
+ 3. Restrictions. Except where expressly permitted by an applicable agreement with SemSwitch or by
18
+ non-waivable law, no right is granted to redistribute, sublicense, sell, rent, publish, modify,
19
+ create derivative works from, reverse engineer, decompile, or disassemble the Setsuna client.
20
+
21
+ 4. Third-party software. The Setsuna client uses third-party software and components that are not
22
+ owned by SemSwitch. They remain governed by their respective licenses and notices, which this
23
+ license does not modify or replace.
24
+
25
+ 5. Term. The right to use the Setsuna client ends when the applicable Setsuna authorization ends.
26
+
27
+ 6. Agreements. If an applicable signed Order or separately negotiated agreement with SemSwitch
28
+ expressly conflicts with this license, that Order or agreement controls.
29
+
30
+ 7. Warranties, liability, and service terms. Warranties, disclaimers, limitations of liability,
31
+ and the terms of the Setsuna service are governed by the applicable agreement and the SemSwitch
32
+ Terms of Service rather than by this license.
package/dist/cli.js CHANGED
@@ -10,6 +10,9 @@ var __export = (target, all) => {
10
10
  };
11
11
 
12
12
  // ../setsuna/src/tui/customer/prompt.ts
13
+ function readText(text, now) {
14
+ return typeof text === "function" ? text(now) : text;
15
+ }
13
16
  var TUI_CANCEL;
14
17
  var init_prompt = __esm({
15
18
  "../setsuna/src/tui/customer/prompt.ts"() {
@@ -76,14 +79,13 @@ function adaptPrompts(clack, guide) {
76
79
  const { confirm, isCancel, select, text } = clack;
77
80
  return {
78
81
  async select(options, io) {
79
- const result = await select({
80
- ...options,
81
- options: [...options.options],
82
- ...guide(options),
83
- input: io.input,
84
- output: io.output
85
- });
86
- return isCancel(result) ? TUI_CANCEL : result;
82
+ const redraw = hasTimedText(options) ? setInterval(() => io.output.emit("resize"), 1e3) : void 0;
83
+ try {
84
+ const result = await select(selectOptions(options, guide(options), io));
85
+ return isCancel(result) ? TUI_CANCEL : result;
86
+ } finally {
87
+ if (redraw !== void 0) clearInterval(redraw);
88
+ }
87
89
  },
88
90
  async text(options, io) {
89
91
  const result = await text({
@@ -106,6 +108,28 @@ function adaptPrompts(clack, guide) {
106
108
  }
107
109
  };
108
110
  }
111
+ function selectOptions(options, guide, io) {
112
+ return {
113
+ get message() {
114
+ return readText(options.message, Date.now());
115
+ },
116
+ options: options.options.map((option) => ({
117
+ value: option.value,
118
+ label: option.label,
119
+ get hint() {
120
+ return option.hint === void 0 ? void 0 : readText(option.hint, Date.now());
121
+ }
122
+ })),
123
+ initialValue: options.initialValue,
124
+ signal: options.signal,
125
+ ...guide,
126
+ input: io.input,
127
+ output: io.output
128
+ };
129
+ }
130
+ function hasTimedText(options) {
131
+ return typeof options.message === "function" || options.options.some(({ hint }) => typeof hint === "function");
132
+ }
109
133
  function adaptSpinner(clack, io, options, guide) {
110
134
  const completion = trackInterruption(clack, io, options.onCancel, guide);
111
135
  const spinner = clack.spinner({
@@ -1089,6 +1113,7 @@ function parsePreviewCliArgs(args) {
1089
1113
  "timeout-ms": { type: "string" },
1090
1114
  "request-timeout-ms": { type: "string" },
1091
1115
  "configuration-id": { type: "string" },
1116
+ duration: { type: "string" },
1092
1117
  attach: { type: "string" },
1093
1118
  json: { type: "boolean", default: false },
1094
1119
  help: { type: "boolean", short: "h", default: false },
@@ -1200,14 +1225,14 @@ function hasOnlyKeys(value, allowed) {
1200
1225
  }
1201
1226
  function parseLease(value, expectedConfigurationId) {
1202
1227
  const sandboxId = parseLeaseSandboxId(value);
1203
- if (!isRecord(value) || typeof value.expiresAt !== "string") {
1228
+ if (!isRecord(value) || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)) || value.duration !== "20m" && value.duration !== "1h" && value.duration !== "2h") {
1204
1229
  throw invalidResponse("sandbox lease");
1205
1230
  }
1206
1231
  const microvmConfiguration = parseMicrovmConfiguration(value.microvmConfiguration);
1207
1232
  if (expectedConfigurationId !== void 0 && microvmConfiguration.id !== expectedConfigurationId) {
1208
1233
  throw invalidResponse("sandbox lease");
1209
1234
  }
1210
- return { sandboxId, expiresAt: value.expiresAt, microvmConfiguration };
1235
+ return { sandboxId, expiresAt: value.expiresAt, duration: value.duration, microvmConfiguration };
1211
1236
  }
1212
1237
  function parseLeaseSandboxId(value) {
1213
1238
  if (!isRecord(value) || !nonEmptyResponseString(value.sandboxId))
@@ -1248,6 +1273,36 @@ function isRecord(value) {
1248
1273
  return typeof value === "object" && value !== null && !Array.isArray(value);
1249
1274
  }
1250
1275
 
1276
+ // ../setsuna/src/client-affinity.ts
1277
+ var FrontDoorAffinity = class {
1278
+ #origin;
1279
+ #cookies = /* @__PURE__ */ new Map();
1280
+ constructor(serviceUrl) {
1281
+ this.#origin = serviceUrl.origin;
1282
+ }
1283
+ headersFor(url, headers) {
1284
+ if (url.origin !== this.#origin || this.#cookies.size === 0) return headers;
1285
+ const result = new Headers(headers);
1286
+ result.set("cookie", [...this.#cookies].map(([name, value]) => `${name}=${value}`).join("; "));
1287
+ return result;
1288
+ }
1289
+ capture(url, response) {
1290
+ if (url.origin !== this.#origin) return;
1291
+ if (response.url !== "" && new URL(response.url).origin !== this.#origin) return;
1292
+ for (const header of response.headers.getSetCookie()) {
1293
+ const pair = header.split(";", 1)[0]?.trim() ?? "";
1294
+ const match = /^(ASLBSA|ASLBSACORS)=([\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*)$/u.exec(
1295
+ pair
1296
+ );
1297
+ if (match === null) continue;
1298
+ const [, name, value] = match;
1299
+ if (name === void 0 || value === void 0) continue;
1300
+ if (value === "") this.#cookies.delete(name);
1301
+ else this.#cookies.set(name, value);
1302
+ }
1303
+ }
1304
+ };
1305
+
1251
1306
  // ../setsuna/src/client-options.ts
1252
1307
  var PREVIEW_SERVICE_URL = "https://api.setsuna.semswitch.com/";
1253
1308
  function normalizeServiceUrl(value) {
@@ -1285,12 +1340,16 @@ function normalizeCreateOptions(options) {
1285
1340
  throw new TypeError("create options must be an object.");
1286
1341
  }
1287
1342
  const unsupported = Object.keys(options).find(
1288
- (key) => key !== "microvmConfigurationId" && key !== "network"
1343
+ (key) => key !== "microvmConfigurationId" && key !== "network" && key !== "duration"
1289
1344
  );
1290
1345
  if (unsupported !== void 0) throw new TypeError(`Unsupported create option: ${unsupported}.`);
1346
+ const duration = options.duration === void 0 ? "20m" : options.duration;
1347
+ if (duration !== "20m" && duration !== "1h" && duration !== "2h")
1348
+ throw new TypeError("duration must be 20m, 1h, or 2h.");
1291
1349
  const microvmConfigurationId = options.microvmConfigurationId === void 0 ? void 0 : nonEmptyString(options.microvmConfigurationId, "microvmConfigurationId");
1292
1350
  const network = options.network === void 0 ? void 0 : validateRoutedNetwork(options.network);
1293
1351
  return {
1352
+ duration,
1294
1353
  ...microvmConfigurationId === void 0 ? {} : { microvmConfigurationId },
1295
1354
  ...network === void 0 ? {} : { network }
1296
1355
  };
@@ -1359,7 +1418,7 @@ function isLoopbackHostname(hostname) {
1359
1418
  }
1360
1419
 
1361
1420
  // ../setsuna/src/client-request.ts
1362
- async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3, options) {
1421
+ async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3, options, affinity) {
1363
1422
  const requestTimeoutMs = options.requestTimeoutMs === void 0 ? defaultTimeoutMs : positiveMilliseconds(options.requestTimeoutMs, "requestTimeoutMs");
1364
1423
  const timeoutSignal = AbortSignal.timeout(requestTimeoutMs);
1365
1424
  const signal = options.signal === void 0 ? timeoutSignal : AbortSignal.any([options.signal, timeoutSignal]);
@@ -1376,7 +1435,8 @@ async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3,
1376
1435
  requestTimeoutMs,
1377
1436
  timeoutSignal,
1378
1437
  signal,
1379
- accessToken
1438
+ accessToken,
1439
+ affinity
1380
1440
  );
1381
1441
  const responseBody = response.status === 204 ? void 0 : await parseResponseJson(response);
1382
1442
  if (!response.ok) throw brokerResponseError(response, responseBody);
@@ -1417,14 +1477,17 @@ function settleBeforeAbort(pending, signal) {
1417
1477
  if (signal.aborted) onAbort();
1418
1478
  });
1419
1479
  }
1420
- async function fetchService(url, options, requestTimeoutMs, timeoutSignal, signal, accessToken) {
1480
+ async function fetchService(url, options, requestTimeoutMs, timeoutSignal, signal, accessToken, affinity) {
1421
1481
  try {
1422
- return await fetch(url, {
1482
+ const headers = requestHeaders(options.body, accessToken);
1483
+ const response = await fetch(url, {
1423
1484
  method: options.method,
1424
- headers: requestHeaders(options.body, accessToken),
1485
+ headers: affinity?.headersFor(url, headers) ?? headers,
1425
1486
  body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1426
1487
  signal
1427
1488
  });
1489
+ affinity?.capture(url, response);
1490
+ return response;
1428
1491
  } catch (error) {
1429
1492
  throwCancellationFailure(error, requestTimeoutMs, timeoutSignal, options.signal);
1430
1493
  throw new SetsunaError("The local Setsuna service could not be reached.", {
@@ -1559,13 +1622,14 @@ function parseProcessList(value) {
1559
1622
  if (!Array.isArray(value)) throw invalidResponse2("process list");
1560
1623
  return value.map(parseProcessInfo);
1561
1624
  }
1562
- function attachProcessStream(serviceUrl, path3, options, getAccessToken) {
1625
+ function attachProcessStream(serviceUrl, path3, options, getAccessToken, affinity) {
1563
1626
  return {
1564
1627
  async *[Symbol.asyncIterator]() {
1565
1628
  const response = await fetchProcessStream(
1566
1629
  new URL(path3, serviceUrl),
1567
1630
  options.signal,
1568
- getAccessToken
1631
+ getAccessToken,
1632
+ affinity
1569
1633
  );
1570
1634
  if (response.body === null) throw invalidResponse2("process stream");
1571
1635
  yield* decodeFrames(response.body, options.signal);
@@ -1586,15 +1650,17 @@ function processFrameToJson(frame) {
1586
1650
  }
1587
1651
  return frame;
1588
1652
  }
1589
- async function fetchProcessStream(url, signal, getAccessToken) {
1653
+ async function fetchProcessStream(url, signal, getAccessToken, affinity) {
1590
1654
  const accessToken = await processAccessToken(getAccessToken, signal);
1591
1655
  let response;
1592
1656
  try {
1657
+ const headers = accessToken === void 0 ? void 0 : { authorization: `Bearer ${accessToken}` };
1593
1658
  response = await fetch(url, {
1594
1659
  method: "GET",
1595
1660
  signal,
1596
- ...accessToken === void 0 ? {} : { headers: { authorization: `Bearer ${accessToken}` } }
1661
+ ...headers === void 0 ? {} : { headers: affinity?.headersFor(url, headers) ?? headers }
1597
1662
  });
1663
+ affinity?.capture(url, response);
1598
1664
  } catch (error) {
1599
1665
  throw requestError(error, signal);
1600
1666
  }
@@ -1768,11 +1834,13 @@ function reportRunPhase(callback, phase) {
1768
1834
 
1769
1835
  // ../setsuna/src/client.ts
1770
1836
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
1771
- var DEFAULT_CLEANUP_TIMEOUT_MS = 1e4;
1837
+ var DEFAULT_RUN_REQUEST_TIMEOUT_MS = 6e4 + 12e4 + 6e4;
1772
1838
  var SetsunaClient = class {
1773
1839
  #serviceUrl;
1774
1840
  #requestTimeoutMs;
1841
+ #explicitRequestTimeoutMs;
1775
1842
  #getAccessToken;
1843
+ #affinity;
1776
1844
  constructor(options = {}) {
1777
1845
  if (options.serviceUrl !== void 0 && options.remote !== void 0) {
1778
1846
  throw new TypeError("serviceUrl and remote are mutually exclusive.");
@@ -1786,7 +1854,9 @@ var SetsunaClient = class {
1786
1854
  }
1787
1855
  this.#serviceUrl = new URL(PREVIEW_SERVICE_URL);
1788
1856
  this.#getAccessToken = options.remote.getAccessToken;
1857
+ this.#affinity = new FrontDoorAffinity(this.#serviceUrl);
1789
1858
  }
1859
+ this.#explicitRequestTimeoutMs = options.requestTimeoutMs;
1790
1860
  this.#requestTimeoutMs = positiveMilliseconds(
1791
1861
  options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
1792
1862
  "requestTimeoutMs"
@@ -1806,20 +1876,7 @@ var SetsunaClient = class {
1806
1876
  method: "POST",
1807
1877
  body: { ...createOptions }
1808
1878
  });
1809
- const sandboxId = parseLeaseSandboxId(response);
1810
- try {
1811
- return parseLease(response, createOptions.microvmConfigurationId);
1812
- } catch (error) {
1813
- try {
1814
- await this.#destroy(sandboxId, DEFAULT_CLEANUP_TIMEOUT_MS);
1815
- } catch (cleanupFailure) {
1816
- throw new AggregateError(
1817
- [error, cleanupFailure],
1818
- "Sandbox lease validation and destruction both failed."
1819
- );
1820
- }
1821
- throw error;
1822
- }
1879
+ return parseLease(response, createOptions.microvmConfigurationId);
1823
1880
  }
1824
1881
  async list() {
1825
1882
  return parseSandboxList(await this.#request("v1/sandboxes", { method: "GET" }));
@@ -1832,14 +1889,6 @@ var SetsunaClient = class {
1832
1889
  })
1833
1890
  );
1834
1891
  }
1835
- async renew(sandboxId) {
1836
- const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1837
- return parseSandboxStatus(
1838
- await this.#request(`v1/sandboxes/${encodeURIComponent(validatedSandboxId)}/renew`, {
1839
- method: "POST"
1840
- })
1841
- );
1842
- }
1843
1892
  async execute(sandboxId, command, options = {}) {
1844
1893
  const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1845
1894
  const validatedCommand = nonEmptyString(command, "command");
@@ -1876,7 +1925,8 @@ var SetsunaClient = class {
1876
1925
  this.#serviceUrl,
1877
1926
  `${processPath(sandboxId)}/${encodeURIComponent(nonEmptyString(processId, "processId"))}/attach${query}`,
1878
1927
  options,
1879
- this.#getAccessToken
1928
+ this.#getAccessToken,
1929
+ this.#affinity
1880
1930
  );
1881
1931
  }
1882
1932
  async writeProcessStdin(sandboxId, processId, data) {
@@ -1913,44 +1963,30 @@ var SetsunaClient = class {
1913
1963
  });
1914
1964
  }
1915
1965
  async run(command, options = {}) {
1916
- let sandboxId;
1917
- const cleanupTimeoutMs = positiveMilliseconds(
1918
- options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS,
1919
- "cleanupTimeoutMs"
1966
+ const unsupported = Object.keys(options).find(
1967
+ (key) => !["microvmConfigurationId", "timeoutMs", "requestTimeoutMs", "signal", "onPhase"].includes(
1968
+ key
1969
+ )
1920
1970
  );
1921
- let executionOutcome;
1922
- try {
1923
- reportRunPhase(options.onPhase, "starting-microvm");
1924
- const lease = await this.create({
1925
- ...options.microvmConfigurationId === void 0 ? {} : { microvmConfigurationId: options.microvmConfigurationId },
1926
- ...options.network === void 0 ? {} : { network: options.network }
1927
- });
1928
- sandboxId = lease.sandboxId;
1929
- reportRunPhase(options.onPhase, "microvm-ready");
1930
- reportRunPhase(options.onPhase, "running-command");
1931
- executionOutcome = { ok: true, result: await this.execute(sandboxId, command, options) };
1932
- } catch (error) {
1933
- executionOutcome = { ok: false, error };
1934
- }
1935
- if (sandboxId !== void 0) {
1936
- try {
1937
- reportRunPhase(options.onPhase, "destroying-microvm");
1938
- await this.#destroy(sandboxId, cleanupTimeoutMs);
1939
- reportRunPhase(options.onPhase, "microvm-destroyed");
1940
- } catch (cleanupFailure) {
1941
- if (!executionOutcome.ok) {
1942
- throw new AggregateError(
1943
- [executionOutcome.error, cleanupFailure],
1944
- "Sandbox execution and destruction both failed."
1945
- );
1946
- }
1947
- throw cleanupFailure;
1948
- }
1949
- }
1950
- if (!executionOutcome.ok) {
1951
- throw executionOutcome.error;
1952
- }
1953
- return executionOutcome.result;
1971
+ if (unsupported !== void 0) throw new TypeError(`Unsupported run option: ${unsupported}.`);
1972
+ const validatedCommand = nonEmptyString(command, "command");
1973
+ const timeoutMs = options.timeoutMs === void 0 ? void 0 : positiveMilliseconds(options.timeoutMs, "timeoutMs");
1974
+ const microvmConfigurationId = options.microvmConfigurationId === void 0 ? void 0 : nonEmptyString(options.microvmConfigurationId, "microvmConfigurationId");
1975
+ reportRunPhase(options.onPhase, "running");
1976
+ const result = parseExecutionResult(
1977
+ await this.#request("v1/run", {
1978
+ method: "POST",
1979
+ body: {
1980
+ command: validatedCommand,
1981
+ ...microvmConfigurationId === void 0 ? {} : { microvmConfigurationId },
1982
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
1983
+ },
1984
+ signal: options.signal,
1985
+ requestTimeoutMs: options.requestTimeoutMs ?? this.#explicitRequestTimeoutMs ?? Math.max(DEFAULT_RUN_REQUEST_TIMEOUT_MS, (timeoutMs ?? 0) + 12e4)
1986
+ })
1987
+ );
1988
+ reportRunPhase(options.onPhase, "complete");
1989
+ return result;
1954
1990
  }
1955
1991
  async #request(path3, options) {
1956
1992
  return requestJson(
@@ -1958,7 +1994,8 @@ var SetsunaClient = class {
1958
1994
  this.#requestTimeoutMs,
1959
1995
  this.#getAccessToken,
1960
1996
  path3,
1961
- options
1997
+ options,
1998
+ this.#affinity
1962
1999
  );
1963
2000
  }
1964
2001
  };
@@ -1979,7 +2016,7 @@ async function runWithSignalCleanup(client, command, options, io, signalRuntime)
1979
2016
  const exitCode = signal === "SIGINT" ? 130 : 143;
1980
2017
  if (interruptedExitCode === void 0) {
1981
2018
  interruptedExitCode = exitCode;
1982
- io.errorOutput.write("Interrupt received; destroying sandbox...\n");
2019
+ io.errorOutput.write("Interrupt received; Setsuna owns run cleanup.\n");
1983
2020
  controller.abort();
1984
2021
  return;
1985
2022
  }
@@ -2081,98 +2118,19 @@ function enterRawMode(input) {
2081
2118
  const wasPaused = input.isPaused();
2082
2119
  input.setRawMode(true);
2083
2120
  input.resume();
2084
- let restored = false;
2121
+ let restored;
2085
2122
  return () => {
2086
- if (restored) return;
2087
- restored = true;
2088
- input.setRawMode?.(wasRaw);
2089
- if (wasPaused) input.pause();
2123
+ restored ??= restoreInput(input, wasRaw, wasPaused);
2124
+ return restored;
2090
2125
  };
2091
2126
  }
2092
-
2093
- // ../setsuna/src/lease-keeper.ts
2094
- var MAX_TIMER_DELAY_MS = 2147483647;
2095
- var SYSTEM_RUNTIME = {
2096
- now: Date.now,
2097
- setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
2098
- clearTimeout: (handle) => clearTimeout(handle)
2099
- };
2100
- var ActiveLeaseKeeper = class {
2101
- #client;
2102
- #sandboxId;
2103
- #runtime;
2104
- #expiresAtMs;
2105
- #failureHandler;
2106
- #running = false;
2107
- #timer;
2108
- constructor(client, sandboxId, runtime = SYSTEM_RUNTIME) {
2109
- this.#client = client;
2110
- this.#sandboxId = sandboxId;
2111
- this.#runtime = runtime;
2112
- }
2113
- async renewNow() {
2114
- if (this.#running) throw new TypeError("Cannot prime a running lease keeper.");
2115
- this.#expiresAtMs = await this.#renewExpiry();
2116
- }
2117
- start(onFailure) {
2118
- if (this.#running) throw new TypeError("The lease keeper is already running.");
2119
- if (this.#expiresAtMs === void 0) {
2120
- throw new TypeError("The lease keeper must renew the sandbox before it starts.");
2121
- }
2122
- this.#running = true;
2123
- this.#failureHandler = onFailure;
2124
- try {
2125
- this.#schedule(this.#expiresAtMs);
2126
- } catch (error) {
2127
- this.#running = false;
2128
- this.#failureHandler = void 0;
2129
- throw error;
2130
- }
2131
- }
2132
- stop() {
2133
- this.#running = false;
2134
- this.#failureHandler = void 0;
2135
- if (this.#timer !== void 0) this.#runtime.clearTimeout(this.#timer);
2136
- this.#timer = void 0;
2137
- }
2138
- async #renewExpiry() {
2139
- const { expiresAt } = await this.#client.renew(this.#sandboxId);
2140
- const expiresAtMs = Date.parse(expiresAt);
2141
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= this.#runtime.now()) {
2142
- throw new TypeError("Sandbox renewal returned an invalid or expired expiresAt value.");
2143
- }
2144
- return expiresAtMs;
2145
- }
2146
- #schedule(expiresAtMs) {
2147
- const remainingMs = expiresAtMs - this.#runtime.now();
2148
- if (!Number.isFinite(remainingMs) || remainingMs <= 0) {
2149
- throw new TypeError(
2150
- "The renewed sandbox lease expired before its next renewal was scheduled."
2151
- );
2152
- }
2153
- const delayMs = Math.min(MAX_TIMER_DELAY_MS, Math.max(1, Math.floor(remainingMs / 2)));
2154
- this.#timer = this.#runtime.setTimeout(() => {
2155
- this.#timer = void 0;
2156
- void this.#renewAndSchedule().catch((error) => this.#fail(error));
2157
- }, delayMs);
2158
- }
2159
- async #renewAndSchedule() {
2160
- const expiresAtMs = await this.#renewExpiry();
2161
- if (!this.#running) return;
2162
- this.#expiresAtMs = expiresAtMs;
2163
- this.#schedule(expiresAtMs);
2164
- }
2165
- #fail(error) {
2166
- if (!this.#running) return;
2167
- this.#running = false;
2168
- const handler = this.#failureHandler;
2169
- this.#failureHandler = void 0;
2170
- try {
2171
- handler?.(error);
2172
- } catch {
2173
- }
2127
+ async function restoreInput(input, wasRaw, wasPaused) {
2128
+ if (wasPaused) {
2129
+ input.pause();
2130
+ await new Promise((resolve) => process.nextTick(resolve));
2174
2131
  }
2175
- };
2132
+ input.setRawMode?.(wasRaw);
2133
+ }
2176
2134
 
2177
2135
  // ../setsuna/src/cli-shell.ts
2178
2136
  var DETACH_BYTE = 29;
@@ -2194,7 +2152,7 @@ async function runShellCliCommand(commandName, commandArguments, attachProcessId
2194
2152
  );
2195
2153
  return true;
2196
2154
  }
2197
- async function runShellCommand(commandArguments, attachProcessId, client, io, terminal, leaseRuntime) {
2155
+ async function runShellCommand(commandArguments, attachProcessId, client, io, terminal) {
2198
2156
  if (commandArguments.length !== 1 || commandArguments[0] === "") {
2199
2157
  throw new TypeError("setsuna shell requires exactly one sandbox ID.");
2200
2158
  }
@@ -2203,21 +2161,9 @@ async function runShellCommand(commandArguments, attachProcessId, client, io, te
2203
2161
  }
2204
2162
  if (attachProcessId === "") throw new TypeError("--attach requires a process ID.");
2205
2163
  const sandboxId = commandArguments[0];
2206
- const leaseKeeper = new ActiveLeaseKeeper(client, sandboxId, leaseRuntime);
2207
- await leaseKeeper.renewNow();
2208
- try {
2209
- const size = terminal.size();
2210
- const processId = await resolveProcess(
2211
- client,
2212
- sandboxId,
2213
- attachProcessId,
2214
- size,
2215
- terminal.term()
2216
- );
2217
- await attachTerminal(client, sandboxId, processId, io, terminal, leaseKeeper);
2218
- } finally {
2219
- leaseKeeper.stop();
2220
- }
2164
+ const size = terminal.size();
2165
+ const processId = await resolveProcess(client, sandboxId, attachProcessId, size, terminal.term());
2166
+ await attachTerminal(client, sandboxId, processId, io, terminal);
2221
2167
  }
2222
2168
  async function resolveProcess(client, sandboxId, attachProcessId, size, localTerm) {
2223
2169
  if (attachProcessId === void 0) {
@@ -2240,21 +2186,12 @@ async function resolveProcess(client, sandboxId, attachProcessId, size, localTer
2240
2186
  if (process2.state === "running") await client.resizeProcess(sandboxId, attachProcessId, size);
2241
2187
  return attachProcessId;
2242
2188
  }
2243
- async function attachTerminal(client, sandboxId, processId, io, terminal, leaseKeeper) {
2189
+ async function attachTerminal(client, sandboxId, processId, io, terminal) {
2244
2190
  const controller = new AbortController();
2245
- const session = createAttachedSession(
2246
- client,
2247
- sandboxId,
2248
- processId,
2249
- io,
2250
- terminal,
2251
- controller,
2252
- () => leaseKeeper.stop()
2253
- );
2191
+ const session = createAttachedSession(client, sandboxId, processId, io, terminal, controller);
2254
2192
  let exitFrame;
2255
2193
  let attachError;
2256
2194
  try {
2257
- leaseKeeper.start(session.fail);
2258
2195
  for await (const frame of client.attachProcess(sandboxId, processId, {
2259
2196
  maxStreamBytes: Number.MAX_SAFE_INTEGER,
2260
2197
  signal: controller.signal
@@ -2281,20 +2218,20 @@ async function attachTerminal(client, sandboxId, processId, io, terminal, leaseK
2281
2218
  }
2282
2219
  if (exitFrame.exitCode !== null) terminal.setExitCode(exitFrame.exitCode);
2283
2220
  }
2284
- function createAttachedSession(client, sandboxId, processId, io, terminal, controller, stopLeaseKeeper) {
2221
+ function createAttachedSession(client, sandboxId, processId, io, terminal, controller) {
2285
2222
  let detached = false;
2286
2223
  let failure;
2287
2224
  let cleaned = false;
2288
2225
  let inputQueue = Promise.resolve();
2289
2226
  let resizeQueue = Promise.resolve();
2227
+ let restored;
2290
2228
  const restoreRawMode = terminal.enterRawMode();
2291
2229
  const cleanup = () => {
2292
2230
  if (cleaned) return;
2293
2231
  cleaned = true;
2294
2232
  io.input.off("data", onInput);
2295
2233
  terminal.offResize(onResize);
2296
- stopLeaseKeeper();
2297
- restoreRawMode();
2234
+ restored = restoreRawMode();
2298
2235
  };
2299
2236
  const fail = (error) => {
2300
2237
  if (failure === void 0) failure = error;
@@ -2339,8 +2276,10 @@ function createAttachedSession(client, sandboxId, processId, io, terminal, contr
2339
2276
  get failure() {
2340
2277
  return failure;
2341
2278
  },
2279
+ // Everything the session started has finished, including handing the terminal back.
2342
2280
  async settle() {
2343
2281
  await Promise.allSettled([inputQueue, resizeQueue]);
2282
+ await restored;
2344
2283
  }
2345
2284
  };
2346
2285
  }
@@ -2382,11 +2321,12 @@ function isDetachAbort(error) {
2382
2321
  // ../setsuna/src/cli-interactive-persistent-actions.ts
2383
2322
  function createPersistentInteractiveActions(client, io, shellTerminal) {
2384
2323
  return {
2385
- async startMicrovm(microvmConfigurationId) {
2386
- const lease = await client.create({ microvmConfigurationId });
2324
+ async startMicrovm(microvmConfigurationId, duration) {
2325
+ const lease = await client.create({ microvmConfigurationId, duration });
2387
2326
  return {
2388
2327
  sandboxId: lease.sandboxId,
2389
2328
  expiresAt: lease.expiresAt,
2329
+ duration: lease.duration,
2390
2330
  vcpuCount: lease.microvmConfiguration.vcpuCount,
2391
2331
  memoryMiB: lease.microvmConfiguration.memoryMiB
2392
2332
  };
@@ -2403,13 +2343,11 @@ function createPersistentInteractiveActions(client, io, shellTerminal) {
2403
2343
  return (await client.listProcesses(sandboxId)).filter((process2) => process2.state === "running" && process2.terminal !== void 0).map(({ processId, pid }) => ({ processId, pid }));
2404
2344
  },
2405
2345
  async runPersistentCommand(sandboxId, command, timeoutMs) {
2406
- await client.renew(sandboxId);
2407
2346
  const result = await client.execute(sandboxId, command, {
2408
2347
  ...timeoutMs === void 0 ? {} : { timeoutMs }
2409
2348
  });
2410
2349
  return interactiveRunResult(result);
2411
2350
  },
2412
- renewMicrovm: (sandboxId) => client.renew(sandboxId),
2413
2351
  destroyMicrovm: (sandboxId) => client.destroy(sandboxId)
2414
2352
  };
2415
2353
  }
@@ -2468,19 +2406,9 @@ var DEFAULT_SIGNAL_RUNTIME = {
2468
2406
 
2469
2407
  // ../setsuna/src/tui/customer/format.ts
2470
2408
  var STANDALONE_BOX = { withGuide: false, width: "auto" };
2409
+ var EXIT_MESSAGE = "Sayonara!";
2471
2410
  function formatRunPhase(phase) {
2472
- switch (phase) {
2473
- case "starting-microvm":
2474
- return "Starting microVM";
2475
- case "microvm-ready":
2476
- return "microVM ready";
2477
- case "running-command":
2478
- return "Running command";
2479
- case "destroying-microvm":
2480
- return "Cleaning up";
2481
- case "microvm-destroyed":
2482
- return "microVM destroyed";
2483
- }
2411
+ return phase === "running" ? "Running on Setsuna" : "Complete";
2484
2412
  }
2485
2413
  function formatRunResult(result) {
2486
2414
  const facts = [`Exit status ${result.exitStatus}`, `${result.durationMs} ms`];
@@ -2519,6 +2447,10 @@ function formatMicrovmState(microvm, now) {
2519
2447
  if (microvm.vcpuCount !== void 0 && microvm.memoryMiB !== void 0) {
2520
2448
  lines.push(`Compute ${formatMicrovmShape(microvm.vcpuCount, microvm.memoryMiB)}`);
2521
2449
  }
2450
+ if (microvm.duration !== void 0) {
2451
+ lines.push(`Duration ${microvm.duration}`);
2452
+ return lines.join("\n");
2453
+ }
2522
2454
  const remaining = remainingTime(microvm.expiresAt, now);
2523
2455
  lines.push(remaining === void 0 ? "Expired" : `Expires in ${remaining}`);
2524
2456
  return lines.join("\n");
@@ -2634,7 +2566,7 @@ async function ensureSignedIn(io, auth, promptUi, session = {}) {
2634
2566
  return false;
2635
2567
  }
2636
2568
  if (choice === "exit") {
2637
- promptUi.outro("Ready when you are.", io);
2569
+ promptUi.outro(EXIT_MESSAGE, io);
2638
2570
  return false;
2639
2571
  }
2640
2572
  status = await signIn(io, auth, promptUi);
@@ -2779,7 +2711,7 @@ async function promptRunAgain(io, promptUi) {
2779
2711
  );
2780
2712
  if (next === TUI_CANCEL || next === "back") return "back-to-main";
2781
2713
  if (next === "exit") {
2782
- promptUi.outro("Ready when you are.", io);
2714
+ promptUi.outro(EXIT_MESSAGE, io);
2783
2715
  return "exit-tui";
2784
2716
  }
2785
2717
  return "prompt-new-command";
@@ -2834,12 +2766,25 @@ async function startMicrovmMenu(io, actions, promptUi) {
2834
2766
  const configurations = (await actions.microvmConfigurations()).configurations;
2835
2767
  const configuration = await promptForMicrovmConfiguration(io, promptUi, configurations);
2836
2768
  if (configuration === null) return;
2837
- const sandbox = await startSandbox(io, actions, promptUi, configuration.id);
2769
+ const duration = await promptUi.select(
2770
+ {
2771
+ message: "Duration",
2772
+ options: [
2773
+ { value: "20m", label: "20 minutes" },
2774
+ { value: "1h", label: "1 hour" },
2775
+ { value: "2h", label: "2 hours" }
2776
+ ],
2777
+ initialValue: "20m"
2778
+ },
2779
+ io
2780
+ );
2781
+ if (duration === TUI_CANCEL) return;
2782
+ const sandbox = await startSandbox(io, actions, promptUi, configuration.id, duration);
2838
2783
  if (sandbox === void 0) return;
2839
2784
  promptUi.box(formatMicrovmState(sandbox, Date.now()), "microVM ready", io, STANDALONE_BOX);
2840
2785
  await manageSandbox(io, actions, promptUi, sandbox);
2841
2786
  }
2842
- async function startSandbox(io, actions, promptUi, microvmConfigurationId) {
2787
+ async function startSandbox(io, actions, promptUi, microvmConfigurationId, duration) {
2843
2788
  let interrupted = false;
2844
2789
  const spinner = promptUi.spinner(io, {
2845
2790
  indicator: "timer",
@@ -2850,7 +2795,7 @@ async function startSandbox(io, actions, promptUi, microvmConfigurationId) {
2850
2795
  spinner.start("Starting microVM");
2851
2796
  let sandbox;
2852
2797
  try {
2853
- sandbox = await actions.startMicrovm(microvmConfigurationId);
2798
+ sandbox = await actions.startMicrovm(microvmConfigurationId, duration);
2854
2799
  } catch (error) {
2855
2800
  spinner.error(formatTuiError(error));
2856
2801
  return void 0;
@@ -2876,7 +2821,6 @@ async function activeMicrovmsMenu(io, actions, promptUi) {
2876
2821
  }
2877
2822
  }
2878
2823
  async function selectSandbox(io, promptUi, sandboxes) {
2879
- const now = Date.now();
2880
2824
  const selected = await promptUi.select(
2881
2825
  {
2882
2826
  message: "Active microVMs",
@@ -2884,7 +2828,7 @@ async function selectSandbox(io, promptUi, sandboxes) {
2884
2828
  ...sandboxes.map((sandbox, index) => ({
2885
2829
  value: `sandbox:${index}`,
2886
2830
  label: sandbox.sandboxId,
2887
- hint: formatExpiry(sandbox.expiresAt, now)
2831
+ hint: (now) => formatExpiry(sandbox.expiresAt, now)
2888
2832
  })),
2889
2833
  { value: "back", label: "Back" }
2890
2834
  ]
@@ -2897,15 +2841,14 @@ async function selectSandbox(io, promptUi, sandboxes) {
2897
2841
  async function manageSandbox(io, actions, promptUi, initial) {
2898
2842
  let microvm = initial;
2899
2843
  while (true) {
2900
- const expiry = formatExpiry(microvm.expiresAt, Date.now());
2844
+ const { sandboxId, expiresAt } = microvm;
2901
2845
  const choice = await promptUi.select(
2902
2846
  {
2903
- message: `Manage microVM ${microvm.sandboxId} \xB7 ${expiry}`,
2847
+ message: (now) => `Manage microVM ${sandboxId} \xB7 ${formatExpiry(expiresAt, now)}`,
2904
2848
  options: [
2905
2849
  { value: "open-terminal", label: "Open terminal" },
2906
2850
  { value: "reattach-terminal", label: "Reattach terminal" },
2907
2851
  { value: "run-command", label: "Run command" },
2908
- { value: "renew", label: "Renew" },
2909
2852
  { value: "destroy", label: "Destroy microVM" },
2910
2853
  { value: "back", label: "Back" }
2911
2854
  ],
@@ -2926,33 +2869,17 @@ async function manageSandbox(io, actions, promptUi, initial) {
2926
2869
  async function dispatchSandboxChoice(choice, microvm, io, actions, promptUi) {
2927
2870
  if (choice === "open-terminal") {
2928
2871
  await actions.openTerminal(microvm.sandboxId);
2929
- return refreshExpiry(actions, microvm);
2872
+ return microvm;
2930
2873
  }
2931
2874
  if (choice === "reattach-terminal") {
2932
- const attached = await reattachTerminal(io, actions, promptUi, microvm.sandboxId);
2933
- return attached ? refreshExpiry(actions, microvm) : microvm;
2875
+ await reattachTerminal(io, actions, promptUi, microvm.sandboxId);
2876
+ return microvm;
2934
2877
  }
2935
2878
  if (choice === "run-command") {
2936
- const ran = await runPersistentCommand(io, actions, promptUi, microvm.sandboxId);
2937
- return ran ? refreshExpiry(actions, microvm) : microvm;
2938
- }
2939
- if (choice === "renew") return renewSandbox(io, actions, promptUi, microvm);
2940
- return await destroySandbox(io, actions, promptUi, microvm.sandboxId) ? void 0 : microvm;
2941
- }
2942
- async function renewSandbox(io, actions, promptUi, microvm) {
2943
- const status = await actions.renewMicrovm(microvm.sandboxId);
2944
- promptUi.success(`Renewed \xB7 ${formatExpiry(status.expiresAt, Date.now())}`, io);
2945
- return { ...microvm, expiresAt: status.expiresAt };
2946
- }
2947
- async function refreshExpiry(actions, microvm) {
2948
- try {
2949
- const current = (await actions.activeMicrovms()).find(
2950
- ({ sandboxId }) => sandboxId === microvm.sandboxId
2951
- );
2952
- return current === void 0 ? microvm : { ...microvm, expiresAt: current.expiresAt };
2953
- } catch {
2879
+ await runPersistentCommand(io, actions, promptUi, microvm.sandboxId);
2954
2880
  return microvm;
2955
2881
  }
2882
+ return await destroySandbox(io, actions, promptUi, microvm.sandboxId) ? void 0 : microvm;
2956
2883
  }
2957
2884
  async function reattachTerminal(io, actions, promptUi, sandboxId) {
2958
2885
  const processes = await actions.terminalProcesses(sandboxId);
@@ -3049,10 +2976,10 @@ async function mainMenu(io, actions, promptUi, session) {
3049
2976
  {
3050
2977
  message: "What would you like to do?",
3051
2978
  options: [
3052
- { value: "start", label: "Start a microVM", hint: "Create a persistent microVM" },
3053
- { value: "active", label: "Active microVMs", hint: "Manage persistent microVMs" },
3054
- { value: "run", label: "Run a command", hint: "Launch a fresh microVM" },
3055
- { value: "account", label: "Account", hint: "Sign-in status and log out" },
2979
+ { value: "start", label: "New microVM", hint: "For interactive or ongoing work" },
2980
+ { value: "active", label: "Active microVMs", hint: "Resume or manage your microVMs" },
2981
+ { value: "run", label: "One-off run", hint: "Isolated single-run workload" },
2982
+ { value: "account", label: "Account" },
3056
2983
  { value: "exit", label: "Exit" }
3057
2984
  ],
3058
2985
  initialValue: "start",
@@ -3065,7 +2992,7 @@ async function mainMenu(io, actions, promptUi, session) {
3065
2992
  return "exit";
3066
2993
  }
3067
2994
  if (choice === "exit") {
3068
- promptUi.outro("Ready when you are.", io);
2995
+ promptUi.outro(EXIT_MESSAGE, io);
3069
2996
  return "exit";
3070
2997
  }
3071
2998
  try {
@@ -3091,68 +3018,18 @@ async function dispatchMainChoice(choice, io, actions, promptUi, session) {
3091
3018
  return await accountMenu(io, actions.auth, promptUi, session) === "signed-out" ? "signed-out" : "continue";
3092
3019
  }
3093
3020
 
3094
- // ../setsuna/src/cli-network-options.ts
3095
- var NETWORK_OPTION_NAMES = [
3096
- "network-mode",
3097
- "host-ip",
3098
- "guest-ip",
3099
- "prefix-length",
3100
- "uplink-interface",
3101
- "dns-resolvers",
3102
- "mtu"
3103
- ];
3104
- function routedNetworkFromCli(parsed) {
3105
- if (!NETWORK_OPTION_NAMES.some((name) => parsed.values[name] !== void 0)) return void 0;
3106
- if (parsed.values["network-mode"] !== "routed") {
3107
- throw new TypeError("--network-mode must be routed.");
3108
- }
3109
- const hostIp = required(parsed.values["host-ip"], "--host-ip");
3110
- const guestIp = required(parsed.values["guest-ip"], "--guest-ip");
3111
- const prefixLength = integer(parsed.values["prefix-length"], "--prefix-length");
3112
- if (prefixLength !== 30) throw new TypeError("--prefix-length must be 30.");
3113
- const uplinkInterface = required(parsed.values["uplink-interface"], "--uplink-interface");
3114
- const dnsResolvers = parsed.values["dns-resolvers"]?.split(",");
3115
- const mtu = optionalInteger2(parsed.values.mtu, "--mtu");
3116
- return {
3117
- mode: "routed",
3118
- hostIp,
3119
- guestIp,
3120
- prefixLength: 30,
3121
- uplinkInterface,
3122
- ...dnsResolvers === void 0 ? {} : { dnsResolvers },
3123
- ...mtu === void 0 ? {} : { mtu }
3124
- };
3125
- }
3126
- function required(value, name) {
3127
- if (value === void 0 || value === "") throw new TypeError(`${name} is required.`);
3128
- return value;
3129
- }
3130
- function integer(value, name) {
3131
- if (value === void 0 || !/^[0-9]+$/u.test(value)) {
3132
- throw new TypeError(`${name} must be an integer.`);
3133
- }
3134
- const parsed = Number(value);
3135
- if (!Number.isSafeInteger(parsed)) throw new TypeError(`${name} is too large.`);
3136
- return parsed;
3137
- }
3138
- function optionalInteger2(value, name) {
3139
- return value === void 0 ? void 0 : integer(value, name);
3140
- }
3141
-
3142
3021
  // ../setsuna/src/cli-commands.ts
3143
3022
  async function runRunCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3144
3023
  if (commandName !== "run") throw new TypeError(`Unknown command: ${commandName ?? ""}`);
3145
3024
  if (commandArguments.length === 0)
3146
3025
  throw new TypeError("setsuna run requires a command after --.");
3147
- const network = routedNetworkFromCli(parsed);
3148
3026
  const result = await runWithSignalCleanup(
3149
3027
  client,
3150
3028
  commandArguments.map(shellQuote).join(" "),
3151
3029
  {
3152
3030
  timeoutMs: optionalInteger(parsed.values["timeout-ms"], "--timeout-ms"),
3153
3031
  requestTimeoutMs,
3154
- ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] },
3155
- ...network === void 0 ? {} : { network }
3032
+ ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] }
3156
3033
  },
3157
3034
  io,
3158
3035
  options.signalRuntime ?? DEFAULT_SIGNAL_RUNTIME
@@ -3295,8 +3172,56 @@ function exactly(arguments_, count, message) {
3295
3172
  return arguments_;
3296
3173
  }
3297
3174
 
3175
+ // ../setsuna/src/cli-network-options.ts
3176
+ var NETWORK_OPTION_NAMES = [
3177
+ "network-mode",
3178
+ "host-ip",
3179
+ "guest-ip",
3180
+ "prefix-length",
3181
+ "uplink-interface",
3182
+ "dns-resolvers",
3183
+ "mtu"
3184
+ ];
3185
+ function routedNetworkFromCli(parsed) {
3186
+ if (!NETWORK_OPTION_NAMES.some((name) => parsed.values[name] !== void 0)) return void 0;
3187
+ if (parsed.values["network-mode"] !== "routed") {
3188
+ throw new TypeError("--network-mode must be routed.");
3189
+ }
3190
+ const hostIp = required(parsed.values["host-ip"], "--host-ip");
3191
+ const guestIp = required(parsed.values["guest-ip"], "--guest-ip");
3192
+ const prefixLength = integer(parsed.values["prefix-length"], "--prefix-length");
3193
+ if (prefixLength !== 30) throw new TypeError("--prefix-length must be 30.");
3194
+ const uplinkInterface = required(parsed.values["uplink-interface"], "--uplink-interface");
3195
+ const dnsResolvers = parsed.values["dns-resolvers"]?.split(",");
3196
+ const mtu = optionalInteger2(parsed.values.mtu, "--mtu");
3197
+ return {
3198
+ mode: "routed",
3199
+ hostIp,
3200
+ guestIp,
3201
+ prefixLength: 30,
3202
+ uplinkInterface,
3203
+ ...dnsResolvers === void 0 ? {} : { dnsResolvers },
3204
+ ...mtu === void 0 ? {} : { mtu }
3205
+ };
3206
+ }
3207
+ function required(value, name) {
3208
+ if (value === void 0 || value === "") throw new TypeError(`${name} is required.`);
3209
+ return value;
3210
+ }
3211
+ function integer(value, name) {
3212
+ if (value === void 0 || !/^[0-9]+$/u.test(value)) {
3213
+ throw new TypeError(`${name} must be an integer.`);
3214
+ }
3215
+ const parsed = Number(value);
3216
+ if (!Number.isSafeInteger(parsed)) throw new TypeError(`${name} is too large.`);
3217
+ return parsed;
3218
+ }
3219
+ function optionalInteger2(value, name) {
3220
+ return value === void 0 ? void 0 : integer(value, name);
3221
+ }
3222
+
3298
3223
  // ../setsuna/src/cli-sandbox-commands.ts
3299
- var SANDBOX_COMMANDS = /* @__PURE__ */ new Set(["create", "list", "status", "renew", "exec", "destroy"]);
3224
+ var SANDBOX_COMMANDS = /* @__PURE__ */ new Set(["create", "list", "status", "exec", "destroy"]);
3300
3225
  async function runSandboxCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3301
3226
  if (commandName === void 0 || !SANDBOX_COMMANDS.has(commandName)) return false;
3302
3227
  if (commandName === "create") {
@@ -3304,6 +3229,7 @@ async function runSandboxCommand(commandName, commandArguments, parsed, client,
3304
3229
  const network = routedNetworkFromCli(parsed);
3305
3230
  writeLease(
3306
3231
  await client.create({
3232
+ duration: parsed.values.duration ?? "20m",
3307
3233
  ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] },
3308
3234
  ...network === void 0 ? {} : { network }
3309
3235
  }),
@@ -3317,9 +3243,6 @@ async function runSandboxCommand(commandName, commandArguments, parsed, client,
3317
3243
  } else if (commandName === "status") {
3318
3244
  const sandboxId = requireSandboxId(commandName, commandArguments);
3319
3245
  writeStatus(await client.status(sandboxId), parsed.values.json, io.output, "Sandbox");
3320
- } else if (commandName === "renew") {
3321
- const sandboxId = requireSandboxId(commandName, commandArguments);
3322
- writeStatus(await client.renew(sandboxId), parsed.values.json, io.output, "Renewed sandbox");
3323
3246
  } else if (commandName === "destroy") {
3324
3247
  const sandboxId = requireSandboxId(commandName, commandArguments);
3325
3248
  await client.destroy(sandboxId);
@@ -3401,7 +3324,7 @@ function writeLease(lease, json, output, verb) {
3401
3324
  `);
3402
3325
  else {
3403
3326
  output.write(
3404
- `${verb} sandbox ${lease.sandboxId} with configuration ${lease.microvmConfiguration.id}; lease expires at ${lease.expiresAt}.
3327
+ `${verb} sandbox ${lease.sandboxId} with configuration ${lease.microvmConfiguration.id}; duration ${lease.duration}, expires at ${lease.expiresAt}.
3405
3328
  `
3406
3329
  );
3407
3330
  }
@@ -3434,6 +3357,8 @@ function writeDestroyed(sandboxId, json, output) {
3434
3357
 
3435
3358
  // ../setsuna/src/cli-client.ts
3436
3359
  async function runClientCli(commandName, commandArguments, parsed, authenticated, io, options) {
3360
+ if (parsed.values.duration !== void 0 && commandName !== "create")
3361
+ throw new TypeError("--duration is supported only by setsuna create.");
3437
3362
  const requestTimeoutMs = optionalInteger(
3438
3363
  parsed.values["request-timeout-ms"],
3439
3364
  "--request-timeout-ms"
@@ -3648,7 +3573,6 @@ function customerSafeSandboxActions(actions) {
3648
3573
  openTerminal: customerSafe(actions.openTerminal),
3649
3574
  terminalProcesses: customerSafe(actions.terminalProcesses),
3650
3575
  runPersistentCommand: customerSafe(actions.runPersistentCommand),
3651
- renewMicrovm: customerSafe(actions.renewMicrovm),
3652
3576
  destroyMicrovm: customerSafe(actions.destroyMicrovm)
3653
3577
  };
3654
3578
  }
@@ -3677,10 +3601,9 @@ var PREVIEW_HELP = `Usage:
3677
3601
  setsuna auth logout [--json]
3678
3602
  setsuna health [--json]
3679
3603
  setsuna configurations [--json]
3680
- setsuna create [--configuration-id ID] [--request-timeout-ms MS] [--json]
3604
+ setsuna create [--duration 20m|1h|2h] [--configuration-id ID] [--request-timeout-ms MS] [--json]
3681
3605
  setsuna list [--request-timeout-ms MS] [--json]
3682
3606
  setsuna status SANDBOX_ID [--request-timeout-ms MS] [--json]
3683
- setsuna renew SANDBOX_ID [--request-timeout-ms MS] [--json]
3684
3607
  setsuna exec SANDBOX_ID [--timeout-ms MS] [--request-timeout-ms MS] [--json] -- COMMAND [ARG...]
3685
3608
  setsuna destroy SANDBOX_ID [--request-timeout-ms MS] [--json]
3686
3609
  setsuna shell SANDBOX_ID [--attach PROCESS_ID] [--request-timeout-ms MS]
@@ -3697,8 +3620,7 @@ Commands:
3697
3620
  configurations List available microVM configurations.
3698
3621
  create Create a persistent sandbox with a selected or default configuration.
3699
3622
  list List active persistent sandboxes.
3700
- status Inspect an active persistent sandbox lease.
3701
- renew Extend an active persistent sandbox lease.
3623
+ status Inspect an active persistent sandbox.
3702
3624
  exec Execute a command without destroying the persistent sandbox.
3703
3625
  destroy Explicitly destroy a persistent sandbox.
3704
3626
  shell Open an interactive shell in a persistent sandbox, or reattach to one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "setsuna-microvm",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Command-line interface for the Setsuna Research Preview.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://semswitch.com/setsuna",