setsuna-microvm 0.5.0

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 (4) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +52 -0
  3. package/dist/cli.js +3753 -0
  4. package/package.json +34 -0
package/dist/cli.js ADDED
@@ -0,0 +1,3753 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // ../setsuna/src/tui/customer/prompt.ts
13
+ var TUI_CANCEL;
14
+ var init_prompt = __esm({
15
+ "../setsuna/src/tui/customer/prompt.ts"() {
16
+ "use strict";
17
+ TUI_CANCEL = /* @__PURE__ */ Symbol("setsuna:tui-cancel");
18
+ }
19
+ });
20
+
21
+ // ../setsuna/src/tui/customer/clack.ts
22
+ var clack_exports = {};
23
+ __export(clack_exports, {
24
+ loadClackPromptUi: () => loadClackPromptUi
25
+ });
26
+ async function loadClackPromptUi(presentation = {}) {
27
+ const clack = await import("@clack/prompts");
28
+ const guide = (options) => ({
29
+ withGuide: options?.withGuide ?? presentation.withGuide
30
+ });
31
+ return {
32
+ ...adaptMessages(clack, guide),
33
+ ...adaptPrompts(clack, guide),
34
+ box(message, title, io, options = {}) {
35
+ clack.box(message, title, { ...options, ...guide(options), output: io.output });
36
+ },
37
+ spinner: (io, options = {}) => adaptSpinner(clack, io, options, guide(options)),
38
+ progress: (options, io) => adaptProgress(clack, io, options, guide(options)),
39
+ taskLog: (options, io) => adaptTaskLog(clack, io, options)
40
+ };
41
+ }
42
+ function adaptMessages(clack, guide) {
43
+ const { cancel, intro, log, note, outro } = clack;
44
+ return {
45
+ intro(title, subtitle, io, presentation) {
46
+ intro(title, { output: io.output, ...guide(presentation) });
47
+ log.message(subtitle, { output: io.output, ...guide(presentation) });
48
+ },
49
+ note(message, title, io, presentation) {
50
+ note(message, title, { output: io.output, ...guide(presentation) });
51
+ },
52
+ outro(message, io, presentation) {
53
+ outro(message, { output: io.output, ...guide(presentation) });
54
+ },
55
+ cancel(message, io, presentation) {
56
+ cancel(message, { output: io.output, ...guide(presentation) });
57
+ },
58
+ info(message, io, presentation) {
59
+ log.info(message, { output: io.output, ...guide(presentation) });
60
+ },
61
+ success(message, io, presentation) {
62
+ log.success(message, { output: io.output, ...guide(presentation) });
63
+ },
64
+ warn(message, io, presentation) {
65
+ log.warn(message, { output: io.output, ...guide(presentation) });
66
+ },
67
+ error(message, io, presentation) {
68
+ log.error(message, { output: io.errorOutput, ...guide(presentation) });
69
+ },
70
+ phase(message, io, presentation) {
71
+ log.step(message, { output: io.output, ...guide(presentation) });
72
+ }
73
+ };
74
+ }
75
+ function adaptPrompts(clack, guide) {
76
+ const { confirm, isCancel, select, text } = clack;
77
+ return {
78
+ 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;
87
+ },
88
+ async text(options, io) {
89
+ const result = await text({
90
+ ...options,
91
+ validate: options.validate === void 0 ? void 0 : (value) => options.validate?.(value ?? ""),
92
+ ...guide(options),
93
+ input: io.input,
94
+ output: io.output
95
+ });
96
+ return isCancel(result) ? TUI_CANCEL : result;
97
+ },
98
+ async confirm(options, io) {
99
+ const result = await confirm({
100
+ ...options,
101
+ ...guide(options),
102
+ input: io.input,
103
+ output: io.output
104
+ });
105
+ return isCancel(result) ? TUI_CANCEL : result;
106
+ }
107
+ };
108
+ }
109
+ function adaptSpinner(clack, io, options, guide) {
110
+ const completion = trackInterruption(clack, io, options.onCancel, guide);
111
+ const spinner = clack.spinner({
112
+ output: io.output,
113
+ indicator: options.indicator,
114
+ onCancel: completion.interrupt,
115
+ ...guide
116
+ });
117
+ return {
118
+ start: (message) => spinner.start(message),
119
+ message: (message) => spinner.message(message),
120
+ stop: (message) => completion.stop(message, spinner.stop),
121
+ error: (message) => completion.error(message, spinner.error)
122
+ };
123
+ }
124
+ function adaptProgress(clack, io, options, guide) {
125
+ const completion = trackInterruption(clack, io, options.onCancel, guide);
126
+ const bar = clack.progress({
127
+ max: options.max,
128
+ size: options.size,
129
+ style: options.style,
130
+ indicator: options.indicator,
131
+ output: io.output,
132
+ onCancel: completion.interrupt,
133
+ ...guide
134
+ });
135
+ return {
136
+ start: (message) => bar.start(message),
137
+ advance: (step, message) => bar.advance(step, message),
138
+ message: (message) => bar.message(message),
139
+ stop: (message) => completion.stop(message, bar.stop),
140
+ cancel: (message) => completion.cancel(message, bar.cancel),
141
+ error: (message) => completion.error(message, bar.error),
142
+ clear: () => completion.clear(bar.clear),
143
+ // Clack 1.7.0 copies the spinner flag once while creating the bar, so it never turns true;
144
+ // the interruption tracked for the fallback logging is the same state.
145
+ get isCancelled() {
146
+ return completion.interrupted;
147
+ }
148
+ };
149
+ }
150
+ function adaptTaskLog(clack, io, options) {
151
+ return clack.taskLog({
152
+ title: options.title,
153
+ limit: options.limit,
154
+ retainLog: options.retainLog,
155
+ output: io.output
156
+ });
157
+ }
158
+ function trackInterruption(clack, io, onCancel, guide) {
159
+ let interrupted = false;
160
+ const fallback = (write, output) => (message, finish) => {
161
+ if (interrupted) write(message ?? "", { output, ...guide });
162
+ else finish(message);
163
+ };
164
+ return {
165
+ get interrupted() {
166
+ return interrupted;
167
+ },
168
+ interrupt() {
169
+ interrupted = true;
170
+ onCancel?.();
171
+ },
172
+ stop: fallback(clack.log.success, io.output),
173
+ cancel: fallback(
174
+ (message, options) => clack.log.message(message, { ...options, symbol: clack.S_STEP_CANCEL }),
175
+ io.output
176
+ ),
177
+ error: fallback(clack.log.error, io.errorOutput),
178
+ clear(clear) {
179
+ if (!interrupted) clear();
180
+ }
181
+ };
182
+ }
183
+ var init_clack = __esm({
184
+ "../setsuna/src/tui/customer/clack.ts"() {
185
+ "use strict";
186
+ init_prompt();
187
+ }
188
+ });
189
+
190
+ // ../setsuna/src/cli-preview.ts
191
+ import { realpathSync } from "fs";
192
+ import { pathToFileURL } from "url";
193
+
194
+ // ../setsuna/src/auth-flow.ts
195
+ import { setTimeout as delay } from "timers/promises";
196
+
197
+ // ../setsuna/src/auth-metadata.ts
198
+ import { randomUUID } from "crypto";
199
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
200
+ import path from "path";
201
+
202
+ // ../setsuna/src/auth-token.ts
203
+ import { createRemoteJWKSet, customFetch, jwtVerify } from "jose";
204
+ var SETSUNA_AUTH0_ISSUER = "https://login.setsuna.semswitch.com/";
205
+ var SETSUNA_AUTH0_AUDIENCE = "https://api.setsuna.semswitch.com";
206
+ var SETSUNA_AUTH0_CLIENT_ID = "SFMnPOMePRs61ixPkzvkHSRZxzCinw2C";
207
+ var SETSUNA_AUTH0_ORG_CLAIM = "https://setsuna.semswitch.com/org_id";
208
+ var Auth0AccessTokenValidator = class {
209
+ #fetch;
210
+ #providedKeyResolver;
211
+ #now;
212
+ #remoteKeyResolver;
213
+ constructor(options = {}) {
214
+ this.#fetch = options.fetch ?? globalThis.fetch;
215
+ this.#providedKeyResolver = options.keyResolver;
216
+ this.#now = options.now ?? (() => /* @__PURE__ */ new Date());
217
+ }
218
+ async validate(token) {
219
+ if (token.trim() === "" || /\s/u.test(token)) throw new Error("Invalid Auth0 access token.");
220
+ const currentDate = this.#now();
221
+ const keyResolver = this.#providedKeyResolver ?? await this.#getRemoteKeyResolver();
222
+ const { payload } = await jwtVerify(token, keyResolver, {
223
+ algorithms: ["RS256"],
224
+ issuer: SETSUNA_AUTH0_ISSUER,
225
+ audience: SETSUNA_AUTH0_AUDIENCE,
226
+ currentDate
227
+ });
228
+ const subject = requiredText(payload.sub);
229
+ const orgId = requiredText(payload[SETSUNA_AUTH0_ORG_CLAIM]);
230
+ const permissions = requiredPermissions(payload.permissions);
231
+ const issuedAt = requiredTimestamp(payload.iat);
232
+ const expiresAt = requiredTimestamp(payload.exp);
233
+ if (expiresAt <= issuedAt || issuedAt > Math.floor(currentDate.getTime() / 1e3) + 60) {
234
+ throw new Error("Invalid Auth0 access token timestamps.");
235
+ }
236
+ return { subject, orgId, permissions, issuedAt, expiresAt };
237
+ }
238
+ #getRemoteKeyResolver() {
239
+ if (this.#remoteKeyResolver === void 0) {
240
+ const pending = this.#loadRemoteKeyResolver();
241
+ this.#remoteKeyResolver = pending;
242
+ void pending.catch(() => {
243
+ if (this.#remoteKeyResolver === pending) this.#remoteKeyResolver = void 0;
244
+ });
245
+ }
246
+ return this.#remoteKeyResolver;
247
+ }
248
+ async #loadRemoteKeyResolver() {
249
+ const response = await this.#fetch(
250
+ new URL(".well-known/openid-configuration", SETSUNA_AUTH0_ISSUER),
251
+ { headers: { accept: "application/json" } }
252
+ );
253
+ if (!response.ok) throw new Error("Auth0 OIDC discovery failed.");
254
+ const document = await response.json();
255
+ if (document.issuer !== SETSUNA_AUTH0_ISSUER || typeof document.jwks_uri !== "string" || document.jwks_uri.trim() === "") {
256
+ throw new Error("Auth0 OIDC discovery response is invalid.");
257
+ }
258
+ const jwksUrl = new URL(document.jwks_uri);
259
+ if (jwksUrl.protocol !== "https:" || jwksUrl.origin !== new URL(SETSUNA_AUTH0_ISSUER).origin) {
260
+ throw new Error("Auth0 OIDC JWKS URL is invalid.");
261
+ }
262
+ return createRemoteJWKSet(jwksUrl, { [customFetch]: this.#fetch });
263
+ }
264
+ };
265
+ function requiredText(value) {
266
+ if (typeof value !== "string" || value.trim() === "") {
267
+ throw new Error("Auth0 access token is missing an identity claim.");
268
+ }
269
+ return value;
270
+ }
271
+ function requiredTimestamp(value) {
272
+ if (!Number.isSafeInteger(value) || value <= 0) {
273
+ throw new Error("Auth0 access token is missing a timestamp.");
274
+ }
275
+ return value;
276
+ }
277
+ function requiredPermissions(value) {
278
+ if (!Array.isArray(value) || value.length === 0 || value.some((permission) => typeof permission !== "string" || permission.trim() === "")) {
279
+ throw new Error("Auth0 access token is missing permissions.");
280
+ }
281
+ return value;
282
+ }
283
+
284
+ // ../setsuna/src/auth-metadata.ts
285
+ var FileAuthMetadataStore = class {
286
+ constructor(path3) {
287
+ this.path = path3;
288
+ }
289
+ path;
290
+ async load() {
291
+ let contents;
292
+ try {
293
+ contents = await readFile(this.path, "utf8");
294
+ } catch (error) {
295
+ if (isFileError(error, "ENOENT")) return null;
296
+ throw new Error("Setsuna credentials could not be read.", { cause: error });
297
+ }
298
+ let credentials2;
299
+ try {
300
+ credentials2 = storedCredentials(JSON.parse(contents));
301
+ } catch {
302
+ throw new Error("Setsuna credentials are invalid.");
303
+ }
304
+ assertConfiguredCredentials(credentials2);
305
+ return credentials2;
306
+ }
307
+ async save(credentials2) {
308
+ const metadata = metadataFields(credentials2);
309
+ const directory = path.dirname(this.path);
310
+ await mkdir(directory, { recursive: true, mode: 448 });
311
+ await restrictPermissions(directory, 448);
312
+ const temporaryPath = path.join(
313
+ directory,
314
+ `.${path.basename(this.path)}.${process.pid}.${randomUUID()}.tmp`
315
+ );
316
+ try {
317
+ await writeFile(temporaryPath, `${JSON.stringify(metadata)}
318
+ `, {
319
+ encoding: "utf8",
320
+ flag: "wx",
321
+ mode: 384
322
+ });
323
+ await restrictPermissions(temporaryPath, 384);
324
+ await rename(temporaryPath, this.path);
325
+ await restrictPermissions(this.path, 384);
326
+ } catch (error) {
327
+ throw new Error("Setsuna credentials could not be saved.", { cause: error });
328
+ } finally {
329
+ await rm(temporaryPath, { force: true });
330
+ }
331
+ }
332
+ async remove() {
333
+ try {
334
+ await rm(this.path, { force: true });
335
+ } catch (error) {
336
+ throw new Error("Setsuna credentials could not be removed.", { cause: error });
337
+ }
338
+ }
339
+ async saveLogin(metadata, persistRefreshToken) {
340
+ const temporaryPath = path.join(
341
+ path.dirname(this.path),
342
+ `.${path.basename(this.path)}.${process.pid}.${randomUUID()}.tmp`
343
+ );
344
+ try {
345
+ await stageLoginMetadata(temporaryPath, metadata);
346
+ await this.remove();
347
+ await persistRefreshToken();
348
+ try {
349
+ await rename(temporaryPath, this.path);
350
+ } catch (error) {
351
+ throw new Error("Setsuna credentials could not be saved.", { cause: error });
352
+ }
353
+ } catch (error) {
354
+ await rm(temporaryPath, { force: true }).catch(() => {
355
+ });
356
+ throw error;
357
+ }
358
+ }
359
+ };
360
+ async function stageLoginMetadata(temporaryPath, credentials2) {
361
+ const metadata = metadataFields(credentials2);
362
+ const directory = path.dirname(temporaryPath);
363
+ try {
364
+ await mkdir(directory, { recursive: true, mode: 448 });
365
+ await restrictPermissions(directory, 448);
366
+ await writeFile(temporaryPath, `${JSON.stringify(metadata)}
367
+ `, {
368
+ encoding: "utf8",
369
+ flag: "wx",
370
+ mode: 384
371
+ });
372
+ await restrictPermissions(temporaryPath, 384);
373
+ } catch (error) {
374
+ throw new Error("Setsuna credentials could not be saved.", { cause: error });
375
+ }
376
+ }
377
+ function metadataFields(record) {
378
+ return {
379
+ schemaVersion: 2,
380
+ issuer: requiredText2(record.issuer),
381
+ audience: requiredText2(record.audience),
382
+ clientId: requiredText2(record.clientId),
383
+ accessToken: requiredText2(record.accessToken),
384
+ accessTokenExpiresAt: requiredPositiveInteger(record.accessTokenExpiresAt)
385
+ };
386
+ }
387
+ function storedCredentials(value) {
388
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error();
389
+ const record = value;
390
+ const allowedKeys = /* @__PURE__ */ new Set([
391
+ "schemaVersion",
392
+ "issuer",
393
+ "audience",
394
+ "clientId",
395
+ "accessToken",
396
+ "accessTokenExpiresAt"
397
+ ]);
398
+ if (record.schemaVersion === 1) allowedKeys.add("refreshToken");
399
+ if (Object.keys(record).some((key) => !allowedKeys.has(key)) || record.schemaVersion !== 1 && record.schemaVersion !== 2)
400
+ throw new Error();
401
+ const metadata = metadataFields(record);
402
+ return record.schemaVersion === 1 ? { ...metadata, schemaVersion: 1, refreshToken: requiredText2(record.refreshToken) } : metadata;
403
+ }
404
+ function assertConfiguredCredentials(value) {
405
+ if (value.issuer !== SETSUNA_AUTH0_ISSUER || value.audience !== SETSUNA_AUTH0_AUDIENCE || value.clientId !== SETSUNA_AUTH0_CLIENT_ID) {
406
+ throw new Error("Stored Setsuna credentials belong to another authentication configuration.");
407
+ }
408
+ }
409
+ async function restrictPermissions(target, mode) {
410
+ try {
411
+ await chmod(target, mode);
412
+ } catch (error) {
413
+ if (process.platform !== "win32") throw error;
414
+ }
415
+ }
416
+ function isFileError(error, code) {
417
+ return error instanceof Error && "code" in error && error.code === code;
418
+ }
419
+ function requiredText2(value) {
420
+ if (typeof value !== "string" || value.trim() === "") throw new Error("invalid credential text");
421
+ return value;
422
+ }
423
+ function requiredPositiveInteger(value) {
424
+ if (!Number.isSafeInteger(value) || value <= 0)
425
+ throw new Error("invalid credential expiration");
426
+ return value;
427
+ }
428
+
429
+ // ../setsuna/src/auth-oauth.ts
430
+ var DEVICE_CODE_ENDPOINT = new URL("oauth/device/code", SETSUNA_AUTH0_ISSUER).toString();
431
+ var TOKEN_ENDPOINT = new URL("oauth/token", SETSUNA_AUTH0_ISSUER).toString();
432
+ var REVOCATION_ENDPOINT = new URL("oauth/revoke", SETSUNA_AUTH0_ISSUER).toString();
433
+ var DEFAULT_POLL_INTERVAL_SECONDS = 5;
434
+ var REFRESH_FAILURE_MESSAGES = {
435
+ permanent: "Your Setsuna session has expired. Run `setsuna auth login` again.",
436
+ transient: "Setsuna authentication is temporarily unavailable. Try again.",
437
+ malformed: "Auth0 returned an invalid refresh response. Try again."
438
+ };
439
+ var AuthRefreshError = class extends Error {
440
+ constructor(kind) {
441
+ super(REFRESH_FAILURE_MESSAGES[kind]);
442
+ this.kind = kind;
443
+ this.name = "AuthRefreshError";
444
+ }
445
+ kind;
446
+ };
447
+ async function requestDeviceAuthorization(fetch_) {
448
+ const response = await postForm(
449
+ DEVICE_CODE_ENDPOINT,
450
+ {
451
+ client_id: SETSUNA_AUTH0_CLIENT_ID,
452
+ audience: SETSUNA_AUTH0_AUDIENCE,
453
+ scope: "openid offline_access"
454
+ },
455
+ fetch_
456
+ );
457
+ if (!response.ok) throw new Error("Auth0 device authorization request failed.");
458
+ const value = await responseRecord(response, "Auth0 returned an invalid device authorization.");
459
+ const verificationUri = requiredHttpsUrl(value.verification_uri);
460
+ const verificationUriComplete = optionalHttpsUrl(value.verification_uri_complete);
461
+ return {
462
+ deviceCode: requiredText3(value.device_code),
463
+ userCode: requiredText3(value.user_code),
464
+ verificationUrl: verificationUriComplete ?? verificationUri,
465
+ expiresInSeconds: requiredPositiveInteger2(value.expires_in),
466
+ intervalSeconds: value.interval === void 0 ? DEFAULT_POLL_INTERVAL_SECONDS : requiredPositiveInteger2(value.interval)
467
+ };
468
+ }
469
+ async function pollDeviceToken(deviceCode, fetch_, signal) {
470
+ const response = await postForm(
471
+ TOKEN_ENDPOINT,
472
+ {
473
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
474
+ device_code: deviceCode,
475
+ client_id: SETSUNA_AUTH0_CLIENT_ID
476
+ },
477
+ fetch_,
478
+ signal
479
+ );
480
+ const value = await responseRecord(response, "Auth0 returned an invalid token response.");
481
+ if (response.ok) return { kind: "token", token: tokenResponse(value, true) };
482
+ if (value.error === "authorization_pending") return { kind: "pending" };
483
+ if (value.error === "slow_down") return { kind: "slow-down" };
484
+ if (value.error === "access_denied") return { kind: "denied" };
485
+ if (value.error === "expired_token") return { kind: "expired" };
486
+ throw new Error("Auth0 device authorization failed.");
487
+ }
488
+ async function refreshAccessToken(refreshToken, fetch_) {
489
+ let response;
490
+ try {
491
+ response = await postForm(
492
+ TOKEN_ENDPOINT,
493
+ {
494
+ grant_type: "refresh_token",
495
+ refresh_token: refreshToken,
496
+ client_id: SETSUNA_AUTH0_CLIENT_ID
497
+ },
498
+ fetch_
499
+ );
500
+ } catch {
501
+ throw new AuthRefreshError("transient");
502
+ }
503
+ if (response.status === 408 || response.status === 429 || response.status >= 500) {
504
+ throw new AuthRefreshError("transient");
505
+ }
506
+ const value = await refreshResponseRecord(response);
507
+ if (!response.ok) {
508
+ if (response.status >= 400 && value.error === "invalid_grant") {
509
+ throw new AuthRefreshError("permanent");
510
+ }
511
+ throw new AuthRefreshError(
512
+ typeof value.error === "string" && value.error !== "" ? "transient" : "malformed"
513
+ );
514
+ }
515
+ try {
516
+ const replacement = tokenResponse(value, false);
517
+ return {
518
+ accessToken: replacement.accessToken,
519
+ refreshToken: replacement.refreshToken || refreshToken
520
+ };
521
+ } catch {
522
+ throw new AuthRefreshError("malformed");
523
+ }
524
+ }
525
+ async function refreshResponseRecord(response) {
526
+ let value;
527
+ try {
528
+ value = await response.json();
529
+ } catch (error) {
530
+ throw new AuthRefreshError(error instanceof SyntaxError ? "malformed" : "transient");
531
+ }
532
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
533
+ throw new AuthRefreshError("malformed");
534
+ }
535
+ return value;
536
+ }
537
+ async function revokeRefreshToken(refreshToken, fetch_, signal) {
538
+ if (signal.aborted) return "unconfirmed";
539
+ let onAbort = () => {
540
+ };
541
+ const aborted = new Promise((resolve) => {
542
+ onAbort = () => resolve("unconfirmed");
543
+ signal.addEventListener("abort", onAbort, { once: true });
544
+ });
545
+ try {
546
+ return await Promise.race([sendRevocation(refreshToken, fetch_, signal), aborted]);
547
+ } finally {
548
+ signal.removeEventListener("abort", onAbort);
549
+ }
550
+ }
551
+ async function sendRevocation(token, fetch_, signal) {
552
+ try {
553
+ const response = await fetch_(REVOCATION_ENDPOINT, {
554
+ method: "POST",
555
+ headers: { "content-type": "application/x-www-form-urlencoded" },
556
+ body: new URLSearchParams({ token, client_id: SETSUNA_AUTH0_CLIENT_ID }).toString(),
557
+ signal,
558
+ redirect: "error"
559
+ });
560
+ void response.body?.cancel().catch(() => {
561
+ });
562
+ return response.status === 200 && !signal.aborted ? "confirmed" : "unconfirmed";
563
+ } catch {
564
+ return "unconfirmed";
565
+ }
566
+ }
567
+ function tokenResponse(value, requireRefreshToken) {
568
+ if (typeof value.token_type !== "string" || value.token_type.toLowerCase() !== "bearer") {
569
+ throw new Error("Auth0 did not return a Bearer access token.");
570
+ }
571
+ const refreshToken = value.refresh_token === void 0 && !requireRefreshToken ? "" : requiredText3(value.refresh_token);
572
+ return { accessToken: requiredText3(value.access_token), refreshToken };
573
+ }
574
+ async function postForm(url, fields, fetch_, signal) {
575
+ try {
576
+ return await fetch_(url, {
577
+ method: "POST",
578
+ headers: {
579
+ accept: "application/json",
580
+ "content-type": "application/x-www-form-urlencoded"
581
+ },
582
+ body: new URLSearchParams(fields).toString(),
583
+ signal
584
+ });
585
+ } catch (error) {
586
+ throw new Error("Auth0 request failed.", { cause: error });
587
+ }
588
+ }
589
+ async function responseRecord(response, message) {
590
+ try {
591
+ const value = await response.json();
592
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
593
+ return value;
594
+ }
595
+ } catch {
596
+ }
597
+ throw new Error(message);
598
+ }
599
+ function requiredText3(value) {
600
+ if (typeof value !== "string" || value.trim() === "") {
601
+ throw new Error("Auth0 response is missing a required value.");
602
+ }
603
+ return value;
604
+ }
605
+ function requiredPositiveInteger2(value) {
606
+ if (!Number.isSafeInteger(value) || value <= 0) {
607
+ throw new Error("Auth0 response contains an invalid duration.");
608
+ }
609
+ return value;
610
+ }
611
+ function requiredHttpsUrl(value) {
612
+ const text = requiredText3(value);
613
+ const url = new URL(text);
614
+ if (url.protocol !== "https:") throw new Error("Auth0 returned an invalid verification URL.");
615
+ return url.toString();
616
+ }
617
+ function optionalHttpsUrl(value) {
618
+ return value === void 0 ? null : requiredHttpsUrl(value);
619
+ }
620
+
621
+ // ../setsuna/src/auth-store.ts
622
+ import { randomUUID as randomUUID2 } from "crypto";
623
+ import { link, mkdir as mkdir2, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
624
+ import { homedir } from "os";
625
+ import path2 from "path";
626
+
627
+ // ../setsuna/src/auth-keyring.ts
628
+ var AUTH_KEYRING_SERVICE = "com.semswitch.setsuna";
629
+ var AUTH_KEYRING_ACCOUNT = "refresh-token";
630
+ var SECURE_STORAGE_UNAVAILABLE = "Secure credential storage is unavailable on this system.";
631
+ var NativeRefreshTokenStore = class {
632
+ async load() {
633
+ try {
634
+ const { keyring } = await import("@zowe/secrets-for-zowe-sdk");
635
+ return await keyring.getPassword(AUTH_KEYRING_SERVICE, AUTH_KEYRING_ACCOUNT);
636
+ } catch {
637
+ throw new Error(SECURE_STORAGE_UNAVAILABLE);
638
+ }
639
+ }
640
+ async save(refreshToken) {
641
+ try {
642
+ const { keyring } = await import("@zowe/secrets-for-zowe-sdk");
643
+ await keyring.setPassword(AUTH_KEYRING_SERVICE, AUTH_KEYRING_ACCOUNT, refreshToken, 2);
644
+ } catch {
645
+ throw new Error(SECURE_STORAGE_UNAVAILABLE);
646
+ }
647
+ }
648
+ async remove() {
649
+ try {
650
+ const { keyring } = await import("@zowe/secrets-for-zowe-sdk");
651
+ await keyring.deletePassword(AUTH_KEYRING_SERVICE, AUTH_KEYRING_ACCOUNT);
652
+ } catch {
653
+ throw new Error(SECURE_STORAGE_UNAVAILABLE);
654
+ }
655
+ }
656
+ };
657
+
658
+ // ../setsuna/src/auth-store.ts
659
+ var LOCK_ACQUISITION_TIMEOUT_MS = 1e4;
660
+ var LOCK_RETRY_MS = 50;
661
+ var NativeAuthCredentialStore = class {
662
+ path;
663
+ #metadata;
664
+ #refreshTokens;
665
+ constructor(userHome = homedir(), refreshTokens = new NativeRefreshTokenStore()) {
666
+ this.path = authCredentialPath(userHome);
667
+ this.#metadata = new FileAuthMetadataStore(this.path);
668
+ this.#refreshTokens = refreshTokens;
669
+ }
670
+ async load() {
671
+ const stored = await this.#metadata.load();
672
+ if (stored === null) return null;
673
+ if (stored.schemaVersion === 1) {
674
+ return this.withRefreshLock(async () => {
675
+ const current = await this.#metadata.load();
676
+ if (current === null) return null;
677
+ if (current.schemaVersion === 1) {
678
+ const migrated = { ...current, schemaVersion: 2 };
679
+ await this.save(migrated);
680
+ return migrated;
681
+ }
682
+ return this.#withRefreshToken(current);
683
+ });
684
+ }
685
+ return this.#withRefreshToken(stored);
686
+ }
687
+ async #withRefreshToken(metadata) {
688
+ const refreshToken = await this.#refreshTokens.load();
689
+ const current = await this.#metadata.load();
690
+ if (current?.accessToken !== metadata.accessToken) return this.load();
691
+ if (refreshToken === null || refreshToken.trim() === "") {
692
+ throw new Error("Run `setsuna auth login` first.");
693
+ }
694
+ return { ...metadata, refreshToken };
695
+ }
696
+ /** Logout holds the refresh lock already and must not migrate credentials just to delete them. */
697
+ async loadRefreshTokenForLogout() {
698
+ const stored = await this.#metadata.load();
699
+ if (stored === null) return null;
700
+ const token = stored.schemaVersion === 1 ? stored.refreshToken : await this.#refreshTokens.load();
701
+ return token === null || token.trim() === "" ? null : token;
702
+ }
703
+ async save(credentials2) {
704
+ assertConfiguredCredentials(credentials2);
705
+ await this.#saveRefreshToken(credentials2.refreshToken);
706
+ await this.#metadata.save(credentials2);
707
+ }
708
+ /** The caller holds the refresh lock, just as for rotation and migration. */
709
+ async saveLogin(credentials2) {
710
+ assertConfiguredCredentials(credentials2);
711
+ await this.#metadata.saveLogin(
712
+ credentials2,
713
+ () => this.#saveRefreshToken(credentials2.refreshToken)
714
+ );
715
+ }
716
+ async #saveRefreshToken(refreshToken) {
717
+ await this.#refreshTokens.save(refreshToken);
718
+ if (await this.#refreshTokens.load() !== refreshToken) {
719
+ throw new Error(SECURE_STORAGE_UNAVAILABLE);
720
+ }
721
+ }
722
+ async remove() {
723
+ await this.#refreshTokens.remove();
724
+ await this.#metadata.remove();
725
+ }
726
+ /** Called under the refresh lock when the installed refresh credential is no longer safe. */
727
+ async invalidate() {
728
+ try {
729
+ await this.#refreshTokens.remove();
730
+ } finally {
731
+ await this.#metadata.remove();
732
+ }
733
+ }
734
+ async withRefreshLock(operation) {
735
+ const release = await acquireRefreshLock(this.path);
736
+ try {
737
+ return await operation();
738
+ } finally {
739
+ await release();
740
+ }
741
+ }
742
+ };
743
+ function authCredentialPath(userHome = homedir()) {
744
+ return path2.join(userHome, ".setsuna", "auth.json");
745
+ }
746
+ async function acquireRefreshLock(credentialPath) {
747
+ const lockPath = `${credentialPath}.lock`;
748
+ const directory = path2.dirname(lockPath);
749
+ await mkdir2(directory, { recursive: true, mode: 448 });
750
+ await restrictPermissions(directory, 448);
751
+ const owner = { pid: process.pid, createdAt: Date.now(), nonce: randomUUID2() };
752
+ const deadline = Date.now() + LOCK_ACQUISITION_TIMEOUT_MS;
753
+ while (true) {
754
+ if (await tryClaimLock(lockPath, owner)) {
755
+ return async () => releaseRefreshLock(lockPath, owner.nonce);
756
+ }
757
+ const existing = await readLockOwner(lockPath);
758
+ if (existing !== null && !processIsAlive(existing.pid)) {
759
+ await removeLockOwnedBy(lockPath, existing.nonce);
760
+ continue;
761
+ }
762
+ if (Date.now() >= deadline) {
763
+ throw new Error("Setsuna authentication is busy; try again.");
764
+ }
765
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
766
+ }
767
+ }
768
+ async function tryClaimLock(lockPath, owner) {
769
+ const candidatePath = `${lockPath}.${owner.nonce}.tmp`;
770
+ try {
771
+ await writeFile2(candidatePath, `${JSON.stringify(owner)}
772
+ `, {
773
+ encoding: "utf8",
774
+ flag: "wx",
775
+ mode: 384
776
+ });
777
+ await restrictPermissions(candidatePath, 384);
778
+ try {
779
+ await link(candidatePath, lockPath);
780
+ await restrictPermissions(lockPath, 384);
781
+ return true;
782
+ } catch (error) {
783
+ if (isFileError(error, "EEXIST")) return false;
784
+ throw error;
785
+ }
786
+ } finally {
787
+ await rm2(candidatePath, { force: true });
788
+ }
789
+ }
790
+ async function readLockOwner(lockPath) {
791
+ try {
792
+ const value = JSON.parse(await readFile2(lockPath, "utf8"));
793
+ if (Number.isSafeInteger(value.pid) && value.pid > 0 && Number.isSafeInteger(value.createdAt) && value.createdAt > 0 && typeof value.nonce === "string" && value.nonce !== "") {
794
+ return value;
795
+ }
796
+ } catch (error) {
797
+ if (isFileError(error, "ENOENT")) return null;
798
+ }
799
+ return null;
800
+ }
801
+ function processIsAlive(pid) {
802
+ try {
803
+ process.kill(pid, 0);
804
+ return true;
805
+ } catch (error) {
806
+ return !isFileError(error, "ESRCH");
807
+ }
808
+ }
809
+ async function removeLockOwnedBy(lockPath, nonce) {
810
+ const owner = await readLockOwner(lockPath);
811
+ if (owner?.nonce === nonce) await rm2(lockPath, { force: true });
812
+ }
813
+ async function releaseRefreshLock(lockPath, nonce) {
814
+ try {
815
+ await removeLockOwnedBy(lockPath, nonce);
816
+ } catch (error) {
817
+ throw new Error("Setsuna authentication lock could not be released.", { cause: error });
818
+ }
819
+ }
820
+
821
+ // ../setsuna/src/auth-flow.ts
822
+ var SLOW_DOWN_INCREMENT_SECONDS = 5;
823
+ var ACCESS_TOKEN_SAFETY_MARGIN_SECONDS = 60;
824
+ var LOGOUT_REVOCATION_TIMEOUT_MS = 5e3;
825
+ function defaultAuthRuntimeDependencies() {
826
+ const fetch_ = globalThis.fetch;
827
+ return {
828
+ fetch: fetch_,
829
+ store: new NativeAuthCredentialStore(),
830
+ validator: new Auth0AccessTokenValidator({ fetch: fetch_ }),
831
+ now: Date.now,
832
+ sleep: (milliseconds, signal) => delay(milliseconds, void 0, { signal }),
833
+ abortAfter: (milliseconds) => AbortSignal.timeout(milliseconds)
834
+ };
835
+ }
836
+ async function loginWithDeviceFlow(dependencies, prompt, signal) {
837
+ const authorization = await requestDeviceAuthorization(dependencies.fetch);
838
+ prompt({
839
+ verificationUrl: authorization.verificationUrl,
840
+ userCode: authorization.userCode
841
+ });
842
+ const deadline = dependencies.now() + authorization.expiresInSeconds * 1e3;
843
+ let intervalSeconds = authorization.intervalSeconds;
844
+ while (true) {
845
+ if (dependencies.now() + intervalSeconds * 1e3 >= deadline) {
846
+ throw new Error("Auth0 device authorization expired.");
847
+ }
848
+ await sleepUnlessCanceled(dependencies, intervalSeconds * 1e3, signal);
849
+ const result = await pollBeforeDeadline(
850
+ authorization.deviceCode,
851
+ deadline,
852
+ dependencies,
853
+ signal
854
+ );
855
+ if (result.kind === "pending") continue;
856
+ if (result.kind === "slow-down") {
857
+ intervalSeconds += SLOW_DOWN_INCREMENT_SECONDS;
858
+ continue;
859
+ }
860
+ if (result.kind === "denied") throw new Error("Auth0 device authorization was denied.");
861
+ if (result.kind === "expired") throw new Error("Auth0 device authorization expired.");
862
+ if (signal?.aborted) throw canceledDeviceAuthorization();
863
+ const validated = await dependencies.validator.validate(result.token.accessToken);
864
+ if (signal?.aborted) throw canceledDeviceAuthorization();
865
+ await dependencies.store.withRefreshLock(
866
+ () => dependencies.store.saveLogin(credentials(result.token, validated))
867
+ );
868
+ return { authenticated: true, expiresAt: validated.expiresAt };
869
+ }
870
+ }
871
+ async function inspectAuthStatus(dependencies) {
872
+ const stored = await dependencies.store.load();
873
+ if (stored === null) return { authenticated: false, expiresAt: null };
874
+ assertConfiguredCredentials(stored);
875
+ if (stored.accessTokenExpiresAt <= Math.floor(dependencies.now() / 1e3)) {
876
+ return { authenticated: false, expiresAt: stored.accessTokenExpiresAt };
877
+ }
878
+ try {
879
+ const validated = await dependencies.validator.validate(stored.accessToken);
880
+ if (validated.expiresAt !== stored.accessTokenExpiresAt) {
881
+ throw new Error("Stored access-token expiration does not match the token.");
882
+ }
883
+ } catch {
884
+ return { authenticated: false, expiresAt: stored.accessTokenExpiresAt };
885
+ }
886
+ return { authenticated: true, expiresAt: stored.accessTokenExpiresAt };
887
+ }
888
+ async function getValidAuthAccessToken(dependencies) {
889
+ const stored = await dependencies.store.load();
890
+ if (stored === null) throw new Error("Run `setsuna auth login` first.");
891
+ assertConfiguredCredentials(stored);
892
+ const refreshAt = Math.floor(dependencies.now() / 1e3) + ACCESS_TOKEN_SAFETY_MARGIN_SECONDS;
893
+ if (stored.accessTokenExpiresAt > refreshAt) {
894
+ const validated = await dependencies.validator.validate(stored.accessToken);
895
+ if (validated.expiresAt !== stored.accessTokenExpiresAt) {
896
+ throw new Error("Stored access-token expiration does not match the token.");
897
+ }
898
+ return stored.accessToken;
899
+ }
900
+ return dependencies.store.withRefreshLock(async () => {
901
+ const authoritative = await dependencies.store.load();
902
+ if (authoritative === null) throw new Error("Run `setsuna auth login` first.");
903
+ assertConfiguredCredentials(authoritative);
904
+ const lockedRefreshAt = Math.floor(dependencies.now() / 1e3) + ACCESS_TOKEN_SAFETY_MARGIN_SECONDS;
905
+ if (authoritative.accessTokenExpiresAt > lockedRefreshAt) {
906
+ const validated = await dependencies.validator.validate(authoritative.accessToken);
907
+ if (validated.expiresAt !== authoritative.accessTokenExpiresAt) {
908
+ throw new Error("Stored access-token expiration does not match the token.");
909
+ }
910
+ return authoritative.accessToken;
911
+ }
912
+ return refreshStoredSession(authoritative.refreshToken, dependencies);
913
+ });
914
+ }
915
+ async function logoutAuth(dependencies) {
916
+ return dependencies.store.withRefreshLock(async () => {
917
+ let remoteRevocation = "unconfirmed";
918
+ try {
919
+ const token = await dependencies.store.loadRefreshTokenForLogout();
920
+ remoteRevocation = token === null ? "not-needed" : await revokeRefreshToken(
921
+ token,
922
+ dependencies.fetch,
923
+ dependencies.abortAfter(LOGOUT_REVOCATION_TIMEOUT_MS)
924
+ );
925
+ } catch {
926
+ } finally {
927
+ await invalidateSession(dependencies.store);
928
+ }
929
+ return { authenticated: false, remoteRevocation };
930
+ });
931
+ }
932
+ async function refreshStoredSession(refreshToken, dependencies) {
933
+ let replacement;
934
+ try {
935
+ replacement = await refreshAccessToken(refreshToken, dependencies.fetch);
936
+ } catch (error) {
937
+ if (error instanceof AuthRefreshError && error.kind === "permanent") {
938
+ await invalidateSession(dependencies.store);
939
+ }
940
+ throw error;
941
+ }
942
+ try {
943
+ const validated = await dependencies.validator.validate(replacement.accessToken);
944
+ await dependencies.store.save(credentials(replacement, validated));
945
+ return replacement.accessToken;
946
+ } catch {
947
+ if (replacement.refreshToken !== refreshToken) {
948
+ await invalidateSession(dependencies.store);
949
+ throw new Error("Your Setsuna session could not be renewed. Run `setsuna auth login` again.");
950
+ }
951
+ throw new Error("Setsuna authentication could not be refreshed. Try again.");
952
+ }
953
+ }
954
+ async function invalidateSession(store) {
955
+ try {
956
+ await store.invalidate();
957
+ } catch {
958
+ throw new Error(
959
+ "Setsuna could not clear the session. Run `setsuna auth logout`, then `setsuna auth login`."
960
+ );
961
+ }
962
+ }
963
+ async function sleepUnlessCanceled(dependencies, milliseconds, signal) {
964
+ try {
965
+ await (signal === void 0 ? dependencies.sleep(milliseconds) : dependencies.sleep(milliseconds, signal));
966
+ } catch (error) {
967
+ if (signal?.aborted) throw canceledDeviceAuthorization();
968
+ throw error;
969
+ }
970
+ if (signal?.aborted) throw canceledDeviceAuthorization();
971
+ }
972
+ async function pollBeforeDeadline(deviceCode, deadline, dependencies, cancelSignal) {
973
+ const remainingMilliseconds = deadline - dependencies.now();
974
+ if (remainingMilliseconds <= 0) throw new Error("Auth0 device authorization expired.");
975
+ const deadlineSignal = dependencies.abortAfter(remainingMilliseconds);
976
+ const signal = cancelSignal === void 0 ? deadlineSignal : AbortSignal.any([deadlineSignal, cancelSignal]);
977
+ let result;
978
+ try {
979
+ result = await pollDeviceToken(deviceCode, dependencies.fetch, signal);
980
+ } catch (error) {
981
+ if (cancelSignal?.aborted) throw canceledDeviceAuthorization();
982
+ if (deadlineSignal.aborted) throw new Error("Auth0 device authorization expired.");
983
+ throw error;
984
+ }
985
+ if (cancelSignal?.aborted) throw canceledDeviceAuthorization();
986
+ return result;
987
+ }
988
+ function canceledDeviceAuthorization() {
989
+ return new Error("Auth0 device authorization was canceled.");
990
+ }
991
+ function credentials(token, validated) {
992
+ return {
993
+ schemaVersion: 2,
994
+ issuer: SETSUNA_AUTH0_ISSUER,
995
+ audience: SETSUNA_AUTH0_AUDIENCE,
996
+ clientId: SETSUNA_AUTH0_CLIENT_ID,
997
+ accessToken: token.accessToken,
998
+ accessTokenExpiresAt: validated.expiresAt,
999
+ refreshToken: token.refreshToken
1000
+ };
1001
+ }
1002
+
1003
+ // ../setsuna/src/cli-auth.ts
1004
+ async function runAuthCommand(arguments_, parsed, io, providedDependencies) {
1005
+ assertAuthOptions(parsed);
1006
+ if (arguments_.length !== 1) {
1007
+ throw new TypeError("setsuna auth requires exactly one of: login, status, logout.");
1008
+ }
1009
+ const dependencies = providedDependencies ?? defaultAuthRuntimeDependencies();
1010
+ const json = parsed.values.json;
1011
+ switch (arguments_[0]) {
1012
+ case "login": {
1013
+ const result = await loginWithDeviceFlow(dependencies, ({ verificationUrl, userCode }) => {
1014
+ if (json) {
1015
+ io.errorOutput.write(`${JSON.stringify({ verificationUrl, userCode })}
1016
+ `);
1017
+ } else {
1018
+ io.output.write(`Verification URL: ${verificationUrl}
1019
+ User code: ${userCode}
1020
+ `);
1021
+ }
1022
+ });
1023
+ writeState(io, json, result.authenticated, result.expiresAt);
1024
+ return;
1025
+ }
1026
+ case "status": {
1027
+ const status = await inspectAuthStatus(dependencies);
1028
+ writeState(io, json, status.authenticated, status.expiresAt);
1029
+ return;
1030
+ }
1031
+ case "logout": {
1032
+ const result = await logoutAuth(dependencies);
1033
+ if (json) io.output.write(`${JSON.stringify(result)}
1034
+ `);
1035
+ else {
1036
+ io.output.write("Logged out.\n");
1037
+ if (result.remoteRevocation === "unconfirmed") {
1038
+ io.errorOutput.write(
1039
+ "Warning: Local session cleared; remote revocation could not be confirmed.\n"
1040
+ );
1041
+ }
1042
+ }
1043
+ return;
1044
+ }
1045
+ default:
1046
+ throw new TypeError(`Unknown auth command: ${arguments_[0]}`);
1047
+ }
1048
+ }
1049
+ function writeState(io, json, authenticated, expiresAt) {
1050
+ const expiration = expiresAt === null ? null : new Date(expiresAt * 1e3).toISOString();
1051
+ if (json) {
1052
+ io.output.write(`${JSON.stringify({ authenticated, expiresAt: expiration })}
1053
+ `);
1054
+ return;
1055
+ }
1056
+ if (authenticated) {
1057
+ io.output.write(`Authenticated. Access token expires at ${expiration}.
1058
+ `);
1059
+ } else if (expiration !== null) {
1060
+ io.output.write(`Not authenticated. Access token expired at ${expiration}.
1061
+ `);
1062
+ } else {
1063
+ io.output.write("Not authenticated.\n");
1064
+ }
1065
+ }
1066
+ function assertAuthOptions(parsed) {
1067
+ const unsupported = Object.entries(parsed.values).find(
1068
+ ([name, value]) => name !== "json" && optionIsPresent(value)
1069
+ );
1070
+ if (unsupported !== void 0) {
1071
+ throw new TypeError(`--${unsupported[0]} is not supported by setsuna auth.`);
1072
+ }
1073
+ }
1074
+ function optionIsPresent(value) {
1075
+ if (Array.isArray(value)) return value.length > 0;
1076
+ if (typeof value === "boolean") return value;
1077
+ return value !== void 0;
1078
+ }
1079
+
1080
+ // ../setsuna/src/cli-parsing.ts
1081
+ import { readFile as readFile3 } from "fs/promises";
1082
+ import { parseArgs } from "util";
1083
+ function parsePreviewCliArgs(args) {
1084
+ return parseArgs({
1085
+ args: [...args],
1086
+ allowPositionals: true,
1087
+ strict: true,
1088
+ options: {
1089
+ "timeout-ms": { type: "string" },
1090
+ "request-timeout-ms": { type: "string" },
1091
+ "configuration-id": { type: "string" },
1092
+ attach: { type: "string" },
1093
+ json: { type: "boolean", default: false },
1094
+ help: { type: "boolean", short: "h", default: false },
1095
+ version: { type: "boolean", short: "v", default: false }
1096
+ }
1097
+ });
1098
+ }
1099
+ async function packageVersion() {
1100
+ const metadata = JSON.parse(
1101
+ await readFile3(new URL("../package.json", import.meta.url), "utf8")
1102
+ );
1103
+ if (typeof metadata !== "object" || metadata === null || !("version" in metadata) || typeof metadata.version !== "string" || metadata.version === "") {
1104
+ throw new Error("Installed package metadata does not contain a version.");
1105
+ }
1106
+ return metadata.version;
1107
+ }
1108
+ function optionalInteger(value, name) {
1109
+ if (value === void 0) return void 0;
1110
+ if (!/^[1-9][0-9]*$/u.test(value)) throw new TypeError(`${name} must be a positive integer.`);
1111
+ const parsed = Number(value);
1112
+ if (!Number.isSafeInteger(parsed)) throw new TypeError(`${name} is too large.`);
1113
+ return parsed;
1114
+ }
1115
+ function shellQuote(value) {
1116
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/u.test(value)) return value;
1117
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
1118
+ }
1119
+
1120
+ // ../setsuna/src/errors.ts
1121
+ var SetsunaError = class extends Error {
1122
+ code;
1123
+ status;
1124
+ constructor(message, options) {
1125
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
1126
+ this.name = "SetsunaError";
1127
+ this.code = options.code;
1128
+ this.status = options.status;
1129
+ }
1130
+ };
1131
+
1132
+ // ../setsuna/src/broker-response.ts
1133
+ async function parseResponseJson(response) {
1134
+ try {
1135
+ return await response.json();
1136
+ } catch (error) {
1137
+ throw new SetsunaError("The Setsuna service returned an invalid JSON response.", {
1138
+ code: "INVALID_RESPONSE",
1139
+ status: response.status,
1140
+ cause: error
1141
+ });
1142
+ }
1143
+ }
1144
+ function brokerResponseError(response, value) {
1145
+ const body = isRecord(value) ? value : void 0;
1146
+ const code = typeof body?.error?.code === "string" ? body.error.code : "SERVICE_ERROR";
1147
+ const message = typeof body?.error?.message === "string" ? body.error.message : `The Setsuna service request failed with HTTP ${response.status}.`;
1148
+ return new SetsunaError(message, { code, status: response.status });
1149
+ }
1150
+ function parseHealth(value) {
1151
+ if (!isRecord(value) || value.ok !== true || typeof value.activeSandbox !== "boolean" || value.startupPhase !== "idle" && value.startupPhase !== "launching" && value.startupPhase !== "ready") {
1152
+ throw invalidResponse("health");
1153
+ }
1154
+ return {
1155
+ ok: true,
1156
+ activeSandbox: value.activeSandbox,
1157
+ startupPhase: value.startupPhase
1158
+ };
1159
+ }
1160
+ function parseMicrovmConfigurationCatalog(value) {
1161
+ if (!isRecord(value) || !hasOnlyKeys(value, [
1162
+ "schemaVersion",
1163
+ "maxConcurrentMicrovms",
1164
+ "defaultConfigurationId",
1165
+ "configurations"
1166
+ ]) || value.schemaVersion !== 1 || !positiveResponseInteger(value.maxConcurrentMicrovms) || typeof value.defaultConfigurationId !== "string" || value.defaultConfigurationId === "" || !Array.isArray(value.configurations) || value.configurations.length === 0) {
1167
+ throw invalidResponse("microVM configuration catalog");
1168
+ }
1169
+ const configurations = value.configurations.map(parseMicrovmConfiguration);
1170
+ const ids = new Set(configurations.map(({ id }) => id));
1171
+ if (ids.size !== configurations.length || !ids.has(value.defaultConfigurationId)) {
1172
+ throw invalidResponse("microVM configuration catalog");
1173
+ }
1174
+ return {
1175
+ schemaVersion: 1,
1176
+ maxConcurrentMicrovms: value.maxConcurrentMicrovms,
1177
+ defaultConfigurationId: value.defaultConfigurationId,
1178
+ configurations
1179
+ };
1180
+ }
1181
+ function parseMicrovmConfiguration(value) {
1182
+ if (!isRecord(value) || !hasOnlyKeys(value, ["id", "vcpuCount", "memoryMiB"]) || !nonEmptyResponseString(value.id) || !positiveResponseInteger(value.vcpuCount) || !positiveResponseInteger(value.memoryMiB)) {
1183
+ throw invalidResponse("microVM configuration");
1184
+ }
1185
+ return {
1186
+ id: value.id,
1187
+ vcpuCount: value.vcpuCount,
1188
+ memoryMiB: value.memoryMiB
1189
+ };
1190
+ }
1191
+ function nonEmptyResponseString(value) {
1192
+ return typeof value === "string" && value.trim() !== "";
1193
+ }
1194
+ function positiveResponseInteger(value) {
1195
+ return Number.isSafeInteger(value) && value > 0;
1196
+ }
1197
+ function hasOnlyKeys(value, allowed) {
1198
+ const keys = new Set(allowed);
1199
+ return Object.keys(value).every((key) => keys.has(key));
1200
+ }
1201
+ function parseLease(value, expectedConfigurationId) {
1202
+ const sandboxId = parseLeaseSandboxId(value);
1203
+ if (!isRecord(value) || typeof value.expiresAt !== "string") {
1204
+ throw invalidResponse("sandbox lease");
1205
+ }
1206
+ const microvmConfiguration = parseMicrovmConfiguration(value.microvmConfiguration);
1207
+ if (expectedConfigurationId !== void 0 && microvmConfiguration.id !== expectedConfigurationId) {
1208
+ throw invalidResponse("sandbox lease");
1209
+ }
1210
+ return { sandboxId, expiresAt: value.expiresAt, microvmConfiguration };
1211
+ }
1212
+ function parseLeaseSandboxId(value) {
1213
+ if (!isRecord(value) || !nonEmptyResponseString(value.sandboxId))
1214
+ throw invalidResponse("sandbox lease");
1215
+ return value.sandboxId;
1216
+ }
1217
+ function parseSandboxStatus(value) {
1218
+ if (!isRecord(value) || typeof value.sandboxId !== "string" || typeof value.expiresAt !== "string" || value.status !== "active") {
1219
+ throw invalidResponse("sandbox status");
1220
+ }
1221
+ return { sandboxId: value.sandboxId, expiresAt: value.expiresAt, status: "active" };
1222
+ }
1223
+ function parseSandboxList(value) {
1224
+ if (!Array.isArray(value)) throw invalidResponse("sandbox list");
1225
+ return value.map(parseSandboxStatus);
1226
+ }
1227
+ function parseExecutionResult(value) {
1228
+ if (!isRecord(value) || value.exitCode !== null && typeof value.exitCode !== "number" || typeof value.stdout !== "string" || typeof value.stderr !== "string" || typeof value.outputTruncated !== "boolean" || typeof value.omittedOutputBytes !== "number" || value.signal !== null && typeof value.signal !== "string" || typeof value.timedOut !== "boolean" || typeof value.durationMs !== "number") {
1229
+ throw invalidResponse("execution result");
1230
+ }
1231
+ return {
1232
+ exitCode: value.exitCode,
1233
+ stdout: value.stdout,
1234
+ stderr: value.stderr,
1235
+ outputTruncated: value.outputTruncated,
1236
+ omittedOutputBytes: value.omittedOutputBytes,
1237
+ signal: value.signal,
1238
+ timedOut: value.timedOut,
1239
+ durationMs: value.durationMs
1240
+ };
1241
+ }
1242
+ function invalidResponse(subject) {
1243
+ return new SetsunaError(`The Setsuna service returned an invalid ${subject}.`, {
1244
+ code: "INVALID_RESPONSE"
1245
+ });
1246
+ }
1247
+ function isRecord(value) {
1248
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1249
+ }
1250
+
1251
+ // ../setsuna/src/client-options.ts
1252
+ var PREVIEW_SERVICE_URL = "https://api.setsuna.semswitch.com/azure-f8amds-v7/";
1253
+ function normalizeServiceUrl(value) {
1254
+ let url;
1255
+ try {
1256
+ url = new URL(value);
1257
+ } catch (error) {
1258
+ throw new SetsunaError("serviceUrl must be a valid loopback HTTP URL.", {
1259
+ code: "INVALID_SERVICE_URL",
1260
+ cause: error
1261
+ });
1262
+ }
1263
+ if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname) || url.username !== "" || url.password !== "") {
1264
+ throw new SetsunaError("serviceUrl must be an unauthenticated loopback HTTP URL.", {
1265
+ code: "INVALID_SERVICE_URL"
1266
+ });
1267
+ }
1268
+ url.pathname = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
1269
+ url.search = "";
1270
+ url.hash = "";
1271
+ return url;
1272
+ }
1273
+ function positiveMilliseconds(value, name) {
1274
+ if (!Number.isSafeInteger(value) || value <= 0) {
1275
+ throw new TypeError(`${name} must be a positive integer.`);
1276
+ }
1277
+ return value;
1278
+ }
1279
+ function nonEmptyString(value, name) {
1280
+ if (value.trim() === "") throw new TypeError(`${name} must not be empty.`);
1281
+ return value;
1282
+ }
1283
+ function normalizeCreateOptions(options) {
1284
+ if (typeof options !== "object" || options === null || Array.isArray(options)) {
1285
+ throw new TypeError("create options must be an object.");
1286
+ }
1287
+ const unsupported = Object.keys(options).find(
1288
+ (key) => key !== "microvmConfigurationId" && key !== "network"
1289
+ );
1290
+ if (unsupported !== void 0) throw new TypeError(`Unsupported create option: ${unsupported}.`);
1291
+ const microvmConfigurationId = options.microvmConfigurationId === void 0 ? void 0 : nonEmptyString(options.microvmConfigurationId, "microvmConfigurationId");
1292
+ const network = options.network === void 0 ? void 0 : validateRoutedNetwork(options.network);
1293
+ return {
1294
+ ...microvmConfigurationId === void 0 ? {} : { microvmConfigurationId },
1295
+ ...network === void 0 ? {} : { network }
1296
+ };
1297
+ }
1298
+ function validateRoutedNetwork(value) {
1299
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1300
+ throw new TypeError("network must be an object.");
1301
+ }
1302
+ const record = value;
1303
+ const supported = /* @__PURE__ */ new Set([
1304
+ "mode",
1305
+ "hostIp",
1306
+ "guestIp",
1307
+ "prefixLength",
1308
+ "uplinkInterface",
1309
+ "dnsResolvers",
1310
+ "mtu"
1311
+ ]);
1312
+ const unsupported = Object.keys(record).find((key) => !supported.has(key));
1313
+ if (unsupported !== void 0) throw new TypeError(`Unsupported network option: ${unsupported}.`);
1314
+ if (record.mode !== "routed") throw new TypeError("network.mode must be routed.");
1315
+ if (record.prefixLength !== 30) throw new TypeError("network.prefixLength must be 30.");
1316
+ const hostIp = ipv4(record.hostIp, "network.hostIp");
1317
+ const guestIp = ipv4(record.guestIp, "network.guestIp");
1318
+ const uplinkInterface = interfaceName(record.uplinkInterface);
1319
+ const dnsResolvers = dns(record.dnsResolvers);
1320
+ const mtu = optionalMtu(record.mtu);
1321
+ return {
1322
+ mode: "routed",
1323
+ hostIp,
1324
+ guestIp,
1325
+ prefixLength: 30,
1326
+ uplinkInterface,
1327
+ ...dnsResolvers === void 0 ? {} : { dnsResolvers },
1328
+ ...mtu === void 0 ? {} : { mtu }
1329
+ };
1330
+ }
1331
+ function ipv4(value, name) {
1332
+ if (typeof value !== "string") throw new TypeError(`${name} must be an IPv4 address.`);
1333
+ const parts = value.split(".");
1334
+ if (parts.length !== 4 || parts.some((part) => !/^(?:0|[1-9][0-9]{0,2})$/u.test(part) || Number(part) > 255)) {
1335
+ throw new TypeError(`${name} must be an IPv4 address.`);
1336
+ }
1337
+ return value;
1338
+ }
1339
+ function interfaceName(value) {
1340
+ if (typeof value !== "string" || !/^[A-Za-z0-9_.:-]{1,64}$/u.test(value)) {
1341
+ throw new TypeError("network.uplinkInterface is invalid.");
1342
+ }
1343
+ return value;
1344
+ }
1345
+ function dns(value) {
1346
+ if (value === void 0) return void 0;
1347
+ if (!Array.isArray(value)) throw new TypeError("network.dnsResolvers must be an array.");
1348
+ return value.map((entry) => ipv4(entry, "network.dnsResolvers"));
1349
+ }
1350
+ function optionalMtu(value) {
1351
+ if (value === void 0) return void 0;
1352
+ if (!Number.isInteger(value) || value < 576 || value > 9e3) {
1353
+ throw new TypeError("network.mtu must be an integer between 576 and 9000.");
1354
+ }
1355
+ return value;
1356
+ }
1357
+ function isLoopbackHostname(hostname) {
1358
+ return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "[::1]";
1359
+ }
1360
+
1361
+ // ../setsuna/src/client-request.ts
1362
+ async function requestJson(serviceUrl, defaultTimeoutMs, getAccessToken, path3, options) {
1363
+ const requestTimeoutMs = options.requestTimeoutMs === void 0 ? defaultTimeoutMs : positiveMilliseconds(options.requestTimeoutMs, "requestTimeoutMs");
1364
+ const timeoutSignal = AbortSignal.timeout(requestTimeoutMs);
1365
+ const signal = options.signal === void 0 ? timeoutSignal : AbortSignal.any([options.signal, timeoutSignal]);
1366
+ const accessToken = await requestAccessToken(
1367
+ getAccessToken,
1368
+ requestTimeoutMs,
1369
+ timeoutSignal,
1370
+ signal,
1371
+ options.signal
1372
+ );
1373
+ const response = await fetchService(
1374
+ new URL(path3, serviceUrl),
1375
+ options,
1376
+ requestTimeoutMs,
1377
+ timeoutSignal,
1378
+ signal,
1379
+ accessToken
1380
+ );
1381
+ const responseBody = response.status === 204 ? void 0 : await parseResponseJson(response);
1382
+ if (!response.ok) throw brokerResponseError(response, responseBody);
1383
+ return responseBody;
1384
+ }
1385
+ async function requestAccessToken(getAccessToken, requestTimeoutMs, timeoutSignal, signal, callerSignal) {
1386
+ if (getAccessToken === void 0) return void 0;
1387
+ let token;
1388
+ try {
1389
+ if (signal.aborted) throw signal.reason;
1390
+ token = await settleBeforeAbort(getAccessToken(), signal);
1391
+ } catch (error) {
1392
+ throwCancellationFailure(error, requestTimeoutMs, timeoutSignal, callerSignal);
1393
+ throw error;
1394
+ }
1395
+ if (typeof token !== "string" || token.trim() === "") {
1396
+ throw new TypeError("remote.getAccessToken must return a non-empty token.");
1397
+ }
1398
+ return token;
1399
+ }
1400
+ function settleBeforeAbort(pending, signal) {
1401
+ return new Promise((resolve, reject) => {
1402
+ const onAbort = () => {
1403
+ signal.removeEventListener("abort", onAbort);
1404
+ reject(signal.reason);
1405
+ };
1406
+ signal.addEventListener("abort", onAbort, { once: true });
1407
+ pending.then(
1408
+ (value) => {
1409
+ signal.removeEventListener("abort", onAbort);
1410
+ resolve(value);
1411
+ },
1412
+ (error) => {
1413
+ signal.removeEventListener("abort", onAbort);
1414
+ reject(error);
1415
+ }
1416
+ );
1417
+ if (signal.aborted) onAbort();
1418
+ });
1419
+ }
1420
+ async function fetchService(url, options, requestTimeoutMs, timeoutSignal, signal, accessToken) {
1421
+ try {
1422
+ return await fetch(url, {
1423
+ method: options.method,
1424
+ headers: requestHeaders(options.body, accessToken),
1425
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
1426
+ signal
1427
+ });
1428
+ } catch (error) {
1429
+ throwCancellationFailure(error, requestTimeoutMs, timeoutSignal, options.signal);
1430
+ throw new SetsunaError("The local Setsuna service could not be reached.", {
1431
+ code: "SERVICE_UNREACHABLE",
1432
+ cause: error
1433
+ });
1434
+ }
1435
+ }
1436
+ function throwCancellationFailure(error, requestTimeoutMs, timeoutSignal, callerSignal) {
1437
+ if (timeoutSignal.aborted && !callerSignal?.aborted) {
1438
+ throw new SetsunaError(`The Setsuna service request exceeded ${requestTimeoutMs} ms.`, {
1439
+ code: "REQUEST_TIMEOUT",
1440
+ cause: error
1441
+ });
1442
+ }
1443
+ if (callerSignal?.aborted) {
1444
+ throw new SetsunaError("The Setsuna service request was aborted.", {
1445
+ code: "REQUEST_ABORTED",
1446
+ cause: error
1447
+ });
1448
+ }
1449
+ }
1450
+ function requestHeaders(body, accessToken) {
1451
+ if (body === void 0 && accessToken === void 0) return void 0;
1452
+ return {
1453
+ ...body === void 0 ? {} : { "content-type": "application/json" },
1454
+ ...accessToken === void 0 ? {} : { authorization: `Bearer ${accessToken}` }
1455
+ };
1456
+ }
1457
+
1458
+ // ../setsuna/src/process-request.ts
1459
+ function processPath(sandboxId) {
1460
+ return `v1/sandboxes/${encodeURIComponent(nonEmpty(sandboxId, "sandboxId"))}/processes`;
1461
+ }
1462
+ function processResourcePath(sandboxId, processId) {
1463
+ return `${processPath(sandboxId)}/${encodeURIComponent(nonEmpty(processId, "processId"))}`;
1464
+ }
1465
+ function appendPositiveInteger(parameters, name, value) {
1466
+ const validated = optionalPositiveInteger(value, name);
1467
+ if (validated !== void 0) parameters.set(name, String(validated));
1468
+ }
1469
+ function optionalPositiveInteger(value, name) {
1470
+ if (value === void 0) return void 0;
1471
+ if (!Number.isSafeInteger(value) || value <= 0) {
1472
+ throw new TypeError(`${name} must be a positive integer.`);
1473
+ }
1474
+ return value;
1475
+ }
1476
+ function validateProcessSpec(spec) {
1477
+ if (typeof spec !== "object" || spec === null || Array.isArray(spec)) {
1478
+ throw new TypeError("spec must be an object.");
1479
+ }
1480
+ const unsupported = Object.keys(spec).find(
1481
+ (key) => key !== "argv" && key !== "cwd" && key !== "env" && key !== "spoolMaxBytes" && key !== "terminal"
1482
+ );
1483
+ if (unsupported !== void 0)
1484
+ throw new TypeError(`Unsupported process spec field: ${unsupported}.`);
1485
+ if (!Array.isArray(spec.argv) || spec.argv.length === 0 || spec.argv.some((value) => typeof value !== "string" || value.length === 0)) {
1486
+ throw new TypeError("argv must be a non-empty array of non-empty strings.");
1487
+ }
1488
+ if (spec.cwd !== void 0 && typeof spec.cwd !== "string") {
1489
+ throw new TypeError("cwd must be a string.");
1490
+ }
1491
+ if (spec.env !== void 0 && (typeof spec.env !== "object" || spec.env === null || Array.isArray(spec.env) || Object.values(spec.env).some((value) => typeof value !== "string"))) {
1492
+ throw new TypeError("env values must be strings.");
1493
+ }
1494
+ optionalPositiveInteger(spec.spoolMaxBytes, "spoolMaxBytes");
1495
+ validateTerminal(spec.terminal);
1496
+ }
1497
+ function processResizeBody(size) {
1498
+ terminalDimension(size.cols, "cols");
1499
+ terminalDimension(size.rows, "rows");
1500
+ return { cols: size.cols, rows: size.rows };
1501
+ }
1502
+ function processStdinBody(data) {
1503
+ if (typeof data === "string") return { data };
1504
+ if (data instanceof Uint8Array) return { dataBase64: Buffer.from(data).toString("base64") };
1505
+ throw new TypeError("data must be a string or Uint8Array.");
1506
+ }
1507
+ function validateTerminal(terminal) {
1508
+ if (terminal === void 0) return;
1509
+ if (typeof terminal !== "object" || terminal === null || Array.isArray(terminal)) {
1510
+ throw new TypeError("terminal must be an object.");
1511
+ }
1512
+ const unsupported = Object.keys(terminal).find(
1513
+ (key) => key !== "cols" && key !== "rows" && key !== "term"
1514
+ );
1515
+ if (unsupported !== void 0) throw new TypeError(`Unsupported terminal field: ${unsupported}.`);
1516
+ terminalDimension(terminal.cols, "terminal.cols");
1517
+ terminalDimension(terminal.rows, "terminal.rows");
1518
+ if (terminal.term !== void 0 && (typeof terminal.term !== "string" || terminal.term.length === 0 || terminal.term.length > 128 || terminal.term.includes("\0"))) {
1519
+ throw new TypeError("terminal.term must be 1-128 characters without NUL bytes.");
1520
+ }
1521
+ }
1522
+ function terminalDimension(value, name) {
1523
+ const dimension = optionalPositiveInteger(value, name);
1524
+ if (dimension === void 0 || dimension > 65535) {
1525
+ throw new TypeError(`${name} must be an integer from 1 through 65535.`);
1526
+ }
1527
+ return dimension;
1528
+ }
1529
+ function nonEmpty(value, name) {
1530
+ if (value.trim() === "") throw new TypeError(`${name} must not be empty.`);
1531
+ return value;
1532
+ }
1533
+
1534
+ // ../setsuna/src/process-response.ts
1535
+ function parseProcessHandle(value) {
1536
+ const record = processRecord(value, "process handle");
1537
+ if (!nonEmptyString2(record.processId) || !positiveInteger(record.pid) || !validDate(record.startedAt)) {
1538
+ throw invalidResponse2("process handle");
1539
+ }
1540
+ return { processId: record.processId, pid: record.pid, startedAt: record.startedAt };
1541
+ }
1542
+ function parseProcessInfo(value) {
1543
+ const handle = parseProcessHandle(value);
1544
+ const record = value;
1545
+ if (record.state !== "running" && record.state !== "exited" || !nonnegativeInteger(record.spooledBytes) || !optionalDate(record.exitedAt) || !optionalNullableInteger(record.exitCode) || !optionalNullableString(record.signal)) {
1546
+ throw invalidResponse2("process information");
1547
+ }
1548
+ return {
1549
+ ...handle,
1550
+ state: record.state,
1551
+ ...record.exitedAt === void 0 ? {} : { exitedAt: record.exitedAt },
1552
+ ...record.exitCode === void 0 ? {} : { exitCode: record.exitCode },
1553
+ ...record.signal === void 0 ? {} : { signal: record.signal },
1554
+ spooledBytes: record.spooledBytes,
1555
+ ...record.terminal === void 0 ? {} : { terminal: parseTerminal(record.terminal) }
1556
+ };
1557
+ }
1558
+ function parseProcessList(value) {
1559
+ if (!Array.isArray(value)) throw invalidResponse2("process list");
1560
+ return value.map(parseProcessInfo);
1561
+ }
1562
+ function attachProcessStream(serviceUrl, path3, options, getAccessToken) {
1563
+ return {
1564
+ async *[Symbol.asyncIterator]() {
1565
+ const response = await fetchProcessStream(
1566
+ new URL(path3, serviceUrl),
1567
+ options.signal,
1568
+ getAccessToken
1569
+ );
1570
+ if (response.body === null) throw invalidResponse2("process stream");
1571
+ yield* decodeFrames(response.body, options.signal);
1572
+ }
1573
+ };
1574
+ }
1575
+ function processFrameToJson(frame) {
1576
+ if (frame.kind === "replay") {
1577
+ return {
1578
+ kind: frame.kind,
1579
+ stream: frame.stream,
1580
+ encoding: "base64",
1581
+ data: frame.data.toString("base64")
1582
+ };
1583
+ }
1584
+ if (frame.kind === "stdout" || frame.kind === "stderr" || frame.kind === "terminal") {
1585
+ return { kind: frame.kind, encoding: "base64", data: frame.data.toString("base64") };
1586
+ }
1587
+ return frame;
1588
+ }
1589
+ async function fetchProcessStream(url, signal, getAccessToken) {
1590
+ const accessToken = await processAccessToken(getAccessToken, signal);
1591
+ let response;
1592
+ try {
1593
+ response = await fetch(url, {
1594
+ method: "GET",
1595
+ signal,
1596
+ ...accessToken === void 0 ? {} : { headers: { authorization: `Bearer ${accessToken}` } }
1597
+ });
1598
+ } catch (error) {
1599
+ throw requestError(error, signal);
1600
+ }
1601
+ if (!response.ok) throw brokerResponseError(response, await parseResponseJson(response));
1602
+ const contentType = response.headers.get("content-type")?.split(";", 1)[0]?.trim();
1603
+ if (contentType !== "application/x-ndjson") throw invalidResponse2("process stream");
1604
+ return response;
1605
+ }
1606
+ async function processAccessToken(getAccessToken, signal) {
1607
+ if (getAccessToken === void 0) return void 0;
1608
+ let token;
1609
+ try {
1610
+ if (signal?.aborted) throw signal.reason;
1611
+ const pending = getAccessToken();
1612
+ token = signal === void 0 ? await pending : await settleBeforeAbort2(pending, signal);
1613
+ } catch (error) {
1614
+ if (signal?.aborted) throw requestError(error, signal);
1615
+ throw error;
1616
+ }
1617
+ if (typeof token !== "string" || token.trim() === "") {
1618
+ throw new TypeError("remote.getAccessToken must return a non-empty token.");
1619
+ }
1620
+ return token;
1621
+ }
1622
+ function settleBeforeAbort2(pending, signal) {
1623
+ return new Promise((resolve, reject) => {
1624
+ const onAbort = () => {
1625
+ signal.removeEventListener("abort", onAbort);
1626
+ reject(signal.reason);
1627
+ };
1628
+ signal.addEventListener("abort", onAbort, { once: true });
1629
+ pending.then(
1630
+ (value) => {
1631
+ signal.removeEventListener("abort", onAbort);
1632
+ resolve(value);
1633
+ },
1634
+ (error) => {
1635
+ signal.removeEventListener("abort", onAbort);
1636
+ reject(error);
1637
+ }
1638
+ );
1639
+ if (signal.aborted) onAbort();
1640
+ });
1641
+ }
1642
+ async function* decodeFrames(body, signal) {
1643
+ const reader = body.getReader();
1644
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1645
+ let pending = "";
1646
+ let completed = false;
1647
+ try {
1648
+ while (true) {
1649
+ const { done, value } = await reader.read();
1650
+ pending += decoder.decode(value, { stream: !done });
1651
+ const lines = pending.split("\n");
1652
+ pending = lines.pop() ?? "";
1653
+ for (const line of lines) {
1654
+ if (line !== "") yield parseWireFrame(line);
1655
+ }
1656
+ if (done) {
1657
+ completed = true;
1658
+ break;
1659
+ }
1660
+ }
1661
+ if (pending !== "") yield parseWireFrame(pending);
1662
+ } catch (error) {
1663
+ if (error instanceof SetsunaError) throw error;
1664
+ throw requestError(error, signal);
1665
+ } finally {
1666
+ if (!completed) await reader.cancel().catch(() => void 0);
1667
+ reader.releaseLock();
1668
+ }
1669
+ }
1670
+ function parseWireFrame(line) {
1671
+ let value;
1672
+ try {
1673
+ value = JSON.parse(line);
1674
+ } catch (error) {
1675
+ throw new SetsunaError("The Setsuna service returned an invalid process stream frame.", {
1676
+ code: "INVALID_RESPONSE",
1677
+ cause: error
1678
+ });
1679
+ }
1680
+ const record = processRecord(value, "process stream frame");
1681
+ if (record.kind === "heartbeat") return { kind: "heartbeat" };
1682
+ if (record.kind === "exit" && nullableInteger(record.exitCode) && nullableString(record.signal)) {
1683
+ return { kind: "exit", exitCode: record.exitCode, signal: record.signal };
1684
+ }
1685
+ if (record.kind !== "replay" && record.kind !== "stdout" && record.kind !== "stderr" && record.kind !== "terminal") {
1686
+ throw invalidResponse2("process stream frame");
1687
+ }
1688
+ if (record.encoding !== "base64" || typeof record.data !== "string") {
1689
+ throw invalidResponse2("process stream frame");
1690
+ }
1691
+ const data = decodeBase64(record.data);
1692
+ if (record.kind === "replay") {
1693
+ if (record.stream !== "stdout" && record.stream !== "stderr" && record.stream !== "terminal")
1694
+ throw invalidResponse2("process stream frame");
1695
+ return { kind: "replay", stream: record.stream, data };
1696
+ }
1697
+ return { kind: record.kind, data };
1698
+ }
1699
+ function parseTerminal(value) {
1700
+ const terminal = processRecord(value, "terminal process information");
1701
+ if (!positiveInteger(terminal.cols) || terminal.cols > 65535 || !positiveInteger(terminal.rows) || terminal.rows > 65535 || !nonEmptyString2(terminal.term) || terminal.term.length > 128 || terminal.term.includes("\0")) {
1702
+ throw invalidResponse2("terminal process information");
1703
+ }
1704
+ return { cols: terminal.cols, rows: terminal.rows, term: terminal.term };
1705
+ }
1706
+ function decodeBase64(value) {
1707
+ const data = Buffer.from(value, "base64");
1708
+ if (data.toString("base64") !== value) throw invalidResponse2("process stream frame");
1709
+ return data;
1710
+ }
1711
+ function requestError(error, signal) {
1712
+ if (signal?.aborted) {
1713
+ return new SetsunaError("The process attachment was detached.", {
1714
+ code: "REQUEST_ABORTED",
1715
+ cause: error
1716
+ });
1717
+ }
1718
+ return new SetsunaError("The process attachment stream was interrupted.", {
1719
+ code: "SERVICE_UNREACHABLE",
1720
+ cause: error
1721
+ });
1722
+ }
1723
+ function processRecord(value, subject) {
1724
+ if (typeof value !== "object" || value === null || Array.isArray(value))
1725
+ throw invalidResponse2(subject);
1726
+ return value;
1727
+ }
1728
+ function nonEmptyString2(value) {
1729
+ return typeof value === "string" && value !== "";
1730
+ }
1731
+ function positiveInteger(value) {
1732
+ return Number.isSafeInteger(value) && value > 0;
1733
+ }
1734
+ function nonnegativeInteger(value) {
1735
+ return Number.isSafeInteger(value) && value >= 0;
1736
+ }
1737
+ function validDate(value) {
1738
+ return typeof value === "string" && !Number.isNaN(Date.parse(value));
1739
+ }
1740
+ function optionalDate(value) {
1741
+ return value === void 0 || validDate(value);
1742
+ }
1743
+ function nullableInteger(value) {
1744
+ return value === null || Number.isSafeInteger(value);
1745
+ }
1746
+ function optionalNullableInteger(value) {
1747
+ return value === void 0 || nullableInteger(value);
1748
+ }
1749
+ function nullableString(value) {
1750
+ return value === null || typeof value === "string";
1751
+ }
1752
+ function optionalNullableString(value) {
1753
+ return value === void 0 || nullableString(value);
1754
+ }
1755
+ function invalidResponse2(subject) {
1756
+ return new SetsunaError(`The Setsuna service returned an invalid ${subject}.`, {
1757
+ code: "INVALID_RESPONSE"
1758
+ });
1759
+ }
1760
+
1761
+ // ../setsuna/src/run-support.ts
1762
+ function reportRunPhase(callback, phase) {
1763
+ try {
1764
+ callback?.(phase);
1765
+ } catch {
1766
+ }
1767
+ }
1768
+
1769
+ // ../setsuna/src/client.ts
1770
+ var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
1771
+ var DEFAULT_CLEANUP_TIMEOUT_MS = 1e4;
1772
+ var SetsunaClient = class {
1773
+ #serviceUrl;
1774
+ #requestTimeoutMs;
1775
+ #getAccessToken;
1776
+ constructor(options = {}) {
1777
+ if (options.serviceUrl !== void 0 && options.remote !== void 0) {
1778
+ throw new TypeError("serviceUrl and remote are mutually exclusive.");
1779
+ }
1780
+ if (options.remote === void 0) {
1781
+ this.#serviceUrl = normalizeServiceUrl(options.serviceUrl ?? "http://127.0.0.1:8787");
1782
+ this.#getAccessToken = void 0;
1783
+ } else {
1784
+ if (typeof options.remote.getAccessToken !== "function") {
1785
+ throw new TypeError("remote.getAccessToken must be a function.");
1786
+ }
1787
+ this.#serviceUrl = new URL(PREVIEW_SERVICE_URL);
1788
+ this.#getAccessToken = options.remote.getAccessToken;
1789
+ }
1790
+ this.#requestTimeoutMs = positiveMilliseconds(
1791
+ options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
1792
+ "requestTimeoutMs"
1793
+ );
1794
+ }
1795
+ async health() {
1796
+ return parseHealth(await this.#request("health", { method: "GET" }));
1797
+ }
1798
+ async microvmConfigurations() {
1799
+ return parseMicrovmConfigurationCatalog(
1800
+ await this.#request("v1/microvm-configurations", { method: "GET" })
1801
+ );
1802
+ }
1803
+ async create(options = {}) {
1804
+ const createOptions = normalizeCreateOptions(options);
1805
+ const response = await this.#request("v1/sandboxes", {
1806
+ method: "POST",
1807
+ body: { ...createOptions }
1808
+ });
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
+ }
1823
+ }
1824
+ async list() {
1825
+ return parseSandboxList(await this.#request("v1/sandboxes", { method: "GET" }));
1826
+ }
1827
+ async status(sandboxId) {
1828
+ const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1829
+ return parseSandboxStatus(
1830
+ await this.#request(`v1/sandboxes/${encodeURIComponent(validatedSandboxId)}`, {
1831
+ method: "GET"
1832
+ })
1833
+ );
1834
+ }
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
+ async execute(sandboxId, command, options = {}) {
1844
+ const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1845
+ const validatedCommand = nonEmptyString(command, "command");
1846
+ const timeoutMs = options.timeoutMs === void 0 ? void 0 : positiveMilliseconds(options.timeoutMs, "timeoutMs");
1847
+ return parseExecutionResult(
1848
+ await this.#request(`v1/sandboxes/${encodeURIComponent(validatedSandboxId)}/execute`, {
1849
+ method: "POST",
1850
+ body: {
1851
+ command: validatedCommand,
1852
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
1853
+ },
1854
+ signal: options.signal,
1855
+ requestTimeoutMs: options.requestTimeoutMs
1856
+ })
1857
+ );
1858
+ }
1859
+ async destroy(sandboxId) {
1860
+ await this.#destroy(sandboxId, this.#requestTimeoutMs);
1861
+ }
1862
+ async spawnProcess(sandboxId, spec) {
1863
+ const path3 = processPath(sandboxId);
1864
+ validateProcessSpec(spec);
1865
+ return parseProcessHandle(await this.#request(path3, { method: "POST", body: { ...spec } }));
1866
+ }
1867
+ async listProcesses(sandboxId) {
1868
+ return parseProcessList(await this.#request(processPath(sandboxId), { method: "GET" }));
1869
+ }
1870
+ attachProcess(sandboxId, processId, options = {}) {
1871
+ const parameters = new URLSearchParams();
1872
+ appendPositiveInteger(parameters, "maxStreamBytes", options.maxStreamBytes);
1873
+ appendPositiveInteger(parameters, "idleTimeoutMs", options.idleTimeoutMs);
1874
+ const query = parameters.size === 0 ? "" : `?${parameters.toString()}`;
1875
+ return attachProcessStream(
1876
+ this.#serviceUrl,
1877
+ `${processPath(sandboxId)}/${encodeURIComponent(nonEmptyString(processId, "processId"))}/attach${query}`,
1878
+ options,
1879
+ this.#getAccessToken
1880
+ );
1881
+ }
1882
+ async writeProcessStdin(sandboxId, processId, data) {
1883
+ await this.#request(`${processResourcePath(sandboxId, processId)}/stdin`, {
1884
+ method: "POST",
1885
+ body: processStdinBody(data)
1886
+ });
1887
+ }
1888
+ async resizeProcess(sandboxId, processId, size) {
1889
+ await this.#request(`${processResourcePath(sandboxId, processId)}/resize`, {
1890
+ method: "POST",
1891
+ body: processResizeBody(size)
1892
+ });
1893
+ }
1894
+ async terminateProcess(sandboxId, processId, options = {}) {
1895
+ const graceMs = optionalPositiveInteger(options.graceMs, "graceMs");
1896
+ const requestTimeoutMs = optionalPositiveInteger(options.requestTimeoutMs, "requestTimeoutMs");
1897
+ return parseProcessInfo(
1898
+ await this.#request(`${processResourcePath(sandboxId, processId)}/terminate`, {
1899
+ method: "POST",
1900
+ body: {
1901
+ ...graceMs === void 0 ? {} : { graceMs },
1902
+ ...requestTimeoutMs === void 0 ? {} : { requestTimeoutMs }
1903
+ },
1904
+ requestTimeoutMs
1905
+ })
1906
+ );
1907
+ }
1908
+ async #destroy(sandboxId, requestTimeoutMs) {
1909
+ const validatedSandboxId = nonEmptyString(sandboxId, "sandboxId");
1910
+ await this.#request(`v1/sandboxes/${encodeURIComponent(validatedSandboxId)}`, {
1911
+ method: "DELETE",
1912
+ requestTimeoutMs
1913
+ });
1914
+ }
1915
+ async run(command, options = {}) {
1916
+ let sandboxId;
1917
+ const cleanupTimeoutMs = positiveMilliseconds(
1918
+ options.cleanupTimeoutMs ?? DEFAULT_CLEANUP_TIMEOUT_MS,
1919
+ "cleanupTimeoutMs"
1920
+ );
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;
1954
+ }
1955
+ async #request(path3, options) {
1956
+ return requestJson(
1957
+ this.#serviceUrl,
1958
+ this.#requestTimeoutMs,
1959
+ this.#getAccessToken,
1960
+ path3,
1961
+ options
1962
+ );
1963
+ }
1964
+ };
1965
+
1966
+ // ../setsuna/src/cli-execution.ts
1967
+ import { spawn } from "child_process";
1968
+ async function runHealth(client, output, json) {
1969
+ const health = await client.health();
1970
+ if (json) output.write(`${JSON.stringify(health)}
1971
+ `);
1972
+ else output.write(`Setsuna service is healthy (${health.startupPhase}).
1973
+ `);
1974
+ }
1975
+ async function runWithSignalCleanup(client, command, options, io, signalRuntime) {
1976
+ const controller = new AbortController();
1977
+ let interruptedExitCode;
1978
+ const handleSignal = (signal) => {
1979
+ const exitCode = signal === "SIGINT" ? 130 : 143;
1980
+ if (interruptedExitCode === void 0) {
1981
+ interruptedExitCode = exitCode;
1982
+ io.errorOutput.write("Interrupt received; destroying sandbox...\n");
1983
+ controller.abort();
1984
+ return;
1985
+ }
1986
+ signalRuntime.forceExit(exitCode);
1987
+ };
1988
+ const handleSigint = () => handleSignal("SIGINT");
1989
+ const handleSigterm = () => handleSignal("SIGTERM");
1990
+ signalRuntime.on("SIGINT", handleSigint);
1991
+ signalRuntime.on("SIGTERM", handleSigterm);
1992
+ try {
1993
+ const result = await client.run(command, { ...options, signal: controller.signal });
1994
+ if (interruptedExitCode !== void 0) {
1995
+ process.exitCode = interruptedExitCode;
1996
+ return void 0;
1997
+ }
1998
+ return result;
1999
+ } catch (error) {
2000
+ if (interruptedExitCode === void 0) throw error;
2001
+ process.exitCode = interruptedExitCode;
2002
+ if (!(error instanceof SetsunaError && error.code === "REQUEST_ABORTED")) {
2003
+ io.errorOutput.write(`${formatCliError(error)}
2004
+ `);
2005
+ }
2006
+ return void 0;
2007
+ } finally {
2008
+ signalRuntime.off("SIGINT", handleSigint);
2009
+ signalRuntime.off("SIGTERM", handleSigterm);
2010
+ }
2011
+ }
2012
+ function writeExecutionResult(result, json, output = process.stdout, errorOutput = process.stderr, updateProcessExitCode = true) {
2013
+ if (json) {
2014
+ output.write(`${JSON.stringify(result)}
2015
+ `);
2016
+ } else {
2017
+ output.write(result.stdout);
2018
+ errorOutput.write(result.stderr);
2019
+ }
2020
+ if (updateProcessExitCode) {
2021
+ if (result.timedOut) process.exitCode = 124;
2022
+ else if (result.exitCode !== null) process.exitCode = result.exitCode;
2023
+ else if (result.signal !== null) process.exitCode = 1;
2024
+ }
2025
+ }
2026
+ function guestExitStatus(result) {
2027
+ if (result.timedOut) return "124 (timed out)";
2028
+ if (result.exitCode !== null) return String(result.exitCode);
2029
+ if (result.signal !== null) return `signal ${result.signal}`;
2030
+ return "unknown";
2031
+ }
2032
+ function formatCliError(error) {
2033
+ if (error instanceof AggregateError) {
2034
+ const details = error.errors.map(
2035
+ (nested) => formatCliError(nested).replace(/^setsuna: /u, " ")
2036
+ );
2037
+ return [`setsuna: ${error.message}`, ...details].join("\n");
2038
+ }
2039
+ if (error instanceof SetsunaError) return `setsuna: ${error.code}: ${error.message}`;
2040
+ if (error instanceof Error) return `setsuna: ${error.message}`;
2041
+ return "setsuna: An unknown error occurred.";
2042
+ }
2043
+
2044
+ // ../setsuna/src/cli-shell.ts
2045
+ import { once } from "events";
2046
+
2047
+ // ../setsuna/src/cli-shell-terminal.ts
2048
+ function createLocalShellTerminal(io) {
2049
+ const input = io.input;
2050
+ const output = io.output;
2051
+ return {
2052
+ isInteractive: () => input.isTTY === true && output.isTTY === true && typeof input.setRawMode === "function",
2053
+ size: () => terminalSize(output),
2054
+ term: () => process.env.TERM,
2055
+ enterRawMode: () => enterRawMode(input),
2056
+ onResize(listener) {
2057
+ process.on("SIGWINCH", listener);
2058
+ },
2059
+ offResize(listener) {
2060
+ process.off("SIGWINCH", listener);
2061
+ },
2062
+ setExitCode(exitCode) {
2063
+ process.exitCode = exitCode;
2064
+ }
2065
+ };
2066
+ }
2067
+ function terminalSize(output) {
2068
+ if (!terminalDimension2(output.columns) || !terminalDimension2(output.rows)) {
2069
+ throw new TypeError("setsuna shell could not determine the local terminal size.");
2070
+ }
2071
+ return { cols: output.columns, rows: output.rows };
2072
+ }
2073
+ function terminalDimension2(value) {
2074
+ return Number.isInteger(value) && value > 0 && value <= 65535;
2075
+ }
2076
+ function enterRawMode(input) {
2077
+ if (typeof input.setRawMode !== "function") {
2078
+ throw new TypeError("setsuna shell requires raw-mode support on local stdin.");
2079
+ }
2080
+ const wasRaw = input.isRaw === true;
2081
+ const wasPaused = input.isPaused();
2082
+ input.setRawMode(true);
2083
+ input.resume();
2084
+ let restored = false;
2085
+ return () => {
2086
+ if (restored) return;
2087
+ restored = true;
2088
+ input.setRawMode?.(wasRaw);
2089
+ if (wasPaused) input.pause();
2090
+ };
2091
+ }
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
+ }
2174
+ }
2175
+ };
2176
+
2177
+ // ../setsuna/src/cli-shell.ts
2178
+ var DETACH_BYTE = 29;
2179
+ var DEFAULT_TERM = "xterm-256color";
2180
+ async function runShellCliCommand(commandName, commandArguments, attachProcessId, json, client, io, terminal) {
2181
+ if (commandName !== "shell") {
2182
+ if (attachProcessId !== void 0) {
2183
+ throw new TypeError("--attach is supported only by setsuna shell.");
2184
+ }
2185
+ return false;
2186
+ }
2187
+ if (json) throw new TypeError("setsuna shell does not support --json.");
2188
+ await runShellCommand(
2189
+ commandArguments,
2190
+ attachProcessId,
2191
+ client,
2192
+ io,
2193
+ terminal ?? createLocalShellTerminal(io)
2194
+ );
2195
+ return true;
2196
+ }
2197
+ async function runShellCommand(commandArguments, attachProcessId, client, io, terminal, leaseRuntime) {
2198
+ if (commandArguments.length !== 1 || commandArguments[0] === "") {
2199
+ throw new TypeError("setsuna shell requires exactly one sandbox ID.");
2200
+ }
2201
+ if (!terminal.isInteractive()) {
2202
+ throw new TypeError("setsuna shell requires an interactive local TTY on stdin and stdout.");
2203
+ }
2204
+ if (attachProcessId === "") throw new TypeError("--attach requires a process ID.");
2205
+ 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
+ }
2221
+ }
2222
+ async function resolveProcess(client, sandboxId, attachProcessId, size, localTerm) {
2223
+ if (attachProcessId === void 0) {
2224
+ const handle = await client.spawnProcess(sandboxId, {
2225
+ argv: ["/bin/bash"],
2226
+ cwd: "/workspace",
2227
+ terminal: { ...size, term: validTerm(localTerm) ? localTerm : DEFAULT_TERM }
2228
+ });
2229
+ return handle.processId;
2230
+ }
2231
+ const process2 = (await client.listProcesses(sandboxId)).find(
2232
+ (entry) => entry.processId === attachProcessId
2233
+ );
2234
+ if (process2 === void 0) {
2235
+ throw new TypeError(`Process '${attachProcessId}' was not found in sandbox '${sandboxId}'.`);
2236
+ }
2237
+ if (process2.terminal === void 0) {
2238
+ throw new TypeError(`Process '${attachProcessId}' is not terminal-backed.`);
2239
+ }
2240
+ if (process2.state === "running") await client.resizeProcess(sandboxId, attachProcessId, size);
2241
+ return attachProcessId;
2242
+ }
2243
+ async function attachTerminal(client, sandboxId, processId, io, terminal, leaseKeeper) {
2244
+ const controller = new AbortController();
2245
+ const session = createAttachedSession(
2246
+ client,
2247
+ sandboxId,
2248
+ processId,
2249
+ io,
2250
+ terminal,
2251
+ controller,
2252
+ () => leaseKeeper.stop()
2253
+ );
2254
+ let exitFrame;
2255
+ let attachError;
2256
+ try {
2257
+ leaseKeeper.start(session.fail);
2258
+ for await (const frame of client.attachProcess(sandboxId, processId, {
2259
+ maxStreamBytes: Number.MAX_SAFE_INTEGER,
2260
+ signal: controller.signal
2261
+ })) {
2262
+ if (frame.kind === "exit") exitFrame = frame;
2263
+ else await writeTerminalFrame(frame, io);
2264
+ }
2265
+ } catch (error) {
2266
+ attachError = error;
2267
+ } finally {
2268
+ session.cleanup();
2269
+ }
2270
+ await session.settle();
2271
+ if (session.failure !== void 0) throw session.failure;
2272
+ if (session.detached) {
2273
+ if (attachError !== void 0 && !isDetachAbort(attachError)) throw attachError;
2274
+ return;
2275
+ }
2276
+ if (attachError !== void 0) throw attachError;
2277
+ if (exitFrame === void 0) {
2278
+ throw new SetsunaError("The terminal attachment ended without a process exit frame.", {
2279
+ code: "SERVICE_UNREACHABLE"
2280
+ });
2281
+ }
2282
+ if (exitFrame.exitCode !== null) terminal.setExitCode(exitFrame.exitCode);
2283
+ }
2284
+ function createAttachedSession(client, sandboxId, processId, io, terminal, controller, stopLeaseKeeper) {
2285
+ let detached = false;
2286
+ let failure;
2287
+ let cleaned = false;
2288
+ let inputQueue = Promise.resolve();
2289
+ let resizeQueue = Promise.resolve();
2290
+ const restoreRawMode = terminal.enterRawMode();
2291
+ const cleanup = () => {
2292
+ if (cleaned) return;
2293
+ cleaned = true;
2294
+ io.input.off("data", onInput);
2295
+ terminal.offResize(onResize);
2296
+ stopLeaseKeeper();
2297
+ restoreRawMode();
2298
+ };
2299
+ const fail = (error) => {
2300
+ if (failure === void 0) failure = error;
2301
+ cleanup();
2302
+ controller.abort();
2303
+ };
2304
+ const onInput = (chunk) => {
2305
+ if (!(chunk instanceof Uint8Array)) {
2306
+ fail(new TypeError("setsuna shell received non-binary data from local stdin."));
2307
+ return;
2308
+ }
2309
+ const data = Buffer.from(chunk);
2310
+ const detachAt = data.indexOf(DETACH_BYTE);
2311
+ const forwarded = detachAt < 0 ? data : data.subarray(0, detachAt);
2312
+ if (forwarded.byteLength > 0) {
2313
+ inputQueue = inputQueue.then(() => client.writeProcessStdin(sandboxId, processId, forwarded));
2314
+ void inputQueue.catch(fail);
2315
+ }
2316
+ if (detachAt >= 0 && !detached) {
2317
+ detached = true;
2318
+ cleanup();
2319
+ controller.abort();
2320
+ writeDetachMessage(io, sandboxId, processId);
2321
+ }
2322
+ };
2323
+ const onResize = () => {
2324
+ resizeQueue = queueResize(resizeQueue, client, sandboxId, processId, terminal, fail);
2325
+ };
2326
+ try {
2327
+ io.input.on("data", onInput);
2328
+ terminal.onResize(onResize);
2329
+ } catch (error) {
2330
+ cleanup();
2331
+ throw error;
2332
+ }
2333
+ return {
2334
+ cleanup,
2335
+ fail,
2336
+ get detached() {
2337
+ return detached;
2338
+ },
2339
+ get failure() {
2340
+ return failure;
2341
+ },
2342
+ async settle() {
2343
+ await Promise.allSettled([inputQueue, resizeQueue]);
2344
+ }
2345
+ };
2346
+ }
2347
+ function queueResize(queue, client, sandboxId, processId, terminal, fail) {
2348
+ try {
2349
+ const size = terminal.size();
2350
+ const next = queue.then(() => client.resizeProcess(sandboxId, processId, size));
2351
+ void next.catch(fail);
2352
+ return next;
2353
+ } catch (error) {
2354
+ fail(error);
2355
+ return queue;
2356
+ }
2357
+ }
2358
+ function writeDetachMessage(io, sandboxId, processId) {
2359
+ io.errorOutput.write(
2360
+ `Detached from sandbox ${sandboxId}, process ${processId}; both remain active.
2361
+ Reattach with: setsuna shell ${sandboxId} --attach ${processId}
2362
+ `
2363
+ );
2364
+ }
2365
+ async function writeTerminalFrame(frame, io) {
2366
+ if (frame.kind === "heartbeat") return;
2367
+ const data = frame.kind === "terminal" || frame.kind === "replay" && frame.stream === "terminal" ? frame.data : void 0;
2368
+ if (data === void 0) {
2369
+ throw new SetsunaError("A terminal attachment received a non-terminal process stream.", {
2370
+ code: "INVALID_RESPONSE"
2371
+ });
2372
+ }
2373
+ if (!io.output.write(data)) await once(io.output, "drain");
2374
+ }
2375
+ function validTerm(value) {
2376
+ return value !== void 0 && /^[A-Za-z0-9][A-Za-z0-9+._-]{0,127}$/u.test(value);
2377
+ }
2378
+ function isDetachAbort(error) {
2379
+ return error instanceof SetsunaError && error.code === "REQUEST_ABORTED";
2380
+ }
2381
+
2382
+ // ../setsuna/src/cli-interactive-persistent-actions.ts
2383
+ function createPersistentInteractiveActions(client, io, shellTerminal) {
2384
+ return {
2385
+ async startMicrovm(microvmConfigurationId) {
2386
+ const lease = await client.create({ microvmConfigurationId });
2387
+ return {
2388
+ sandboxId: lease.sandboxId,
2389
+ expiresAt: lease.expiresAt,
2390
+ vcpuCount: lease.microvmConfiguration.vcpuCount,
2391
+ memoryMiB: lease.microvmConfiguration.memoryMiB
2392
+ };
2393
+ },
2394
+ activeMicrovms: () => client.list(),
2395
+ openTerminal: (sandboxId, processId) => runShellCommand(
2396
+ [sandboxId],
2397
+ processId,
2398
+ client,
2399
+ io,
2400
+ shellTerminal ?? createLocalShellTerminal(io)
2401
+ ),
2402
+ async terminalProcesses(sandboxId) {
2403
+ return (await client.listProcesses(sandboxId)).filter((process2) => process2.state === "running" && process2.terminal !== void 0).map(({ processId, pid }) => ({ processId, pid }));
2404
+ },
2405
+ async runPersistentCommand(sandboxId, command, timeoutMs) {
2406
+ await client.renew(sandboxId);
2407
+ const result = await client.execute(sandboxId, command, {
2408
+ ...timeoutMs === void 0 ? {} : { timeoutMs }
2409
+ });
2410
+ return interactiveRunResult(result);
2411
+ },
2412
+ renewMicrovm: (sandboxId) => client.renew(sandboxId),
2413
+ destroyMicrovm: (sandboxId) => client.destroy(sandboxId)
2414
+ };
2415
+ }
2416
+ function interactiveRunResult(result) {
2417
+ return {
2418
+ exitStatus: guestExitStatus(result),
2419
+ durationMs: result.durationMs,
2420
+ timedOut: result.timedOut,
2421
+ outputTruncated: result.outputTruncated,
2422
+ omittedOutputBytes: result.omittedOutputBytes,
2423
+ stdout: result.stdout,
2424
+ stderr: result.stderr
2425
+ };
2426
+ }
2427
+
2428
+ // ../setsuna/src/cli-interactive-sandbox-actions.ts
2429
+ function createSandboxInteractiveActions(client, io, signalRuntime, shellTerminal) {
2430
+ return {
2431
+ async runCommand(command, options) {
2432
+ const result = await runWithSignalCleanup(client, command, options, io, signalRuntime);
2433
+ return result === void 0 ? null : interactiveRunResult(result);
2434
+ },
2435
+ async microvmConfigurations() {
2436
+ const catalog = await client.microvmConfigurations();
2437
+ return {
2438
+ maxConcurrentMicrovms: catalog.maxConcurrentMicrovms,
2439
+ configurations: catalog.configurations.map((configuration) => ({
2440
+ id: configuration.id,
2441
+ isDefault: configuration.id === catalog.defaultConfigurationId,
2442
+ vcpuCount: configuration.vcpuCount,
2443
+ memoryMiB: configuration.memoryMiB
2444
+ }))
2445
+ };
2446
+ },
2447
+ ...createPersistentInteractiveActions(client, io, shellTerminal)
2448
+ };
2449
+ }
2450
+
2451
+ // ../setsuna/src/cli-io.ts
2452
+ var DEFAULT_IO = {
2453
+ input: process.stdin,
2454
+ output: process.stdout,
2455
+ errorOutput: process.stderr
2456
+ };
2457
+ var DEFAULT_SIGNAL_RUNTIME = {
2458
+ on(signal, listener) {
2459
+ process.on(signal, listener);
2460
+ },
2461
+ off(signal, listener) {
2462
+ process.off(signal, listener);
2463
+ },
2464
+ forceExit(exitCode) {
2465
+ process.exit(exitCode);
2466
+ }
2467
+ };
2468
+
2469
+ // ../setsuna/src/tui/customer/format.ts
2470
+ var STANDALONE_BOX = { withGuide: false, width: "auto" };
2471
+ 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
+ }
2484
+ }
2485
+ function formatRunResult(result) {
2486
+ const facts = [`Exit status ${result.exitStatus}`, `${result.durationMs} ms`];
2487
+ if (result.outputTruncated) {
2488
+ facts.push(`Output truncated (${result.omittedOutputBytes} bytes omitted)`);
2489
+ }
2490
+ return facts.join(" \xB7 ");
2491
+ }
2492
+ function formatTimeoutHint(timeoutMs) {
2493
+ return timeoutMs === void 0 ? "No timeout" : `Timeout ${formatSeconds(timeoutMs)} seconds`;
2494
+ }
2495
+ function formatSeconds(timeoutMs) {
2496
+ return String(timeoutMs / 1e3);
2497
+ }
2498
+ function formatGib(memoryMiB) {
2499
+ return Number.isInteger(memoryMiB / 1024) ? String(memoryMiB / 1024) : (memoryMiB / 1024).toFixed(2);
2500
+ }
2501
+ function formatMicrovmShape(vcpuCount, memoryMiB) {
2502
+ return `${vcpuCount} vCPU \xB7 ${formatMemory(memoryMiB)}`;
2503
+ }
2504
+ function formatMicrovmConfigurationHint(configuration) {
2505
+ return configuration.isDefault ? "Default" : void 0;
2506
+ }
2507
+ function formatMicrovmConfigurationIdentifier(configuration) {
2508
+ return configuration.isDefault ? `${configuration.id} (default)` : configuration.id;
2509
+ }
2510
+ function formatMemory(memoryMiB) {
2511
+ return memoryMiB < 1024 ? `${memoryMiB} MiB` : `${formatGib(memoryMiB)} GiB`;
2512
+ }
2513
+ function formatExpiry(expiresAt, now) {
2514
+ const remaining = remainingTime(expiresAt, now);
2515
+ return remaining === void 0 ? "Expired" : `Expires in ${remaining}`;
2516
+ }
2517
+ function formatMicrovmState(microvm, now) {
2518
+ const lines = [`ID ${microvm.sandboxId}`];
2519
+ if (microvm.vcpuCount !== void 0 && microvm.memoryMiB !== void 0) {
2520
+ lines.push(`Compute ${formatMicrovmShape(microvm.vcpuCount, microvm.memoryMiB)}`);
2521
+ }
2522
+ const remaining = remainingTime(microvm.expiresAt, now);
2523
+ lines.push(remaining === void 0 ? "Expired" : `Expires in ${remaining}`);
2524
+ return lines.join("\n");
2525
+ }
2526
+ function remainingTime(expiresAt, now) {
2527
+ const seconds = Math.floor((Date.parse(expiresAt) - now) / 1e3);
2528
+ if (Number.isNaN(seconds) || seconds <= 0) return void 0;
2529
+ const minutes = Math.floor(seconds / 60);
2530
+ const rest = seconds % 60;
2531
+ return minutes === 0 ? `${rest}s` : `${minutes}m ${String(rest).padStart(2, "0")}s`;
2532
+ }
2533
+ function requiredText4(message) {
2534
+ return (value) => value.trim() === "" ? message : void 0;
2535
+ }
2536
+ function optionalPositiveNumber(message) {
2537
+ return (value) => {
2538
+ if (value.trim() === "") return void 0;
2539
+ const number = Number(value);
2540
+ return Number.isFinite(number) && number > 0 ? void 0 : message;
2541
+ };
2542
+ }
2543
+ function formatTuiError(error) {
2544
+ const message = error instanceof AggregateError ? [error.message, ...error.errors.map(formatTuiErrorCause)].join("\n") : error instanceof Error ? error.message : String(error);
2545
+ return formatTuiText(message);
2546
+ }
2547
+ function formatTuiErrorCause(error) {
2548
+ if (!(error instanceof Error)) return String(error);
2549
+ const code = "code" in error && typeof error.code === "string" ? `${error.code}: ` : "";
2550
+ return `${code}${error.message}`;
2551
+ }
2552
+ function formatTuiText(message) {
2553
+ return message.replace(/broker/giu, "Setsuna service").replace(/guest/giu, "microVM");
2554
+ }
2555
+ function formatSignInStatus(status) {
2556
+ return status.authenticated ? "Signed in" : "Not signed in";
2557
+ }
2558
+ function formatAccountStatus(status) {
2559
+ const lines = [`Status ${formatSignInStatus(status)}`];
2560
+ if (status.expiresAt !== null) {
2561
+ const end = new Date(status.expiresAt * 1e3).toISOString();
2562
+ lines.push(`Session ${status.authenticated ? "Active until" : "Expired"} ${end}`);
2563
+ }
2564
+ return lines.join("\n");
2565
+ }
2566
+ function formatDeviceAuthorization(authorization) {
2567
+ return [`Open ${authorization.verificationUrl}`, `Code ${authorization.userCode}`].join(
2568
+ "\n"
2569
+ );
2570
+ }
2571
+
2572
+ // ../setsuna/src/tui/customer/configuration-menu.ts
2573
+ init_prompt();
2574
+ async function promptForMicrovmConfiguration(io, promptUi, configurations, selection = {}) {
2575
+ const extraOptions = selection.extraOptions ?? [];
2576
+ const defaultIndex = Math.max(
2577
+ 0,
2578
+ configurations.findIndex(({ isDefault }) => isDefault)
2579
+ );
2580
+ const selected = await promptUi.select(
2581
+ {
2582
+ message: "MicroVM configuration",
2583
+ options: [
2584
+ ...configurations.map(
2585
+ (configuration, index) => configurationOption(configuration, index, selection.withIdentifiers === true)
2586
+ ),
2587
+ ...extraOptions,
2588
+ { value: "back", label: "Back" }
2589
+ ],
2590
+ initialValue: configurations.length === 0 ? extraOptions[0]?.value ?? "back" : `configuration:${defaultIndex}`
2591
+ },
2592
+ io
2593
+ );
2594
+ if (selected === TUI_CANCEL || selected === "back") return null;
2595
+ const extra = extraOptions.find(({ value }) => value === selected);
2596
+ if (extra !== void 0) return extra.value;
2597
+ return configurations[Number(selected.slice("configuration:".length))] ?? null;
2598
+ }
2599
+ function configurationOption(configuration, index, withIdentifiers) {
2600
+ const hint = withIdentifiers ? formatMicrovmConfigurationIdentifier(configuration) : formatMicrovmConfigurationHint(configuration);
2601
+ return {
2602
+ value: `configuration:${index}`,
2603
+ label: formatMicrovmShape(configuration.vcpuCount, configuration.memoryMiB),
2604
+ ...hint === void 0 ? {} : { hint }
2605
+ };
2606
+ }
2607
+
2608
+ // ../setsuna/src/tui/customer/account-menu.ts
2609
+ init_prompt();
2610
+
2611
+ // ../setsuna/src/tui/customer/session.ts
2612
+ function shellPrompt(session) {
2613
+ return session.signal === void 0 ? {} : { signal: AbortSignal.any([session.signal]) };
2614
+ }
2615
+
2616
+ // ../setsuna/src/tui/customer/account-menu.ts
2617
+ async function ensureSignedIn(io, auth, promptUi, session = {}) {
2618
+ let status = await checkSignIn(io, auth, promptUi);
2619
+ while (status?.authenticated !== true) {
2620
+ const choice = await promptUi.select(
2621
+ {
2622
+ message: "Sign in to continue",
2623
+ options: [
2624
+ { value: "sign-in", label: "Sign in", hint: "Approve this device in your browser" },
2625
+ { value: "exit", label: "Exit" }
2626
+ ],
2627
+ initialValue: "sign-in",
2628
+ ...shellPrompt(session)
2629
+ },
2630
+ io
2631
+ );
2632
+ if (choice === TUI_CANCEL) {
2633
+ promptUi.cancel("Exited.", io);
2634
+ return false;
2635
+ }
2636
+ if (choice === "exit") {
2637
+ promptUi.outro("Ready when you are.", io);
2638
+ return false;
2639
+ }
2640
+ status = await signIn(io, auth, promptUi);
2641
+ }
2642
+ return true;
2643
+ }
2644
+ async function checkSignIn(io, auth, promptUi) {
2645
+ const spinner = promptUi.spinner(io);
2646
+ spinner.start("Checking sign-in");
2647
+ try {
2648
+ const status = await auth.status();
2649
+ spinner.stop(formatSignInStatus(status));
2650
+ return status;
2651
+ } catch (error) {
2652
+ spinner.error(formatTuiError(error));
2653
+ return void 0;
2654
+ }
2655
+ }
2656
+ async function signIn(io, auth, promptUi) {
2657
+ const controller = new AbortController();
2658
+ const spinner = promptUi.spinner(io, {
2659
+ indicator: "timer",
2660
+ onCancel: () => controller.abort()
2661
+ });
2662
+ let waiting = false;
2663
+ try {
2664
+ const status = await auth.login((authorization) => {
2665
+ promptUi.box(formatDeviceAuthorization(authorization), "Sign in", io, STANDALONE_BOX);
2666
+ spinner.start("Waiting for approval");
2667
+ waiting = true;
2668
+ }, controller.signal);
2669
+ spinner.stop("Signed in");
2670
+ return status;
2671
+ } catch (error) {
2672
+ if (controller.signal.aborted) return void 0;
2673
+ if (waiting) spinner.error(formatTuiError(error));
2674
+ else promptUi.error(formatTuiError(error), io);
2675
+ return void 0;
2676
+ }
2677
+ }
2678
+ async function accountMenu(io, auth, promptUi, session = {}) {
2679
+ promptUi.box(formatAccountStatus(await auth.status()), "Account", io, STANDALONE_BOX);
2680
+ const choice = await promptUi.select(
2681
+ {
2682
+ message: "Account",
2683
+ options: [
2684
+ { value: "logout", label: "Log out", hint: "Remove the saved sign-in from this device" },
2685
+ { value: "back", label: "Back" }
2686
+ ],
2687
+ initialValue: "back",
2688
+ ...shellPrompt(session)
2689
+ },
2690
+ io
2691
+ );
2692
+ if (choice === TUI_CANCEL || choice === "back") return "back";
2693
+ const result = await auth.logout();
2694
+ promptUi.success("Logged out.", io);
2695
+ if (result.remoteRevocation === "unconfirmed") {
2696
+ promptUi.warn("Local session cleared; remote revocation could not be confirmed.", io);
2697
+ }
2698
+ return "signed-out";
2699
+ }
2700
+
2701
+ // ../setsuna/src/tui/customer/persistent-menu.ts
2702
+ init_prompt();
2703
+
2704
+ // ../setsuna/src/tui/customer/run-menu.ts
2705
+ init_prompt();
2706
+ async function runCommandMenu(io, actions, promptUi) {
2707
+ let configurations;
2708
+ try {
2709
+ configurations = (await actions.microvmConfigurations()).configurations;
2710
+ } catch (error) {
2711
+ promptUi.error(formatTuiError(error), io);
2712
+ return true;
2713
+ }
2714
+ const configuration = await promptForMicrovmConfiguration(io, promptUi, configurations);
2715
+ if (configuration === null) return true;
2716
+ let previousCommand;
2717
+ while (true) {
2718
+ const command = await promptUi.text(
2719
+ {
2720
+ message: "Command to run",
2721
+ placeholder: "uname -a",
2722
+ ...previousCommand === void 0 ? {} : { initialValue: previousCommand },
2723
+ validate: requiredText4("Enter a command.")
2724
+ },
2725
+ io
2726
+ );
2727
+ if (command === TUI_CANCEL) return true;
2728
+ const outcome = await runSingleCommandSession(io, actions, promptUi, command, configuration.id);
2729
+ if (outcome === "back-to-main") return true;
2730
+ if (outcome === "exit-tui") return false;
2731
+ previousCommand = command;
2732
+ }
2733
+ }
2734
+ async function runSingleCommandSession(io, actions, promptUi, command, microvmConfigurationId) {
2735
+ let timeoutMs;
2736
+ while (true) {
2737
+ const choice = await promptUi.select(
2738
+ {
2739
+ message: "Run command",
2740
+ options: [
2741
+ { value: "execute", label: "Run", hint: formatTimeoutHint(timeoutMs) },
2742
+ { value: "timeout", label: "Set timeout" },
2743
+ { value: "back", label: "Back" }
2744
+ ],
2745
+ initialValue: "execute"
2746
+ },
2747
+ io
2748
+ );
2749
+ if (choice === TUI_CANCEL || choice === "back") return "back-to-main";
2750
+ if (choice === "timeout") {
2751
+ const nextTimeoutMs = await promptCommandTimeout(io, promptUi, timeoutMs);
2752
+ if (nextTimeoutMs !== TUI_CANCEL) timeoutMs = nextTimeoutMs;
2753
+ continue;
2754
+ }
2755
+ const outcome = await executeCommandAndReport(
2756
+ io,
2757
+ actions,
2758
+ promptUi,
2759
+ command,
2760
+ microvmConfigurationId,
2761
+ timeoutMs
2762
+ );
2763
+ if (outcome === "no-active-session") return "exit-tui";
2764
+ return promptRunAgain(io, promptUi);
2765
+ }
2766
+ }
2767
+ async function promptRunAgain(io, promptUi) {
2768
+ const next = await promptUi.select(
2769
+ {
2770
+ message: "What next?",
2771
+ options: [
2772
+ { value: "again", label: "Run another" },
2773
+ { value: "back", label: "Back" },
2774
+ { value: "exit", label: "Exit" }
2775
+ ],
2776
+ initialValue: "again"
2777
+ },
2778
+ io
2779
+ );
2780
+ if (next === TUI_CANCEL || next === "back") return "back-to-main";
2781
+ if (next === "exit") {
2782
+ promptUi.outro("Ready when you are.", io);
2783
+ return "exit-tui";
2784
+ }
2785
+ return "prompt-new-command";
2786
+ }
2787
+ async function promptCommandTimeout(io, promptUi, timeoutMs) {
2788
+ const timeout = await promptUi.text(
2789
+ {
2790
+ message: "Timeout in seconds",
2791
+ placeholder: "No timeout",
2792
+ // The current timeout is there to edit; clearing it means no timeout, as the placeholder says.
2793
+ ...timeoutMs === void 0 ? {} : { initialValue: formatSeconds(timeoutMs) },
2794
+ validate: optionalPositiveNumber(
2795
+ "Enter a positive number of seconds, or leave blank for no timeout."
2796
+ )
2797
+ },
2798
+ io
2799
+ );
2800
+ if (timeout === TUI_CANCEL) return TUI_CANCEL;
2801
+ return timeout.trim() === "" ? void 0 : Math.round(Number(timeout) * 1e3);
2802
+ }
2803
+ function presentRunResult(io, promptUi, result) {
2804
+ io.output.write(result.stdout);
2805
+ io.errorOutput.write(result.stderr);
2806
+ const summary = formatRunResult(result);
2807
+ if (result.timedOut || result.outputTruncated) promptUi.warn(summary, io);
2808
+ else promptUi.info(summary, io);
2809
+ }
2810
+ async function executeCommandAndReport(io, actions, promptUi, command, microvmConfigurationId, timeoutMs) {
2811
+ const log = promptUi.taskLog({ title: "Run command" }, io);
2812
+ let result;
2813
+ try {
2814
+ result = await actions.runCommand(command, {
2815
+ microvmConfigurationId,
2816
+ ...timeoutMs === void 0 ? {} : { timeoutMs },
2817
+ onPhase: (phase) => log.message(formatRunPhase(phase))
2818
+ });
2819
+ } catch (error) {
2820
+ log.error(formatTuiError(error));
2821
+ return "completed";
2822
+ }
2823
+ if (result === null) {
2824
+ log.error("Interrupted");
2825
+ return "no-active-session";
2826
+ }
2827
+ log.success("Complete");
2828
+ presentRunResult(io, promptUi, result);
2829
+ return "completed";
2830
+ }
2831
+
2832
+ // ../setsuna/src/tui/customer/persistent-menu.ts
2833
+ async function startMicrovmMenu(io, actions, promptUi) {
2834
+ const configurations = (await actions.microvmConfigurations()).configurations;
2835
+ const configuration = await promptForMicrovmConfiguration(io, promptUi, configurations);
2836
+ if (configuration === null) return;
2837
+ const sandbox = await startSandbox(io, actions, promptUi, configuration.id);
2838
+ if (sandbox === void 0) return;
2839
+ promptUi.box(formatMicrovmState(sandbox, Date.now()), "microVM ready", io, STANDALONE_BOX);
2840
+ await manageSandbox(io, actions, promptUi, sandbox);
2841
+ }
2842
+ async function startSandbox(io, actions, promptUi, microvmConfigurationId) {
2843
+ let interrupted = false;
2844
+ const spinner = promptUi.spinner(io, {
2845
+ indicator: "timer",
2846
+ onCancel: () => {
2847
+ interrupted = true;
2848
+ }
2849
+ });
2850
+ spinner.start("Starting microVM");
2851
+ let sandbox;
2852
+ try {
2853
+ sandbox = await actions.startMicrovm(microvmConfigurationId);
2854
+ } catch (error) {
2855
+ spinner.error(formatTuiError(error));
2856
+ return void 0;
2857
+ }
2858
+ spinner.stop("microVM ready");
2859
+ if (!interrupted) return sandbox;
2860
+ promptUi.warn(
2861
+ `Interrupted after microVM ${sandbox.sandboxId} was created; it stays active under Active microVMs.`,
2862
+ io
2863
+ );
2864
+ return void 0;
2865
+ }
2866
+ async function activeMicrovmsMenu(io, actions, promptUi) {
2867
+ while (true) {
2868
+ const sandboxes = await actions.activeMicrovms();
2869
+ if (sandboxes.length === 0) {
2870
+ promptUi.info("No active microVMs.", io);
2871
+ return;
2872
+ }
2873
+ const sandbox = await selectSandbox(io, promptUi, sandboxes);
2874
+ if (sandbox === void 0) return;
2875
+ await manageSandbox(io, actions, promptUi, sandbox);
2876
+ }
2877
+ }
2878
+ async function selectSandbox(io, promptUi, sandboxes) {
2879
+ const now = Date.now();
2880
+ const selected = await promptUi.select(
2881
+ {
2882
+ message: "Active microVMs",
2883
+ options: [
2884
+ ...sandboxes.map((sandbox, index) => ({
2885
+ value: `sandbox:${index}`,
2886
+ label: sandbox.sandboxId,
2887
+ hint: formatExpiry(sandbox.expiresAt, now)
2888
+ })),
2889
+ { value: "back", label: "Back" }
2890
+ ]
2891
+ },
2892
+ io
2893
+ );
2894
+ if (selected === TUI_CANCEL || selected === "back") return void 0;
2895
+ return sandboxes[Number(selected.slice("sandbox:".length))];
2896
+ }
2897
+ async function manageSandbox(io, actions, promptUi, initial) {
2898
+ let microvm = initial;
2899
+ while (true) {
2900
+ const expiry = formatExpiry(microvm.expiresAt, Date.now());
2901
+ const choice = await promptUi.select(
2902
+ {
2903
+ message: `Manage microVM ${microvm.sandboxId} \xB7 ${expiry}`,
2904
+ options: [
2905
+ { value: "open-terminal", label: "Open terminal" },
2906
+ { value: "reattach-terminal", label: "Reattach terminal" },
2907
+ { value: "run-command", label: "Run command" },
2908
+ { value: "renew", label: "Renew" },
2909
+ { value: "destroy", label: "Destroy microVM" },
2910
+ { value: "back", label: "Back" }
2911
+ ],
2912
+ initialValue: "open-terminal"
2913
+ },
2914
+ io
2915
+ );
2916
+ if (choice === TUI_CANCEL || choice === "back") return;
2917
+ try {
2918
+ const next = await dispatchSandboxChoice(choice, microvm, io, actions, promptUi);
2919
+ if (next === void 0) return;
2920
+ microvm = next;
2921
+ } catch (error) {
2922
+ promptUi.error(formatTuiError(error), io);
2923
+ }
2924
+ }
2925
+ }
2926
+ async function dispatchSandboxChoice(choice, microvm, io, actions, promptUi) {
2927
+ if (choice === "open-terminal") {
2928
+ await actions.openTerminal(microvm.sandboxId);
2929
+ return refreshExpiry(actions, microvm);
2930
+ }
2931
+ if (choice === "reattach-terminal") {
2932
+ const attached = await reattachTerminal(io, actions, promptUi, microvm.sandboxId);
2933
+ return attached ? refreshExpiry(actions, microvm) : microvm;
2934
+ }
2935
+ 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 {
2954
+ return microvm;
2955
+ }
2956
+ }
2957
+ async function reattachTerminal(io, actions, promptUi, sandboxId) {
2958
+ const processes = await actions.terminalProcesses(sandboxId);
2959
+ if (processes.length === 0) {
2960
+ promptUi.info("No terminal sessions to reattach.", io);
2961
+ return false;
2962
+ }
2963
+ const selected = await promptUi.select(
2964
+ {
2965
+ message: "Reattach terminal",
2966
+ options: [
2967
+ ...processes.map((process3, index) => ({
2968
+ value: `terminal:${index}`,
2969
+ label: process3.processId
2970
+ })),
2971
+ { value: "back", label: "Back" }
2972
+ ]
2973
+ },
2974
+ io
2975
+ );
2976
+ if (selected === TUI_CANCEL || selected === "back") return false;
2977
+ const process2 = processes[Number(selected.slice("terminal:".length))];
2978
+ if (process2 === void 0) return false;
2979
+ await actions.openTerminal(sandboxId, process2.processId);
2980
+ return true;
2981
+ }
2982
+ async function runPersistentCommand(io, actions, promptUi, sandboxId) {
2983
+ const command = await promptUi.text(
2984
+ {
2985
+ message: "Command to run",
2986
+ placeholder: "uname -a",
2987
+ validate: requiredText4("Enter a command.")
2988
+ },
2989
+ io
2990
+ );
2991
+ if (command === TUI_CANCEL) return false;
2992
+ let timeoutMs;
2993
+ while (true) {
2994
+ const choice = await promptUi.select(
2995
+ {
2996
+ message: "Run command",
2997
+ options: [
2998
+ { value: "execute", label: "Run", hint: formatTimeoutHint(timeoutMs) },
2999
+ { value: "timeout", label: "Set timeout" },
3000
+ { value: "back", label: "Back" }
3001
+ ],
3002
+ initialValue: "execute"
3003
+ },
3004
+ io
3005
+ );
3006
+ if (choice === TUI_CANCEL || choice === "back") return false;
3007
+ if (choice === "timeout") {
3008
+ const timeout = await promptCommandTimeout(io, promptUi, timeoutMs);
3009
+ if (timeout !== TUI_CANCEL) timeoutMs = timeout;
3010
+ } else {
3011
+ const result = await actions.runPersistentCommand(sandboxId, command, timeoutMs);
3012
+ presentRunResult(io, promptUi, result);
3013
+ return true;
3014
+ }
3015
+ }
3016
+ }
3017
+ async function destroySandbox(io, actions, promptUi, sandboxId) {
3018
+ const confirmed = await promptUi.confirm(
3019
+ { message: `Destroy microVM ${sandboxId}?`, initialValue: false },
3020
+ io
3021
+ );
3022
+ if (confirmed === TUI_CANCEL || !confirmed) return false;
3023
+ await actions.destroyMicrovm(sandboxId);
3024
+ promptUi.success(`Destroyed microVM ${sandboxId}.`, io);
3025
+ return true;
3026
+ }
3027
+
3028
+ // ../setsuna/src/tui/customer/index.ts
3029
+ init_prompt();
3030
+ var TUI_TITLE = "Setsuna \u5239\u90A3";
3031
+ var PREVIEW_TUI_SUBTITLE = "Research Preview";
3032
+ var UNAVAILABLE_MESSAGE = "Setsuna is temporarily unavailable. Try again shortly.";
3033
+ async function runCustomerTui(io, actions, promptUi, session = {}) {
3034
+ promptUi.intro(TUI_TITLE, PREVIEW_TUI_SUBTITLE, io);
3035
+ while (await ensureSignedIn(io, actions.auth, promptUi, session)) {
3036
+ await checkAvailability(io, actions.service, promptUi);
3037
+ if (await mainMenu(io, actions, promptUi, session) === "exit") return;
3038
+ }
3039
+ }
3040
+ async function checkAvailability(io, service, promptUi) {
3041
+ const spinner = promptUi.spinner(io);
3042
+ spinner.start("Connecting");
3043
+ if (await service.available()) spinner.stop("Connected");
3044
+ else spinner.error(UNAVAILABLE_MESSAGE);
3045
+ }
3046
+ async function mainMenu(io, actions, promptUi, session) {
3047
+ while (true) {
3048
+ const choice = await promptUi.select(
3049
+ {
3050
+ message: "What would you like to do?",
3051
+ 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" },
3056
+ { value: "exit", label: "Exit" }
3057
+ ],
3058
+ initialValue: "start",
3059
+ ...shellPrompt(session)
3060
+ },
3061
+ io
3062
+ );
3063
+ if (choice === TUI_CANCEL) {
3064
+ promptUi.cancel("Exited.", io);
3065
+ return "exit";
3066
+ }
3067
+ if (choice === "exit") {
3068
+ promptUi.outro("Ready when you are.", io);
3069
+ return "exit";
3070
+ }
3071
+ try {
3072
+ const outcome = await dispatchMainChoice(choice, io, actions, promptUi, session);
3073
+ if (outcome !== "continue") return outcome;
3074
+ } catch (error) {
3075
+ promptUi.error(formatTuiError(error), io);
3076
+ }
3077
+ }
3078
+ }
3079
+ async function dispatchMainChoice(choice, io, actions, promptUi, session) {
3080
+ if (choice === "start") {
3081
+ await startMicrovmMenu(io, actions.sandbox, promptUi);
3082
+ return "continue";
3083
+ }
3084
+ if (choice === "active") {
3085
+ await activeMicrovmsMenu(io, actions.sandbox, promptUi);
3086
+ return "continue";
3087
+ }
3088
+ if (choice === "run") {
3089
+ return await runCommandMenu(io, actions.sandbox, promptUi) ? "continue" : "exit";
3090
+ }
3091
+ return await accountMenu(io, actions.auth, promptUi, session) === "signed-out" ? "signed-out" : "continue";
3092
+ }
3093
+
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
+ // ../setsuna/src/cli-commands.ts
3143
+ async function runRunCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3144
+ if (commandName !== "run") throw new TypeError(`Unknown command: ${commandName ?? ""}`);
3145
+ if (commandArguments.length === 0)
3146
+ throw new TypeError("setsuna run requires a command after --.");
3147
+ const network = routedNetworkFromCli(parsed);
3148
+ const result = await runWithSignalCleanup(
3149
+ client,
3150
+ commandArguments.map(shellQuote).join(" "),
3151
+ {
3152
+ timeoutMs: optionalInteger(parsed.values["timeout-ms"], "--timeout-ms"),
3153
+ requestTimeoutMs,
3154
+ ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] },
3155
+ ...network === void 0 ? {} : { network }
3156
+ },
3157
+ io,
3158
+ options.signalRuntime ?? DEFAULT_SIGNAL_RUNTIME
3159
+ );
3160
+ if (result !== void 0)
3161
+ writeExecutionResult(result, parsed.values.json, io.output, io.errorOutput, true);
3162
+ }
3163
+
3164
+ // ../setsuna/src/cli-configurations.ts
3165
+ async function runConfigurationsCommand(client, json, output) {
3166
+ const catalog = await client.microvmConfigurations();
3167
+ if (json) {
3168
+ output.write(`${JSON.stringify(catalog)}
3169
+ `);
3170
+ return;
3171
+ }
3172
+ output.write(`Installed microVM configurations (default: ${catalog.defaultConfigurationId}):
3173
+ `);
3174
+ for (const configuration of catalog.configurations) {
3175
+ const defaultLabel = configuration.id === catalog.defaultConfigurationId ? " (default)" : "";
3176
+ output.write(
3177
+ `${configuration.id}${defaultLabel} ${configuration.vcpuCount} vCPU ${configuration.memoryMiB} MiB
3178
+ `
3179
+ );
3180
+ }
3181
+ }
3182
+
3183
+ // ../setsuna/src/cli-process-commands.ts
3184
+ var PROCESS_COMMANDS = /* @__PURE__ */ new Set(["spawn", "list", "attach", "stdin", "terminate"]);
3185
+ async function runProcessCommand(commandArguments, parsed, client, io, signalRuntime = DEFAULT_SIGNAL_RUNTIME) {
3186
+ const [command, ...arguments_] = commandArguments;
3187
+ if (command === void 0 || !PROCESS_COMMANDS.has(command)) {
3188
+ throw new TypeError("setsuna process requires spawn, list, attach, stdin, or terminate.");
3189
+ }
3190
+ if (command === "spawn") return spawnProcess(arguments_, parsed, client, io.output);
3191
+ if (command === "list") return listProcesses(arguments_, parsed, client, io.output);
3192
+ if (command === "attach") return attachProcess(arguments_, parsed, client, io, signalRuntime);
3193
+ if (command === "stdin") return writeStdin(arguments_, parsed, client, io.output);
3194
+ return terminateProcess(arguments_, parsed, client, io.output);
3195
+ }
3196
+ async function spawnProcess(arguments_, parsed, client, output) {
3197
+ const [sandboxId, ...argv] = arguments_;
3198
+ if (sandboxId === void 0 || argv.length === 0) {
3199
+ throw new TypeError("setsuna process spawn requires a sandbox ID and argv after --.");
3200
+ }
3201
+ const handle = await client.spawnProcess(sandboxId, { argv });
3202
+ writeHandle(handle, parsed.values.json, output);
3203
+ }
3204
+ async function listProcesses(arguments_, parsed, client, output) {
3205
+ const [sandboxId] = exactly(arguments_, 1, "setsuna process list requires one sandbox ID.");
3206
+ const processes = await client.listProcesses(sandboxId);
3207
+ if (parsed.values.json) output.write(`${JSON.stringify(processes)}
3208
+ `);
3209
+ else if (processes.length === 0) output.write("No processes in this sandbox.\n");
3210
+ else for (const process2 of processes) writeProcess(process2, output);
3211
+ }
3212
+ async function attachProcess(arguments_, parsed, client, io, signalRuntime) {
3213
+ const [sandboxId, processId] = exactly(
3214
+ arguments_,
3215
+ 2,
3216
+ "setsuna process attach requires a sandbox ID and process ID."
3217
+ );
3218
+ const controller = new AbortController();
3219
+ let detached = false;
3220
+ const handleSignal = (signal) => {
3221
+ if (!detached) {
3222
+ detached = true;
3223
+ io.errorOutput.write("Detached; process and sandbox remain active.\n");
3224
+ controller.abort();
3225
+ } else signalRuntime.forceExit(signal === "SIGINT" ? 130 : 143);
3226
+ };
3227
+ const sigint = () => handleSignal("SIGINT");
3228
+ const sigterm = () => handleSignal("SIGTERM");
3229
+ signalRuntime.on("SIGINT", sigint);
3230
+ signalRuntime.on("SIGTERM", sigterm);
3231
+ try {
3232
+ for await (const frame of client.attachProcess(sandboxId, processId, {
3233
+ signal: controller.signal
3234
+ })) {
3235
+ writeFrame(frame, parsed.values.json, io);
3236
+ }
3237
+ } catch (error) {
3238
+ if (!detached || !(error instanceof SetsunaError && error.code === "REQUEST_ABORTED")) {
3239
+ if (!detached) throw error;
3240
+ io.errorOutput.write(`${formatCliError(error)}
3241
+ `);
3242
+ }
3243
+ } finally {
3244
+ signalRuntime.off("SIGINT", sigint);
3245
+ signalRuntime.off("SIGTERM", sigterm);
3246
+ }
3247
+ }
3248
+ async function writeStdin(arguments_, parsed, client, output) {
3249
+ const [sandboxId, processId, data] = exactly(
3250
+ arguments_,
3251
+ 3,
3252
+ "setsuna process stdin requires a sandbox ID, process ID, and data."
3253
+ );
3254
+ await client.writeProcessStdin(sandboxId, processId, data);
3255
+ if (parsed.values.json)
3256
+ output.write(`${JSON.stringify({ sandboxId, processId, written: true })}
3257
+ `);
3258
+ else output.write(`Wrote stdin to process ${processId}.
3259
+ `);
3260
+ }
3261
+ async function terminateProcess(arguments_, parsed, client, output) {
3262
+ const [sandboxId, processId] = exactly(
3263
+ arguments_,
3264
+ 2,
3265
+ "setsuna process terminate requires a sandbox ID and process ID."
3266
+ );
3267
+ const process2 = await client.terminateProcess(sandboxId, processId);
3268
+ if (parsed.values.json) output.write(`${JSON.stringify(process2)}
3269
+ `);
3270
+ else output.write(`Terminated process ${process2.processId}; state is ${process2.state}.
3271
+ `);
3272
+ }
3273
+ function writeHandle(handle, json, output) {
3274
+ if (json) output.write(`${JSON.stringify(handle)}
3275
+ `);
3276
+ else output.write(`Spawned process ${handle.processId} (PID ${handle.pid}).
3277
+ `);
3278
+ }
3279
+ function writeProcess(process2, output) {
3280
+ output.write(`${process2.processId} PID ${process2.pid} ${process2.state}
3281
+ `);
3282
+ }
3283
+ function writeFrame(frame, json, io) {
3284
+ if (json) {
3285
+ io.output.write(`${JSON.stringify(processFrameToJson(frame))}
3286
+ `);
3287
+ } else if (frame.kind === "replay") {
3288
+ (frame.stream === "stderr" ? io.errorOutput : io.output).write(frame.data);
3289
+ } else if (frame.kind === "stdout") io.output.write(frame.data);
3290
+ else if (frame.kind === "stderr") io.errorOutput.write(frame.data);
3291
+ else if (frame.kind === "terminal") io.output.write(frame.data);
3292
+ }
3293
+ function exactly(arguments_, count, message) {
3294
+ if (arguments_.length !== count) throw new TypeError(message);
3295
+ return arguments_;
3296
+ }
3297
+
3298
+ // ../setsuna/src/cli-sandbox-commands.ts
3299
+ var SANDBOX_COMMANDS = /* @__PURE__ */ new Set(["create", "list", "status", "renew", "exec", "destroy"]);
3300
+ async function runSandboxCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3301
+ if (commandName === void 0 || !SANDBOX_COMMANDS.has(commandName)) return false;
3302
+ if (commandName === "create") {
3303
+ requireArgumentCount(commandName, commandArguments, 0);
3304
+ const network = routedNetworkFromCli(parsed);
3305
+ writeLease(
3306
+ await client.create({
3307
+ ...parsed.values["configuration-id"] === void 0 ? {} : { microvmConfigurationId: parsed.values["configuration-id"] },
3308
+ ...network === void 0 ? {} : { network }
3309
+ }),
3310
+ parsed.values.json,
3311
+ io.output,
3312
+ "Created"
3313
+ );
3314
+ } else if (commandName === "list") {
3315
+ requireArgumentCount(commandName, commandArguments, 0);
3316
+ writeList(await client.list(), parsed.values.json, io.output);
3317
+ } else if (commandName === "status") {
3318
+ const sandboxId = requireSandboxId(commandName, commandArguments);
3319
+ 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
+ } else if (commandName === "destroy") {
3324
+ const sandboxId = requireSandboxId(commandName, commandArguments);
3325
+ await client.destroy(sandboxId);
3326
+ writeDestroyed(sandboxId, parsed.values.json, io.output);
3327
+ } else {
3328
+ await runExec(commandArguments, parsed, client, requestTimeoutMs, io, options);
3329
+ }
3330
+ return true;
3331
+ }
3332
+ async function runExec(commandArguments, parsed, client, requestTimeoutMs, io, options) {
3333
+ const [sandboxId, ...guestArguments] = commandArguments;
3334
+ if (sandboxId === void 0 || guestArguments.length === 0) {
3335
+ throw new TypeError("setsuna exec requires a sandbox ID and command after --.");
3336
+ }
3337
+ const result = await executeWithSignals(
3338
+ client,
3339
+ sandboxId,
3340
+ guestArguments.map(shellQuote).join(" "),
3341
+ {
3342
+ timeoutMs: optionalInteger(parsed.values["timeout-ms"], "--timeout-ms"),
3343
+ requestTimeoutMs
3344
+ },
3345
+ io,
3346
+ options.signalRuntime ?? DEFAULT_SIGNAL_RUNTIME
3347
+ );
3348
+ if (result !== void 0) {
3349
+ writeExecutionResult(result, parsed.values.json, io.output, io.errorOutput, true);
3350
+ }
3351
+ }
3352
+ async function executeWithSignals(client, sandboxId, command, options, io, signalRuntime) {
3353
+ const controller = new AbortController();
3354
+ let interruptedExitCode;
3355
+ const handleSignal = (signal) => {
3356
+ const exitCode = signal === "SIGINT" ? 130 : 143;
3357
+ if (interruptedExitCode === void 0) {
3358
+ interruptedExitCode = exitCode;
3359
+ io.errorOutput.write("Interrupt received; sandbox remains active.\n");
3360
+ controller.abort();
3361
+ } else {
3362
+ signalRuntime.forceExit(exitCode);
3363
+ }
3364
+ };
3365
+ const handleSigint = () => handleSignal("SIGINT");
3366
+ const handleSigterm = () => handleSignal("SIGTERM");
3367
+ signalRuntime.on("SIGINT", handleSigint);
3368
+ signalRuntime.on("SIGTERM", handleSigterm);
3369
+ try {
3370
+ const result = await client.execute(sandboxId, command, {
3371
+ ...options,
3372
+ signal: controller.signal
3373
+ });
3374
+ if (interruptedExitCode === void 0) return result;
3375
+ } catch (error) {
3376
+ if (interruptedExitCode === void 0) throw error;
3377
+ if (!(error instanceof SetsunaError && error.code === "REQUEST_ABORTED")) {
3378
+ io.errorOutput.write(`${formatCliError(error)}
3379
+ `);
3380
+ }
3381
+ } finally {
3382
+ signalRuntime.off("SIGINT", handleSigint);
3383
+ signalRuntime.off("SIGTERM", handleSigterm);
3384
+ }
3385
+ process.exitCode = interruptedExitCode;
3386
+ return void 0;
3387
+ }
3388
+ function requireSandboxId(commandName, arguments_) {
3389
+ requireArgumentCount(commandName, arguments_, 1);
3390
+ const sandboxId = arguments_[0];
3391
+ if (sandboxId === void 0) throw new TypeError(`setsuna ${commandName} requires a sandbox ID.`);
3392
+ return sandboxId;
3393
+ }
3394
+ function requireArgumentCount(commandName, arguments_, expected) {
3395
+ if (arguments_.length === expected) return;
3396
+ if (expected === 0) throw new TypeError(`setsuna ${commandName} does not accept arguments.`);
3397
+ throw new TypeError(`setsuna ${commandName} requires exactly one sandbox ID.`);
3398
+ }
3399
+ function writeLease(lease, json, output, verb) {
3400
+ if (json) output.write(`${JSON.stringify(lease)}
3401
+ `);
3402
+ else {
3403
+ output.write(
3404
+ `${verb} sandbox ${lease.sandboxId} with configuration ${lease.microvmConfiguration.id}; lease expires at ${lease.expiresAt}.
3405
+ `
3406
+ );
3407
+ }
3408
+ }
3409
+ function writeStatus(status, json, output, subject) {
3410
+ if (json) output.write(`${JSON.stringify(status)}
3411
+ `);
3412
+ else output.write(`${subject} ${status.sandboxId} is active until ${status.expiresAt}.
3413
+ `);
3414
+ }
3415
+ function writeList(statuses, json, output) {
3416
+ if (json) {
3417
+ output.write(`${JSON.stringify(statuses)}
3418
+ `);
3419
+ } else if (statuses.length === 0) {
3420
+ output.write("No active sandboxes.\n");
3421
+ } else {
3422
+ for (const status of statuses) {
3423
+ output.write(`${status.sandboxId} active until ${status.expiresAt}
3424
+ `);
3425
+ }
3426
+ }
3427
+ }
3428
+ function writeDestroyed(sandboxId, json, output) {
3429
+ if (json) output.write(`${JSON.stringify({ sandboxId, destroyed: true })}
3430
+ `);
3431
+ else output.write(`Destroyed sandbox ${sandboxId}.
3432
+ `);
3433
+ }
3434
+
3435
+ // ../setsuna/src/cli-client.ts
3436
+ async function runClientCli(commandName, commandArguments, parsed, authenticated, io, options) {
3437
+ const requestTimeoutMs = optionalInteger(
3438
+ parsed.values["request-timeout-ms"],
3439
+ "--request-timeout-ms"
3440
+ );
3441
+ const remote = authenticated ? remoteClientOptions(options.authDependencies) : void 0;
3442
+ const client = new SetsunaClient({
3443
+ serviceUrl: options.serviceUrl,
3444
+ remote,
3445
+ requestTimeoutMs
3446
+ });
3447
+ await runClientCommand(
3448
+ commandName,
3449
+ commandArguments,
3450
+ parsed,
3451
+ client,
3452
+ requestTimeoutMs,
3453
+ io,
3454
+ options
3455
+ );
3456
+ }
3457
+ function remoteClientOptions(providedDependencies) {
3458
+ const authDependencies = providedDependencies ?? defaultAuthRuntimeDependencies();
3459
+ return {
3460
+ getAccessToken: () => getValidAuthAccessToken(authDependencies)
3461
+ };
3462
+ }
3463
+ async function runClientCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options) {
3464
+ if (commandName === "health") {
3465
+ await runHealth(client, io.output, parsed.values.json);
3466
+ return;
3467
+ }
3468
+ if (commandName === "configurations") {
3469
+ if (commandArguments.length !== 0) {
3470
+ throw new TypeError("setsuna configurations does not accept arguments.");
3471
+ }
3472
+ await runConfigurationsCommand(client, parsed.values.json, io.output);
3473
+ return;
3474
+ }
3475
+ if (await runShellCliCommand(
3476
+ commandName,
3477
+ commandArguments,
3478
+ parsed.values.attach,
3479
+ parsed.values.json,
3480
+ client,
3481
+ io,
3482
+ options.shellTerminal
3483
+ ))
3484
+ return;
3485
+ if (commandName === "process") {
3486
+ await runProcessCommand(
3487
+ commandArguments,
3488
+ parsed,
3489
+ client,
3490
+ io,
3491
+ options.signalRuntime ?? DEFAULT_SIGNAL_RUNTIME
3492
+ );
3493
+ return;
3494
+ }
3495
+ if (await runSandboxCommand(
3496
+ commandName,
3497
+ commandArguments,
3498
+ parsed,
3499
+ client,
3500
+ requestTimeoutMs,
3501
+ io,
3502
+ options
3503
+ )) {
3504
+ return;
3505
+ }
3506
+ await runRunCommand(commandName, commandArguments, parsed, client, requestTimeoutMs, io, options);
3507
+ }
3508
+
3509
+ // ../setsuna/src/tui/customer/failure.ts
3510
+ var CUSTOMER_FAILURE = {
3511
+ session: "Your Setsuna session has expired. Sign in again.",
3512
+ forbidden: "This action is not available for your account.",
3513
+ missing: "This microVM is no longer active.",
3514
+ conflict: "This action is no longer available for this microVM.",
3515
+ capacity: "Preview capacity is temporarily busy. Try again shortly.",
3516
+ unreachable: "Setsuna could not be reached. Try again shortly.",
3517
+ unexpected: "Setsuna returned an unexpected response. Try again shortly.",
3518
+ unknown: "Something went wrong while completing that action."
3519
+ };
3520
+ var SIGN_IN_FAILURE = {
3521
+ check: "The saved sign-in could not be checked.",
3522
+ signIn: "Sign-in could not be completed. Try again.",
3523
+ signOut: "Log out could not be completed. Try again."
3524
+ };
3525
+ var FAILURE_BY_CODE = /* @__PURE__ */ new Map([
3526
+ ["UNAUTHORIZED", CUSTOMER_FAILURE.session],
3527
+ ["SANDBOX_NOT_FOUND", CUSTOMER_FAILURE.missing],
3528
+ ["SANDBOX_EXPIRED", CUSTOMER_FAILURE.missing],
3529
+ // The request matched no route: the contract is off, the microVM has not gone anywhere.
3530
+ ["ROUTE_NOT_FOUND", CUSTOMER_FAILURE.unexpected],
3531
+ // A 409 that means every slot is taken, not a conflict with this microVM.
3532
+ ["SANDBOX_BUSY", CUSTOMER_FAILURE.capacity],
3533
+ ["SERVICE_UNREACHABLE", CUSTOMER_FAILURE.unreachable],
3534
+ ["REQUEST_TIMEOUT", CUSTOMER_FAILURE.unreachable],
3535
+ ["INVALID_RESPONSE", CUSTOMER_FAILURE.unexpected]
3536
+ ]);
3537
+ var FAILURE_BY_STATUS = /* @__PURE__ */ new Map([
3538
+ [401, CUSTOMER_FAILURE.session],
3539
+ [403, CUSTOMER_FAILURE.forbidden],
3540
+ [409, CUSTOMER_FAILURE.conflict],
3541
+ [429, CUSTOMER_FAILURE.capacity]
3542
+ ]);
3543
+ function describeCustomerFailure(error) {
3544
+ const { code, status } = failureEvidence(error);
3545
+ if (status !== void 0 && status >= 502 && status <= 504) return CUSTOMER_FAILURE.unreachable;
3546
+ return (code === void 0 ? void 0 : FAILURE_BY_CODE.get(code)) ?? (status === void 0 ? void 0 : FAILURE_BY_STATUS.get(status)) ?? CUSTOMER_FAILURE.unknown;
3547
+ }
3548
+ function primaryFailure(error) {
3549
+ return error instanceof AggregateError ? primaryFailure(error.errors[0]) : error;
3550
+ }
3551
+ function failureEvidence(error) {
3552
+ const primary = primaryFailure(error);
3553
+ if (!(primary instanceof Error)) return {};
3554
+ return {
3555
+ code: "code" in primary && typeof primary.code === "string" ? primary.code : void 0,
3556
+ status: "status" in primary && typeof primary.status === "number" ? primary.status : void 0
3557
+ };
3558
+ }
3559
+
3560
+ // ../setsuna/src/cli-preview-interactive.ts
3561
+ async function runPreviewInteractive(io, options) {
3562
+ const authDependencies = options.authDependencies ?? defaultAuthRuntimeDependencies();
3563
+ const client = new SetsunaClient({
3564
+ remote: { getAccessToken: () => previewAccessToken(authDependencies) }
3565
+ });
3566
+ const promptUi = options.promptUi ?? await loadPromptUi();
3567
+ await runCustomerTui(
3568
+ io,
3569
+ {
3570
+ auth: createPreviewAuthActions(authDependencies),
3571
+ service: createPreviewServiceActions(client),
3572
+ sandbox: customerSafeSandboxActions(
3573
+ options.interactiveActions ?? createSandboxInteractiveActions(
3574
+ client,
3575
+ io,
3576
+ options.signalRuntime ?? DEFAULT_SIGNAL_RUNTIME,
3577
+ options.shellTerminal
3578
+ )
3579
+ )
3580
+ },
3581
+ promptUi
3582
+ );
3583
+ }
3584
+ async function loadPromptUi() {
3585
+ const { loadClackPromptUi: loadClackPromptUi2 } = await Promise.resolve().then(() => (init_clack(), clack_exports));
3586
+ return loadClackPromptUi2({ withGuide: true });
3587
+ }
3588
+ async function previewAccessToken(dependencies) {
3589
+ try {
3590
+ return await getValidAuthAccessToken(dependencies);
3591
+ } catch (error) {
3592
+ if (isSecureStorageNotice(error) || isExpiredSession(error)) throw error;
3593
+ throw new Error("The saved sign-in could not authorize the request.", { cause: error });
3594
+ }
3595
+ }
3596
+ function createPreviewAuthActions(dependencies) {
3597
+ return {
3598
+ status: () => customerSafeAuth(previewAuthStatus(dependencies), SIGN_IN_FAILURE.check),
3599
+ login: (onAuthorization, signal) => customerSafeAuth(
3600
+ loginWithDeviceFlow(dependencies, onAuthorization, signal),
3601
+ SIGN_IN_FAILURE.signIn
3602
+ ),
3603
+ logout: () => customerSafeAuth(logoutAuth(dependencies), SIGN_IN_FAILURE.signOut)
3604
+ };
3605
+ }
3606
+ async function previewAuthStatus(dependencies) {
3607
+ const saved = await inspectAuthStatus(dependencies);
3608
+ if (saved.authenticated) return saved;
3609
+ try {
3610
+ await getValidAuthAccessToken(dependencies);
3611
+ } catch {
3612
+ return saved;
3613
+ }
3614
+ return inspectAuthStatus(dependencies);
3615
+ }
3616
+ async function customerSafeAuth(pending, copy) {
3617
+ try {
3618
+ return await pending;
3619
+ } catch (error) {
3620
+ if (isSecureStorageNotice(error)) throw error;
3621
+ throw new Error(copy, { cause: error });
3622
+ }
3623
+ }
3624
+ function isSecureStorageNotice(error) {
3625
+ return error instanceof Error && error.message === SECURE_STORAGE_UNAVAILABLE;
3626
+ }
3627
+ function isExpiredSession(error) {
3628
+ return error instanceof AuthRefreshError && error.kind === "permanent";
3629
+ }
3630
+ function createPreviewServiceActions(client) {
3631
+ return {
3632
+ async available() {
3633
+ try {
3634
+ await client.health();
3635
+ return true;
3636
+ } catch {
3637
+ return false;
3638
+ }
3639
+ }
3640
+ };
3641
+ }
3642
+ function customerSafeSandboxActions(actions) {
3643
+ return {
3644
+ runCommand: customerSafe(actions.runCommand),
3645
+ microvmConfigurations: customerSafe(actions.microvmConfigurations),
3646
+ startMicrovm: customerSafe(actions.startMicrovm),
3647
+ activeMicrovms: customerSafe(actions.activeMicrovms),
3648
+ openTerminal: customerSafe(actions.openTerminal),
3649
+ terminalProcesses: customerSafe(actions.terminalProcesses),
3650
+ runPersistentCommand: customerSafe(actions.runPersistentCommand),
3651
+ renewMicrovm: customerSafe(actions.renewMicrovm),
3652
+ destroyMicrovm: customerSafe(actions.destroyMicrovm)
3653
+ };
3654
+ }
3655
+ function customerSafe(action) {
3656
+ return async (...arguments_) => {
3657
+ try {
3658
+ return await action(...arguments_);
3659
+ } catch (error) {
3660
+ throw new Error(customerCopy(error), { cause: error });
3661
+ }
3662
+ };
3663
+ }
3664
+ function customerCopy(error) {
3665
+ const primary = primaryFailure(error);
3666
+ if (isSecureStorageNotice(primary)) return SECURE_STORAGE_UNAVAILABLE;
3667
+ if (isExpiredSession(primary)) return CUSTOMER_FAILURE.session;
3668
+ return describeCustomerFailure(error);
3669
+ }
3670
+
3671
+ // ../setsuna/src/cli-preview.ts
3672
+ var PREVIEW_HELP = `Usage:
3673
+ setsuna --help
3674
+ setsuna --version
3675
+ setsuna auth login
3676
+ setsuna auth status [--json]
3677
+ setsuna auth logout [--json]
3678
+ setsuna health [--json]
3679
+ setsuna configurations [--json]
3680
+ setsuna create [--configuration-id ID] [--request-timeout-ms MS] [--json]
3681
+ setsuna list [--request-timeout-ms MS] [--json]
3682
+ setsuna status SANDBOX_ID [--request-timeout-ms MS] [--json]
3683
+ setsuna renew SANDBOX_ID [--request-timeout-ms MS] [--json]
3684
+ setsuna exec SANDBOX_ID [--timeout-ms MS] [--request-timeout-ms MS] [--json] -- COMMAND [ARG...]
3685
+ setsuna destroy SANDBOX_ID [--request-timeout-ms MS] [--json]
3686
+ setsuna shell SANDBOX_ID [--attach PROCESS_ID] [--request-timeout-ms MS]
3687
+ setsuna process spawn SANDBOX_ID [--json] -- ARGV [ARG...]
3688
+ setsuna process list SANDBOX_ID [--json]
3689
+ setsuna process attach SANDBOX_ID PROCESS_ID [--json]
3690
+ setsuna process stdin SANDBOX_ID PROCESS_ID DATA [--json]
3691
+ setsuna process terminate SANDBOX_ID PROCESS_ID [--json]
3692
+ setsuna run [--configuration-id ID] [--timeout-ms MS] [--request-timeout-ms MS] [--json] -- COMMAND [ARG...]
3693
+
3694
+ Commands:
3695
+ auth Log in with Auth0 Device Flow, inspect local credentials, or log out.
3696
+ health Print the Setsuna service health response.
3697
+ configurations List available microVM configurations.
3698
+ create Create a persistent sandbox with a selected or default configuration.
3699
+ list List active persistent sandboxes.
3700
+ status Inspect an active persistent sandbox lease.
3701
+ renew Extend an active persistent sandbox lease.
3702
+ exec Execute a command without destroying the persistent sandbox.
3703
+ destroy Explicitly destroy a persistent sandbox.
3704
+ shell Open an interactive shell in a persistent sandbox, or reattach to one.
3705
+ process Spawn, list, attach, write stdin, or terminate a guest process.
3706
+ run Create a sandbox, execute a command, and always destroy the sandbox.
3707
+
3708
+ `;
3709
+ async function runPreviewCli(args, options = {}) {
3710
+ const io = options.io ?? DEFAULT_IO;
3711
+ const parsed = parsePreviewCliArgs(args);
3712
+ const [commandName, ...commandArguments] = parsed.positionals;
3713
+ if (parsed.values.version) {
3714
+ io.output.write(`${await packageVersion()}
3715
+ `);
3716
+ return;
3717
+ }
3718
+ if (parsed.values.help) {
3719
+ io.output.write(PREVIEW_HELP);
3720
+ return;
3721
+ }
3722
+ if (parsed.positionals.length === 0) {
3723
+ if (args.length === 0 && io.input.isTTY === true && io.output.isTTY === true) {
3724
+ return runPreviewInteractive(io, options);
3725
+ }
3726
+ io.output.write(PREVIEW_HELP);
3727
+ return;
3728
+ }
3729
+ if (commandName === "auth") {
3730
+ return runAuthCommand(commandArguments, parsed, io, options.authDependencies);
3731
+ }
3732
+ await runClientCli(commandName, commandArguments, parsed, true, io, options);
3733
+ }
3734
+ function isDirectExecution() {
3735
+ const entrypoint = process.argv[1];
3736
+ if (entrypoint === void 0) return false;
3737
+ try {
3738
+ return import.meta.url === pathToFileURL(realpathSync(entrypoint)).href;
3739
+ } catch {
3740
+ return import.meta.url === pathToFileURL(entrypoint).href;
3741
+ }
3742
+ }
3743
+ if (isDirectExecution()) {
3744
+ runPreviewCli(process.argv.slice(2)).catch((error) => {
3745
+ process.stderr.write(`${formatCliError(error)}
3746
+ `);
3747
+ process.exitCode = 1;
3748
+ });
3749
+ }
3750
+ export {
3751
+ PREVIEW_HELP,
3752
+ runPreviewCli
3753
+ };