setsuna-microvm 0.5.3 → 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 +128 -279
  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
@@ -1113,6 +1113,7 @@ function parsePreviewCliArgs(args) {
1113
1113
  "timeout-ms": { type: "string" },
1114
1114
  "request-timeout-ms": { type: "string" },
1115
1115
  "configuration-id": { type: "string" },
1116
+ duration: { type: "string" },
1116
1117
  attach: { type: "string" },
1117
1118
  json: { type: "boolean", default: false },
1118
1119
  help: { type: "boolean", short: "h", default: false },
@@ -1224,14 +1225,14 @@ function hasOnlyKeys(value, allowed) {
1224
1225
  }
1225
1226
  function parseLease(value, expectedConfigurationId) {
1226
1227
  const sandboxId = parseLeaseSandboxId(value);
1227
- 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") {
1228
1229
  throw invalidResponse("sandbox lease");
1229
1230
  }
1230
1231
  const microvmConfiguration = parseMicrovmConfiguration(value.microvmConfiguration);
1231
1232
  if (expectedConfigurationId !== void 0 && microvmConfiguration.id !== expectedConfigurationId) {
1232
1233
  throw invalidResponse("sandbox lease");
1233
1234
  }
1234
- return { sandboxId, expiresAt: value.expiresAt, microvmConfiguration };
1235
+ return { sandboxId, expiresAt: value.expiresAt, duration: value.duration, microvmConfiguration };
1235
1236
  }
1236
1237
  function parseLeaseSandboxId(value) {
1237
1238
  if (!isRecord(value) || !nonEmptyResponseString(value.sandboxId))
@@ -1339,12 +1340,16 @@ function normalizeCreateOptions(options) {
1339
1340
  throw new TypeError("create options must be an object.");
1340
1341
  }
1341
1342
  const unsupported = Object.keys(options).find(
1342
- (key) => key !== "microvmConfigurationId" && key !== "network"
1343
+ (key) => key !== "microvmConfigurationId" && key !== "network" && key !== "duration"
1343
1344
  );
1344
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.");
1345
1349
  const microvmConfigurationId = options.microvmConfigurationId === void 0 ? void 0 : nonEmptyString(options.microvmConfigurationId, "microvmConfigurationId");
1346
1350
  const network = options.network === void 0 ? void 0 : validateRoutedNetwork(options.network);
1347
1351
  return {
1352
+ duration,
1348
1353
  ...microvmConfigurationId === void 0 ? {} : { microvmConfigurationId },
1349
1354
  ...network === void 0 ? {} : { network }
1350
1355
  };
@@ -1829,10 +1834,11 @@ function reportRunPhase(callback, phase) {
1829
1834
 
1830
1835
  // ../setsuna/src/client.ts
1831
1836
  var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
1832
- var DEFAULT_CLEANUP_TIMEOUT_MS = 1e4;
1837
+ var DEFAULT_RUN_REQUEST_TIMEOUT_MS = 6e4 + 12e4 + 6e4;
1833
1838
  var SetsunaClient = class {
1834
1839
  #serviceUrl;
1835
1840
  #requestTimeoutMs;
1841
+ #explicitRequestTimeoutMs;
1836
1842
  #getAccessToken;
1837
1843
  #affinity;
1838
1844
  constructor(options = {}) {
@@ -1850,6 +1856,7 @@ var SetsunaClient = class {
1850
1856
  this.#getAccessToken = options.remote.getAccessToken;
1851
1857
  this.#affinity = new FrontDoorAffinity(this.#serviceUrl);
1852
1858
  }
1859
+ this.#explicitRequestTimeoutMs = options.requestTimeoutMs;
1853
1860
  this.#requestTimeoutMs = positiveMilliseconds(
1854
1861
  options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
1855
1862
  "requestTimeoutMs"
@@ -1869,20 +1876,7 @@ var SetsunaClient = class {
1869
1876
  method: "POST",
1870
1877
  body: { ...createOptions }
1871
1878
  });
1872
- const sandboxId = parseLeaseSandboxId(response);
1873
- try {
1874
- return parseLease(response, createOptions.microvmConfigurationId);
1875
- } catch (error) {
1876
- try {
1877
- await this.#destroy(sandboxId, DEFAULT_CLEANUP_TIMEOUT_MS);
1878
- } catch (cleanupFailure) {
1879
- throw new AggregateError(
1880
- [error, cleanupFailure],
1881
- "Sandbox lease validation and destruction both failed."
1882
- );
1883
- }
1884
- throw error;
1885
- }
1879
+ return parseLease(response, createOptions.microvmConfigurationId);
1886
1880
  }
1887
1881
  async list() {
1888
1882
  return parseSandboxList(await this.#request("v1/sandboxes", { method: "GET" }));
@@ -1895,14 +1889,6 @@ var SetsunaClient = class {
1895
1889
  })
1896
1890
  );
1897
1891
  }
1898
- async renew(sandboxId) {
1899
- const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1900
- return parseSandboxStatus(
1901
- await this.#request(`v1/sandboxes/${encodeURIComponent(validatedSandboxId)}/renew`, {
1902
- method: "POST"
1903
- })
1904
- );
1905
- }
1906
1892
  async execute(sandboxId, command, options = {}) {
1907
1893
  const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1908
1894
  const validatedCommand = nonEmptyString(command, "command");
@@ -1977,44 +1963,30 @@ var SetsunaClient = class {
1977
1963
  });
