@addai/node 0.4.0 → 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.
- package/dist/cli.js +2 -3
- package/dist/lockfile.d.ts +43 -3
- package/dist/lockfile.js +114 -38
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +4 -1
- package/dist/tui/app.d.ts +24 -2
- package/dist/tui/app.js +85 -16
- package/dist/tui/console-capture.d.ts +8 -3
- package/dist/tui/console-capture.js +73 -15
- package/dist/tui/dashboard.d.ts +12 -12
- package/dist/tui/dashboard.js +114 -111
- package/dist/tui/harnesses.d.ts +50 -13
- package/dist/tui/harnesses.js +176 -171
- package/dist/tui/logs.d.ts +18 -0
- package/dist/tui/logs.js +157 -0
- package/dist/tui/render.d.ts +40 -0
- package/dist/tui/render.js +99 -0
- package/dist/tui/request-row.d.ts +29 -0
- package/dist/tui/request-row.js +133 -0
- package/dist/tui/requests.d.ts +10 -2
- package/dist/tui/requests.js +110 -18
- package/dist/tui/run.d.ts +2 -2
- package/dist/tui/run.js +146 -85
- package/dist/tui/transcript.d.ts +3 -2
- package/dist/tui/transcript.js +39 -15
- package/package.json +1 -1
- package/scripts/probe-tui.mjs +122 -0
- package/dist/tui.d.ts +0 -1
- package/dist/tui.js +0 -314
package/dist/tui/harnesses.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
3
|
-
// Claude Code terminal idiom: alt-screen, rounded panels, dim chrome,
|
|
4
|
-
// ❯ selection, braille spinner, ⏺ status dots. Hand-rolled ANSI — the
|
|
5
|
-
// daemon deliberately has no TUI framework dependency.
|
|
2
|
+
// Which agent CLIs this box has, and who they are logged in as.
|
|
6
3
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
4
|
+
// This used to be a second, parallel TUI with its own ANSI helpers, its own
|
|
5
|
+
// panel, its own key loop and its own alt screen; entering it from the
|
|
6
|
+
// dashboard suspended the whole app and the two fought over the terminal.
|
|
7
|
+
// It is now a screen on the stack like any other, so it inherits the diff
|
|
8
|
+
// painter, the footer grammar and the `?` help for free.
|
|
9
|
+
//
|
|
10
|
+
// One thing genuinely does need the raw terminal: a harness login runs the
|
|
11
|
+
// vendor's own CLI flow, which draws its own prompts. That still leaves the
|
|
12
|
+
// alt screen and hands the TTY over — but as a scoped suspend, not as a
|
|
13
|
+
// parallel application.
|
|
11
14
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
15
|
if (k2 === undefined) k2 = k;
|
|
13
16
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -42,48 +45,28 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
45
|
};
|
|
43
46
|
})();
|
|
44
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.
|
|
48
|
+
exports.actionFor = actionFor;
|
|
49
|
+
exports.formatVersion = formatVersion;
|
|
50
|
+
exports.accountLabel = accountLabel;
|
|
51
|
+
exports.harnessColumns = harnessColumns;
|
|
52
|
+
exports.harnessRow = harnessRow;
|
|
53
|
+
exports.renderHarnesses = renderHarnesses;
|
|
54
|
+
exports.logoutSelected = logoutSelected;
|
|
55
|
+
exports.createHarnessesScreen = createHarnessesScreen;
|
|
56
|
+
exports.plainStatus = plainStatus;
|
|
46
57
|
const child_process_1 = require("child_process");
|
|
47
|
-
const readline = __importStar(require("readline"));
|
|
48
|
-
const capabilities_1 = require("../capabilities");
|
|
49
|
-
const win_1 = require("../win");
|
|
50
58
|
const fs = __importStar(require("fs"));
|
|
51
59
|
const os = __importStar(require("os"));
|
|
52
60
|
const path = __importStar(require("path"));
|
|
61
|
+
const capabilities_1 = require("../capabilities");
|
|
62
|
+
const win_1 = require("../win");
|
|
53
63
|
const harness_registry_1 = require("../harness-registry");
|
|
54
64
|
const render_1 = require("./render");
|
|
55
|
-
|
|
56
|
-
const ESC = '\x1b[';
|
|
57
|
-
const dim = (s) => `${ESC}2m${s}${ESC}22m`;
|
|
58
|
-
const bold = (s) => `${ESC}1m${s}${ESC}22m`;
|
|
59
|
-
const fg = (n, s) => `${ESC}38;5;${n}m${s}${ESC}39m`;
|
|
60
|
-
const green = (s) => fg(114, s);
|
|
61
|
-
const yellow = (s) => fg(179, s);
|
|
62
|
-
const grey = (s) => fg(244, s);
|
|
63
|
-
const cyan = (s) => fg(80, s);
|
|
64
|
-
const altOn = () => process.stdout.write(`${ESC}?1049h${ESC}?25l`);
|
|
65
|
-
const altOff = () => process.stdout.write(`${ESC}?1049l${ESC}?25h`);
|
|
66
|
-
const home = () => process.stdout.write(`${ESC}H${ESC}2J`);
|
|
67
|
-
const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
68
|
-
const width = () => Math.min(process.stdout.columns || 100, 110);
|
|
69
|
-
function panel(title, lines) {
|
|
70
|
-
const w = width() - 2;
|
|
71
|
-
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
72
|
-
const top = `${dim('╭─')} ${bold(title)} ${dim('─'.repeat(Math.max(1, w - strip(title).length - 4)) + '╮')}`;
|
|
73
|
-
const body = lines.map(raw => {
|
|
74
|
-
// Rows are wider than a narrow terminal: without a cut they run past
|
|
75
|
-
// the right border and the panel frame comes apart.
|
|
76
|
-
const l = (0, render_1.truncate)(raw, w - 2);
|
|
77
|
-
const pad = Math.max(0, w - 2 - strip(l).length);
|
|
78
|
-
return `${dim('│')} ${l}${' '.repeat(pad)} ${dim('│')}`;
|
|
79
|
-
});
|
|
80
|
-
const bottom = dim(`╰${'─'.repeat(w)}╯`);
|
|
81
|
-
return [top, ...body, bottom].join('\n');
|
|
82
|
-
}
|
|
65
|
+
const app_1 = require("./app");
|
|
83
66
|
function dot(p) {
|
|
84
67
|
if (!p?.available)
|
|
85
|
-
return grey('⏺');
|
|
86
|
-
return p.authed ? green('⏺') : yellow('⏺');
|
|
68
|
+
return (0, render_1.grey)('⏺');
|
|
69
|
+
return p.authed ? (0, render_1.green)('⏺') : (0, render_1.yellow)('⏺');
|
|
87
70
|
}
|
|
88
71
|
function actionFor(id, p) {
|
|
89
72
|
if (!p?.available)
|
|
@@ -92,49 +75,102 @@ function actionFor(id, p) {
|
|
|
92
75
|
return 'log in';
|
|
93
76
|
return harness_registry_1.HARNESSES[id].logout ? 'log out' : 'ok';
|
|
94
77
|
}
|
|
95
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Harness CLIs report their version in whatever shape they feel like:
|
|
80
|
+
* `2.1.220 (Claude Code)`, `codex-cli 0.139.0`, `0.52.0`. Prefixing all of
|
|
81
|
+
* those with a `v` produced `vcodex-cli 0.139.0`. Pull out the version
|
|
82
|
+
* itself and leave the branding to the label column.
|
|
83
|
+
*/
|
|
84
|
+
function formatVersion(v) {
|
|
85
|
+
if (!v)
|
|
86
|
+
return '—';
|
|
87
|
+
const m = v.match(/\d+\.\d+(\.\d+)?\S*/);
|
|
88
|
+
return m ? `v${m[0]}` : v;
|
|
89
|
+
}
|
|
90
|
+
function accountLabel(p) {
|
|
91
|
+
if (!p?.available)
|
|
92
|
+
return 'not installed';
|
|
93
|
+
if (!p.authed)
|
|
94
|
+
return 'not logged in';
|
|
95
|
+
const who = p.account ?? p.accountKind ?? 'logged in';
|
|
96
|
+
return p.plan ? `${who} · ${p.plan}` : who;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Column widths.
|
|
100
|
+
*
|
|
101
|
+
* The account and the effort ladder vary most, so they take the spare width;
|
|
102
|
+
* version and action are fixed at what their values actually need. The old
|
|
103
|
+
* fixed widths cut `codex-cli` down to `vcodex-cli` and
|
|
104
|
+
* `low/medium/high/xhigh` to `low/medium/high/x`.
|
|
105
|
+
*/
|
|
106
|
+
function harnessColumns(width) {
|
|
107
|
+
// dot, name, version, account, models, efforts, action
|
|
108
|
+
return (0, render_1.layoutColumns)([
|
|
109
|
+
{ min: 1 },
|
|
110
|
+
{ min: 12 },
|
|
111
|
+
{ min: 9, grow: 1 },
|
|
112
|
+
{ min: 16, grow: 2 },
|
|
113
|
+
{ min: 8 },
|
|
114
|
+
// claude's ladder is low/medium/high/xhigh/max — 25 columns of it.
|
|
115
|
+
{ min: 14, grow: 3 },
|
|
116
|
+
{ min: 14 },
|
|
117
|
+
], Math.max(40, width - 4));
|
|
118
|
+
}
|
|
119
|
+
function harnessRow(id, p, selected, width) {
|
|
96
120
|
const spec = harness_registry_1.HARNESSES[id];
|
|
97
|
-
|
|
98
|
-
// wide informational columns give up space before it does. At 100 cols
|
|
99
|
-
// the old widths pushed the action past the border and it was cut.
|
|
100
|
-
const name = spec.label.padEnd(13);
|
|
101
|
-
const ver = (p?.version ? `v${p.version}` : '—').padEnd(11).slice(0, 11);
|
|
102
|
-
const who = (p?.authed
|
|
103
|
-
? `${p.account ?? p.accountKind ?? 'logged in'}${p.plan ? ` · ${p.plan}` : ''}`
|
|
104
|
-
: p?.available ? 'not logged in' : 'not installed').padEnd(24).slice(0, 24);
|
|
105
|
-
const models = `${p?.models?.length ?? 0} models`.padEnd(9);
|
|
106
|
-
const efforts = spec.efforts.length ? spec.efforts.join('/') : '—';
|
|
121
|
+
const w = harnessColumns(width);
|
|
107
122
|
const act = actionFor(id, p);
|
|
108
|
-
const
|
|
109
|
-
const line =
|
|
110
|
-
|
|
123
|
+
const action = act === 'ok' ? (0, render_1.green)('✓ ready') : act === 'log out' ? (0, render_1.dim)(`⏎ ${act}`) : (0, render_1.cyan)(`⏎ ${act}`);
|
|
124
|
+
const line = (0, render_1.tableRow)([
|
|
125
|
+
dot(p),
|
|
126
|
+
selected ? (0, render_1.bold)(spec.label) : spec.label,
|
|
127
|
+
(0, render_1.dim)(formatVersion(p?.version)),
|
|
128
|
+
accountLabel(p),
|
|
129
|
+
(0, render_1.dim)(`${p?.models?.length ?? 0} models`),
|
|
130
|
+
(0, render_1.dim)(spec.efforts.length ? spec.efforts.join('/') : '—'),
|
|
131
|
+
action,
|
|
132
|
+
], w);
|
|
133
|
+
return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
|
|
111
134
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
const
|
|
135
|
+
function renderHarnesses(st, width, height) {
|
|
136
|
+
const out = [];
|
|
137
|
+
const authed = st.caps ? harness_registry_1.HARNESS_IDS.filter(id => st.caps?.[id]?.authed).length : 0;
|
|
138
|
+
out.push((0, app_1.heading)('HARNESSES', st.caps ? `${authed}/${harness_registry_1.HARNESS_IDS.length} ready` : 'probing…'));
|
|
139
|
+
out.push('');
|
|
115
140
|
if (!st.caps) {
|
|
116
|
-
|
|
141
|
+
out.push(` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${(0, render_1.dim)('probing installed harnesses…')}`);
|
|
117
142
|
}
|
|
118
143
|
else {
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
const w = harnessColumns(width);
|
|
145
|
+
out.push((0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'HARNESS', 'VERSION', 'ACCOUNT', 'MODELS', 'EFFORTS', ''], w)));
|
|
146
|
+
harness_registry_1.HARNESS_IDS.forEach((id, i) => out.push(harnessRow(id, st.caps?.[id], i === st.sel, width)));
|
|
121
147
|
}
|
|
122
148
|
if (st.busy)
|
|
123
|
-
|
|
149
|
+
out.push('', ` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${st.busy}`);
|
|
124
150
|
if (st.log.length) {
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
out.push('');
|
|
152
|
+
// Whatever room is left after the chrome, so an install's output fills
|
|
153
|
+
// the screen rather than a fixed eight lines.
|
|
154
|
+
const room = Math.max(3, height - out.length - 3);
|
|
155
|
+
st.log.slice(-room).forEach(l => out.push((0, render_1.dim)(` ${l}`)));
|
|
127
156
|
}
|
|
128
157
|
if (st.note)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
158
|
+
out.push('', ` ${st.note}`);
|
|
159
|
+
while (out.length < height - 1)
|
|
160
|
+
out.push('');
|
|
161
|
+
out.push((0, app_1.footerHint)([
|
|
162
|
+
{ keys: '↑↓', label: 'move' },
|
|
163
|
+
{ keys: '⏎', label: 'install / log in / log out' },
|
|
164
|
+
{ keys: 'r', label: 'refresh' },
|
|
165
|
+
{ keys: 'q', label: 'back' },
|
|
166
|
+
]));
|
|
167
|
+
return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
132
168
|
}
|
|
133
169
|
/* ── actions ─────────────────────────────────────────────────────────── */
|
|
134
170
|
async function installSelected(st, id, redraw) {
|
|
135
171
|
const spec = harness_registry_1.HARNESSES[id];
|
|
136
172
|
if (!(0, harness_registry_1.isInstallable)(id)) {
|
|
137
|
-
st.note = yellow(`${spec.label}: ${spec.login.instructions}`);
|
|
173
|
+
st.note = (0, render_1.yellow)(`${spec.label}: ${spec.login.instructions}`);
|
|
138
174
|
return;
|
|
139
175
|
}
|
|
140
176
|
st.busy = `installing ${spec.label} (npm i -g ${spec.npmPackage})`;
|
|
@@ -151,10 +187,10 @@ async function installSelected(st, id, redraw) {
|
|
|
151
187
|
child.stderr?.on('data', onChunk);
|
|
152
188
|
child.on('exit', code => {
|
|
153
189
|
st.busy = null;
|
|
154
|
-
st.note = code === 0 ? green(`✓ ${spec.label} installed`) :
|
|
190
|
+
st.note = code === 0 ? (0, render_1.green)(`✓ ${spec.label} installed`) : (0, render_1.red)(`✗ npm exited ${code}`);
|
|
155
191
|
resolve();
|
|
156
192
|
});
|
|
157
|
-
child.on('error', err => { st.busy = null; st.note =
|
|
193
|
+
child.on('error', err => { st.busy = null; st.note = (0, render_1.red)(`✗ ${err.message}`); resolve(); });
|
|
158
194
|
});
|
|
159
195
|
}
|
|
160
196
|
function logoutSelected(id) {
|
|
@@ -186,93 +222,70 @@ function logoutSelected(id) {
|
|
|
186
222
|
}
|
|
187
223
|
return null;
|
|
188
224
|
}
|
|
225
|
+
/** Runs with the real terminal handed over — see `suspend` below. */
|
|
189
226
|
function loginSelected(id) {
|
|
190
227
|
const spec = harness_registry_1.HARNESSES[id];
|
|
191
228
|
const bin = spec.findBinary();
|
|
192
229
|
if (!bin)
|
|
193
230
|
return `${spec.label} is not installed`;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.log(cyan(`\n— ${spec.label} login —`));
|
|
197
|
-
console.log(dim(spec.login.instructions) + '\n');
|
|
231
|
+
console.log((0, render_1.cyan)(`\n— ${spec.label} login —`));
|
|
232
|
+
console.log((0, render_1.dim)(spec.login.instructions) + '\n');
|
|
198
233
|
const args = spec.login.strategy === 'pty-url-code' ? (spec.login.loginArgs ?? []) : [];
|
|
199
234
|
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
200
235
|
const res = (0, child_process_1.spawnSync)(inv.file, inv.args, { stdio: 'inherit', env: process.env });
|
|
201
|
-
altOn();
|
|
202
236
|
return res.status === 0 ? null : `login exited with code ${res.status ?? '?'}`;
|
|
203
237
|
}
|
|
204
|
-
/* ──
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
208
|
-
const p = caps[id];
|
|
209
|
-
const state = !p?.available ? 'not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'not logged in';
|
|
210
|
-
console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Run the harness manager.
|
|
215
|
-
*
|
|
216
|
-
* The returned promise resolves when the USER LEAVES the screen — not when
|
|
217
|
-
* the first capability probe finishes. Resolving early is what made the
|
|
218
|
-
* embedded case flicker: the dashboard re-attached its own keypress handler
|
|
219
|
-
* and repainted while this screen was still live, and the two fought over
|
|
220
|
-
* the terminal.
|
|
221
|
-
*
|
|
222
|
-
* `embedded` distinguishes `ainode harnesses` (q exits the process) from
|
|
223
|
-
* the dashboard's `h` (q hands control back to the caller). Ctrl-C always
|
|
224
|
-
* ends the process.
|
|
225
|
-
*/
|
|
226
|
-
async function runHarnessesTui(opts = {}) {
|
|
227
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
228
|
-
await plainStatus();
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const st = { caps: null, sel: 0, spin: 0, busy: null, log: [], note: null, confirmLogout: null };
|
|
232
|
-
const redraw = () => render(st);
|
|
233
|
-
altOn();
|
|
234
|
-
readline.emitKeypressEvents(process.stdin);
|
|
235
|
-
process.stdin.setRawMode(true);
|
|
236
|
-
process.stdin.resume();
|
|
237
|
-
let done = null;
|
|
238
|
-
const spinTimer = setInterval(() => { st.spin++; if (st.busy || !st.caps)
|
|
239
|
-
redraw(); }, 90);
|
|
240
|
-
const detach = () => {
|
|
241
|
-
clearInterval(spinTimer);
|
|
242
|
-
process.stdin.off('keypress', onKeypress);
|
|
243
|
-
};
|
|
244
|
-
const quit = (hard) => {
|
|
245
|
-
detach();
|
|
246
|
-
if (opts.embedded && !hard) {
|
|
247
|
-
done?.();
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
process.stdin.setRawMode(false);
|
|
251
|
-
process.stdin.pause();
|
|
252
|
-
altOff();
|
|
253
|
-
process.exit(0);
|
|
238
|
+
/* ── screen ──────────────────────────────────────────────────────────── */
|
|
239
|
+
function createHarnessesScreen(deps) {
|
|
240
|
+
const st = {
|
|
241
|
+
caps: null, sel: 0, spin: 0, busy: null, confirmLogout: null, log: [], note: null,
|
|
254
242
|
};
|
|
243
|
+
const probe = deps.probe ?? (() => (0, capabilities_1.probeCapabilities)());
|
|
244
|
+
const redraw = () => deps.host.redraw();
|
|
245
|
+
let acting = false;
|
|
255
246
|
const reprobe = async () => {
|
|
256
247
|
st.caps = null;
|
|
257
248
|
redraw();
|
|
258
|
-
st.caps = await (
|
|
249
|
+
st.caps = await probe();
|
|
259
250
|
redraw();
|
|
260
251
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
252
|
+
return {
|
|
253
|
+
id: 'harnesses',
|
|
254
|
+
title: 'Harnesses',
|
|
255
|
+
render: (width, height) => renderHarnesses(st, width, height),
|
|
256
|
+
// Probing spawns five CLIs; do it on open and then only on request.
|
|
257
|
+
pollMs: () => 120_000,
|
|
258
|
+
poll: reprobe,
|
|
259
|
+
tick(n) {
|
|
260
|
+
st.spin = n;
|
|
261
|
+
// The only moving parts are the probe and install spinners.
|
|
262
|
+
return st.busy !== null || st.caps === null;
|
|
263
|
+
},
|
|
264
|
+
keys: () => [
|
|
265
|
+
{ keys: '↑↓ / jk', label: 'move between harnesses' },
|
|
266
|
+
{ keys: '⏎', label: 'install, log in, or arm a log out' },
|
|
267
|
+
{ keys: 'y', label: 'confirm a log out' },
|
|
268
|
+
{ keys: 'r', label: 'probe again' },
|
|
269
|
+
],
|
|
270
|
+
async onKey(key) {
|
|
271
|
+
if (acting || !key)
|
|
265
272
|
return;
|
|
266
|
-
if (key.
|
|
267
|
-
|
|
273
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
274
|
+
st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
|
|
275
|
+
redraw();
|
|
268
276
|
return;
|
|
269
277
|
}
|
|
270
|
-
if (key.name === '
|
|
271
|
-
|
|
278
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
279
|
+
st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
|
|
280
|
+
redraw();
|
|
272
281
|
return;
|
|
273
282
|
}
|
|
274
283
|
if (!st.caps)
|
|
275
284
|
return;
|
|
285
|
+
if (key.name === 'r') {
|
|
286
|
+
await reprobe();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
276
289
|
if (st.confirmLogout) {
|
|
277
290
|
const target = st.confirmLogout;
|
|
278
291
|
st.confirmLogout = null;
|
|
@@ -280,7 +293,7 @@ async function runHarnessesTui(opts = {}) {
|
|
|
280
293
|
acting = true;
|
|
281
294
|
try {
|
|
282
295
|
const err = logoutSelected(target);
|
|
283
|
-
st.note = err ?
|
|
296
|
+
st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
|
|
284
297
|
await reprobe();
|
|
285
298
|
}
|
|
286
299
|
finally {
|
|
@@ -288,25 +301,11 @@ async function runHarnessesTui(opts = {}) {
|
|
|
288
301
|
}
|
|
289
302
|
}
|
|
290
303
|
else {
|
|
291
|
-
st.note = dim('logout cancelled');
|
|
304
|
+
st.note = (0, render_1.dim)('logout cancelled');
|
|
292
305
|
redraw();
|
|
293
306
|
}
|
|
294
307
|
return;
|
|
295
308
|
}
|
|
296
|
-
if (key.name === 'up' || key.name === 'k') {
|
|
297
|
-
st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
|
|
298
|
-
redraw();
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (key.name === 'down' || key.name === 'j') {
|
|
302
|
-
st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
|
|
303
|
-
redraw();
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
if (key.name === 'r') {
|
|
307
|
-
await reprobe();
|
|
308
|
-
return;
|
|
309
|
-
}
|
|
310
309
|
if (key.name !== 'return')
|
|
311
310
|
return;
|
|
312
311
|
const id = harness_registry_1.HARNESS_IDS[st.sel];
|
|
@@ -316,39 +315,45 @@ async function runHarnessesTui(opts = {}) {
|
|
|
316
315
|
if (!p?.available) {
|
|
317
316
|
await installSelected(st, id, redraw);
|
|
318
317
|
await reprobe();
|
|
318
|
+
return;
|
|
319
319
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
if (!p.authed) {
|
|
321
|
+
let err = null;
|
|
322
|
+
// The vendor CLI draws its own prompts, so it needs the real
|
|
323
|
+
// terminal — and it must have it back before we repaint.
|
|
324
|
+
await deps.suspend(() => { err = loginSelected(id); });
|
|
325
|
+
st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)('✓ login flow finished — verifying…');
|
|
326
|
+
redraw();
|
|
323
327
|
await reprobe();
|
|
324
328
|
const now = st.caps?.[id];
|
|
325
329
|
if (now?.authed)
|
|
326
|
-
st.note = green(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
|
|
330
|
+
st.note = (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
|
|
327
331
|
else if (!err)
|
|
328
|
-
st.note = yellow(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
|
|
332
|
+
st.note = (0, render_1.yellow)(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
|
|
329
333
|
redraw();
|
|
334
|
+
return;
|
|
330
335
|
}
|
|
331
|
-
|
|
336
|
+
if (harness_registry_1.HARNESSES[id].logout) {
|
|
332
337
|
st.confirmLogout = id;
|
|
333
|
-
st.note = yellow(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
|
|
334
|
-
redraw();
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
st.note = green(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
|
|
338
|
+
st.note = (0, render_1.yellow)(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
|
|
338
339
|
redraw();
|
|
340
|
+
return;
|
|
339
341
|
}
|
|
342
|
+
st.note = (0, render_1.green)(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
|
|
343
|
+
redraw();
|
|
340
344
|
}
|
|
341
345
|
finally {
|
|
342
346
|
acting = false;
|
|
343
347
|
}
|
|
344
|
-
}
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
/** `ainode harnesses` on a pipe: no screen, just the facts. */
|
|
352
|
+
async function plainStatus() {
|
|
353
|
+
const caps = await (0, capabilities_1.probeCapabilities)();
|
|
354
|
+
for (const id of harness_registry_1.HARNESS_IDS) {
|
|
355
|
+
const p = caps[id];
|
|
356
|
+
const state = !p?.available ? 'not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'not logged in';
|
|
357
|
+
console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
|
|
345
358
|
}
|
|
346
|
-
// The exit promise is created BEFORE the first probe, not after it.
|
|
347
|
-
// Probing five CLIs takes seconds; if `q` arrives during that window and
|
|
348
|
-
// `done` isn't wired yet, quit() detaches the key listener and resolves
|
|
349
|
-
// nothing — the screen wedges with no way out.
|
|
350
|
-
const exited = new Promise(resolve => { done = resolve; });
|
|
351
|
-
process.stdin.on('keypress', onKeypress);
|
|
352
|
-
void reprobe();
|
|
353
|
-
await exited;
|
|
354
359
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type AppHost, type Screen } from './app';
|
|
2
|
+
import type { CapturedLog, ConsoleCapture } from './console-capture';
|
|
3
|
+
export interface LogsState {
|
|
4
|
+
scroll: number;
|
|
5
|
+
follow: boolean;
|
|
6
|
+
filter: string;
|
|
7
|
+
filtering: boolean;
|
|
8
|
+
viewportRows: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function logLine(l: CapturedLog, width: number): string;
|
|
11
|
+
export declare function filterLogs(lines: CapturedLog[], filter: string): CapturedLog[];
|
|
12
|
+
export declare function renderLogs(st: LogsState, lines: CapturedLog[], width: number, height: number, emptyHint?: string): string[];
|
|
13
|
+
export declare function createLogsScreen(deps: {
|
|
14
|
+
host: AppHost;
|
|
15
|
+
logs: ConsoleCapture;
|
|
16
|
+
/** Shown instead of "nothing yet" when this process isn't the daemon. */
|
|
17
|
+
emptyHint?: string;
|
|
18
|
+
}): Screen;
|
package/dist/tui/logs.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// What the daemon is saying.
|
|
3
|
+
//
|
|
4
|
+
// The console UI has to take stdout away from the daemon to paint at all, so
|
|
5
|
+
// without this screen the pump's own account of itself — backoffs, wake
|
|
6
|
+
// detection, session chatter — is invisible for as long as the UI is open.
|
|
7
|
+
// Here it is, tailing live, with the same scrollback grammar as the
|
|
8
|
+
// transcript screen.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.logLine = logLine;
|
|
11
|
+
exports.filterLogs = filterLogs;
|
|
12
|
+
exports.renderLogs = renderLogs;
|
|
13
|
+
exports.createLogsScreen = createLogsScreen;
|
|
14
|
+
const render_1 = require("./render");
|
|
15
|
+
const app_1 = require("./app");
|
|
16
|
+
/** Lines the screen spends on things that are not log lines. */
|
|
17
|
+
const CHROME = 4;
|
|
18
|
+
function stamp(at) {
|
|
19
|
+
const d = new Date(at);
|
|
20
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
21
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
22
|
+
}
|
|
23
|
+
function logLine(l, width) {
|
|
24
|
+
const colour = l.level === 'error' ? render_1.red : l.level === 'warn' ? render_1.yellow : render_1.dim;
|
|
25
|
+
const text = l.text.replace(/\s+/g, ' ');
|
|
26
|
+
const prefix = ` ${(0, render_1.grey)(stamp(l.at))} `;
|
|
27
|
+
return (0, render_1.truncate)(prefix + colour(text), width);
|
|
28
|
+
}
|
|
29
|
+
function filterLogs(lines, filter) {
|
|
30
|
+
const f = filter.trim().toLowerCase();
|
|
31
|
+
if (!f)
|
|
32
|
+
return lines;
|
|
33
|
+
return lines.filter(l => l.text.toLowerCase().includes(f));
|
|
34
|
+
}
|
|
35
|
+
function renderLogs(st, lines, width, height, emptyHint) {
|
|
36
|
+
const shown = filterLogs(lines, st.filter);
|
|
37
|
+
const rows = Math.max(1, height - CHROME);
|
|
38
|
+
st.viewportRows = rows;
|
|
39
|
+
const out = [];
|
|
40
|
+
const scope = st.filter
|
|
41
|
+
? `${shown.length} matching “${st.filter}”`
|
|
42
|
+
: `${shown.length} lines this session`;
|
|
43
|
+
out.push((0, app_1.heading)('LOGS', scope));
|
|
44
|
+
out.push('');
|
|
45
|
+
if (shown.length === 0) {
|
|
46
|
+
if (st.filter)
|
|
47
|
+
out.push((0, render_1.dim)(' nothing matches that filter'));
|
|
48
|
+
else
|
|
49
|
+
out.push((0, render_1.dim)(` ${emptyHint ?? 'the daemon has said nothing yet'}`));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
const maxScroll = Math.max(0, shown.length - rows);
|
|
53
|
+
const start = st.follow ? maxScroll : Math.min(st.scroll, maxScroll);
|
|
54
|
+
st.scroll = start;
|
|
55
|
+
shown.slice(start, start + rows).forEach(l => out.push(logLine(l, width)));
|
|
56
|
+
}
|
|
57
|
+
while (out.length < height - 1)
|
|
58
|
+
out.push('');
|
|
59
|
+
const tail = st.follow ? ` ${(0, render_1.dim)('tailing')}` : '';
|
|
60
|
+
out.push(st.filtering
|
|
61
|
+
? ` /${st.filter}▏`
|
|
62
|
+
: (0, app_1.footerHint)([
|
|
63
|
+
{ keys: '↑↓ PgUp PgDn', label: 'scroll' },
|
|
64
|
+
{ keys: 'G', label: 'tail' },
|
|
65
|
+
{ keys: '/', label: 'filter' },
|
|
66
|
+
{ keys: 'q', label: 'back' },
|
|
67
|
+
]) + tail);
|
|
68
|
+
return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
69
|
+
}
|
|
70
|
+
function createLogsScreen(deps) {
|
|
71
|
+
const st = { scroll: 0, follow: true, filter: '', filtering: false, viewportRows: 20 };
|
|
72
|
+
return {
|
|
73
|
+
id: 'logs',
|
|
74
|
+
title: 'Logs',
|
|
75
|
+
render: (width, height) => renderLogs(st, deps.logs.lines(), width, height, deps.emptyHint),
|
|
76
|
+
// Nothing to fetch — new lines arrive by callback. The poll only exists
|
|
77
|
+
// so a tailing view repaints while the daemon talks.
|
|
78
|
+
pollMs: () => 1000,
|
|
79
|
+
capturesKeys: () => st.filtering,
|
|
80
|
+
async poll() { if (st.follow)
|
|
81
|
+
deps.host.redraw(); },
|
|
82
|
+
keys: () => [
|
|
83
|
+
{ keys: '↑↓ / jk', label: 'scroll a line — stops tailing' },
|
|
84
|
+
{ keys: 'PgUp/PgDn', label: 'scroll a screenful' },
|
|
85
|
+
{ keys: 'g / G', label: 'first line / tail the newest' },
|
|
86
|
+
{ keys: '/', label: 'filter lines by text' },
|
|
87
|
+
],
|
|
88
|
+
onKey(key) {
|
|
89
|
+
if (st.filtering) {
|
|
90
|
+
if (key.name === 'return') {
|
|
91
|
+
st.filtering = false;
|
|
92
|
+
deps.host.redraw();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key.name === 'escape') {
|
|
96
|
+
st.filtering = false;
|
|
97
|
+
st.filter = '';
|
|
98
|
+
deps.host.redraw();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.name === 'backspace') {
|
|
102
|
+
st.filter = st.filter.slice(0, -1);
|
|
103
|
+
deps.host.redraw();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const ch = key.sequence ?? key.name ?? '';
|
|
107
|
+
if (ch.length === 1 && ch >= ' ') {
|
|
108
|
+
st.filter += ch;
|
|
109
|
+
deps.host.redraw();
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const page = Math.max(1, st.viewportRows - 1);
|
|
114
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
115
|
+
st.follow = false;
|
|
116
|
+
st.scroll = Math.max(0, st.scroll - 1);
|
|
117
|
+
deps.host.redraw();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
121
|
+
st.follow = false;
|
|
122
|
+
st.scroll += 1;
|
|
123
|
+
deps.host.redraw();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (key.name === 'pageup') {
|
|
127
|
+
st.follow = false;
|
|
128
|
+
st.scroll = Math.max(0, st.scroll - page);
|
|
129
|
+
deps.host.redraw();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (key.name === 'pagedown') {
|
|
133
|
+
st.follow = false;
|
|
134
|
+
st.scroll += page;
|
|
135
|
+
deps.host.redraw();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.name === 'g' && !key.shift) {
|
|
139
|
+
st.follow = false;
|
|
140
|
+
st.scroll = 0;
|
|
141
|
+
deps.host.redraw();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (key.name === 'g' && key.shift) {
|
|
145
|
+
st.follow = true;
|
|
146
|
+
deps.host.redraw();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (key.name === '/' || key.sequence === '/') {
|
|
150
|
+
st.filtering = true;
|
|
151
|
+
st.filter = '';
|
|
152
|
+
deps.host.redraw();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|