@aisa-one/cli 0.3.0 → 0.5.1

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 (103) hide show
  1. package/CHANGELOG.md +165 -1
  2. package/LICENSE +21 -0
  3. package/README.md +164 -170
  4. package/dist/api.d.ts +10 -0
  5. package/dist/api.js +14 -3
  6. package/dist/catalog.d.ts +1 -18
  7. package/dist/catalog.js +1 -24
  8. package/dist/cli-error.d.ts +17 -0
  9. package/dist/cli-error.js +25 -0
  10. package/dist/cli-guidance.d.ts +14 -0
  11. package/dist/cli-guidance.js +55 -0
  12. package/dist/commands/account.d.ts +17 -0
  13. package/dist/commands/account.js +45 -1
  14. package/dist/commands/api.d.ts +0 -11
  15. package/dist/commands/api.js +6 -175
  16. package/dist/commands/auth.d.ts +2 -1
  17. package/dist/commands/auth.js +45 -11
  18. package/dist/commands/brand-logos.d.ts +6 -0
  19. package/dist/commands/brand-logos.js +21 -0
  20. package/dist/commands/configCmd.js +8 -3
  21. package/dist/commands/connect-shared.d.ts +136 -0
  22. package/dist/commands/connect-shared.js +143 -0
  23. package/dist/commands/connect-t2.d.ts +18 -0
  24. package/dist/commands/connect-t2.js +1599 -0
  25. package/dist/commands/connect-terminal.d.ts +65 -0
  26. package/dist/commands/connect-terminal.js +822 -0
  27. package/dist/commands/connect.d.ts +31 -0
  28. package/dist/commands/connect.js +3181 -0
  29. package/dist/commands/flow.d.ts +685 -0
  30. package/dist/commands/flow.js +697 -0
  31. package/dist/commands/install.d.ts +91 -0
  32. package/dist/commands/install.js +258 -0
  33. package/dist/commands/llm-config.d.ts +103 -0
  34. package/dist/commands/llm-config.js +452 -0
  35. package/dist/commands/manifest.d.ts +68 -0
  36. package/dist/commands/manifest.js +90 -0
  37. package/dist/commands/mcp.d.ts +59 -0
  38. package/dist/commands/mcp.js +122 -44
  39. package/dist/commands/oauth-login.d.ts +33 -0
  40. package/dist/commands/oauth-login.js +333 -0
  41. package/dist/commands/prompt.d.ts +96 -0
  42. package/dist/commands/prompt.js +397 -0
  43. package/dist/commands/serve-results.d.ts +61 -0
  44. package/dist/commands/serve-results.js +245 -0
  45. package/dist/commands/serve-signin.d.ts +28 -0
  46. package/dist/commands/serve-signin.js +90 -0
  47. package/dist/commands/signin-page.d.ts +49 -0
  48. package/dist/commands/signin-page.js +250 -0
  49. package/dist/commands/skills.js +87 -50
  50. package/dist/commands/tool-help.d.ts +69 -0
  51. package/dist/commands/tool-help.js +189 -0
  52. package/dist/commands/tool-input.d.ts +22 -0
  53. package/dist/commands/tool-input.js +220 -0
  54. package/dist/commands/tools.d.ts +12 -0
  55. package/dist/commands/tools.js +306 -0
  56. package/dist/commands/update.d.ts +11 -0
  57. package/dist/commands/update.js +92 -0
  58. package/dist/commands/vscode.d.ts +65 -0
  59. package/dist/commands/vscode.js +173 -0
  60. package/dist/commands/wrappers.d.ts +23 -0
  61. package/dist/commands/wrappers.js +102 -0
  62. package/dist/completion.js +3 -80
  63. package/dist/config.d.ts +3 -0
  64. package/dist/config.js +80 -3
  65. package/dist/constants.d.ts +51 -38
  66. package/dist/constants.js +53 -109
  67. package/dist/index.js +152 -355
  68. package/dist/json-text.d.ts +12 -0
  69. package/dist/json-text.js +60 -0
  70. package/dist/router.d.ts +39 -0
  71. package/dist/router.js +59 -0
  72. package/dist/skills-registry.js +12 -3
  73. package/dist/utils/banner.d.ts +9 -0
  74. package/dist/utils/banner.js +41 -0
  75. package/dist/utils/browser.d.ts +19 -0
  76. package/dist/utils/browser.js +31 -0
  77. package/dist/utils/exec.d.ts +43 -0
  78. package/dist/utils/exec.js +61 -0
  79. package/dist/utils/http.d.ts +53 -0
  80. package/dist/utils/http.js +124 -0
  81. package/dist/utils/install-method.d.ts +36 -0
  82. package/dist/utils/install-method.js +48 -0
  83. package/dist/utils/journal.d.ts +57 -0
  84. package/dist/utils/journal.js +188 -0
  85. package/dist/utils/key-check.d.ts +20 -0
  86. package/dist/utils/key-check.js +24 -0
  87. package/dist/utils/update-check.d.ts +23 -0
  88. package/dist/utils/update-check.js +116 -0
  89. package/dist/utils/width.d.ts +32 -0
  90. package/dist/utils/width.js +73 -0
  91. package/package.json +8 -4
  92. package/vscode-extension/README.md +6 -0
  93. package/vscode-extension/aisa-vscode.vsix +0 -0
  94. package/dist/commands/finance.d.ts +0 -17
  95. package/dist/commands/finance.js +0 -342
  96. package/dist/commands/run.d.ts +0 -28
  97. package/dist/commands/run.js +0 -182
  98. package/dist/commands/search.d.ts +0 -9
  99. package/dist/commands/search.js +0 -81
  100. package/dist/commands/twitter.d.ts +0 -138
  101. package/dist/commands/twitter.js +0 -374
  102. package/dist/commands/video.d.ts +0 -49
  103. package/dist/commands/video.js +0 -311