1978
1964
  }
1979
1965
  async run(command, options = {}) {
1980
- let sandboxId;
1981
- const cleanupTimeoutMs = positiveMilliseconds(
1982
- options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS,
1983
- "cleanupTimeoutMs"
1966
+ const unsupported = Object.keys(options).find(
1967
+ (key) => !["microvmConfigurationId", "timeoutMs", "requestTimeoutMs", "signal", "onPhase"].includes(
1968
+ key
1969
+ )
1984
1970
  );
1985
- let executionOutcome;
1986
- try {
1987
- reportRunPhase(options.onPhase, "starting-microvm");
1988
- const lease = await this.create({
1989
- ...options.microvmConfigurationId === void 0 ? {} : { microvmConfigurationId: options.microvmConfigurationId },
1990
- ...options.network === void 0 ? {} : { network: options.network }
1991
- });
1992
- sandboxId = lease.sandboxId;
1993
- reportRunPhase(options.onPhase, "microvm-ready");
1994
- reportRunPhase(options.onPhase, "running-command");
1995
- executionOutcome = { ok: true, result: await this.execute(sandboxId, command, options) };
1996
- } catch (error) {
1997
- executionOutcome = { ok: false, error };
1998
- }
1999
- if (sandboxId !== void 0) {
2000
- try {
2001
- reportRunPhase(options.onPhase, "destroying-microvm");
2002
- await this.#destroy(sandboxId, cleanupTimeoutMs);
2003
- reportRunPhase(options.onPhase, "microvm-destroyed");
2004
- } catch (cleanupFailure) {
2005
- if (!executionOutcome.ok) {
2006
- throw new AggregateError(
2007
- [executionOutcome.error, cleanupFailure],
2008
- "Sandbox execution and destruction both failed."
2009
- );
2010
- }
2011
- throw cleanupFailure;
2012
- }
2013
- }
2014
- if (!executionOutcome.ok) {
2015
- throw executionOutcome.error;
2016
- }
2017
- 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;
2018
1990
  }
2019
1991
  async #request(path3, options) {
2020
1992
  return requestJson(
@@ -2044,7 +2016,7 @@ async function runWithSignalCleanup(client, command, options, io, signalRuntime)
2044
2016
  const exitCode = signal === "SIGINT" ? 130 : 143;
2045
2017
  if (interruptedExitCode === void 0) {
2046
2018
  interruptedExitCode = exitCode;
2047
- io.errorOutput.write("Interrupt received; destroying sandbox...\n");
2019
+ io.errorOutput.write("Interrupt received; Setsuna owns run cleanup.\n");
2048
2020
  controller.abort();
2049
2021
  return;
2050
2022
  }
@@ -2160,90 +2132,6 @@ async function restoreInput(input, wasRaw, wasPaused) {
2160
2132
  input.setRawMode?.(wasRaw);
2161
2133
  }
2162
2134
 
2163
- // ../setsuna/src/lease-keeper.ts
2164
- var MAX_TIMER_DELAY_MS = 2147483647;
2165
- var SYSTEM_RUNTIME = {
2166
- now: Date.now,
2167
- setTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
2168
- clearTimeout: (handle) => clearTimeout(handle)
2169
- };
2170
- var ActiveLeaseKeeper = class {
2171
- #client;
2172
- #sandboxId;
2173
- #runtime;
2174
- #expiresAtMs;
2175
- #failureHandler;
2176
- #running = false;
2177
- #timer;
2178
- constructor(client, sandboxId, runtime = SYSTEM_RUNTIME) {
2179
- this.#client = client;
2180
- this.#sandboxId = sandboxId;
2181
- this.#runtime = runtime;
2182
- }
2183
- async renewNow() {
2184
- if (this.#running) throw new TypeError("Cannot prime a running lease keeper.");
2185
- this.#expiresAtMs = await this.#renewExpiry();
2186
- }
2187
- start(onFailure) {
2188
- if (this.#running) throw new TypeError("The lease keeper is already running.");
2189
- if (this.#expiresAtMs === void 0) {
2190
- throw new TypeError("The lease keeper must renew the sandbox before it starts.");
2191
- }
2192
- this.#running = true;
2193
- this.#failureHandler = onFailure;
2194
- try {
2195
- this.#schedule(this.#expiresAtMs);
2196
- } catch (error) {
2197
- this.#running = false;
2198
- this.#failureHandler = void 0;
2199
- throw error;
2200
- }
2201
- }
2202
- stop() {
2203
- this.#running = false;
2204
- this.#failureHandler = void 0;
2205
- if (this.#timer !== void 0) this.#runtime.clearTimeout(this.#timer);
2206
- this.#timer = void 0;
2207
- }
2208
- async #renewExpiry() {
2209
- const { expiresAt } = await this.#client.renew(this.#sandboxId);
2210
- const expiresAtMs = Date.parse(expiresAt);
2211
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= this.#runtime.now()) {
2212
- throw new TypeError("Sandbox renewal returned an invalid or expired expiresAt value.");
2213
- }
2214
- return expiresAtMs;
2215
- }
2216
- #schedule(expiresAtMs) {
2217
- const remainingMs = expiresAtMs - this.#runtime.now();
2218
- if (!Number.isFinite(remainingMs) || remainingMs <= 0) {
2219
- throw new TypeError(
2220
- "The renewed sandbox lease expired before its next renewal was scheduled."
2221
- );
2222
- }
2223
- const delayMs = Math.min(MAX_TIMER_DELAY_MS, Math.max(1, Math.floor(remainingMs / 2)));
2224
- this.#timer = this.#runtime.setTimeout(() => {
2225
- this.#timer = void 0;
2226
- void this.#renewAndSchedule().catch((error) => this.#fail(error));
2227
- }, delayMs);
2228
- }
2229
- async #renewAndSchedule() {
2230
- const expiresAtMs = await this.#renewExpiry();
2231
- if (!this.#running) return;
2232
- this.#expiresAtMs = expiresAtMs;
2233
- this.#schedule(expiresAtMs);
2234
- }
2235
- #fail(error) {
2236
- if (!this.#running) return;
2237
- this.#running = false;
2238
- const handler = this.#failureHandler;
2239
- this.#failureHandler = void 0;
2240
- try {
2241
- handler?.(error);
2242
- } catch {
2243
- }
2244
- }
2245
- };
2246
-
2247
2135
  // ../setsuna/src/cli-shell.ts
