@addai/node 0.8.2 → 0.10.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.
- package/README.md +96 -96
- package/dist/autostart-mac.js +30 -30
- package/dist/autostart-win.js +50 -50
- package/dist/cli.js +32 -34
- package/dist/config.d.ts +1 -0
- package/dist/config.js +18 -3
- package/dist/heartbeat.js +46 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +7 -7
- package/dist/open-browser.d.ts +1 -0
- package/dist/open-browser.js +42 -0
- package/dist/pair-destination.d.ts +20 -0
- package/dist/pair-destination.js +59 -0
- package/dist/pairing.d.ts +4 -2
- package/dist/pairing.js +64 -16
- package/dist/request-pump.d.ts +1 -0
- package/dist/request-pump.js +9 -0
- package/dist/tui/dashboard.d.ts +2 -0
- package/dist/tui/dashboard.js +60 -2
- package/dist/tui/pair.d.ts +33 -0
- package/dist/tui/pair.js +178 -0
- package/dist/unpair.d.ts +20 -0
- package/dist/unpair.js +71 -0
- package/package.json +60 -60
- package/scripts/fix-pty-helper.js +28 -28
- package/scripts/precompact-capture.js +292 -292
- package/scripts/probe-tui.mjs +122 -122
- package/scripts/smoke-test.sh +74 -74
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Open a URL in whatever the user calls their browser.
|
|
3
|
+
//
|
|
4
|
+
// No dependency for this. The three commands are stable, and a package that
|
|
5
|
+
// shells out for you is a package that can break pairing on a platform we
|
|
6
|
+
// cannot test from here.
|
|
7
|
+
//
|
|
8
|
+
// Windows deliberately does NOT go through cmd.exe. `start` is a shell
|
|
9
|
+
// builtin, so reaching it means spawning a shell around a URL, and this
|
|
10
|
+
// codebase has already been burned by putting cmd.exe near strings it did not
|
|
11
|
+
// control. rundll32 takes the URL as a plain argument and hands it to the
|
|
12
|
+
// registered handler.
|
|
13
|
+
//
|
|
14
|
+
// Everything here is best-effort by design. A browser that fails to open is
|
|
15
|
+
// not a failed pairing — the terminal still shows the code and the link, and
|
|
16
|
+
// the user carries on by hand. So this never throws and never blocks: it
|
|
17
|
+
// reports whether the launch was ACCEPTED, which is all the caller can
|
|
18
|
+
// honestly say.
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.openBrowser = openBrowser;
|
|
21
|
+
const child_process_1 = require("child_process");
|
|
22
|
+
function openBrowser(url, platform = process.platform) {
|
|
23
|
+
const [cmd, args] = platform === 'darwin' ? ['open', [url]]
|
|
24
|
+
: platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
|
|
25
|
+
: ['xdg-open', [url]];
|
|
26
|
+
try {
|
|
27
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
28
|
+
stdio: 'ignore',
|
|
29
|
+
// The browser outlives us — and on a machine where the daemon is about
|
|
30
|
+
// to take over the terminal, a child holding the TTY would fight it.
|
|
31
|
+
detached: true,
|
|
32
|
+
});
|
|
33
|
+
// A missing xdg-open surfaces here rather than as an unhandled error that
|
|
34
|
+
// takes the daemon down with it.
|
|
35
|
+
child.on('error', () => { });
|
|
36
|
+
child.unref();
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PairDestination {
|
|
2
|
+
/** What to call it on screen. */
|
|
3
|
+
product: string;
|
|
4
|
+
/** Where the user goes to confirm. */
|
|
5
|
+
url: string;
|
|
6
|
+
/** That URL carrying the code, which is what actually gets opened. */
|
|
7
|
+
link(code: string): string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* @param argv process.argv.slice(2)
|
|
11
|
+
* @param env process.env
|
|
12
|
+
*/
|
|
13
|
+
export declare function pairDestination(argv: string[], env?: NodeJS.ProcessEnv): PairDestination;
|
|
14
|
+
export interface BrowserEnv {
|
|
15
|
+
isTTY: boolean;
|
|
16
|
+
platform: NodeJS.Platform | string;
|
|
17
|
+
env: NodeJS.ProcessEnv;
|
|
18
|
+
}
|
|
19
|
+
/** Would opening a browser here actually show the user anything? */
|
|
20
|
+
export declare function canOpenBrowser({ isTTY, platform, env }: BrowserEnv): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Which product this machine is joining, and whether we can open a browser
|
|
3
|
+
// to finish the job.
|
|
4
|
+
//
|
|
5
|
+
// The command the user ran is the answer to the first question. They copied it
|
|
6
|
+
// off a page that already knew — so asking "which product?" in the terminal
|
|
7
|
+
// would be asking them to repeat themselves. `ainode entities` goes to Entity
|
|
8
|
+
// Studio; everything else keeps going to Vault, exactly as it always has.
|
|
9
|
+
//
|
|
10
|
+
// The second question is the one that decides whether pairing feels like
|
|
11
|
+
// magic or like a broken promise. Opening a browser on a machine nobody is
|
|
12
|
+
// sitting at does nothing; opening one over SSH opens it on the wrong
|
|
13
|
+
// computer entirely. Both are common, so both are checked, and when the
|
|
14
|
+
// answer is no the code gets printed like it always did.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.pairDestination = pairDestination;
|
|
17
|
+
exports.canOpenBrowser = canOpenBrowser;
|
|
18
|
+
const config_1 = require("./config");
|
|
19
|
+
const destination = (product, url) => ({
|
|
20
|
+
product,
|
|
21
|
+
url,
|
|
22
|
+
link: (code) => `${url}/${encodeURIComponent(code)}`,
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* @param argv process.argv.slice(2)
|
|
26
|
+
* @param env process.env
|
|
27
|
+
*/
|
|
28
|
+
function pairDestination(argv, env = process.env) {
|
|
29
|
+
const entities = argv[0] === 'entities';
|
|
30
|
+
// A dev override has to move BOTH products or a local Studio ends up
|
|
31
|
+
// pairing against production Vault, which is a confusing way to lose an
|
|
32
|
+
// afternoon.
|
|
33
|
+
const base = env.AINODE_PAIR_BASE?.replace(/\/+$/, '');
|
|
34
|
+
if (base) {
|
|
35
|
+
return entities
|
|
36
|
+
? destination('+Ai Entities', `${base}/connect`)
|
|
37
|
+
: destination('+Ai Vault', `${base}/add/entity`);
|
|
38
|
+
}
|
|
39
|
+
return entities
|
|
40
|
+
? destination('+Ai Entities', config_1.ENTITIES_PAIR_URL)
|
|
41
|
+
: destination('+Ai Vault', config_1.VAULT_PAIR_URL);
|
|
42
|
+
}
|
|
43
|
+
/** Would opening a browser here actually show the user anything? */
|
|
44
|
+
function canOpenBrowser({ isTTY, platform, env }) {
|
|
45
|
+
if (!isTTY)
|
|
46
|
+
return false;
|
|
47
|
+
if (env.AINODE_NO_BROWSER === '1')
|
|
48
|
+
return false;
|
|
49
|
+
// Over SSH the browser would open on the machine being administered, where
|
|
50
|
+
// nobody is looking at it — and the terminal would sit there claiming it
|
|
51
|
+
// had done something helpful.
|
|
52
|
+
if (env.SSH_TTY || env.SSH_CONNECTION)
|
|
53
|
+
return false;
|
|
54
|
+
// A Linux box with no display server has no browser to open. macOS and
|
|
55
|
+
// Windows always have one.
|
|
56
|
+
if (platform === 'linux' && !env.DISPLAY && !env.WAYLAND_DISPLAY)
|
|
57
|
+
return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
package/dist/pairing.d.ts
CHANGED
|
@@ -14,7 +14,9 @@ export declare function requestPairCode(): Promise<string>;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare function awaitPairing(code: string): Promise<FinalizeResult>;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Run the whole first-run flow: ask, open, wait, save.
|
|
18
|
+
*
|
|
19
|
+
* @param argv process.argv.slice(2) — decides which product to join
|
|
18
20
|
*/
|
|
19
|
-
export declare function
|
|
21
|
+
export declare function runPairing(argv: string[]): Promise<FinalizeResult>;
|
|
20
22
|
export {};
|
package/dist/pairing.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// First-run pairing flow. Called when state.json doesn't exist.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// First-run pairing flow. Called when state.json doesn't exist.
|
|
3
|
+
//
|
|
4
|
+
// The machine now does the walking. It asks one question, opens the user's
|
|
5
|
+
// browser at the confirm page carrying its own code, and waits — so the
|
|
6
|
+
// person types nothing and never has to know a code existed. Where that
|
|
7
|
+
// cannot work (no terminal, over SSH, headless Linux) it prints the code and
|
|
8
|
+
// the link exactly as it always did, because those machines are real and are
|
|
9
|
+
// often the ones that most need a node on them.
|
|
10
|
+
//
|
|
11
|
+
// Which product the browser lands on comes from the command the user ran:
|
|
12
|
+
// `ainode entities` goes to Entity Studio, everything else to Vault.
|
|
5
13
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
14
|
if (k2 === undefined) k2 = k;
|
|
7
15
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -38,10 +46,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
47
|
exports.requestPairCode = requestPairCode;
|
|
40
48
|
exports.awaitPairing = awaitPairing;
|
|
41
|
-
exports.
|
|
49
|
+
exports.runPairing = runPairing;
|
|
42
50
|
const os = __importStar(require("os"));
|
|
43
51
|
const supabase_client_1 = require("./supabase-client");
|
|
44
52
|
const store_1 = require("./store");
|
|
53
|
+
const pair_destination_1 = require("./pair-destination");
|
|
54
|
+
const open_browser_1 = require("./open-browser");
|
|
55
|
+
const pair_1 = require("./tui/pair");
|
|
45
56
|
const config_1 = require("./config");
|
|
46
57
|
function platform() {
|
|
47
58
|
const p = process.platform;
|
|
@@ -113,17 +124,54 @@ async function awaitPairing(code) {
|
|
|
113
124
|
throw new Error('pair_timeout');
|
|
114
125
|
}
|
|
115
126
|
/**
|
|
116
|
-
*
|
|
127
|
+
* Run the whole first-run flow: ask, open, wait, save.
|
|
128
|
+
*
|
|
129
|
+
* @param argv process.argv.slice(2) — decides which product to join
|
|
117
130
|
*/
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
async function runPairing(argv) {
|
|
132
|
+
const dest = (0, pair_destination_1.pairDestination)(argv);
|
|
133
|
+
const hostname = os.hostname();
|
|
134
|
+
const interactive = (0, pair_destination_1.canOpenBrowser)({
|
|
135
|
+
isTTY: Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY),
|
|
136
|
+
platform: process.platform,
|
|
137
|
+
env: process.env,
|
|
138
|
+
});
|
|
139
|
+
// Ask BEFORE minting a code. A code starts a 15-minute clock the moment it
|
|
140
|
+
// exists, and someone who walks away at the question should not come back
|
|
141
|
+
// to an expired one.
|
|
142
|
+
const choice = interactive ? await (0, pair_1.promptConnect)({ product: dest.product, hostname }) : 'manual';
|
|
143
|
+
const code = await requestPairCode();
|
|
144
|
+
const link = dest.link(code);
|
|
145
|
+
const opened = choice === 'open' ? (0, open_browser_1.openBrowser)(link) : false;
|
|
146
|
+
let waiting = null;
|
|
147
|
+
if (interactive) {
|
|
148
|
+
waiting = (0, pair_1.renderWaiting)({ product: dest.product, link, code, opened });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Headless: plain lines, no spinner, no cursor games. This output ends up
|
|
152
|
+
// in a log file as often as on a screen.
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(` Connect this machine to ${dest.product}`);
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log(` 1. Open ${dest.url}`);
|
|
157
|
+
console.log(` 2. Enter this code: ${code}`);
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log(` (or open directly: ${link})`);
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(' Waiting for pairing… (expires in 15 minutes)');
|
|
162
|
+
console.log('');
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const result = await awaitPairing(code);
|
|
166
|
+
waiting?.stop();
|
|
167
|
+
if (interactive)
|
|
168
|
+
(0, pair_1.renderConnected)(hostname);
|
|
169
|
+
else
|
|
170
|
+
console.log(` ✓ Connected as ${hostname}`);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
waiting?.stop();
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
129
177
|
}
|
package/dist/request-pump.d.ts
CHANGED
package/dist/request-pump.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// arrives.
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.inflightCount = inflightCount;
|
|
8
|
+
exports.activeRequestIdList = activeRequestIdList;
|
|
8
9
|
exports.start = start;
|
|
9
10
|
exports.stop = stop;
|
|
10
11
|
exports.drain = drain;
|
|
@@ -59,6 +60,14 @@ let lastHealthBlockAt = 0;
|
|
|
59
60
|
// Exported so /stats can surface "pump stuck at N inflight" without
|
|
60
61
|
// having to guess.
|
|
61
62
|
function inflightCount() { return inflight; }
|
|
63
|
+
// The runs this daemon is holding a child process for, right now. This is the
|
|
64
|
+
// only honest answer to "is that run alive?" — the server used to infer it from
|
|
65
|
+
// how recently the agent emitted an event, which measures how chatty the agent
|
|
66
|
+
// is, not whether it is alive. A grok turn routinely goes ten minutes between
|
|
67
|
+
// its last streamed token and turn_complete, and the server was failing those
|
|
68
|
+
// runs mid-flight and apologising to the user for a reply that had already
|
|
69
|
+
// arrived. Reported every 30s by the heartbeat.
|
|
70
|
+
function activeRequestIdList() { return [...activeRequestIds]; }
|
|
62
71
|
async function tick() {
|
|
63
72
|
if (stopped)
|
|
64
73
|
return;
|
package/dist/tui/dashboard.d.ts
CHANGED
package/dist/tui/dashboard.js
CHANGED
|
@@ -19,6 +19,8 @@ exports.createDashboardScreen = createDashboardScreen;
|
|
|
19
19
|
const render_1 = require("./render");
|
|
20
20
|
const request_row_1 = require("./request-row");
|
|
21
21
|
const app_1 = require("./app");
|
|
22
|
+
const unpair_1 = require("../unpair");
|
|
23
|
+
const store_1 = require("../store");
|
|
22
24
|
const autostart_1 = require("../autostart");
|
|
23
25
|
/** The landing screen's menu. */
|
|
24
26
|
exports.MENU = [
|
|
@@ -71,7 +73,7 @@ function headerLines(st) {
|
|
|
71
73
|
if (!st.paired) {
|
|
72
74
|
return [
|
|
73
75
|
(0, render_1.yellow)('Not paired'),
|
|
74
|
-
(0, render_1.dim)('run `ainode`
|
|
76
|
+
(0, render_1.dim)('run `ainode` to pair again — it opens your browser to finish up'),
|
|
75
77
|
];
|
|
76
78
|
}
|
|
77
79
|
const self = st.self;
|
|
@@ -174,7 +176,13 @@ function nowLines(st, width, rows) {
|
|
|
174
176
|
function renderDashboard(st, width, height) {
|
|
175
177
|
const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`@addai/node v${st.version}`)}`, headerLines(st), width);
|
|
176
178
|
if (!st.paired) {
|
|
177
|
-
|
|
179
|
+
// The note has to survive the transition into this screen. Disconnecting
|
|
180
|
+
// flips `paired` to false, and without this the sentence saying how it
|
|
181
|
+
// went — including "+Ai could not be told" — was rendered for a frame and
|
|
182
|
+
// then replaced by a screen that had no room for it. The outcome of the
|
|
183
|
+
// one destructive action in here must not be the thing you cannot read.
|
|
184
|
+
const note = st.autostartNote ? ['', ` ${st.autostartNote}`] : [];
|
|
185
|
+
return [...head, ...note, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
|
|
178
186
|
}
|
|
179
187
|
const stats = statsLines(st).map(l => ` ${l}`);
|
|
180
188
|
// The shortcut letters line up in a column of their own, so the eye can
|
|
@@ -198,6 +206,10 @@ function renderDashboard(st, width, height) {
|
|
|
198
206
|
{ keys: '?', label: 'keys' },
|
|
199
207
|
{ keys: 'q', label: 'quit' },
|
|
200
208
|
]);
|
|
209
|
+
// 'd' is deliberately absent from this row. The footer is the set of things
|
|
210
|
+
// it is safe to press to find out what they do, and disconnect is not one
|
|
211
|
+
// of them; it lives in the `?` list, where you go when you are looking for
|
|
212
|
+
// something specific.
|
|
201
213
|
// Everything but the NOW band is fixed height, so the band takes what is
|
|
202
214
|
// left: heading + header row + rows + the "+N more" line.
|
|
203
215
|
const fixed = head.length + 1 + stats.length + 1 + 3 + 1 + menu.length + 1 + 1;
|
|
@@ -277,6 +289,40 @@ function createDashboardScreen(deps) {
|
|
|
277
289
|
// line — a note that never clears becomes furniture.
|
|
278
290
|
setTimeout(() => { st.autostartNote = null; deps.host.redraw(); }, 8000).unref?.();
|
|
279
291
|
};
|
|
292
|
+
/* ── disconnect ────────────────────────────────────────────────────────
|
|
293
|
+
The one key in here that takes work away from people, so it is armed
|
|
294
|
+
rather than instant: the first press asks, naming what it will cancel,
|
|
295
|
+
and the second does it. Any other key disarms, which means a stray 'd'
|
|
296
|
+
costs nothing.
|
|
297
|
+
|
|
298
|
+
Afterwards the daemon has no token and cannot do anything useful, so the
|
|
299
|
+
console closes with it — except in viewer mode, where the daemon is a
|
|
300
|
+
different process this console does not own and can only report on. */
|
|
301
|
+
const disconnect = async () => {
|
|
302
|
+
if (!st.disconnectArmed) {
|
|
303
|
+
st.disconnectArmed = true;
|
|
304
|
+
st.autostartNote = `Disconnect this node? ${(0, unpair_1.describeDisconnect)(st.inflight)} Press d again to confirm.`;
|
|
305
|
+
deps.host.redraw();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
st.disconnectArmed = false;
|
|
309
|
+
st.autostartNote = 'Disconnecting…';
|
|
310
|
+
deps.host.redraw();
|
|
311
|
+
await new Promise(r => setTimeout(r, 0));
|
|
312
|
+
const r = await (0, unpair_1.disconnectNode)();
|
|
313
|
+
st.paired = (0, store_1.isPaired)();
|
|
314
|
+
st.autostartNote = (0, unpair_1.summariseDisconnect)(r);
|
|
315
|
+
deps.host.redraw();
|
|
316
|
+
if (!r.ok)
|
|
317
|
+
return;
|
|
318
|
+
if (st.viewerMode) {
|
|
319
|
+
st.autostartNote = `${(0, unpair_1.summariseDisconnect)(r)} The daemon (pid ${st.pid ?? '?'}) is still running — stop it to finish.`;
|
|
320
|
+
deps.host.redraw();
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
// Let the sentence land before the screen goes away.
|
|
324
|
+
setTimeout(() => deps.host.quit(), 1600).unref?.();
|
|
325
|
+
};
|
|
280
326
|
/**
|
|
281
327
|
* One cursor over two stacked lists: the NOW band sits above the menu, so
|
|
282
328
|
* ↑ off the top of the menu lands on the LAST live row (the one nearest the
|
|
@@ -337,6 +383,7 @@ function createDashboardScreen(deps) {
|
|
|
337
383
|
{ keys: 'l', label: 'logs — daemon output' },
|
|
338
384
|
{ keys: 's', label: 'start this node at login (on / off)' },
|
|
339
385
|
{ keys: 'r', label: 'refresh now' },
|
|
386
|
+
{ keys: 'd', label: 'disconnect this node from +Ai' },
|
|
340
387
|
],
|
|
341
388
|
async onKey(key) {
|
|
342
389
|
if (key.name === 'up' || key.name === 'k') {
|
|
@@ -369,6 +416,17 @@ function createDashboardScreen(deps) {
|
|
|
369
416
|
await refresh();
|
|
370
417
|
return;
|
|
371
418
|
}
|
|
419
|
+
if (key.name === 'd') {
|
|
420
|
+
await disconnect();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
// Anything else disarms a pending disconnect: an armed destructive
|
|
424
|
+
// action must not survive the user walking away and coming back.
|
|
425
|
+
if (st.disconnectArmed) {
|
|
426
|
+
st.disconnectArmed = false;
|
|
427
|
+
st.autostartNote = null;
|
|
428
|
+
deps.host.redraw();
|
|
429
|
+
}
|
|
372
430
|
if (key.name === 'return') {
|
|
373
431
|
// A selected run wins over the menu: the cursor is visibly on it.
|
|
374
432
|
const live = selectableNow(st)[nowIndex(st)];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PairChoice = 'open' | 'manual';
|
|
2
|
+
interface PromptOpts {
|
|
3
|
+
/** '+Ai Entities' — what the user thinks they are joining. */
|
|
4
|
+
product: string;
|
|
5
|
+
hostname: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Ask whether to open the browser. Resolves the user's answer; never rejects.
|
|
9
|
+
*
|
|
10
|
+
* Ctrl-C here means "I did not want to do this at all", so it exits rather
|
|
11
|
+
* than falling through to a code the user never asked for.
|
|
12
|
+
*/
|
|
13
|
+
export declare function promptConnect({ product, hostname }: PromptOpts): Promise<PairChoice>;
|
|
14
|
+
export interface WaitingHandle {
|
|
15
|
+
/** Stop the spinner and leave the block on screen. */
|
|
16
|
+
stop(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The block that stays up while the daemon waits to be claimed.
|
|
20
|
+
*
|
|
21
|
+
* It shows the code and the link even when the browser opened fine. That is
|
|
22
|
+
* not clutter — a browser that opened on the wrong profile, or behind another
|
|
23
|
+
* window, is invisible from here, and the only recovery is the thing this
|
|
24
|
+
* block is holding.
|
|
25
|
+
*/
|
|
26
|
+
export declare function renderWaiting(opts: {
|
|
27
|
+
product: string;
|
|
28
|
+
link: string;
|
|
29
|
+
code: string;
|
|
30
|
+
opened: boolean;
|
|
31
|
+
}): WaitingHandle;
|
|
32
|
+
export declare function renderConnected(name: string): void;
|
|
33
|
+
export {};
|
package/dist/tui/pair.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The first thing anyone sees: one question, then the browser opens.
|
|
3
|
+
//
|
|
4
|
+
// This is not a screen on the console stack, and deliberately so. That stack
|
|
5
|
+
// owns the alt screen and exits the process when it quits — both wrong here.
|
|
6
|
+
// Pairing has to hand back to a daemon that then runs for months, and the code
|
|
7
|
+
// and link have to stay in scrollback afterwards, because the most likely
|
|
8
|
+
// reason someone is still reading this block is that the browser didn't open.
|
|
9
|
+
//
|
|
10
|
+
// So: no alt screen, no takeover. A question that repaints in place, then a
|
|
11
|
+
// block that stays on the terminal and a spinner line that updates until the
|
|
12
|
+
// machine is through.
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.promptConnect = promptConnect;
|
|
48
|
+
exports.renderWaiting = renderWaiting;
|
|
49
|
+
exports.renderConnected = renderConnected;
|
|
50
|
+
const readline = __importStar(require("readline"));
|
|
51
|
+
const render_1 = require("./render");
|
|
52
|
+
const E = '\x1b[';
|
|
53
|
+
const write = (s) => { process.stdout.write(s); };
|
|
54
|
+
/** Move up n lines and clear from there down — how a block repaints in place. */
|
|
55
|
+
const rewind = (n) => { if (n > 0)
|
|
56
|
+
write(`${E}${n}A${E}0J`); };
|
|
57
|
+
const CHOICES = [
|
|
58
|
+
{ id: 'open', label: 'Yes, open my browser' },
|
|
59
|
+
{ id: 'manual', label: 'No, show me a code instead' },
|
|
60
|
+
];
|
|
61
|
+
/**
|
|
62
|
+
* Ask whether to open the browser. Resolves the user's answer; never rejects.
|
|
63
|
+
*
|
|
64
|
+
* Ctrl-C here means "I did not want to do this at all", so it exits rather
|
|
65
|
+
* than falling through to a code the user never asked for.
|
|
66
|
+
*/
|
|
67
|
+
function promptConnect({ product, hostname }) {
|
|
68
|
+
return new Promise(resolve => {
|
|
69
|
+
let sel = 0;
|
|
70
|
+
let painted = 0;
|
|
71
|
+
const frame = () => [
|
|
72
|
+
'',
|
|
73
|
+
` ${(0, render_1.bold)((0, render_1.cyan)('✦'))} ${(0, render_1.bold)('+Ai Node')}`,
|
|
74
|
+
'',
|
|
75
|
+
` Connect ${(0, render_1.bold)(hostname)} to ${(0, render_1.bold)(product)}?`,
|
|
76
|
+
'',
|
|
77
|
+
...CHOICES.map((c, i) => i === sel ? ` ${(0, render_1.green)('▸')} ${c.label}` : ` ${(0, render_1.grey)(c.label)}`),
|
|
78
|
+
'',
|
|
79
|
+
` ${(0, render_1.dim)('↑↓ to choose · ⏎ to continue · ^C to cancel')}`,
|
|
80
|
+
'',
|
|
81
|
+
];
|
|
82
|
+
const paint = () => {
|
|
83
|
+
rewind(painted);
|
|
84
|
+
const lines = frame();
|
|
85
|
+
write(lines.join('\n') + '\n');
|
|
86
|
+
painted = lines.length;
|
|
87
|
+
};
|
|
88
|
+
const done = (choice) => {
|
|
89
|
+
rewind(painted);
|
|
90
|
+
process.stdin.off('keypress', onKey);
|
|
91
|
+
if (process.stdin.isTTY)
|
|
92
|
+
process.stdin.setRawMode(false);
|
|
93
|
+
process.stdin.pause();
|
|
94
|
+
resolve(choice);
|
|
95
|
+
};
|
|
96
|
+
function onKey(_s, key) {
|
|
97
|
+
if (!key)
|
|
98
|
+
return;
|
|
99
|
+
if (key.ctrl && key.name === 'c') {
|
|
100
|
+
rewind(painted);
|
|
101
|
+
write(` ${(0, render_1.dim)('Cancelled. Run the command again when you are ready.')}\n\n`);
|
|
102
|
+
process.exit(130);
|
|
103
|
+
}
|
|
104
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
105
|
+
sel = (sel + CHOICES.length - 1) % CHOICES.length;
|
|
106
|
+
paint();
|
|
107
|
+
}
|
|
108
|
+
else if (key.name === 'down' || key.name === 'j') {
|
|
109
|
+
sel = (sel + 1) % CHOICES.length;
|
|
110
|
+
paint();
|
|
111
|
+
}
|
|
112
|
+
else if (key.name === 'y')
|
|
113
|
+
done('open');
|
|
114
|
+
else if (key.name === 'n')
|
|
115
|
+
done('manual');
|
|
116
|
+
else if (key.name === 'return' || key.name === 'space')
|
|
117
|
+
done(CHOICES[sel].id);
|
|
118
|
+
}
|
|
119
|
+
readline.emitKeypressEvents(process.stdin);
|
|
120
|
+
if (process.stdin.isTTY)
|
|
121
|
+
process.stdin.setRawMode(true);
|
|
122
|
+
process.stdin.resume();
|
|
123
|
+
process.stdin.on('keypress', onKey);
|
|
124
|
+
paint();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The block that stays up while the daemon waits to be claimed.
|
|
129
|
+
*
|
|
130
|
+
* It shows the code and the link even when the browser opened fine. That is
|
|
131
|
+
* not clutter — a browser that opened on the wrong profile, or behind another
|
|
132
|
+
* window, is invisible from here, and the only recovery is the thing this
|
|
133
|
+
* block is holding.
|
|
134
|
+
*/
|
|
135
|
+
function renderWaiting(opts) {
|
|
136
|
+
const { product, link, code, opened } = opts;
|
|
137
|
+
write('\n');
|
|
138
|
+
if (opened) {
|
|
139
|
+
write(` ${(0, render_1.green)('✦')} ${(0, render_1.bold)(`Waiting in your browser…`)}\n`);
|
|
140
|
+
write('\n');
|
|
141
|
+
write(` ${(0, render_1.dim)('Nothing opened? Go to')} ${(0, render_1.cyan)(link)}\n`);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
write(` ${(0, render_1.bold)(`Connect this machine to ${product}`)}\n`);
|
|
145
|
+
write('\n');
|
|
146
|
+
write(` ${(0, render_1.dim)('1.')} Open ${(0, render_1.cyan)(link.replace(`/${code}`, ''))}\n`);
|
|
147
|
+
write(` ${(0, render_1.dim)('2.')} Enter this code: ${(0, render_1.bold)((0, render_1.green)(code))}\n`);
|
|
148
|
+
write('\n');
|
|
149
|
+
write(` ${(0, render_1.dim)('or open this directly:')} ${(0, render_1.cyan)(link)}\n`);
|
|
150
|
+
}
|
|
151
|
+
if (opened)
|
|
152
|
+
write(` ${(0, render_1.dim)('or enter this code:')} ${(0, render_1.bold)((0, render_1.green)(code))}\n`);
|
|
153
|
+
write('\n');
|
|
154
|
+
let n = 0;
|
|
155
|
+
let painted = false;
|
|
156
|
+
const line = () => ` ${(0, render_1.green)(render_1.SPIN[n % render_1.SPIN.length])} ${(0, render_1.dim)('waiting for you to confirm · expires in 15 minutes')}`;
|
|
157
|
+
const tick = () => {
|
|
158
|
+
if (painted)
|
|
159
|
+
rewind(1);
|
|
160
|
+
write(line() + '\n');
|
|
161
|
+
painted = true;
|
|
162
|
+
n++;
|
|
163
|
+
};
|
|
164
|
+
tick();
|
|
165
|
+
const timer = setInterval(tick, 120);
|
|
166
|
+
// A spinner is not a reason to keep the process alive.
|
|
167
|
+
timer.unref?.();
|
|
168
|
+
return {
|
|
169
|
+
stop() {
|
|
170
|
+
clearInterval(timer);
|
|
171
|
+
if (painted)
|
|
172
|
+
rewind(1);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function renderConnected(name) {
|
|
177
|
+
write(` ${(0, render_1.green)('✓')} ${(0, render_1.bold)('Connected')} ${(0, render_1.dim)('as')} ${name}\n\n`);
|
|
178
|
+
}
|
package/dist/unpair.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface DisconnectResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
cancelled: number;
|
|
4
|
+
/** The local pairing was cleared but +Ai was not told. */
|
|
5
|
+
serverFailed?: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
/** The question to ask before doing it. */
|
|
9
|
+
export declare function describeDisconnect(inflight: number): string;
|
|
10
|
+
/** What to say once it is done. */
|
|
11
|
+
export declare function summariseDisconnect(r: DisconnectResult): string;
|
|
12
|
+
/**
|
|
13
|
+
* Tell +Ai, then forget the pairing locally.
|
|
14
|
+
*
|
|
15
|
+
* Local cleanup happens even when the server call fails. A node whose token
|
|
16
|
+
* the user has decided to abandon should not keep using it because the network
|
|
17
|
+
* was down at the wrong moment — and the alternative, refusing to disconnect,
|
|
18
|
+
* strands them on a machine they may be trying to hand back.
|
|
19
|
+
*/
|
|
20
|
+
export declare function disconnectNode(): Promise<DisconnectResult>;
|