@addai/node 0.4.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/dist/cli.js +2 -3
- package/dist/lockfile.d.ts +43 -3
- package/dist/lockfile.js +114 -38
- package/dist/memory-capture.js +10 -1
- 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 +23 -12
- package/dist/tui/dashboard.js +169 -129
- package/dist/tui/harnesses.d.ts +50 -13
- package/dist/tui/harnesses.js +179 -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 +35 -0
- package/dist/tui/request-row.js +144 -0
- package/dist/tui/requests.d.ts +10 -2
- package/dist/tui/requests.js +112 -20
- 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/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,105 @@ 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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
// The verb is Title Case in the row the same way every other label is; the
|
|
124
|
+
// value actionFor returns stays lowercase because the key handler reads it.
|
|
125
|
+
const verb = act.charAt(0).toUpperCase() + act.slice(1);
|
|
126
|
+
const action = act === 'ok' ? (0, render_1.green)('✓ Ready') : act === 'log out' ? (0, render_1.dim)(`⏎ ${verb}`) : (0, render_1.cyan)(`⏎ ${verb}`);
|
|
127
|
+
const line = (0, render_1.tableRow)([
|
|
128
|
+
dot(p),
|
|
129
|
+
selected ? (0, render_1.bold)(spec.label) : spec.label,
|
|
130
|
+
(0, render_1.dim)(formatVersion(p?.version)),
|
|
131
|
+
accountLabel(p),
|
|
132
|
+
(0, render_1.dim)(`${p?.models?.length ?? 0} models`),
|
|
133
|
+
(0, render_1.dim)(spec.efforts.length ? spec.efforts.join('/') : '—'),
|
|
134
|
+
action,
|
|
135
|
+
], w);
|
|
136
|
+
return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
|
|
111
137
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
const
|
|
138
|
+
function renderHarnesses(st, width, height) {
|
|
139
|
+
const out = [];
|
|
140
|
+
const authed = st.caps ? harness_registry_1.HARNESS_IDS.filter(id => st.caps?.[id]?.authed).length : 0;
|
|
141
|
+
out.push((0, app_1.heading)('Harnesses', st.caps ? `${authed}/${harness_registry_1.HARNESS_IDS.length} ready` : 'Probing…'));
|
|
142
|
+
out.push('');
|
|
115
143
|
if (!st.caps) {
|
|
116
|
-
|
|
144
|
+
out.push(` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${(0, render_1.dim)('Probing installed harnesses…')}`);
|
|
117
145
|
}
|
|
118
146
|
else {
|
|
119
|
-
|
|
120
|
-
|
|
147
|
+
const w = harnessColumns(width);
|
|
148
|
+
out.push((0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'Harness', 'Version', 'Account', 'Models', 'Efforts', ''], w)));
|
|
149
|
+
harness_registry_1.HARNESS_IDS.forEach((id, i) => out.push(harnessRow(id, st.caps?.[id], i === st.sel, width)));
|
|
121
150
|
}
|
|
122
151
|
if (st.busy)
|
|
123
|
-
|
|
152
|
+
out.push('', ` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${st.busy}`);
|
|
124
153
|
if (st.log.length) {
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
out.push('');
|
|
155
|
+
// Whatever room is left after the chrome, so an install's output fills
|
|
156
|
+
// the screen rather than a fixed eight lines.
|
|
157
|
+
const room = Math.max(3, height - out.length - 3);
|
|
158
|
+
st.log.slice(-room).forEach(l => out.push((0, render_1.dim)(` ${l}`)));
|
|
127
159
|
}
|
|
128
160
|
if (st.note)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
161
|
+
out.push('', ` ${st.note}`);
|
|
162
|
+
while (out.length < height - 1)
|
|
163
|
+
out.push('');
|
|
164
|
+
out.push((0, app_1.footerHint)([
|
|
165
|
+
{ keys: '↑↓', label: 'move' },
|
|
166
|
+
{ keys: '⏎', label: 'install / log in / log out' },
|
|
167
|
+
{ keys: 'r', label: 'refresh' },
|
|
168
|
+
{ keys: 'q', label: 'back' },
|
|
169
|
+
]));
|
|
170
|
+
return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
132
171
|
}
|
|
133
172
|
/* ── actions ─────────────────────────────────────────────────────────── */
|
|
134
173
|
async function installSelected(st, id, redraw) {
|
|
135
174
|
const spec = harness_registry_1.HARNESSES[id];
|
|
136
175
|
if (!(0, harness_registry_1.isInstallable)(id)) {
|
|
137
|
-
st.note = yellow(`${spec.label}: ${spec.login.instructions}`);
|
|
176
|
+
st.note = (0, render_1.yellow)(`${spec.label}: ${spec.login.instructions}`);
|
|
138
177
|
return;
|
|
139
178
|
}
|
|
140
179
|
st.busy = `installing ${spec.label} (npm i -g ${spec.npmPackage})`;
|
|
@@ -151,10 +190,10 @@ async function installSelected(st, id, redraw) {
|
|
|
151
190
|
child.stderr?.on('data', onChunk);
|
|
152
191
|
child.on('exit', code => {
|
|
153
192
|
st.busy = null;
|
|
154
|
-
st.note = code === 0 ? green(`✓ ${spec.label} installed`) :
|
|
193
|
+
st.note = code === 0 ? (0, render_1.green)(`✓ ${spec.label} installed`) : (0, render_1.red)(`✗ npm exited ${code}`);
|
|
155
194
|
resolve();
|
|
156
195
|
});
|
|
157
|
-
child.on('error', err => { st.busy = null; st.note =
|
|
196
|
+
child.on('error', err => { st.busy = null; st.note = (0, render_1.red)(`✗ ${err.message}`); resolve(); });
|
|
158
197
|
});
|
|
159
198
|
}
|
|
160
199
|
function logoutSelected(id) {
|
|
@@ -186,93 +225,70 @@ function logoutSelected(id) {
|
|
|
186
225
|
}
|
|
187
226
|
return null;
|
|
188
227
|
}
|
|
228
|
+
/** Runs with the real terminal handed over — see `suspend` below. */
|
|
189
229
|
function loginSelected(id) {
|
|
190
230
|
const spec = harness_registry_1.HARNESSES[id];
|
|
191
231
|
const bin = spec.findBinary();
|
|
192
232
|
if (!bin)
|
|
193
233
|
return `${spec.label} is not installed`;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.log(cyan(`\n— ${spec.label} login —`));
|
|
197
|
-
console.log(dim(spec.login.instructions) + '\n');
|
|
234
|
+
console.log((0, render_1.cyan)(`\n— ${spec.label} login —`));
|
|
235
|
+
console.log((0, render_1.dim)(spec.login.instructions) + '\n');
|
|
198
236
|
const args = spec.login.strategy === 'pty-url-code' ? (spec.login.loginArgs ?? []) : [];
|
|
199
237
|
const inv = (0, win_1.resolveCliInvocation)(bin, args);
|
|
200
238
|
const res = (0, child_process_1.spawnSync)(inv.file, inv.args, { stdio: 'inherit', env: process.env });
|
|
201
|
-
altOn();
|
|
202
239
|
return res.status === 0 ? null : `login exited with code ${res.status ?? '?'}`;
|
|
203
240
|
}
|
|
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);
|
|
241
|
+
/* ── screen ──────────────────────────────────────────────────────────── */
|
|
242
|
+
function createHarnessesScreen(deps) {
|
|
243
|
+
const st = {
|
|
244
|
+
caps: null, sel: 0, spin: 0, busy: null, confirmLogout: null, log: [], note: null,
|
|
254
245
|
};
|
|
246
|
+
const probe = deps.probe ?? (() => (0, capabilities_1.probeCapabilities)());
|
|
247
|
+
const redraw = () => deps.host.redraw();
|
|
248
|
+
let acting = false;
|
|
255
249
|
const reprobe = async () => {
|
|
256
250
|
st.caps = null;
|
|
257
251
|
redraw();
|
|
258
|
-
st.caps = await (
|
|
252
|
+
st.caps = await probe();
|
|
259
253
|
redraw();
|
|
260
254
|
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
255
|
+
return {
|
|
256
|
+
id: 'harnesses',
|
|
257
|
+
title: 'Harnesses',
|
|
258
|
+
render: (width, height) => renderHarnesses(st, width, height),
|
|
259
|
+
// Probing spawns five CLIs; do it on open and then only on request.
|
|
260
|
+
pollMs: () => 120_000,
|
|
261
|
+
poll: reprobe,
|
|
262
|
+
tick(n) {
|
|
263
|
+
st.spin = n;
|
|
264
|
+
// The only moving parts are the probe and install spinners.
|
|
265
|
+
return st.busy !== null || st.caps === null;
|
|
266
|
+
},
|
|
267
|
+
keys: () => [
|
|
268
|
+
{ keys: '↑↓ / jk', label: 'move between harnesses' },
|
|
269
|
+
{ keys: '⏎', label: 'install, log in, or arm a log out' },
|
|
270
|
+
{ keys: 'y', label: 'confirm a log out' },
|
|
271
|
+
{ keys: 'r', label: 'probe again' },
|
|
272
|
+
],
|
|
273
|
+
async onKey(key) {
|
|
274
|
+
if (acting || !key)
|
|
265
275
|
return;
|
|
266
|
-
if (key.
|
|
267
|
-
|
|
276
|
+
if (key.name === 'up' || key.name === 'k') {
|
|
277
|
+
st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
|
|
278
|
+
redraw();
|
|
268
279
|
return;
|
|
269
280
|
}
|
|
270
|
-
if (key.name === '
|
|
271
|
-
|
|
281
|
+
if (key.name === 'down' || key.name === 'j') {
|
|
282
|
+
st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
|
|
283
|
+
redraw();
|
|
272
284
|
return;
|
|
273
285
|
}
|
|
274
286
|
if (!st.caps)
|
|
275
287
|
return;
|
|
288
|
+
if (key.name === 'r') {
|
|
289
|
+
await reprobe();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
276
292
|
if (st.confirmLogout) {
|
|
277
293
|
const target = st.confirmLogout;
|
|
278
294
|
st.confirmLogout = null;
|
|
@@ -280,7 +296,7 @@ async function runHarnessesTui(opts = {}) {
|
|
|
280
296
|
acting = true;
|
|
281
297
|
try {
|
|
282
298
|
const err = logoutSelected(target);
|
|
283
|
-
st.note = err ?
|
|
299
|
+
st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
|
|
284
300
|
await reprobe();
|
|
285
301
|
}
|
|
286
302
|
finally {
|
|
@@ -288,25 +304,11 @@ async function runHarnessesTui(opts = {}) {
|
|
|
288
304
|
}
|
|
289
305
|
}
|
|
290
306
|
else {
|
|
291
|
-
st.note = dim('logout cancelled');
|
|
307
|
+
st.note = (0, render_1.dim)('logout cancelled');
|
|
292
308
|
redraw();
|
|
293
309
|
}
|
|
294
310
|
return;
|
|
295
311
|
}
|
|
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
312
|
if (key.name !== 'return')
|
|
311
313
|
return;
|
|
312
314
|
const id = harness_registry_1.HARNESS_IDS[st.sel];
|
|
@@ -316,39 +318,45 @@ async function runHarnessesTui(opts = {}) {
|
|
|
316
318
|
if (!p?.available) {
|
|
317
319
|
await installSelected(st, id, redraw);
|
|
318
320
|
await reprobe();
|
|
321
|
+
return;
|
|
319
322
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
if (!p.authed) {
|
|
324
|
+
let err = null;
|
|
325
|
+
// The vendor CLI draws its own prompts, so it needs the real
|
|
326
|
+
// terminal — and it must have it back before we repaint.
|
|
327
|
+
await deps.suspend(() => { err = loginSelected(id); });
|
|
328
|
+
st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)('✓ login flow finished — verifying…');
|
|
329
|
+
redraw();
|
|
323
330
|
await reprobe();
|
|
324
331
|
const now = st.caps?.[id];
|
|
325
332
|
if (now?.authed)
|
|
326
|
-
st.note = green(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
|
|
333
|
+
st.note = (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
|
|
327
334
|
else if (!err)
|
|
328
|
-
st.note = yellow(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
|
|
335
|
+
st.note = (0, render_1.yellow)(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
|
|
329
336
|
redraw();
|
|
337
|
+
return;
|
|
330
338
|
}
|
|
331
|
-
|
|
339
|
+
if (harness_registry_1.HARNESSES[id].logout) {
|
|
332
340
|
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.`);
|
|
341
|
+
st.note = (0, render_1.yellow)(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
|
|
338
342
|
redraw();
|
|
343
|
+
return;
|
|
339
344
|
}
|
|
345
|
+
st.note = (0, render_1.green)(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
|
|
346
|
+
redraw();
|
|
340
347
|
}
|
|
341
348
|
finally {
|
|
342
349
|
acting = false;
|
|
343
350
|
}
|
|
344
|
-
}
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/** `ainode harnesses` on a pipe: no screen, just the facts. */
|
|
355
|
+
async function plainStatus() {
|
|
356
|
+
const caps = await (0, capabilities_1.probeCapabilities)();
|
|
357
|
+
for (const id of harness_registry_1.HARNESS_IDS) {
|
|
358
|
+
const p = caps[id];
|
|
359
|
+
const state = !p?.available ? 'Not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'Not logged in';
|
|
360
|
+
console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
|
|
345
361
|
}
|
|
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
362
|
}
|
|
@@ -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
|
+
}
|