setsuna-microvm 0.5.2 → 0.5.3

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 (2) hide show
  1. package/dist/cli.js +109 -36
  2. package/package.json +1 -1
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({
@@ -1248,6 +1272,36 @@ function isRecord(value) {
1248
1272
  return typeof value === "object" && value !== null && !Array.isArray(value);
1249
1273
  }
1250
1274
 
1275
+ // ../setsuna/src/client-affinity.ts
1276
+ var FrontDoorAffinity = class {
1277
+ #origin;
1278
+ #cookies = /* @__PURE__ */ new Map();
1279
+ constructor(serviceUrl) {
1280
+ this.#origin = serviceUrl.origin;
1281
+ }
1282
+ headersFor(url, headers) {
1283
+ if (url.origin !== this.#origin || this.#cookies.size === 0) return headers;
1284
+ const result = new Headers(headers);
1285
+ result.set("cookie", [...this.#cookies].map(([name, value]) => `${name}=${value}`).join("; "));
1286
+ return result;
1287
+ }
1288
+ capture(url, response) {
1289
+ if (url.origin !== this.#origin) return;
1290
+ if (response.url !== "" && new URL(response.url).origin !== this.#origin) return;
1291
+ for (const header of response.headers.getSetCookie()) {
1292
+ const pair = header.split(";", 1)[0]?.trim() ?? "";
1293
+ const match = /^(ASLBSA|ASLBSACORS)=([\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*)$/u.exec(
1294
+ pair
1295
+ );
1296
+ if (match === null) continue;
1297
+ const [, name, value] = match;
1298
+ if (name === void 0 || value === void 0) continue;
1299
+ if (value === "") this.#cookies.delete(name);
1300
+ else this.#cookies.set(name, value);
1301
+ }
1302
+ }
1303
+ };
1304
+
1251
1305
  // ../setsuna/src/client-options.ts
1252
1306
  var PREVIEW_SERVICE_URL = "https://api.setsuna.semswitch.com/";
1253
1307
  function normalizeServiceUrl(value) {
@@ -1359,7 +1413,7 @@ function isLoopbackHostname(hostname) {
1359
1413
  }
1360
1414
 
1361
1415
  // ../setsuna/src/client-request.ts
1362
- async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3, options) {
1416
+ async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3, options, affinity) {
1363
1417
  const requestTimeoutMs = options.requestTimeoutMs === void 0 ? defaultTimeoutMs : positiveMilliseconds(options.requestTimeoutMs, "requestTimeoutMs");
1364
1418
  const timeoutSignal = AbortSignal.timeout(requestTimeoutMs);
1365
1419
  const signal = options.signal === void 0 ? timeoutSignal : AbortSignal.any([options.signal, timeoutSignal]);
@@ -1376,7 +1430,8 @@ async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3,
1376
1430
  requestTimeoutMs,
1377
1431
  timeoutSignal,
1378
1432
  signal,
1379
- accessToken
1433
+ accessToken,
1434
+ affinity
1380
1435
  );
1381
1436
  const responseBody = response.status === 204 ? void 0 : await parseResponseJson(response);
1382
1437
  if (!response.ok) throw brokerResponseError(response, responseBody);
@@ -1417,14 +1472,17 @@ function settleBeforeAbort(pending, signal) {
1417
1472
  if (signal.aborted) onAbort();
1418
1473
  });
1419
1474
  }
1420
- async function fetchService(url, options, requestTimeoutMs, timeoutSignal, signal, accessToken) {
1475
+ async function fetchService(url, options, requestTimeoutMs, timeoutSignal, signal, accessToken, affinity) {
1421
1476
  try {
1422
- return await fetch(url, {
1477
+ const headers = requestHeaders(options.body, accessToken);
1478
+ const response = await fetch(url, {
1423
1479
  method: options.method,
1424
- headers: requestHeaders(options.body, accessToken),
1480
+ headers: affinity?.headersFor(url, headers) ?? headers,
1425
1481
  body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1426
1482
  signal
1427
1483
  });
1484
+ affinity?.capture(url, response);
1485
+ return response;
1428
1486
  } catch (error) {
1429
1487
  throwCancellationFailure(error, requestTimeoutMs, timeoutSignal, options.signal);
1430
1488
  throw new SetsunaError("The local Setsuna service could not be reached.", {
@@ -1559,13 +1617,14 @@ function parseProcessList(value) {
1559
1617
  if (!Array.isArray(value)) throw invalidResponse2("process list");
1560
1618
  return value.map(parseProcessInfo);
1561
1619
  }
1562
- function attachProcessStream(serviceUrl, path3, options, getAccessToken) {
1620
+ function attachProcessStream(serviceUrl, path3, options, getAccessToken, affinity) {
1563
1621
  return {
1564
1622
  async *[Symbol.asyncIterator]() {
1565
1623
  const response = await fetchProcessStream(
1566
1624
  new URL(path3, serviceUrl),
1567
1625
  options.signal,
1568
- getAccessToken
1626
+ getAccessToken,
1627
+ affinity
1569
1628
  );
1570
1629
  if (response.body === null) throw invalidResponse2("process stream");
1571
1630
  yield* decodeFrames(response.body, options.signal);
@@ -1586,15 +1645,17 @@ function processFrameToJson(frame) {
1586
1645
  }
1587
1646
  return frame;
1588
1647
  }
1589
- async function fetchProcessStream(url, signal, getAccessToken) {
1648
+ async function fetchProcessStream(url, signal, getAccessToken, affinity) {
1590
1649
  const accessToken = await processAccessToken(getAccessToken, signal);
1591
1650
  let response;
1592
1651
  try {
1652
+ const headers = accessToken === void 0 ? void 0 : { authorization: `Bearer ${accessToken}` };
1593
1653
  response = await fetch(url, {
1594
1654
  method: "GET",
1595
1655
  signal,
1596
- ...accessToken === void 0 ? {} : { headers: { authorization: `Bearer ${accessToken}` } }
1656
+ ...headers === void 0 ? {} : { headers: affinity?.headersFor(url, headers) ?? headers }
1597
1657
  });
1658
+ affinity?.capture(url, response);
1598
1659
  } catch (error) {
1599
1660
  throw requestError(error, signal);
1600
1661
  }
@@ -1773,6 +1834,7 @@ var SetsunaClient = class {
1773
1834
  #serviceUrl;
1774
1835
  #requestTimeoutMs;
1775
1836
  #getAccessToken;
1837
+ #affinity;
1776
1838
  constructor(options = {}) {
1777
1839
  if (options.serviceUrl !== void 0 && options.remote !== void 0) {
1778
1840
  throw new TypeError("serviceUrl and remote are mutually exclusive.");
@@ -1786,6 +1848,7 @@ var SetsunaClient = class {
1786
1848
  }
1787
1849
  this.#serviceUrl = new URL(PREVIEW_SERVICE_URL);
1788
1850
  this.#getAccessToken = options.remote.getAccessToken;
1851
+ this.#affinity = new FrontDoorAffinity(this.#serviceUrl);
1789
1852
  }
1790
1853
  this.#requestTimeoutMs = positiveMilliseconds(
1791
1854
  options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
@@ -1876,7 +1939,8 @@ var SetsunaClient = class {
1876
1939
  this.#serviceUrl,
1877
1940
  `${processPath(sandboxId)}/${encodeURIComponent(nonEmptyString(processId, "processId"))}/attach${query}`,
1878
1941
  options,
1879
- this.#getAccessToken
1942
+ this.#getAccessToken,
1943
+ this.#affinity
1880
1944
  );
1881
1945
  }
1882
1946
  async writeProcessStdin(sandboxId, processId, data) {
@@ -1958,7 +2022,8 @@ var SetsunaClient = class {
1958
2022
  this.#requestTimeoutMs,
1959
2023
  this.#getAccessToken,
1960
2024
  path3,
1961
- options
2025
+ options,
2026
+ this.#affinity
1962
2027
  );
1963
2028
  }
1964
2029
  };
@@ -2081,14 +2146,19 @@ function enterRawMode(input) {
2081
2146
  const wasPaused = input.isPaused();
2082
2147
  input.setRawMode(true);
2083
2148
  input.resume();
2084
- let restored = false;
2149
+ let restored;
2085
2150
  return () => {
2086
- if (restored) return;
2087
- restored = true;
2088
- input.setRawMode?.(wasRaw);
2089
- if (wasPaused) input.pause();
2151
+ restored ??= restoreInput(input, wasRaw, wasPaused);
2152
+ return restored;
2090
2153
  };
2091
2154
  }
2155
+ async function restoreInput(input, wasRaw, wasPaused) {
2156
+ if (wasPaused) {
2157
+ input.pause();
2158
+ await new Promise((resolve) => process.nextTick(resolve));
2159
+ }
2160
+ input.setRawMode?.(wasRaw);
2161
+ }
2092
2162
 
2093
2163
  // ../setsuna/src/lease-keeper.ts
2094
2164
  var MAX_TIMER_DELAY_MS = 2147483647;
@@ -2287,6 +2357,7 @@ function createAttachedSession(client, sandboxId, processId, io, terminal, contr
2287
2357
  let cleaned = false;
2288
2358
  let inputQueue = Promise.resolve();
2289
2359
  let resizeQueue = Promise.resolve();
2360
+ let restored;
2290
2361
  const restoreRawMode = terminal.enterRawMode();
2291
2362
  const cleanup = () => {
2292
2363
  if (cleaned) return;
@@ -2294,7 +2365,7 @@ function createAttachedSession(client, sandboxId, processId, io, terminal, contr
2294
2365
  io.input.off("data", onInput);
2295
2366
  terminal.offResize(onResize);
2296
2367
  stopLeaseKeeper();
2297
- restoreRawMode();
2368
+ restored = restoreRawMode();
2298
2369
  };
2299
2370
  const fail = (error) => {
2300
2371
  if (failure === void 0) failure = error;
@@ -2339,8 +2410,10 @@ function createAttachedSession(client, sandboxId, processId, io, terminal, contr
2339
2410
  get failure() {
2340
2411
  return failure;
2341
2412
  },
2413
+ // Everything the session started has finished, including handing the terminal back.
2342
2414
  async settle() {
2343
2415
  await Promise.allSettled([inputQueue, resizeQueue]);
2416
+ await restored;
2344
2417
  }
2345
2418
  };
2346
2419
  }
@@ -2468,6 +2541,7 @@ var DEFAULT_SIGNAL_RUNTIME = {
2468
2541
 
2469
2542
  // ../setsuna/src/tui/customer/format.ts
2470
2543
  var STANDALONE_BOX = { withGuide: false, width: "auto" };
2544
+ var EXIT_MESSAGE = "Sayonara!";
2471
2545
  function formatRunPhase(phase) {
2472
2546
  switch (phase) {
2473
2547
  case "starting-microvm":
@@ -2634,7 +2708,7 @@ async function ensureSignedIn(io, auth, promptUi, session = {}) {
2634
2708
  return false;
2635
2709
  }
2636
2710
  if (choice === "exit") {
2637
- promptUi.outro("Ready when you are.", io);
2711
+ promptUi.outro(EXIT_MESSAGE, io);
2638
2712
  return false;
2639
2713
  }
2640
2714
  status = await signIn(io, auth, promptUi);
@@ -2779,7 +2853,7 @@ async function promptRunAgain(io, promptUi) {
2779
2853
  );
2780
2854
  if (next === TUI_CANCEL || next === "back") return "back-to-main";
2781
2855
  if (next === "exit") {
2782
- promptUi.outro("Ready when you are.", io);
2856
+ promptUi.outro(EXIT_MESSAGE, io);
2783
2857
  return "exit-tui";
2784
2858
  }
2785
2859
  return "prompt-new-command";
@@ -2876,7 +2950,6 @@ async function activeMicrovmsMenu(io, actions, promptUi) {
2876
2950
  }
2877
2951
  }
2878
2952
  async function selectSandbox(io, promptUi, sandboxes) {
2879
- const now = Date.now();
2880
2953
  const selected = await promptUi.select(
2881
2954
  {
2882
2955
  message: "Active microVMs",
@@ -2884,7 +2957,7 @@ async function selectSandbox(io, promptUi, sandboxes) {
2884
2957
  ...sandboxes.map((sandbox, index) => ({
2885
2958
  value: `sandbox:${index}`,
2886
2959
  label: sandbox.sandboxId,
2887
- hint: formatExpiry(sandbox.expiresAt, now)
2960
+ hint: (now) => formatExpiry(sandbox.expiresAt, now)
2888
2961
  })),
2889
2962
  { value: "back", label: "Back" }
2890
2963
  ]
@@ -2897,10 +2970,10 @@ async function selectSandbox(io, promptUi, sandboxes) {
2897
2970
  async function manageSandbox(io, actions, promptUi, initial) {
2898
2971
  let microvm = initial;
2899
2972
  while (true) {
2900
- const expiry = formatExpiry(microvm.expiresAt, Date.now());
2973
+ const { sandboxId, expiresAt } = microvm;
2901
2974
  const choice = await promptUi.select(
2902
2975
  {
2903
- message: `Manage microVM ${microvm.sandboxId} \xB7 ${expiry}`,
2976
+ message: (now) => `Manage microVM ${sandboxId} \xB7 ${formatExpiry(expiresAt, now)}`,
2904
2977
  options: [
2905
2978
  { value: "open-terminal", label: "Open terminal" },
2906
2979
  { value: "reattach-terminal", label: "Reattach terminal" },
@@ -3049,10 +3122,10 @@ async function mainMenu(io, actions, promptUi, session) {
3049
3122
  {
3050
3123
  message: "What would you like to do?",
3051
3124
  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" },
3125
+ { value: "start", label: "New microVM", hint: "For interactive or ongoing work" },
3126
+ { value: "active", label: "Active microVMs", hint: "Resume or manage your microVMs" },
3127
+ { value: "run", label: "One-off run", hint: "Isolated single-run workload" },
3128
+ { value: "account", label: "Account" },
3056
3129
  { value: "exit", label: "Exit" }
3057
3130
  ],
3058
3131
  initialValue: "start",
@@ -3065,7 +3138,7 @@ async function mainMenu(io, actions, promptUi, session) {
3065
3138
  return "exit";
3066
3139
  }
3067
3140
  if (choice === "exit") {
3068
- promptUi.outro("Ready when you are.", io);
3141
+ promptUi.outro(EXIT_MESSAGE, io);
3069
3142
  return "exit";
3070
3143
  }
3071
3144
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "setsuna-microvm",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
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",