@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.
- package/CHANGELOG.md +165 -1
- package/LICENSE +21 -0
- package/README.md +164 -170
- package/dist/api.d.ts +10 -0
- package/dist/api.js +14 -3
- package/dist/catalog.d.ts +1 -18
- package/dist/catalog.js +1 -24
- package/dist/cli-error.d.ts +17 -0
- package/dist/cli-error.js +25 -0
- package/dist/cli-guidance.d.ts +14 -0
- package/dist/cli-guidance.js +55 -0
- package/dist/commands/account.d.ts +17 -0
- package/dist/commands/account.js +45 -1
- package/dist/commands/api.d.ts +0 -11
- package/dist/commands/api.js +6 -175
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +45 -11
- package/dist/commands/brand-logos.d.ts +6 -0
- package/dist/commands/brand-logos.js +21 -0
- package/dist/commands/configCmd.js +8 -3
- package/dist/commands/connect-shared.d.ts +136 -0
- package/dist/commands/connect-shared.js +143 -0
- package/dist/commands/connect-t2.d.ts +18 -0
- package/dist/commands/connect-t2.js +1599 -0
- package/dist/commands/connect-terminal.d.ts +65 -0
- package/dist/commands/connect-terminal.js +822 -0
- package/dist/commands/connect.d.ts +31 -0
- package/dist/commands/connect.js +3181 -0
- package/dist/commands/flow.d.ts +685 -0
- package/dist/commands/flow.js +697 -0
- package/dist/commands/install.d.ts +91 -0
- package/dist/commands/install.js +258 -0
- package/dist/commands/llm-config.d.ts +103 -0
- package/dist/commands/llm-config.js +452 -0
- package/dist/commands/manifest.d.ts +68 -0
- package/dist/commands/manifest.js +90 -0
- package/dist/commands/mcp.d.ts +59 -0
- package/dist/commands/mcp.js +122 -44
- package/dist/commands/oauth-login.d.ts +33 -0
- package/dist/commands/oauth-login.js +333 -0
- package/dist/commands/prompt.d.ts +96 -0
- package/dist/commands/prompt.js +397 -0
- package/dist/commands/serve-results.d.ts +61 -0
- package/dist/commands/serve-results.js +245 -0
- package/dist/commands/serve-signin.d.ts +28 -0
- package/dist/commands/serve-signin.js +90 -0
- package/dist/commands/signin-page.d.ts +49 -0
- package/dist/commands/signin-page.js +250 -0
- package/dist/commands/skills.js +87 -50
- package/dist/commands/tool-help.d.ts +69 -0
- package/dist/commands/tool-help.js +189 -0
- package/dist/commands/tool-input.d.ts +22 -0
- package/dist/commands/tool-input.js +220 -0
- package/dist/commands/tools.d.ts +12 -0
- package/dist/commands/tools.js +306 -0
- package/dist/commands/update.d.ts +11 -0
- package/dist/commands/update.js +92 -0
- package/dist/commands/vscode.d.ts +65 -0
- package/dist/commands/vscode.js +173 -0
- package/dist/commands/wrappers.d.ts +23 -0
- package/dist/commands/wrappers.js +102 -0
- package/dist/completion.js +3 -80
- package/dist/config.d.ts +3 -0
- package/dist/config.js +80 -3
- package/dist/constants.d.ts +51 -38
- package/dist/constants.js +53 -109
- package/dist/index.js +152 -355
- package/dist/json-text.d.ts +12 -0
- package/dist/json-text.js +60 -0
- package/dist/router.d.ts +39 -0
- package/dist/router.js +59 -0
- package/dist/skills-registry.js +12 -3
- package/dist/utils/banner.d.ts +9 -0
- package/dist/utils/banner.js +41 -0
- package/dist/utils/browser.d.ts +19 -0
- package/dist/utils/browser.js +31 -0
- package/dist/utils/exec.d.ts +43 -0
- package/dist/utils/exec.js +61 -0
- package/dist/utils/http.d.ts +53 -0
- package/dist/utils/http.js +124 -0
- package/dist/utils/install-method.d.ts +36 -0
- package/dist/utils/install-method.js +48 -0
- package/dist/utils/journal.d.ts +57 -0
- package/dist/utils/journal.js +188 -0
- package/dist/utils/key-check.d.ts +20 -0
- package/dist/utils/key-check.js +24 -0
- package/dist/utils/update-check.d.ts +23 -0
- package/dist/utils/update-check.js +116 -0
- package/dist/utils/width.d.ts +32 -0
- package/dist/utils/width.js +73 -0
- package/package.json +8 -4
- package/vscode-extension/README.md +6 -0
- package/vscode-extension/aisa-vscode.vsix +0 -0
- package/dist/commands/finance.d.ts +0 -17
- package/dist/commands/finance.js +0 -342
- package/dist/commands/run.d.ts +0 -28
- package/dist/commands/run.js +0 -182
- package/dist/commands/search.d.ts +0 -9
- package/dist/commands/search.js +0 -81
- package/dist/commands/twitter.d.ts +0 -138
- package/dist/commands/twitter.js +0 -374
- package/dist/commands/video.d.ts +0 -49
- 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 === "[A" || data === "k",
|
|
123
|
+
down: data === "[B" || 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[0J`);
|
|
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>;
|