2248
2136
  var DETACH_BYTE = 29;
2249
2137
  var DEFAULT_TERM = "xterm-256color";
@@ -2264,7 +2152,7 @@ async function runShellCliCommand(commandName, commandArguments, attachProcessId
2264
2152
  );
2265
2153
  return true;
2266
2154
  }
2267
- async function runShellCommand(commandArguments, attachProcessId, client, io, terminal, leaseRuntime) {
2155
+ async function runShellCommand(commandArguments, attachProcessId, client, io, terminal) {
2268
2156
  if (commandArguments.length !== 1 || commandArguments[0] === "") {
2269
2157
  throw new TypeError("setsuna shell requires exactly one sandbox ID.");
2270
2158
  }
@@ -2273,21 +2161,9 @@ async function runShellCommand(commandArguments, attachProcessId, client, io, te
2273
2161
  }
2274
2162
  if (attachProcessId === "") throw new TypeError("--attach requires a process ID.");
2275
2163
  const sandboxId = commandArguments[0];
2276
- const leaseKeeper = new ActiveLeaseKeeper(client, sandboxId, leaseRuntime);
2277
- await leaseKeeper.renewNow();
2278
- try {
2279
- const size = terminal.size();
2280
- const processId = await resolveProcess(
2281
- client,
2282
- sandboxId,
2283
- attachProcessId,
2284
- size,
2285
- terminal.term()
2286
- );
2287
- await attachTerminal(client, sandboxId, processId, io, terminal, leaseKeeper);
2288
- } finally {
2289
- leaseKeeper.stop();
2290
- }
2164
+ const size = terminal.size();
2165
+ const processId = await resolveProcess(client, sandboxId, attachProcessId, size, terminal.term());
2166
+ await attachTerminal(client, sandboxId, processId, io, terminal);
2291
2167
  }
2292
2168
  async function resolveProcess(client, sandboxId, attachProcessId, size, localTerm) {
2293
2169
  if (attachProcessId === void 0) {
@@ -2310,21 +2186,12 @@ async function resolveProcess(client, sandboxId, attachProcessId, size, localTer
2310
2186
  if (process2.state === "running") await client.resizeProcess(sandboxId, attachProcessId, size);
2311
2187
  return attachProcessId;
2312
2188
  }
2313
- async function attachTerminal(client, sandboxId, processId, io, terminal, leaseKeeper) {
2189
+ async function attachTerminal(client, sandboxId, processId, io, terminal) {
2314
2190
  const controller = new AbortController();
2315
- const session = createAttachedSession(
2316
- client,
2317
- sandboxId,
2318
- processId,
2319
- io,
2320
- terminal,
2321
- controller,
2322
- () => leaseKeeper.stop()
2323
- );
2191
+ const session = createAttachedSession(client, sandboxId, processId, io, terminal, controller);
2324
2192
  let exitFrame;
2325
2193
  let attachError;
2326
2194
  try {
2327
- leaseKeeper.start(session.fail);
2328
2195
  for await (const frame of client.attachProcess(sandboxId, processId, {
2329
2196
  maxStreamBytes: Number.MAX_SAFE_INTEGER,
2330
2197
  signal: controller.signal
@@ -2351,7 +2218,7 @@ async function attachTerminal(client, sandboxId, processId, io, terminal, leaseK
2351
2218
  }
2352
2219
  if (exitFrame.exitCode !== null) terminal.setExitCode(exitFrame.exitCode);
2353
2220
  }
2354
- function createAttachedSession(client, sandboxId, processId, io, terminal, controller, stopLeaseKeeper) {
2221
+ function createAttachedSession(client, sandboxId, processId, io, terminal, controller) {
2355
2222
  let detached = false;
2356
2223
  let failure;
2357
2224
  let cleaned = false;
@@ -2364,7 +2231,6 @@ function createAttachedSession(client, sandboxId, processId, io, terminal, contr
2364
2231
  cleaned = true;
2365
2232
  io.input.off("data", onInput);
2366
2233
  terminal.offResize(onResize);
2367
- stopLeaseKeeper();
2368
2234
  restored = restoreRawMode();
2369
2235
  };
2370
2236
  const fail = (error) => {
@@ -2455,11 +2321,12 @@ function isDetachAbort(error) {
2455
2321
  // ../setsuna/src/cli-interactive-persistent-actions.ts
2456
2322
  function createPersistentInteractiveActions(client, io, shellTerminal) {
2457
2323
  return {
2458
- async startMicrovm(microvmConfigurationId) {
2459
- const lease = await client.create({ microvmConfigurationId });
2324
+ async startMicrovm(microvmConfigurationId, duration) {
2325
+ const lease = await client.create({ microvmConfigurationId, duration });
2460
2326
  return {
2461
2327
  sandboxId: lease.sandboxId,
2462
2328
  expiresAt: lease.expiresAt,
2329
+ duration: lease.duration,
2463
2330
  vcpuCount: lease.microvmConfiguration.vcpuCount,
2464
2331
  memoryMiB: lease.microvmConfiguration.memoryMiB
2465
2332
  };
@@ -2476,13 +2343,11 @@ function createPersistentInteractiveActions(client, io, shellTerminal) {
2476
2343
  return (await client.listProcesses(sandboxId)).filter((process2) => process2.state === "running" && process2.terminal !== void 0).map(({ processId, pid }) => ({ processId, pid }));
2477
2344
  },
2478
2345
  async runPersistentCommand(sandboxId, command, timeoutMs) {
2479
- await client.renew(sandboxId);
2480
2346
  const result = await client.execute(sandboxId, command, {
2481
2347
  ...timeoutMs === void 0 ? {} : { timeoutMs }
2482
2348
  });
2483
2349
  return interactiveRunResult(result);
2484
2350
  },
2485
- renewMicrovm: (sandboxId) => client.renew(sandboxId),
2486
2351
  destroyMicrovm: (sandboxId) => client.destroy(sandboxId)
2487
2352
  };
2488
2353
  }
@@ -2543,18 +2408,7 @@ var DEFAULT_SIGNAL_RUNTIME = {
2543
2408
  var STANDALONE_BOX = { withGuide: false, width: "auto" };
2544
2409
  var EXIT_MESSAGE = "Sayonara!";
2545
2410
  function formatRunPhase(phase) {
2546
- switch (phase) {
2547
- case "starting-microvm":
2548
- return "Starting microVM";
2549
- case "microvm-ready":
2550
- return "microVM ready";
2551
- case "running-command":
2552
- return "Running command";
2553
- case "destroying-microvm":
2554
- return "Cleaning up";
2555
- case "microvm-destroyed":
2556
- return "microVM destroyed";
2557
- }
2411
+ return phase === "running" ? "Running on Setsuna" : "Complete";
2558
2412
  }
2559
2413
  function formatRunResult(result) {
2560
2414
  const facts = [`Exit status ${result.exitStatus}`, `${result.durationMs} ms`];
@@ -2593,6 +2447,10 @@ function formatMicrovmState(microvm, now) {
2593
2447
  if (microvm.vcpuCount !== void 0 && microvm.memoryMiB !== void 0) {
2594
2448
  lines.push(`Compute ${formatMicrovmShape(microvm.vcpuCount, microvm.memoryMiB)}`);
2595
2449
  }
2450
+ if (microvm.duration !== void 0) {
2451
+ lines.push(`Duration ${microvm.duration}`);
2452
+ return lines.join("\n");
2453
+ }
2596
2454
  const remaining = remainingTime(microvm.expiresAt, now);
2597
2455
  lines.push(remaining === void 0 ? "Expired" : `Expires in ${remaining}`);
2598
2456
  return lines.join("\n");
@@ -2908,12 +2766,25 @@ async function startMicrovmMenu(io, actions, promptUi) {
2908
2766
  const configurations = (await actions.microvmConfigurations()).configurations;
2909
2767
  const configuration = await promptForMicrovmConfiguration(io, promptUi, configurations);
2910
2768
  if (configuration === null) return;
2911
- 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);
2912
2783
  if (sandbox === void 0) return;
2913
2784
  promptUi.box(formatMicrovmState(sandbox, Date.now()), "microVM ready", io, STANDALONE_BOX);
2914
2785
  await manageSandbox(io, actions, promptUi, sandbox);
2915
2786
  }
2916
- async function startSandbox(io, actions, promptUi, microvmConfigurationId) {
2787
+ async function startSandbox(io, actions, promptUi, microvmConfigurationId, duration) {
2917
2788
  let interrupted = false;
2918
2789
  const spinner = promptUi.spinner(io, {
2919
2790
  indicator: "timer",
@@ -2924,7 +2795,7 @@ async function startSandbox(io, actions, promptUi, microvmConfigurationId) {
2924
2795
  spinner.start("Starting microVM");
2925
2796
  let sandbox;
2926
2797
  try {
2927
- sandbox = await actions.startMicrovm(microvmConfigurationId);
2798
+ sandbox = await actions.startMicrovm(microvmConfigurationId, duration);
2928
2799
  } catch (error) {
2929
2800
  spinner.error(formatTuiError(error));
2930
2801
  return void 0;
@@ -2978,7 +2849,6 @@ async function manageSandbox(io, actions, promptUi, initial) {
2978
2849
  { value: "open-terminal", label: "Open terminal" },
2979
2850
  { value: "reattach-terminal", label: "Reattach terminal" },
2980
2851
  { value: "run-command", label: "Run command" },
2981
- { value: "renew", label: "Renew" },
2982
2852
  { value: "destroy", label: "Destroy microVM" },
2983
2853
  { value: "back", label: "Back" }
2984
2854
  ],
@@ -2999,33 +2869,17 @@ async function manageSandbox(io, actions, promptUi, initial) {
2999
2869
  async function dispatchSandboxChoice(choice, microvm, io, actions, promptUi) {
3000
2870
  if (choice === "open-terminal") {
3001
2871
  await actions.openTerminal(microvm.sandboxId);
3002
- return refreshExpiry(actions, microvm);
2872
+ return microvm;
3003
2873
  }
3004
2874
  if (choice === "reattach-terminal") {
3005
- const attached = await reattachTerminal(io, actions, promptUi, microvm.sandboxId);
3006
- return attached ? refreshExpiry(actions, microvm) : microvm;
2875
+ await reattachTerminal(io, actions, promptUi, microvm.sandboxId);
2876
+ return microvm;
3007
2877
  }
3008
2878
  if (choice === "run-command") {
3009
- const ran = await runPersistentCommand(io, actions, promptUi, microvm.sandboxId);
3010
- return ran ? refreshExpiry(actions, microvm) : microvm;
3011
- }
3012
- if (choice === "renew") return renewSandbox(io, actions, promptUi, microvm);
3013
- return await destroySandbox(io, actions, promptUi, microvm.sandboxId) ? void 0 : microvm;
3014
- }
3015
- async function renewSandbox(io, actions, promptUi, microvm) {
3016
- const status = await actions.renewMicrovm(microvm.sandboxId);
3017
- promptUi.success(`Renewed \xB7 ${formatExpiry(status.expiresAt, Date.now())}`, io);
3018
- return { ...microvm, expiresAt: status.expiresAt };
3019
- }
3020
- async function refreshExpiry(actions, microvm) {
3021
- try {
3022
- const current = (await actions.activeMicrovms()).find(
3023
- ({ sandboxId }) => sandboxId === microvm.sandboxId
3024
- );
3025
- return current === void 0 ? microvm : { ...microvm, expiresAt: current.expiresAt };
3026
- } catch {
2879
+ await runPersistentCommand(io, actions, promptUi, microvm.sandboxId);
3027
2880
  return microvm;
3028
2881
  }
2882
+ return await destroySandbox(io, actions, promptUi, microvm.sandboxId) ? void 0 : microvm;
3029
2883
  }
3030
2884
  async function reattachTerminal(io, actions, promptUi, sandboxId) {
3031
2885
  const processes = await actions.terminalProcesses(sandboxId);
@@ -3164,68 +3018,18 @@ async function dispatchMainChoice(choice, io, actions, promptUi, session) {
3164
3018
  return await accountMenu(io, actions.auth, promptUi, session) === "signed-out" ? "signed-out" : "continue";
3165
3019
  }
3166
3020
 
3167
- // ../setsuna/src/cli-network-options.ts
3168
- var NETWORK_OPTION_NAMES = [
3169
- "network-mode",
3170
- "host-ip",
3171
- "guest-ip",
3172
- "prefix-length",
3173
- "uplink-interface",
3174
- "dns-resolvers",
3175
- "mtu"
3176
- ];
3177
- function routedNetworkFromCli(parsed) {
3178
- if (!NETWORK_OPTION_NAMES.some((name) => parsed.values[name] !== void 0)) return void 0;
3179
- if (parsed.values["network-mode"] !== "routed") {
3180
- throw new TypeError("--network-mode must be routed.");
3181
- }
3182
- const hostIp = required(parsed.values["host-ip"], "--host-ip");
3183
- const guestIp = required(parsed.values["guest-ip"], "--guest-ip");
3184
- const prefixLength = integer(parsed.values["prefix-length"], "--prefix-length");
3185
- if (prefixLength !== 30) throw new TypeError("--prefix-length must be 30.");
3186
- const uplinkInterface = required(parsed.values["uplink-interface"], "--uplink-interface");
3187
- const dnsResolvers = parsed.values["dns-resolvers"]?.split(",");
3188
- const mtu = optionalInteger2(parsed.values.mtu, "--mtu");
3189
- return {
3190
- mode: "routed",
3191
- hostIp,
3192
- guestIp,
3193
- prefixLength: 30,
3194
- uplinkInterface,
3195
- ...dnsResolvers === void 0 ? {} : { dnsResolvers },
3196
- ...mtu === void 0 ? {} : { mtu }
3197
- };
3198
- }
3199
- function required(value, name) {
3200
- if (value === void 0 || value === "") throw new TypeError(`${name} is required.`);
3201
- return value;
3202
- }
3203
- function integer(value, name) {
3204
- if (value === void 0 || !/^[0-9]+$/u.test(value)) {
3205
- throw new TypeError(`${name} must be an integer.`);
3206
- }
3207
- const parsed = Number(value);
3208
- if (!Number.isSafeInteger(parsed)) throw new TypeError(`${name} is too large.`);
3209
- return parsed;
3210
- }
3211
- function optionalInteger2(value, name) {
3212
- return value === void 0 ? void 0 : integer(value, name);
3213
- }
3214
-
3215
3021
  // ../setsuna/src/cli-commands.ts
3216
3022
  async function runRunCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3217
3023
  if (commandName !== "run") throw new TypeError(`Unknown command: ${commandName ?? ""}`);
3218
3024
  if (commandArguments.length === 0)
3219
3025
  throw new TypeError("setsuna run requires a command after --.");
3220
- const network = routedNetworkFromCli(parsed);
3221
3026
  const result = await runWithSignalCleanup(
3222
3027
  client,
3223
3028
  commandArguments.map(shellQuote).join(" "),
3224
3029
  {
3225
3030
  timeoutMs: optionalInteger(parsed.values["timeout-ms"], "--timeout-ms"),
3226
3031
  requestTimeoutMs,
3227
- ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] },
3228
- ...network === void 0 ? {} : { network }
3032
+ ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] }
3229
3033
  },
3230
3034
  io,
3231
3035
  options.signalRuntime ?? DEFAULT_SIGNAL_RUNTIME
@@ -3368,8 +3172,56 @@ function exactly(arguments_, count, message) {
3368
3172
  return arguments_;
3369
3173
  }
3370
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
+
3371
3223
  // ../setsuna/src/cli-sandbox-commands.ts
3372
- var SANDBOX_COMMANDS = /* @__PURE__ */ new Set(["create", "list", "status", "renew", "exec", "destroy"]);
3224
+ var SANDBOX_COMMANDS = /* @__PURE__ */ new Set(["create", "list", "status", "exec", "destroy"]);
3373
3225
  async function runSandboxCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3374
3226
  if (commandName === void 0 || !SANDBOX_COMMANDS.has(commandName)) return false;
3375
3227
  if (commandName === "create") {
@@ -3377,6 +3229,7 @@ async function runSandboxCommand(commandName, commandArguments, parsed, client,
3377
3229
  const network = routedNetworkFromCli(parsed);
3378
3230
  writeLease(
3379
3231
  await client.create({
3232
+ duration: parsed.values.duration ?? "20m",
3380
3233
  ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] },
3381
3234
  ...network === void 0 ? {} : { network }
3382
3235
  }),
@@ -3390,9 +3243,6 @@ async function runSandboxCommand(commandName, commandArguments, parsed, client,
3390
3243
  } else if (commandName === "status") {
3391
3244
  const sandboxId = requireSandboxId(commandName, commandArguments);
3392
3245
  writeStatus(await client.status(sandboxId), parsed.values.json, io.output, "Sandbox");
3393
- } else if (commandName === "renew") {
3394
- const sandboxId = requireSandboxId(commandName, commandArguments);
3395
- writeStatus(await client.renew(sandboxId), parsed.values.json, io.output, "Renewed sandbox");
3396
3246
  } else if (commandName === "destroy") {
3397
3247
  const sandboxId = requireSandboxId(commandName, commandArguments);
3398
3248
  await client.destroy(sandboxId);
@@ -3474,7 +3324,7 @@ function writeLease(lease, json, output, verb) {
3474
3324
  `);
3475
3325
  else {
3476
3326
  output.write(
3477
- `${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}.
3478
3328
  `
3479
3329
  );
3480
3330
  }
@@ -3507,6 +3357,8 @@ function writeDestroyed(sandboxId, json, output) {
3507
3357
 
3508
3358
  // ../setsuna/src/cli-client.ts
3509
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.");
3510
3362
  const requestTimeoutMs = optionalInteger(
3511
3363
  parsed.values["request-timeout-ms"],
3512
3364
  "--request-timeout-ms"
@@ -3721,7 +3573,6 @@ function customerSafeSandboxActions(actions) {
3721
3573
  openTerminal: customerSafe(actions.openTerminal),
3722
3574
  terminalProcesses: customerSafe(actions.terminalProcesses),
3723
3575
  runPersistentCommand: customerSafe(actions.runPersistentCommand),
3724
- renewMicrovm: customerSafe(actions.renewMicrovm),
3725
3576
  destroyMicrovm: customerSafe(actions.destroyMicrovm)
3726
3577
  };
3727
3578
  }
@@ -3750,10 +3601,9 @@ var PREVIEW_HELP = `Usage:
3750
3601
  setsuna auth logout [--json]
3751
3602
  setsuna health [--json]
3752
3603
  setsuna configurations [--json]
3753
- 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]
3754
3605
  setsuna list [--request-timeout-ms MS] [--json]
3755
3606
  setsuna status SANDBOX_ID [--request-timeout-ms MS] [--json]
3756
- setsuna renew SANDBOX_ID [--request-timeout-ms MS] [--json]
3757
3607
  setsuna exec SANDBOX_ID [--timeout-ms MS] [--request-timeout-ms MS] [--json] -- COMMAND [ARG...]
3758
3608
  setsuna destroy SANDBOX_ID [--request-timeout-ms MS] [--json]
3759
3609
  setsuna shell SANDBOX_ID [--attach PROCESS_ID] [--request-timeout-ms MS]
@@ -3770,8 +3620,7 @@ Commands:
3770
3620
  configurations List available microVM configurations.
3771
3621
  create Create a persistent sandbox with a selected or default configuration.
3772
3622
  list List active persistent sandboxes.
3773
- status Inspect an active persistent sandbox lease.
3774
- renew Extend an active persistent sandbox lease.
3623
+ status Inspect an active persistent sandbox.
3775
3624
  exec Execute a command without destroying the persistent sandbox.
3776
3625
  destroy Explicitly destroy a persistent sandbox.
3777
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.3",
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",