@@ -0,0 +1,397 @@
1
+ import chalk from "chalk";
2
+ import { cells, padTo, truncate } from "../utils/width.js";
3
+ /**
4
+ * Arrow-key pickers for the connect flow.
5
+ *
6
+ * The first version asked for numbers: "type 3 5 to toggle". It worked, but
7
+ * nothing moved while you thought — you typed into the dark and found out
8
+ * afterwards what you had done. Twenty-five servers is far too many to hold
9
+ * in your head that way.
10
+ *
11
+ * These redraw in place instead, so the cursor and the ticks are where your
12
+ * attention already is.
13
+ *
14
+ * ── Falling back ──────────────────────────────────────────────────────────
15
+ * Raw mode needs a real terminal. Piped input, CI, an editor's embedded
16
+ * console — none of them can deliver an arrow key, so `interactive()` says so
17
+ * and the caller keeps the typed-number path for those. The flow must work in
18
+ * both; this is a nicer way through, not the only one.
19
+ *
20
+ * ── Redrawing ─────────────────────────────────────────────────────────────
21
+ * Every frame is "move the cursor up as many lines as were printed, clear to
22
+ * the bottom, print again". That means the number of lines has to be exact —
23
+ * a wrapped line counts twice and the frame walks up the screen — so the
24
+ * viewport is sized against the window and long labels are cut, not wrapped.
25
+ */
26
+ /**
27
+ * Raw mode, and getting out of it.
28
+ *
29
+ * A process that exits while the terminal is raw leaves the user with no echo
30
+ * and no line editing — a shell that looks hung and is usually fixed by
31
+ * closing the window. That happened, so leaving it is not left to the happy
32
+ * path: the handlers below run on every way out, including a crash.
33
+ */
34
+ let rawDepth = 0;
35
+ let handlersInstalled = false;
36
+ function leaveRaw() {
37
+ if (rawDepth === 0)
38
+ return;
39
+ rawDepth = 0;
40
+ try {
41
+ if (process.stdin.isTTY)
42
+ process.stdin.setRawMode(false);
43
+ process.stdin.resume();
44
+ }
45
+ catch {
46
+ /* the terminal is going away anyway */
47
+ }
48
+ }
49
+ function enterRaw() {
50
+ if (!handlersInstalled) {
51
+ handlersInstalled = true;
52
+ process.on("exit", leaveRaw);
53
+ for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) {
54
+ process.on(sig, () => {
55
+ leaveRaw();
56
+ process.exit(130);
57
+ });
58
+ }
59
+ process.on("uncaughtException", (e) => {
60
+ leaveRaw();
61
+ throw e;
62
+ });
63
+ }
64
+ rawDepth++;
65
+ process.stdin.setRawMode(true);
66
+ process.stdin.resume();
67
+ }
68
+ /** Put the terminal back the way a shell expects it. Safe to call twice. */
69
+ export function restoreTerminal() {
70
+ leaveRaw();
71
+ drain();
72
+ }
73
+ /**
74
+ * Let a child process own the keyboard.
75
+ *
76
+ * leaveRaw resumes stdin, which is right when this process is about to read
77
+ * it again and wrong when something else is. A resumed stream with no `data`
78
+ * listener still pulls bytes off the file descriptor and drops them — so with
79
+ * stdio inherited, every keystroke was a race between this process and the
80
+ * agent it had just started. The user saw it as characters going missing and
81
+ * lines submitting themselves: enter is the keystroke where losing the race
82
+ * has consequences.
83
+ *
84
+ * Paused, this process reads nothing and the child gets all of it. Anything
85
+ * here that wants stdin back takes it — the pickers call enterRaw and the
86
+ * prompts build a fresh readline, and both resume.
87
+ */
88
+ export function handOffStdin() {
89
+ leaveRaw();
90
+ drain();
91
+ try {
92
+ process.stdin.pause();
93
+ }
94
+ catch {
95
+ /* not a stream we own; the child will manage */
96
+ }
97
+ }
98
+ /**
99
+ * Throw away anything typed but not yet consumed.
100
+ *
101
+ * Switching between raw mode and readline leaves whatever was in flight for
102
+ * the other reader: the return that confirmed a picker arrived at the next
103
+ * readline as an empty line, which re-asked, which read the next stray byte —
104
+ * the prompt repeated down the screen and ran into the one after it.
105
+ */
106
+ function drain() {
107
+ try {
108
+ while (process.stdin.read() !== null) {
109
+ /* discard */
110
+ }
111
+ }
112
+ catch {
113
+ /* nothing buffered, or not readable — either is fine */
114
+ }
115
+ }
116
+ /** Can this terminal deliver arrow keys? */
117
+ export function interactive() {
118
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
119
+ }
120
+ function decode(data) {
121
+ return {
122
+ up: data === "" || data === "k",
123
+ down: data === "" || data === "j",
124
+ space: data === " ",
125
+ enter: data === "\r" || data === "\n",
126
+ abort: data === "",
127
+ // Escape leaves the question rather than answering it. It used to share
128
+ // the abort path, where "abort" meant the caller took the highlighted row
129
+ // — so the key that means "no, wait" quietly agreed to whatever the
130
+ // cursor was on. Now it has its own way out and callers decide where it
131
+ // goes: back a step, in the flow that uses it.
132
+ escape: data === "",
133
+ all: data === "a",
134
+ digit: /^[0-9]$/.test(data) ? Number(data) : undefined,
135
+ };
136
+ }
137
+ /** Room for the list, leaving the header and footer their lines. */
138
+ function viewport(total, reserved) {
139
+ const rows = process.stdout.rows || 24;
140
+ return Math.max(3, Math.min(total, rows - reserved));
141
+ }
142
+ /**
143
+ * How many lines the cursor's description gets.
144
+ *
145
+ * Two, and always two whether the row has a description or not. A block that
146
+ * grows and shrinks as you move makes the list itself jump under the cursor,
147
+ * which is a worse cost than a blank line on the rows that have nothing to
148
+ * say — and the viewport has to reserve the space either way.
149
+ */
150
+ const DETAIL_ROWS = 3;
151
+ /** Break to a cell width. Plain text only; details arrive unpainted. */
152
+ function wrapCells(text, width, max) {
153
+ const out = [];
154
+ let line = "";
155
+ for (const word of text.split(/\s+/)) {
156
+ if (!line) {
157
+ line = word;
158
+ continue;
159
+ }
160
+ if (cells(line) + 1 + cells(word) > width) {
161
+ out.push(line);
162
+ if (out.length === max)
163
+ return capped(out, text, width, max);
164
+ line = word;
165
+ }
166
+ else {
167
+ line += " " + word;
168
+ }
169
+ }
170
+ if (line)
171
+ out.push(line);
172
+ return out.slice(0, max);
173
+ }
174
+ /** The last line says there is more, rather than stopping mid-sentence. */
175
+ function capped(out, _text, width, max) {
176
+ // truncate adds its own ellipsis when it cuts; adding a second one here
177
+ // produced "…at four depths, web plus academic searc……".
178
+ const last = truncate(out[max - 1] + " …", Math.max(1, width));
179
+ return [...out.slice(0, max - 1), last];
180
+ }
181
+ function frame(o) {
182
+ const lines = [];
183
+ const width = Math.min(process.stdout.columns || 80, 90) - 6;
184
+ // Size both columns to the content instead of to a constant.
185
+ //
186
+ // The right column was capped at eighteen cells — right for a list of
187
+ // twenty-five servers whose metas are all "29 tools", wrong for the
188
+ // three-row menu that ends a run, where the meta is the sentence saying
189
+ // what the command does. "gpt-5.3-codex via AIsa — your usual codex is
190
+ // untouched" arrived as "gpt-5.3-codex via …", so the row that mattered
191
+ // most was the one least readable.
192
+ //
193
+ // Labels are padded to a common width too, so the metas start in one
194
+ // column rather than wherever each label happened to end.
195
+ // The cap only reserves room for a meta column when there is one. Charging
196
+ // every list twenty-four columns for a column it may not have is what cut
197
+ // "Add AIsa beside it (recommended)" — a four-row menu whose rows carry
198
+ // nothing but their label, paying for a second column that was never drawn.
199
+ const anyMeta = o.choices.some((c) => c.meta);
200
+ const anyTag = o.choices.some((c) => c.tag);
201
+ const labelW = Math.min(Math.max(...o.choices.map((c) => cells(c.label))), Math.max(8, width - (anyMeta ? 24 : 4)));
202
+ // With a tag column the metas need a width of their own, or the tags start
203
+ // wherever each meta happened to end and the run they mark is invisible.
204
+ const metaColW = anyTag ? Math.max(...o.choices.map((c) => cells(c.meta ?? ""))) : 0;
205
+ const metaW = Math.max(10, width - labelW - 6 - (metaColW ? metaColW + 2 : 0));
206
+ for (let i = o.offset; i < Math.min(o.offset + o.rows, o.choices.length); i++) {
207
+ const c = o.choices[i];
208
+ const here = i === o.cursor;
209
+ const on = Boolean(o.selected?.has(i));
210
+ // Four states, four looks. Before this, "where I am" and "what I picked"
211
+ // were drawn the same way — a cyan bar for the cursor and a green tick
212
+ // inside it — so on the row you were standing on there was no telling
213
+ // whether it was ticked. Chosen is a filled green bar; the cursor alone
214
+ // is an outline; the rest recede.
215
+ const mark = o.selected ? (on ? "✓" : " ") : here ? "▶" : " ";
216
+ const body = padTo(truncate(c.label, labelW), labelW);
217
+ const meta = c.meta ? " " + (metaColW ? padTo(c.meta, metaColW) : truncate(c.meta, metaW)) : "";
218
+ const startsHere = i === o.offset || o.choices[i - 1]?.tag !== c.tag;
219
+ const tag = c.tag && startsHere ? " " + truncate(c.tag, metaW) : "";
220
+ const row = ` ${mark} ${body}${meta}${tag} `;
221
+ let painted;
222
+ if (on && here)
223
+ painted = chalk.bgGreen.black.bold(row);
224
+ else if (on)
225
+ painted = chalk.green.bold(row);
226
+ else if (here)
227
+ painted = chalk.bgWhite.black(row);
228
+ else
229
+ painted = " " + mark + " " + body + chalk.gray(meta) + chalk.gray(tag) + " ";
230
+ lines.push(chalk.gray("│") + painted);
231
+ // Under the cursor only. Twenty-five rows each carrying a sentence is
232
+ // twenty-five sentences to read past; one that follows the cursor is a
233
+ // sentence about the thing you are looking at, and moving is already the
234
+ // gesture for "tell me about the next one" — nothing to explain.
235
+ if (here && o.detailWidth) {
236
+ const body = o.choices[i].detail
237
+ ? wrapCells(o.choices[i].detail, o.detailWidth, DETAIL_ROWS)
238
+ : [];
239
+ for (let k = 0; k < DETAIL_ROWS; k++) {
240
+ lines.push(chalk.gray("│ " + (body[k] ?? "")));
241
+ }
242
+ }
243
+ }
244
+ if (o.choices.length > o.rows) {
245
+ lines.push(chalk.gray(`│ ${o.offset + 1}–${Math.min(o.offset + o.rows, o.choices.length)} / ${o.choices.length}`));
246
+ }
247
+ // The keys are the one thing a first-time reader has to see. Dimming them
248
+ // put the only instructions on the screen below everything else in
249
+ // contrast — exactly backwards.
250
+ lines.push(chalk.gray("│ ") + chalk.cyan.bold(o.hint));
251
+ return lines;
252
+ }
253
+ /**
254
+ * Draw a picker until the user commits, or `watch` resolves.
255
+ *
256
+ * `multi` decides whether space toggles and enter confirms a set, or the
257
+ * cursor itself is the answer.
258
+ */
259
+ export async function pick(opts) {
260
+ const selected = new Set(opts.initial ?? []);
261
+ let cursor = opts.cursor ?? (opts.initial?.[0] ?? 0);
262
+ let offset = 0;
263
+ // The block under the cursor is part of the frame, so the viewport has to
264
+ // pay for it — otherwise the frame outgrows the window and the redraw walks
265
+ // up the screen.
266
+ const hasDetail = opts.choices.some((c) => c.detail);
267
+ // Seven of these columns are the gutter this block is printed in; the rest
268
+ // is sentence. The first version left fourteen unused, which cost it most
269
+ // of a line on every wrap.
270
+ const detailWidth = hasDetail ? Math.min(process.stdout.columns || 80, 96) - 9 : 0;
271
+ const rows = viewport(opts.choices.length, 10 + (hasDetail ? DETAIL_ROWS + 1 : 0));
272
+ let printed = 0;
273
+ const sorted = () => [...selected].sort((a, b) => a - b);
274
+ const draw = () => {
275
+ if (cursor < offset)
276
+ offset = cursor;
277
+ if (cursor >= offset + rows)
278
+ offset = cursor - rows + 1;
279
+ const lines = frame({ ...opts, cursor, selected: opts.multi ? selected : undefined, offset, rows, detailWidth });
280
+ if (printed)
281
+ process.stdout.write(`[${printed}A`);
282
+ process.stdout.write(lines.join("\n") + "\n");
283
+ printed = lines.length;
284
+ };
285
+ const stdin = process.stdin;
286
+ stdin.setEncoding("utf8");
287
+ enterRaw();
288
+ draw();
289
+ const watchdog = new AbortController();
290
+ return await new Promise((resolve) => {
291
+ let done = false;
292
+ const finish = (r) => {
293
+ if (done)
294
+ return;
295
+ done = true;
296
+ // Before anything else: whoever is watching stops now.
297
+ watchdog.abort();
298
+ stdin.off("data", onData);
299
+ // Hand the terminal back the way a shell expects it: cooked, flowing,
300
+ // no listener of ours left on it. Restoring the previous mode and
301
+ // pausing was wrong on both counts — the next readline prompt got a
302
+ // stdin that was still raw and no longer flowing, so nothing echoed and
303
+ // nothing was accepted, and the terminal had to be killed to recover.
304
+ leaveRaw();
305
+ resolve(r);
306
+ };
307
+ const onData = (data) => {
308
+ const k = decode(data);
309
+ if (k.abort) {
310
+ // Raw mode swallows the interrupt — the terminal driver hands Ctrl-C
311
+ // over as a byte instead of a signal, so nothing above this ever
312
+ // heard it. Put the terminal back and deliver it, which is what the
313
+ // person pressing it asked for.
314
+ // Deliberately never resolves: the caller would print "you chose X"
315
+ // for whatever the cursor was on, which is not what happened. The
316
+ // interrupt is on its way and every handler for it exits.
317
+ watchdog.abort();
318
+ stdin.off("data", onData);
319
+ leaveRaw();
320
+ process.kill(process.pid, "SIGINT");
321
+ return;
322
+ }
323
+ const moved = () => {
324
+ draw();
325
+ if (!opts.multi)
326
+ opts.onToggle?.([cursor]);
327
+ };
328
+ if (k.escape)
329
+ return finish({ escaped: true });
330
+ if (k.up) {
331
+ cursor = (cursor - 1 + opts.choices.length) % opts.choices.length;
332
+ moved();
333
+ return;
334
+ }
335
+ if (k.down) {
336
+ cursor = (cursor + 1) % opts.choices.length;
337
+ moved();
338
+ return;
339
+ }
340
+ if (opts.multi && k.all) {
341
+ if (selected.size === opts.choices.length)
342
+ selected.clear();
343
+ else
344
+ opts.choices.forEach((_, i) => selected.add(i));
345
+ draw();
346
+ opts.onToggle?.(sorted());
347
+ return;
348
+ }
349
+ if (k.digit !== undefined) {
350
+ // Numbers still work: muscle memory from the version this replaces,
351
+ // and the only way to reach item 12 in one keystroke.
352
+ const i = k.digit === 0 ? 9 : k.digit - 1;
353
+ if (i < opts.choices.length) {
354
+ cursor = i;
355
+ moved();
356
+ }
357
+ return;
358
+ }
359
+ if (opts.multi && k.space) {
360
+ if (selected.has(cursor))
361
+ selected.delete(cursor);
362
+ else
363
+ selected.add(cursor);
364
+ draw();
365
+ opts.onToggle?.(sorted());
366
+ return;
367
+ }
368
+ if (k.enter) {
369
+ if (!opts.multi)
370
+ return finish({ picked: [cursor] });
371
+ return finish({ picked: sorted() });
372
+ }
373
+ };
374
+ stdin.on("data", onData);
375
+ if (opts.watch) {
376
+ void opts
377
+ .watch(watchdog.signal, (indexes) => {
378
+ if (done)
379
+ return;
380
+ selected.clear();
381
+ for (const i of indexes)
382
+ if (i >= 0 && i < opts.choices.length)
383
+ selected.add(i);
384
+ if (!opts.multi && indexes.length)
385
+ cursor = indexes[0];
386
+ draw();
387
+ })
388
+ .then((v) => {
389
+ if (v !== undefined)
390
+ finish({ interrupted: v });
391
+ })
392
+ .catch(() => {
393
+ /* aborted, which is the normal way out */
394
+ });
395
+ }
396
+ });
397
+ }
@@ -0,0 +1,61 @@
1
+ import type { ClientInfo, RunState } from "./connect-shared.js";
2
+ import type { LiveServer } from "./mcp.js";
3
+ import type { Lang } from "./flow.js";
4
+ /**
5
+ * Keeping the results page up after the terminal has been handed back.
6
+ *
7
+ * A shell waits on the process it started, so there is no way to return the
8
+ * prompt and keep serving in the same one: the choice used to be the page or
9
+ * the prompt, and picking Exit gave neither — the run said it was handing the
10
+ * terminal back and then sat on it for half an hour.
11
+ *
12
+ * So the page is handed over instead. The foreground process writes down what
13
+ * the page needs, starts a detached copy of itself that serves only that, and
14
+ * exits. The browser tab keeps working on the same URL because the child
15
+ * takes the same port, which the parent has just released.
16
+ *
17
+ * The child holds no state of its own and does nothing but answer: no MCP
18
+ * writes, no installs, no apply route. Whatever the run did is already done.
19
+ */
20
+ /** Where a detached results server writes down that it exists. */
21
+ export declare function resultsPidPath(): string;
22
+ /**
23
+ * Close a results page left over from an earlier run.
24
+ *
25
+ * Called at the start of every `connect`: the new run is what the user is
26
+ * looking at now, and an older page still answering on some port only invites
27
+ * reading a result that no longer describes this machine.
28
+ */
29
+ export declare function closeStaleResults(): Promise<void>;
30
+ /** How long a superseded page stays readable before it closes. */
31
+ export declare const SUPERSEDE_GRACE_MS: number;
32
+ /** Everything the done page needs, and nothing else. */
33
+ export interface ResultsHandover {
34
+ port: number;
35
+ token: string;
36
+ template: "t1" | "t2";
37
+ lang: Lang;
38
+ keyed: boolean;
39
+ canInstall: boolean;
40
+ servers: LiveServer[];
41
+ clients: ClientInfo[];
42
+ chosenServers: LiveServer[];
43
+ chosenClients: string[];
44
+ state: RunState;
45
+ /** Absolute epoch ms; the child stops serving at this point. */
46
+ until: number;
47
+ }
48
+ /**
49
+ * Hand the page to a process that outlives this terminal.
50
+ *
51
+ * Returns false when the handover could not be started, which the caller
52
+ * should treat as "the page is closing" rather than pretending otherwise.
53
+ */
54
+ export declare function handOverResults(h: ResultsHandover, closeParent: () => void): boolean;
55
+ /**
56
+ * The detached half: serve the done page until its deadline, then stop.
57
+ *
58
+ * Read-only by construction — the routes that change a machine are simply not
59
+ * here, so a page left open cannot start anything.
60
+ */
61
+ export declare function serveResultsAction(file: string): Promise<void>;