@nonbot/cli 0.9.5 → 0.9.7
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/commands/daemon.js +154 -20
- package/dist/commands/run-prompt-hook.js +11 -0
- package/dist/lib/activations.js +3 -0
- package/dist/lib/bounded-set.js +13 -0
- package/dist/lib/choir/isolated-session.js +20 -2
- package/dist/lib/completion.js +12 -8
- package/dist/lib/machine.js +6 -0
- package/dist/lib/output.js +177 -24
- package/dist/lib/pane-title.js +22 -0
- package/dist/lib/payload-validator.js +25 -3
- package/dist/lib/run-prompt.js +33 -5
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -3,16 +3,17 @@ import { loadAuth, getActiveProfile } from '../lib/auth.js';
|
|
|
3
3
|
import * as activations from '../lib/activations.js';
|
|
4
4
|
import { fireActivation, makeHeadlessSpawner, } from '../lib/activations.js';
|
|
5
5
|
import { checkCompletions } from '../lib/completion.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { evictOldestToCap } from '../lib/bounded-set.js';
|
|
7
|
+
import { deliverAnsweredPrompts, capturePane, parsePromptMenu, mintPromptId, } from '../lib/run-prompt.js';
|
|
8
|
+
import { loadOrCreateMachineId, resolveMachineName, shortMachineId } from '../lib/machine.js';
|
|
8
9
|
import { emitRunStage as defaultEmitRunStage, startRunHeartbeat as defaultStartRunHeartbeat, RUN_STAGE, } from '../lib/choir/run-progress.js';
|
|
9
10
|
import { groupBySession, launchCoordinatedSet, setIsIsolated, } from '../lib/choir/coordinated-set.js';
|
|
10
11
|
import { IsolatedSessionTracker, reconcileAndCleanup as defaultReconcileAndCleanup, resolveBaseBranch as defaultResolveBaseBranch, } from '../lib/choir/isolated-session.js';
|
|
11
|
-
import { applyPaneTitle } from '../lib/pane-title.js';
|
|
12
|
+
import { applyPaneTitle, applyPaneState } from '../lib/pane-title.js';
|
|
12
13
|
import { installService, uninstallService } from '../lib/service.js';
|
|
13
14
|
import { detectTmuxSession, inTmuxSession, nonbotTmuxOptOut, resolveTerminal, } from '../lib/terminal.js';
|
|
14
15
|
import { VERSION } from '../version.js';
|
|
15
|
-
import { errorBlock, statusRow, daemonOpener, daemonCloser,
|
|
16
|
+
import { errorBlock, statusRow, daemonOpener, daemonCloser, activationCard, runSummary, formatElapsed, liveFooter, needsYouBanner, resumedLine, buildPaneBorderFormat, buildTmuxStatusLeft, buildTmuxStatusRight, WORDMARK_WIDTH, c, } from '../lib/output.js';
|
|
16
17
|
export const POLL_FAST_MS = 2000;
|
|
17
18
|
export const POLL_MAX_MS = 30000;
|
|
18
19
|
export const POLL_INTERVAL_MS = POLL_FAST_MS;
|
|
@@ -81,6 +82,21 @@ export function applyNonbotTmuxConfig(deps = {}) {
|
|
|
81
82
|
tmux('set-option', 'set-titles', 'on');
|
|
82
83
|
tmux('set-option', 'set-titles-string', 'NONBOT TMUX │ #W');
|
|
83
84
|
tmux('refresh-client', '-S');
|
|
85
|
+
if (deps.chrome) {
|
|
86
|
+
const ascii = env.NONBOT_ASCII_ONLY === '1' || env.NONBOT_ASCII_ONLY === 'true';
|
|
87
|
+
tmux('set-option', '-g', 'pane-border-status', 'top');
|
|
88
|
+
tmux('set-option', '-g', 'pane-border-format', buildPaneBorderFormat({ ascii }));
|
|
89
|
+
tmux('set-option', '-g', 'pane-border-lines', 'heavy');
|
|
90
|
+
tmux('set-option', '-g', 'status', 'on');
|
|
91
|
+
tmux('set-option', '-g', 'status-interval', '5');
|
|
92
|
+
tmux('set-option', '-g', 'status-style', 'bg=#0a0d17,fg=#8a93a6');
|
|
93
|
+
tmux('set-option', '-g', 'status-left-length', '60');
|
|
94
|
+
tmux('set-option', '-g', 'status-right-length', '120');
|
|
95
|
+
tmux('set-option', '-g', 'status-left', buildTmuxStatusLeft({ ascii }));
|
|
96
|
+
tmux('set-option', '-g', 'status-right', buildTmuxStatusRight({ ascii }));
|
|
97
|
+
tmux('set-option', '-g', 'window-status-current-style', 'fg=#0a0d17,bg=#6c8cff,bold');
|
|
98
|
+
tmux('refresh-client', '-S');
|
|
99
|
+
}
|
|
84
100
|
if (!stdoutIsTTY)
|
|
85
101
|
return;
|
|
86
102
|
const insideTmux = inTmuxSession(env);
|
|
@@ -135,7 +151,7 @@ export function maybeReexecIntoTmux(deps = {}) {
|
|
|
135
151
|
return 'reexeced';
|
|
136
152
|
}
|
|
137
153
|
export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
138
|
-
const
|
|
154
|
+
const rawLog = deps.log ?? ((s) => process.stdout.write(s));
|
|
139
155
|
const errLog = deps.errLog ?? ((s) => process.stderr.write(s));
|
|
140
156
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
141
157
|
const loader = deps.loadAuth ?? loadAuth;
|
|
@@ -147,7 +163,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
147
163
|
const install = deps.installService ?? installService;
|
|
148
164
|
const res = await install();
|
|
149
165
|
if (res.ok)
|
|
150
|
-
|
|
166
|
+
rawLog(statusRow('✓', 'Service installed', res.message) + '\n');
|
|
151
167
|
else
|
|
152
168
|
errLog(statusRow('✗', 'Service install failed', res.message, { stream: process.stderr }) + '\n');
|
|
153
169
|
return res.ok ? 0 : 1;
|
|
@@ -156,7 +172,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
156
172
|
const uninstall = deps.uninstallService ?? uninstallService;
|
|
157
173
|
const res = await uninstall();
|
|
158
174
|
if (res.ok)
|
|
159
|
-
|
|
175
|
+
rawLog(statusRow('✓', 'Service removed', res.message) + '\n');
|
|
160
176
|
else
|
|
161
177
|
errLog(statusRow('✗', 'Service uninstall failed', res.message, { stream: process.stderr }) + '\n');
|
|
162
178
|
return res.ok ? 0 : 1;
|
|
@@ -169,7 +185,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
169
185
|
if (headless) {
|
|
170
186
|
deps.headless = true;
|
|
171
187
|
if (!deps.spawnTerminal) {
|
|
172
|
-
deps.spawnTerminal = makeHeadlessSpawner({ wait: false, log, errLog });
|
|
188
|
+
deps.spawnTerminal = makeHeadlessSpawner({ wait: false, log: rawLog, errLog });
|
|
173
189
|
}
|
|
174
190
|
}
|
|
175
191
|
const auth = await loader();
|
|
@@ -181,8 +197,11 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
181
197
|
const tmuxSessionName = detectTmux();
|
|
182
198
|
const machineId = deps.machineId ?? loadOrCreateMachineId();
|
|
183
199
|
const machineName = resolveMachineName();
|
|
200
|
+
const tmuxChromeEnabled = tmuxSessionName !== null &&
|
|
201
|
+
!nonbotTmuxOptOut() &&
|
|
202
|
+
process.env.NONBOT_NO_TMUX_STATUS !== '1';
|
|
184
203
|
if (tmuxSessionName) {
|
|
185
|
-
applyNonbotTmuxConfig({ spawnSync: deps.spawnSync });
|
|
204
|
+
applyNonbotTmuxConfig({ spawnSync: deps.spawnSync, chrome: tmuxChromeEnabled });
|
|
186
205
|
}
|
|
187
206
|
const resolveTerminalFn = deps.resolveTerminal ?? resolveTerminal;
|
|
188
207
|
const resolvedTerminalProfile = headless ? null : resolveTerminalFn(undefined);
|
|
@@ -201,6 +220,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
201
220
|
const trackedPanes = new Map();
|
|
202
221
|
const killedByStop = new Set();
|
|
203
222
|
const injectedPrompts = new Set();
|
|
223
|
+
const openPrompts = new Map();
|
|
204
224
|
const emitRunStageFn = deps.emitRunStage ?? defaultEmitRunStage;
|
|
205
225
|
const startRunHeartbeatFn = deps.startRunHeartbeat ?? defaultStartRunHeartbeat;
|
|
206
226
|
const runHeartbeats = new Map();
|
|
@@ -235,13 +255,116 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
235
255
|
let doneCount = 0;
|
|
236
256
|
let failedCount = 0;
|
|
237
257
|
const paneTitlingEnabled = tmuxSessionName !== null && !nonbotTmuxOptOut();
|
|
258
|
+
const spawnForTmux = deps.spawnSync;
|
|
238
259
|
const retitlePane = (state, paneId, story) => {
|
|
239
260
|
if (!paneTitlingEnabled || !paneId)
|
|
240
261
|
return;
|
|
241
|
-
applyPaneTitle(paneId, state, story,
|
|
262
|
+
applyPaneTitle(paneId, state, story, spawnForTmux);
|
|
263
|
+
applyPaneState(paneId, state, spawnForTmux);
|
|
242
264
|
};
|
|
265
|
+
const pushTmuxStatus = (needsYou = openPrompts.size, halted = false) => {
|
|
266
|
+
if (!tmuxChromeEnabled)
|
|
267
|
+
return;
|
|
268
|
+
const spawn = deps.spawnSync ?? nodeSpawnSync;
|
|
269
|
+
const set = (name, value) => {
|
|
270
|
+
try {
|
|
271
|
+
spawn('tmux', ['set-option', '-g', name, String(value)], {
|
|
272
|
+
encoding: 'utf-8',
|
|
273
|
+
timeout: 1000,
|
|
274
|
+
windowsHide: true,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
set('@nb_running', trackedPanes.size);
|
|
281
|
+
set('@nb_done', doneCount);
|
|
282
|
+
set('@nb_failed', failedCount);
|
|
283
|
+
set('@nb_needsyou', needsYou);
|
|
284
|
+
set('@nb_poll', Math.round((fixedInterval ?? currentInterval) / 1000));
|
|
285
|
+
set('@nb_halted', halted ? 1 : 0);
|
|
286
|
+
try {
|
|
287
|
+
spawn('tmux', ['refresh-client', '-S'], { encoding: 'utf-8', timeout: 1000, windowsHide: true });
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const log = (s) => rawLog(s);
|
|
293
|
+
let lastPollOk = true;
|
|
294
|
+
const noBell = process.env.NONBOT_NO_BELL === '1' || process.env.NONBOT_NO_BELL === 'true';
|
|
295
|
+
const stdoutIsTTY = deps.stdoutIsTTY ?? (process.stdout?.isTTY === true && !process.env.VITEST);
|
|
296
|
+
const pollHealth = () => {
|
|
297
|
+
if (openPrompts.size > 0)
|
|
298
|
+
return 'waiting';
|
|
299
|
+
if (!lastPollOk)
|
|
300
|
+
return 'reconnecting';
|
|
301
|
+
if (fixedInterval === undefined && currentInterval > POLL_FAST_MS)
|
|
302
|
+
return 'backoff';
|
|
303
|
+
return 'live';
|
|
304
|
+
};
|
|
305
|
+
const heartbeatLine = () => liveFooter({
|
|
306
|
+
running: trackedPanes.size,
|
|
307
|
+
waiting: openPrompts.size,
|
|
308
|
+
done: doneCount,
|
|
309
|
+
failed: failedCount,
|
|
310
|
+
nextPollMs: fixedInterval ?? currentInterval,
|
|
311
|
+
version: `v${VERSION}`,
|
|
312
|
+
machine: machineName,
|
|
313
|
+
health: pollHealth(),
|
|
314
|
+
});
|
|
243
315
|
const emitSummary = () => {
|
|
244
316
|
log(runSummary({ running: trackedPanes.size, done: doneCount, failed: failedCount }) + '\n');
|
|
317
|
+
pushTmuxStatus();
|
|
318
|
+
};
|
|
319
|
+
const sweepPanePrompts = () => {
|
|
320
|
+
if (!paneTitlingEnabled || trackedPanes.size === 0)
|
|
321
|
+
return;
|
|
322
|
+
let changed = false;
|
|
323
|
+
for (const [activationId, paneId] of trackedPanes) {
|
|
324
|
+
let menu;
|
|
325
|
+
try {
|
|
326
|
+
menu = parsePromptMenu(capturePane(paneId, spawnForTmux), '');
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const meta = trackedMeta.get(activationId);
|
|
332
|
+
if (menu.options.length > 0) {
|
|
333
|
+
const promptId = mintPromptId(activationId, paneId, menu);
|
|
334
|
+
if (openPrompts.get(activationId) === promptId)
|
|
335
|
+
continue;
|
|
336
|
+
openPrompts.set(activationId, promptId);
|
|
337
|
+
log('\n' +
|
|
338
|
+
needsYouBanner({
|
|
339
|
+
activationId,
|
|
340
|
+
story: meta?.story,
|
|
341
|
+
paneId,
|
|
342
|
+
provider: meta?.provider,
|
|
343
|
+
options: menu.options,
|
|
344
|
+
elapsedMs: meta ? Date.now() - meta.startedAt : undefined,
|
|
345
|
+
}) +
|
|
346
|
+
'\n');
|
|
347
|
+
retitlePane('waiting', paneId, meta?.story ?? '');
|
|
348
|
+
if (!noBell && stdoutIsTTY)
|
|
349
|
+
rawLog('\x07');
|
|
350
|
+
changed = true;
|
|
351
|
+
}
|
|
352
|
+
else if (openPrompts.has(activationId)) {
|
|
353
|
+
openPrompts.delete(activationId);
|
|
354
|
+
log(resumedLine(activationId) + '\n');
|
|
355
|
+
retitlePane('running', paneId, meta?.story ?? '');
|
|
356
|
+
changed = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const id of [...openPrompts.keys()]) {
|
|
360
|
+
if (!trackedPanes.has(id)) {
|
|
361
|
+
openPrompts.delete(id);
|
|
362
|
+
changed = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (changed) {
|
|
366
|
+
pushTmuxStatus();
|
|
367
|
+
}
|
|
245
368
|
};
|
|
246
369
|
let running = true;
|
|
247
370
|
let sigHandler;
|
|
@@ -250,7 +373,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
250
373
|
running = false;
|
|
251
374
|
for (const id of [...runHeartbeats.keys()])
|
|
252
375
|
stopHeartbeat(id);
|
|
253
|
-
|
|
376
|
+
pushTmuxStatus(0, true);
|
|
377
|
+
rawLog('\n' + statusRow('✓', 'daemon stopped', 'Ctrl-C received') + '\n');
|
|
254
378
|
process.exit(0);
|
|
255
379
|
};
|
|
256
380
|
process.on('SIGINT', sigHandler);
|
|
@@ -275,17 +399,23 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
275
399
|
];
|
|
276
400
|
if (tmuxSessionName)
|
|
277
401
|
badgeSublines.push(`tmux host: ${tmuxSessionName}`);
|
|
402
|
+
const stdoutCols = deps.stdoutColumns ?? process.stdout?.columns ?? 0;
|
|
403
|
+
const stdoutRows = deps.stdoutRows ?? process.stdout?.rows ?? 0;
|
|
404
|
+
const compactMast = stdoutIsTTY &&
|
|
405
|
+
((stdoutCols > 0 && stdoutCols < WORDMARK_WIDTH + 4) || (stdoutRows > 0 && stdoutRows < 16));
|
|
278
406
|
log(daemonOpener(`v${VERSION}`, {
|
|
279
407
|
state: 'IDLE',
|
|
280
408
|
badgeColor: 'green',
|
|
281
409
|
badgeLabel: 'CONNECTED',
|
|
282
410
|
badgeSublines,
|
|
411
|
+
compact: compactMast,
|
|
283
412
|
}) + '\n\n');
|
|
284
413
|
const headerMarker = c.green('[ + ]');
|
|
285
414
|
const headerLabel = c.white('LISTENING · READY FOR ACTIVATIONS');
|
|
286
415
|
log(`${headerMarker} ${c.bold(headerLabel)}\n`);
|
|
287
416
|
const ctxRows = [
|
|
288
417
|
['YOU', auth.email],
|
|
418
|
+
['MACHINE', `${machineName} · ${shortMachineId(machineId)}`],
|
|
289
419
|
['LISTENING', `${hostUrl}/api/cli/activations/pending`],
|
|
290
420
|
['PROFILE', profileName],
|
|
291
421
|
['LANDING', terminalLabel],
|
|
@@ -303,6 +433,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
303
433
|
inTmux: tmuxSessionName !== null,
|
|
304
434
|
}) + '\n');
|
|
305
435
|
log('\n');
|
|
436
|
+
pushTmuxStatus();
|
|
306
437
|
while (running) {
|
|
307
438
|
let firedThisPoll = false;
|
|
308
439
|
try {
|
|
@@ -329,6 +460,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
329
460
|
return 1;
|
|
330
461
|
}
|
|
331
462
|
if (res.ok) {
|
|
463
|
+
lastPollOk = true;
|
|
332
464
|
const body = (await res.json());
|
|
333
465
|
const pendingKills = body?.pendingKills ?? [];
|
|
334
466
|
if (pendingKills.length > 0) {
|
|
@@ -496,6 +628,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
496
628
|
baseUrl: auth.baseUrl,
|
|
497
629
|
pat: auth.pat,
|
|
498
630
|
injected: injectedPrompts,
|
|
631
|
+
trackedPanes,
|
|
499
632
|
machineId,
|
|
500
633
|
fetchImpl,
|
|
501
634
|
spawnImpl: deps.spawnSync,
|
|
@@ -504,6 +637,11 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
504
637
|
}
|
|
505
638
|
catch {
|
|
506
639
|
}
|
|
640
|
+
try {
|
|
641
|
+
sweepPanePrompts();
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
}
|
|
507
645
|
if (trackedPanes.size > 0) {
|
|
508
646
|
const reported = await checkCompletions({
|
|
509
647
|
tracked: trackedPanes,
|
|
@@ -537,7 +675,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
537
675
|
doneCount++;
|
|
538
676
|
trackedMeta.delete(id);
|
|
539
677
|
safeEmit(id, RUN_STAGE.FINISHED);
|
|
540
|
-
stopHeartbeat(id, '
|
|
678
|
+
stopHeartbeat(id, 'finished');
|
|
541
679
|
try {
|
|
542
680
|
const session = isolatedSessions.noteTerminal(id);
|
|
543
681
|
if (session) {
|
|
@@ -556,18 +694,14 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
556
694
|
emitSummary();
|
|
557
695
|
}
|
|
558
696
|
}
|
|
559
|
-
if (!firedThisPoll && pendingKills.length === 0) {
|
|
560
|
-
log(
|
|
561
|
-
pending: 0,
|
|
562
|
-
sleepMs: fixedInterval ?? currentInterval,
|
|
563
|
-
backingOff: fixedInterval === undefined && currentInterval > POLL_FAST_MS,
|
|
564
|
-
}) + '\n');
|
|
697
|
+
if (!firedThisPoll && pendingKills.length === 0 && !tmuxChromeEnabled) {
|
|
698
|
+
log(heartbeatLine() + '\n');
|
|
565
699
|
}
|
|
566
|
-
|
|
567
|
-
seen.clear();
|
|
700
|
+
evictOldestToCap(seen, 500);
|
|
568
701
|
}
|
|
569
702
|
}
|
|
570
703
|
catch (e) {
|
|
704
|
+
lastPollOk = false;
|
|
571
705
|
errLog(statusRow('⚠', 'poll error', e.message, { stream: process.stderr }) + '\n');
|
|
572
706
|
}
|
|
573
707
|
if (options.oneShot)
|
|
@@ -60,6 +60,17 @@ export async function runRunPromptHookCommand(deps = {}) {
|
|
|
60
60
|
}
|
|
61
61
|
if (!question && parsed.options.length === 0)
|
|
62
62
|
return 0;
|
|
63
|
+
if (paneId && /^%\d+$/.test(paneId)) {
|
|
64
|
+
try {
|
|
65
|
+
spawnSync('tmux', ['set-option', '-p', '-t', paneId, '@nb_state', 'waiting'], {
|
|
66
|
+
encoding: 'utf-8',
|
|
67
|
+
timeout: 1000,
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
}
|
|
73
|
+
}
|
|
63
74
|
const promptId = mintPromptId(activationId, paneId, { question, options: parsed.options });
|
|
64
75
|
const ok = await reportPrompt({
|
|
65
76
|
baseUrl,
|
package/dist/lib/activations.js
CHANGED
|
@@ -9,6 +9,7 @@ import { appendActivityLog } from './activity-log.js';
|
|
|
9
9
|
import { activationCard, clockTime } from './output.js';
|
|
10
10
|
import { buildCommandFromParams, hookSettingsPathFor, shellQuoteSingle, } from './command-builders.js';
|
|
11
11
|
import { validatePayload, validateActivationId, validateRepoPath, ValidationError, extractTerminalTheme, } from './payload-validator.js';
|
|
12
|
+
const PANE_ID_RE = /^%\d+$/;
|
|
12
13
|
export const BUILT_COMMAND_MAX_LENGTH = 32 * 1024;
|
|
13
14
|
export const WIRE_COMMAND_MAX_LENGTH = 4096;
|
|
14
15
|
export const COMMAND_WARN_LENGTH = 8192;
|
|
@@ -450,6 +451,8 @@ export async function executePendingKills(kills, baseUrl, token) {
|
|
|
450
451
|
return;
|
|
451
452
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
452
453
|
const runOne = async (k) => {
|
|
454
|
+
if (!PANE_ID_RE.test(k.tmuxPaneId))
|
|
455
|
+
return;
|
|
453
456
|
try {
|
|
454
457
|
spawnSync('tmux', ['send-keys', '-t', k.tmuxPaneId, 'C-c'], {
|
|
455
458
|
encoding: 'utf-8',
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function evictOldestToCap(set, cap) {
|
|
2
|
+
if (set.size <= cap)
|
|
3
|
+
return;
|
|
4
|
+
const overflow = set.size - cap;
|
|
5
|
+
const victims = [];
|
|
6
|
+
for (const id of set) {
|
|
7
|
+
victims.push(id);
|
|
8
|
+
if (victims.length >= overflow)
|
|
9
|
+
break;
|
|
10
|
+
}
|
|
11
|
+
for (const id of victims)
|
|
12
|
+
set.delete(id);
|
|
13
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
-
import { removeWorktree as defaultRemoveWorktree } from './worktree.js';
|
|
2
|
+
import { removeWorktree as defaultRemoveWorktree, pollGitStatus as defaultPollGitStatus } from './worktree.js';
|
|
3
3
|
import { runReconcile as defaultRunReconcile } from './reconcile.js';
|
|
4
4
|
import { isValidBranch } from './names.js';
|
|
5
5
|
export class IsolatedSessionTracker {
|
|
@@ -55,10 +55,21 @@ export function resolveBaseBranch(repoRoot, spawnImpl = nodeSpawnSync) {
|
|
|
55
55
|
}
|
|
56
56
|
return 'main';
|
|
57
57
|
}
|
|
58
|
+
const LAUNCHER_ARTIFACTS = new Set(['.mcp.json']);
|
|
59
|
+
function worktreeHasUnmergedWork(worktreePath, baseBranch, spawnImpl, pollGitStatusImpl) {
|
|
60
|
+
try {
|
|
61
|
+
const status = pollGitStatusImpl(worktreePath, spawnImpl, baseBranch);
|
|
62
|
+
return status.dirtyPaths.some((p) => !LAUNCHER_ARTIFACTS.has(p));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
export function reconcileAndCleanup(session, deps = {}) {
|
|
59
69
|
const spawnImpl = deps.spawnImpl ?? nodeSpawnSync;
|
|
60
70
|
const runReconcileImpl = deps.runReconcileImpl ?? defaultRunReconcile;
|
|
61
71
|
const removeWorktreeImpl = deps.removeWorktreeImpl ?? defaultRemoveWorktree;
|
|
72
|
+
const pollGitStatusImpl = deps.pollGitStatusImpl ?? defaultPollGitStatus;
|
|
62
73
|
const log = deps.log;
|
|
63
74
|
const branches = session.panes.map((p) => p.branch);
|
|
64
75
|
const reconcile = runReconcileImpl({
|
|
@@ -72,7 +83,12 @@ export function reconcileAndCleanup(session, deps = {}) {
|
|
|
72
83
|
});
|
|
73
84
|
const removed = [];
|
|
74
85
|
if (!reconcile.blocked) {
|
|
86
|
+
let kept = 0;
|
|
75
87
|
for (const p of session.panes) {
|
|
88
|
+
if (worktreeHasUnmergedWork(p.worktreePath, session.baseBranch, spawnImpl, pollGitStatusImpl)) {
|
|
89
|
+
kept++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
76
92
|
try {
|
|
77
93
|
removeWorktreeImpl({ repoRoot: session.repoRoot, worktreePath: p.worktreePath, force: true });
|
|
78
94
|
removed.push(p.worktreePath);
|
|
@@ -82,7 +98,9 @@ export function reconcileAndCleanup(session, deps = {}) {
|
|
|
82
98
|
}
|
|
83
99
|
log?.(`[reconcile] ${session.sessionId} — merged ${reconcile.merged.length} ` +
|
|
84
100
|
`branch${reconcile.merged.length === 1 ? '' : 'es'}, ` +
|
|
85
|
-
`removed ${removed.length} worktree${removed.length === 1 ? '' : 's'}
|
|
101
|
+
`removed ${removed.length} worktree${removed.length === 1 ? '' : 's'}` +
|
|
102
|
+
(kept > 0 ? `, kept ${kept} with uncommitted work for the human` : '') +
|
|
103
|
+
`\n`);
|
|
86
104
|
}
|
|
87
105
|
else {
|
|
88
106
|
log?.(`[reconcile] ${session.sessionId} — BLOCKED (${reconcile.reason}); ` +
|
package/dist/lib/completion.js
CHANGED
|
@@ -6,13 +6,13 @@ export function listLivePaneIds(spawnImpl = nodeSpawnSync) {
|
|
|
6
6
|
timeout: 1500,
|
|
7
7
|
windowsHide: true,
|
|
8
8
|
});
|
|
9
|
+
if (r.status !== 0)
|
|
10
|
+
return null;
|
|
9
11
|
const out = typeof r.stdout === 'string' ? r.stdout : '';
|
|
10
|
-
if (r.status !== 0 && !out)
|
|
11
|
-
return new Set();
|
|
12
12
|
return new Set(out.split('\n').map((s) => s.trim()).filter((s) => /^%\d+$/.test(s)));
|
|
13
13
|
}
|
|
14
14
|
catch {
|
|
15
|
-
return
|
|
15
|
+
return null;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
export function detectCompletedActivations(tracked, livePaneIds, killed) {
|
|
@@ -48,9 +48,11 @@ export async function checkCompletions(opts) {
|
|
|
48
48
|
if (tracked.size === 0)
|
|
49
49
|
return [];
|
|
50
50
|
const live = listLivePaneIds(opts.spawnImpl);
|
|
51
|
+
if (live === null)
|
|
52
|
+
return [];
|
|
51
53
|
const completed = detectCompletedActivations(tracked, live, killed);
|
|
52
54
|
for (const [id, pane] of [...tracked.entries()]) {
|
|
53
|
-
if (!live.has(pane)) {
|
|
55
|
+
if (!live.has(pane) && killed.has(id)) {
|
|
54
56
|
tracked.delete(id);
|
|
55
57
|
killed.delete(id);
|
|
56
58
|
}
|
|
@@ -58,11 +60,13 @@ export async function checkCompletions(opts) {
|
|
|
58
60
|
const reported = [];
|
|
59
61
|
for (const id of completed) {
|
|
60
62
|
const ok = await postCompletion(baseUrl, pat, id, opts.fetchImpl);
|
|
61
|
-
if (ok) {
|
|
62
|
-
|
|
63
|
-
const card = opts.renderComplete?.(id);
|
|
64
|
-
opts.log?.(card && card.length > 0 ? card : `✓ ${id} · run completed (pane closed)\n`);
|
|
63
|
+
if (!ok) {
|
|
64
|
+
continue;
|
|
65
65
|
}
|
|
66
|
+
tracked.delete(id);
|
|
67
|
+
reported.push(id);
|
|
68
|
+
const card = opts.renderComplete?.(id);
|
|
69
|
+
opts.log?.(card && card.length > 0 ? card : `✓ ${id} · run completed (pane closed)\n`);
|
|
66
70
|
}
|
|
67
71
|
return reported;
|
|
68
72
|
}
|
package/dist/lib/machine.js
CHANGED
|
@@ -36,6 +36,12 @@ export function loadOrCreateMachineId(deps = {}) {
|
|
|
36
36
|
}
|
|
37
37
|
return machineId;
|
|
38
38
|
}
|
|
39
|
+
export function shortMachineId(id) {
|
|
40
|
+
const hex = (id || '').replace(/[^a-fA-F0-9]/g, '').toLowerCase();
|
|
41
|
+
if (hex.length === 0)
|
|
42
|
+
return 'm_unknown';
|
|
43
|
+
return `m_${hex.slice(0, 6)}`;
|
|
44
|
+
}
|
|
39
45
|
export function resolveMachineName(env = process.env) {
|
|
40
46
|
const override = env.NONBOT_MACHINE_NAME;
|
|
41
47
|
if (override && override.trim().length > 0)
|
package/dist/lib/output.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { ANSI, renderBannerHeader } from './banner.js';
|
|
2
2
|
export { ANSI };
|
|
3
3
|
export const BRAND = {
|
|
4
|
-
white: '\x1b[38;2;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
white: '\x1b[38;2;240;235;226m',
|
|
5
|
+
cream: '\x1b[38;2;240;235;226m',
|
|
6
|
+
blue: '\x1b[38;2;108;140;255m',
|
|
7
|
+
indigo: '\x1b[38;2;155;108;240m',
|
|
8
|
+
cyan: '\x1b[38;2;88;199;230m',
|
|
9
|
+
green: '\x1b[38;2;69;209;127m',
|
|
10
|
+
amber: '\x1b[38;2;255;180;84m',
|
|
11
|
+
red: '\x1b[38;2;255;92;108m',
|
|
12
|
+
muted: '\x1b[38;2;138;147;166m',
|
|
10
13
|
bold: '\x1b[1m',
|
|
11
14
|
reset: '\x1b[0m',
|
|
12
15
|
underline: '\x1b[4m',
|
|
@@ -31,6 +34,9 @@ function paint(s, code, stream = process.stdout) {
|
|
|
31
34
|
}
|
|
32
35
|
export const c = {
|
|
33
36
|
white: (s, stream) => paintRaw(s, BRAND.white, stream),
|
|
37
|
+
cream: (s, stream) => paintRaw(s, BRAND.cream, stream),
|
|
38
|
+
blue: (s, stream) => paintRaw(s, BRAND.blue, stream),
|
|
39
|
+
indigo: (s, stream) => paintRaw(s, BRAND.indigo, stream),
|
|
34
40
|
cyan: (s, stream) => paintRaw(s, BRAND.cyan, stream),
|
|
35
41
|
green: (s, stream) => paintRaw(s, BRAND.green, stream),
|
|
36
42
|
amber: (s, stream) => paintRaw(s, BRAND.amber, stream),
|
|
@@ -137,17 +143,7 @@ const WORDMARK_LINES = [
|
|
|
137
143
|
];
|
|
138
144
|
export const WORDMARK_WIDTH = 63;
|
|
139
145
|
export const WORDMARK_HEIGHT = 6;
|
|
140
|
-
export const
|
|
141
|
-
'#13b9e2', '#1abadc', '#20bad7', '#27bbd1', '#2ebbcb', '#35bcc5', '#3bbdc0',
|
|
142
|
-
'#42bdba', '#49beb4', '#50bfae', '#50bfae', '#5dc0a3', '#64c09d', '#6bc197',
|
|
143
|
-
'#71c292', '#78c28c', '#7fc386', '#86c380', '#8cc47b', '#93c575', '#93c575',
|
|
144
|
-
'#a1c669', '#a7c664', '#aec75e', '#b5c858', '#bcc852', '#c2c94d', '#c9ca47',
|
|
145
|
-
'#d0ca41', '#d7cb3b', '#ddcb36', '#e4cc30', '#e0c536', '#dcbf3b', '#d8b841',
|
|
146
|
-
'#d4b246', '#d0ab4c', '#cca551', '#c89e57', '#c4975d', '#c09162', '#bc8a68',
|
|
147
|
-
'#bc8a68', '#bc8a68', '#b07679', '#ac707e', '#a86984', '#a36389', '#9f5c8f',
|
|
148
|
-
'#9b5694', '#974f9a', '#9348a0', '#8f42a5', '#8f42a5', '#8735b0', '#832eb6',
|
|
149
|
-
'#7f27bc', '#7b21c1', '#771ac7', '#7314cc', '#6f0dd2', '#6b07d7', '#6700dd',
|
|
150
|
-
];
|
|
146
|
+
export const WORDMARK_DOT_COLS = [31, 32, 33];
|
|
151
147
|
function hexFg(hex) {
|
|
152
148
|
const n = parseInt(hex.slice(1), 16);
|
|
153
149
|
return `\x1b[38;2;${(n >> 16) & 255};${(n >> 8) & 255};${n & 255}m`;
|
|
@@ -163,8 +159,8 @@ function paintWordmarkLine(line, stream = process.stdout) {
|
|
|
163
159
|
out += ' ';
|
|
164
160
|
continue;
|
|
165
161
|
}
|
|
166
|
-
const
|
|
167
|
-
out += BRAND.bold +
|
|
162
|
+
const colour = WORDMARK_DOT_COLS.includes(i) ? BRAND.blue : BRAND.cream;
|
|
163
|
+
out += BRAND.bold + colour + ch + BRAND.reset;
|
|
168
164
|
}
|
|
169
165
|
return out;
|
|
170
166
|
}
|
|
@@ -333,6 +329,43 @@ export function fixBlock(args) {
|
|
|
333
329
|
});
|
|
334
330
|
return lines.join('\n');
|
|
335
331
|
}
|
|
332
|
+
export function promptLine(opts = {}) {
|
|
333
|
+
const stream = opts.stream ?? process.stdout;
|
|
334
|
+
const cursor = asciiOnly() ? '_' : '▌';
|
|
335
|
+
return (`${c.cyan('~', stream)} ${c.green('$', stream)} ` +
|
|
336
|
+
`${c.bold(c.cream('nonbot', stream), stream)} ${c.blue('run', stream)}` +
|
|
337
|
+
c.cream(cursor, stream));
|
|
338
|
+
}
|
|
339
|
+
export function gradientRule(width, opts = {}) {
|
|
340
|
+
const stream = opts.stream ?? process.stdout;
|
|
341
|
+
const n = Math.max(0, Math.floor(width));
|
|
342
|
+
const glyph = asciiOnly() ? '-' : '━';
|
|
343
|
+
if (!isTTY(stream) || n === 0)
|
|
344
|
+
return glyph.repeat(n);
|
|
345
|
+
const from = [0x6c, 0x8c, 0xff];
|
|
346
|
+
const to = [0x9b, 0x6c, 0xf0];
|
|
347
|
+
let out = '';
|
|
348
|
+
for (let i = 0; i < n; i++) {
|
|
349
|
+
const t = n === 1 ? 0 : i / (n - 1);
|
|
350
|
+
const r = Math.round(from[0] + (to[0] - from[0]) * t);
|
|
351
|
+
const g = Math.round(from[1] + (to[1] - from[1]) * t);
|
|
352
|
+
const b = Math.round(from[2] + (to[2] - from[2]) * t);
|
|
353
|
+
out += `\x1b[38;2;${r};${g};${b}m${glyph}`;
|
|
354
|
+
}
|
|
355
|
+
return out + BRAND.reset;
|
|
356
|
+
}
|
|
357
|
+
export function compactMasthead(version, opts = {}) {
|
|
358
|
+
const stream = opts.stream ?? process.stdout;
|
|
359
|
+
const state = opts.state ?? 'IDLE';
|
|
360
|
+
const badgeColor = opts.badgeColor ?? 'green';
|
|
361
|
+
const badgeLabel = opts.badgeLabel ?? 'CONNECTED';
|
|
362
|
+
const dotGlyph = asciiOnly() ? '*' : '●';
|
|
363
|
+
const brand = `${c.bold(c.cream('non.bot', stream), stream)}${c.blue('.', stream)}`;
|
|
364
|
+
const badge = colorize(`${dotGlyph} ${badgeLabel}`, badgeColor, stream);
|
|
365
|
+
const sep = c.muted('·', stream);
|
|
366
|
+
const tail = [c.muted(`daemon ${version}`, stream), sep, c.muted(state, stream)].join(' ');
|
|
367
|
+
return `${brand} ${badge} ${tail}`;
|
|
368
|
+
}
|
|
336
369
|
export const DAEMON_TAGLINES = [
|
|
337
370
|
"click ▶ on the canvas. terminal opens here. that's it.",
|
|
338
371
|
];
|
|
@@ -348,10 +381,19 @@ export function daemonOpener(version, opts = {}) {
|
|
|
348
381
|
const seed = opts.seed ?? Date.now();
|
|
349
382
|
const tagline = pickDaemonTagline(seed, opts.pool);
|
|
350
383
|
const state = opts.state ?? 'IDLE';
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
384
|
+
if (opts.compact) {
|
|
385
|
+
return compactMasthead(version, {
|
|
386
|
+
state,
|
|
387
|
+
badgeColor: opts.badgeColor ?? 'green',
|
|
388
|
+
badgeLabel: opts.badgeLabel ?? 'CONNECTED',
|
|
389
|
+
stream,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const topLine = opts.breadcrumb === ''
|
|
393
|
+
? ''
|
|
394
|
+
: opts.breadcrumb
|
|
395
|
+
? ` ${c.cyan(opts.breadcrumb, stream)}\n\n`
|
|
396
|
+
: ` ${promptLine({ stream })}\n\n`;
|
|
355
397
|
const stamp = renderWordmarkWithBadge({
|
|
356
398
|
badgeColor: opts.badgeColor ?? 'green',
|
|
357
399
|
badgeLabel: opts.badgeLabel ?? 'CONNECTED',
|
|
@@ -369,8 +411,10 @@ export function daemonOpener(version, opts = {}) {
|
|
|
369
411
|
subParts.push(c.muted(trackedCaps(state), stream));
|
|
370
412
|
}
|
|
371
413
|
const subtitleLine = ` ${subParts.join(' ')}`;
|
|
414
|
+
const ruleWidth = Math.min(46, Math.max(10, terminalWidth(opts.width) - 2));
|
|
415
|
+
const ruleLine = ` ${gradientRule(ruleWidth, { stream })}`;
|
|
372
416
|
const taglineLine = tagline ? `\n\n ${c.muted(tagline, stream)}` : '';
|
|
373
|
-
return `${
|
|
417
|
+
return `${topLine}${stamp}\n\n${ruleLine}\n\n${subtitleLine}${taglineLine}`;
|
|
374
418
|
}
|
|
375
419
|
export function daemonCloser(opts = {}) {
|
|
376
420
|
const stream = opts.stream ?? process.stdout;
|
|
@@ -390,3 +434,112 @@ export function daemonCloser(opts = {}) {
|
|
|
390
434
|
}
|
|
391
435
|
return parts.join('');
|
|
392
436
|
}
|
|
437
|
+
function stripControlBytes(s) {
|
|
438
|
+
return (s ?? '')
|
|
439
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
440
|
+
.replace(/ {2,}/g, ' ')
|
|
441
|
+
.trim();
|
|
442
|
+
}
|
|
443
|
+
const FOOTER_DOT_COLOR = {
|
|
444
|
+
live: 'green',
|
|
445
|
+
backoff: 'amber',
|
|
446
|
+
reconnecting: 'red',
|
|
447
|
+
waiting: 'red',
|
|
448
|
+
};
|
|
449
|
+
export function liveFooter(args) {
|
|
450
|
+
const stream = args.stream ?? process.stdout;
|
|
451
|
+
const failed = args.failed ?? 0;
|
|
452
|
+
const health = args.health ?? (args.waiting > 0 ? 'waiting' : 'live');
|
|
453
|
+
const dotGlyph = asciiOnly() ? '*' : '●';
|
|
454
|
+
const dotColor = FOOTER_DOT_COLOR[health];
|
|
455
|
+
const dot = colorize(dotGlyph, dotColor, stream);
|
|
456
|
+
const healthLabel = colorize(health, dotColor, stream);
|
|
457
|
+
const sep = c.muted('·', stream);
|
|
458
|
+
const segs = [`${dot} ${healthLabel}`];
|
|
459
|
+
segs.push(c.green(`${args.running} running`, stream));
|
|
460
|
+
if (args.waiting > 0)
|
|
461
|
+
segs.push(c.bold(c.amber(`${args.waiting} waiting`, stream), stream));
|
|
462
|
+
segs.push(c.muted(`${args.done} done`, stream));
|
|
463
|
+
if (failed > 0)
|
|
464
|
+
segs.push(c.red(`${failed} failed`, stream));
|
|
465
|
+
if (args.nextPollMs !== undefined) {
|
|
466
|
+
segs.push(c.muted(`next poll ${formatMsToS(args.nextPollMs)}`, stream));
|
|
467
|
+
}
|
|
468
|
+
if (args.version)
|
|
469
|
+
segs.push(c.muted(args.version, stream));
|
|
470
|
+
if (args.machine)
|
|
471
|
+
segs.push(c.cyan(args.machine, stream));
|
|
472
|
+
return segs.join(` ${sep} `);
|
|
473
|
+
}
|
|
474
|
+
export function needsYouBanner(args) {
|
|
475
|
+
const stream = args.stream ?? process.stdout;
|
|
476
|
+
const ascii = asciiOnly();
|
|
477
|
+
const bang = ascii ? '!!' : '‼';
|
|
478
|
+
const ptr = ascii ? '>' : '❯';
|
|
479
|
+
const tl = ascii ? '+' : '╔';
|
|
480
|
+
const tr = ascii ? '+' : '╗';
|
|
481
|
+
const bl = ascii ? '+' : '╚';
|
|
482
|
+
const br = ascii ? '+' : '╝';
|
|
483
|
+
const h = ascii ? '-' : '═';
|
|
484
|
+
const v = ascii ? '|' : '║';
|
|
485
|
+
const inner = Math.max(40, Math.min(args.width ?? 72, 100)) - 4;
|
|
486
|
+
const titleRaw = `${bang} NEEDS YOU · a run is waiting on your answer`;
|
|
487
|
+
const title = titleRaw.length > inner ? titleRaw.slice(0, inner - 1) + '…' : titleRaw.padEnd(inner, ' ');
|
|
488
|
+
const sep = c.muted('·', stream);
|
|
489
|
+
const lines = [];
|
|
490
|
+
lines.push(c.amber(` ${tl}${h.repeat(inner + 2)}${tr}`, stream));
|
|
491
|
+
lines.push(` ${c.amber(v, stream)} ${c.amber(c.bold(title, stream), stream)} ${c.amber(v, stream)}`);
|
|
492
|
+
lines.push(c.amber(` ${bl}${h.repeat(inner + 2)}${br}`, stream));
|
|
493
|
+
const story = stripControlBytes(args.story ?? '');
|
|
494
|
+
const idCell = c.bold(c.white(args.activationId, stream), stream);
|
|
495
|
+
lines.push(` ${idCell}${story ? ` ${sep} ${c.white(story, stream)}` : ''}`);
|
|
496
|
+
const metaParts = [c.amber('WAITING', stream)];
|
|
497
|
+
if (args.elapsedMs !== undefined)
|
|
498
|
+
metaParts.push(c.muted(formatElapsed(args.elapsedMs), stream));
|
|
499
|
+
metaParts.push(c.muted(`pane ${args.paneId}`, stream));
|
|
500
|
+
if (args.provider)
|
|
501
|
+
metaParts.push(c.muted(args.provider, stream));
|
|
502
|
+
lines.push(` ${metaParts.join(` ${sep} `)}`);
|
|
503
|
+
if (args.options && args.options.length > 0) {
|
|
504
|
+
const opts = args.options.slice(0, 4).map((o, i) => {
|
|
505
|
+
const label = stripControlBytes(o.label).slice(0, 40);
|
|
506
|
+
const lead = i === 0 ? `${c.cyan(ptr, stream)} ` : '';
|
|
507
|
+
return `${lead}${c.white(`${o.index}. ${label}`, stream)}`;
|
|
508
|
+
});
|
|
509
|
+
lines.push(` ${opts.join(' ')}`);
|
|
510
|
+
}
|
|
511
|
+
lines.push(` ${c.muted('answer it → on the canvas, on your phone, or', stream)} ` +
|
|
512
|
+
c.cyan(`tmux select-window -t ${args.paneId}`, stream));
|
|
513
|
+
return lines.join('\n');
|
|
514
|
+
}
|
|
515
|
+
export function resumedLine(activationId, stream = process.stdout) {
|
|
516
|
+
const check = asciiOnly() ? 'OK' : '✓';
|
|
517
|
+
return (` ${c.green(check, stream)} ${c.white(activationId, stream)} ` +
|
|
518
|
+
c.muted('· resumed · answer received', stream));
|
|
519
|
+
}
|
|
520
|
+
export function buildPaneBorderFormat(opts = {}) {
|
|
521
|
+
const warn = opts.ascii ? '!! ' : '⚠ ';
|
|
522
|
+
return (`#{?#{==:#{@nb_state},waiting},#[fg=#ffb454 bold]${warn},}` +
|
|
523
|
+
'#[fg=' +
|
|
524
|
+
'#{?#{==:#{@nb_state},waiting},#ffb454,' +
|
|
525
|
+
'#{?#{==:#{@nb_state},failed},#ff5c6c,' +
|
|
526
|
+
'#{?#{==:#{@nb_state},completed},#45d17f,' +
|
|
527
|
+
'#{?#{==:#{@nb_state},stopping},#ffb454,#58c7e6}}}}' +
|
|
528
|
+
']' +
|
|
529
|
+
' #{pane_title} ');
|
|
530
|
+
}
|
|
531
|
+
export function buildTmuxStatusLeft(opts = {}) {
|
|
532
|
+
const dot = opts.ascii ? '* ' : '● ';
|
|
533
|
+
return (' #[fg=#f0ebe2,bold]non#[fg=#6c8cff].#[fg=#f0ebe2]bot#[default] ' +
|
|
534
|
+
'#{?#{==:#{@nb_halted},1},#[fg=#ff5c6c]HALTED,' +
|
|
535
|
+
`#[fg=#45d17f]${dot}CONNECTED}#[default] `);
|
|
536
|
+
}
|
|
537
|
+
export function buildTmuxStatusRight(opts = {}) {
|
|
538
|
+
const warn = opts.ascii ? '!!' : '⚠';
|
|
539
|
+
const dot = '·';
|
|
540
|
+
return ('#[fg=#45d17f]#{@nb_running} run#[default] ' +
|
|
541
|
+
`#[fg=#8a93a6]${dot} #{@nb_done} done` +
|
|
542
|
+
`#{?#{@nb_failed}, #[fg=#ff5c6c]${dot} #{@nb_failed} fail,}#[default] ` +
|
|
543
|
+
`#{?#{@nb_needsyou},#[fg=#ffb454,bold]${dot} ${warn} #{@nb_needsyou} NEEDS YOU ,}` +
|
|
544
|
+
`#[fg=#58c7e6]${dot} #{host_short} ${dot} poll #{@nb_poll}s `);
|
|
545
|
+
}
|
package/dist/lib/pane-title.js
CHANGED
|
@@ -4,12 +4,14 @@ const STATE_GLYPH = {
|
|
|
4
4
|
completed: '✓',
|
|
5
5
|
failed: '✗',
|
|
6
6
|
stopping: '■',
|
|
7
|
+
waiting: '‼',
|
|
7
8
|
};
|
|
8
9
|
const STATE_GLYPH_ASCII = {
|
|
9
10
|
running: '>',
|
|
10
11
|
completed: 'OK',
|
|
11
12
|
failed: 'XX',
|
|
12
13
|
stopping: '#',
|
|
14
|
+
waiting: '!!',
|
|
13
15
|
};
|
|
14
16
|
function asciiOnly(env = process.env) {
|
|
15
17
|
return env.NONBOT_ASCII_ONLY === '1' || env.NONBOT_ASCII_ONLY === 'true';
|
|
@@ -34,6 +36,26 @@ export function buildPaneTitleCommand(paneId, state, story, env = process.env) {
|
|
|
34
36
|
const title = formatPaneTitle(state, story, env);
|
|
35
37
|
return { cmd: 'tmux', args: ['select-pane', '-t', paneId, '-T', title] };
|
|
36
38
|
}
|
|
39
|
+
export function buildPaneStateOptionCommand(paneId, state) {
|
|
40
|
+
if (!/^%\d+$/.test(paneId))
|
|
41
|
+
return null;
|
|
42
|
+
return { cmd: 'tmux', args: ['set-option', '-p', '-t', paneId, '@nb_state', state] };
|
|
43
|
+
}
|
|
44
|
+
export function applyPaneState(paneId, state, spawnImpl = nodeSpawnSync) {
|
|
45
|
+
const command = buildPaneStateOptionCommand(paneId, state);
|
|
46
|
+
if (!command)
|
|
47
|
+
return false;
|
|
48
|
+
try {
|
|
49
|
+
spawnImpl(command.cmd, command.args, {
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
timeout: 1000,
|
|
52
|
+
windowsHide: true,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
37
59
|
export function applyPaneTitle(paneId, state, story, spawnImpl = nodeSpawnSync, env = process.env) {
|
|
38
60
|
const command = buildPaneTitleCommand(paneId, state, story, env);
|
|
39
61
|
if (!command)
|
|
@@ -94,6 +94,9 @@ export function validateProvider(s) {
|
|
|
94
94
|
const TMUX_SAFE_DIRECTIVES = new Set([
|
|
95
95
|
'set', 'set-option', 'setw', 'set-window-option',
|
|
96
96
|
]);
|
|
97
|
+
const TMUX_FORBIDDEN_OPTIONS = new Set([
|
|
98
|
+
'default-command', 'default-shell', 'command-alias',
|
|
99
|
+
]);
|
|
97
100
|
export function isSafeTmuxConf(conf) {
|
|
98
101
|
for (const rawLine of conf.split(/\r?\n/)) {
|
|
99
102
|
const line = rawLine.trim();
|
|
@@ -103,13 +106,32 @@ export function isSafeTmuxConf(conf) {
|
|
|
103
106
|
continue;
|
|
104
107
|
if (line.includes('#(') || line.includes('$(') || line.includes('`'))
|
|
105
108
|
return false;
|
|
106
|
-
|
|
107
|
-
if (!TMUX_SAFE_DIRECTIVES.has(firstToken))
|
|
109
|
+
if (line.includes('\\'))
|
|
108
110
|
return false;
|
|
111
|
+
for (const segment of line.split(';')) {
|
|
112
|
+
const cmd = segment.trim();
|
|
113
|
+
if (cmd === '')
|
|
114
|
+
continue;
|
|
115
|
+
const tokens = cmd.split(/\s+/);
|
|
116
|
+
if (!TMUX_SAFE_DIRECTIVES.has(tokens[0]))
|
|
117
|
+
return false;
|
|
118
|
+
for (const tok of tokens) {
|
|
119
|
+
if (TMUX_FORBIDDEN_OPTIONS.has(tok))
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
124
|
return true;
|
|
111
125
|
}
|
|
112
|
-
const ITERM_FORBIDDEN_KEYS = [
|
|
126
|
+
const ITERM_FORBIDDEN_KEYS = [
|
|
127
|
+
'Command',
|
|
128
|
+
'Initial Text',
|
|
129
|
+
'Send Text at Start',
|
|
130
|
+
'Triggers',
|
|
131
|
+
'Smart Selection Rules',
|
|
132
|
+
'Semantic History',
|
|
133
|
+
'Bound Hosts',
|
|
134
|
+
];
|
|
113
135
|
export function isSafeItermProfileJson(jsonStr) {
|
|
114
136
|
let parsed;
|
|
115
137
|
try {
|
package/dist/lib/run-prompt.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
3
3
|
import { VERSION } from '../version.js';
|
|
4
|
+
import { evictOldestToCap } from './bounded-set.js';
|
|
4
5
|
const QUESTION_MAX = 200;
|
|
5
6
|
const LABEL_MAX = 80;
|
|
6
7
|
const OPTIONS_MAX = 8;
|
|
@@ -59,6 +60,21 @@ export function parsePromptMenu(paneText, fallbackMessage = '') {
|
|
|
59
60
|
question = (question || fallback).slice(0, QUESTION_MAX);
|
|
60
61
|
return { question, options };
|
|
61
62
|
}
|
|
63
|
+
export function capturePane(paneId, spawnImpl = nodeSpawnSync) {
|
|
64
|
+
if (!PANE_ID_RE.test(paneId))
|
|
65
|
+
return '';
|
|
66
|
+
try {
|
|
67
|
+
const r = spawnImpl('tmux', ['capture-pane', '-p', '-t', paneId], {
|
|
68
|
+
encoding: 'utf-8',
|
|
69
|
+
timeout: 2000,
|
|
70
|
+
windowsHide: true,
|
|
71
|
+
});
|
|
72
|
+
return typeof r.stdout === 'string' ? r.stdout : '';
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return '';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
62
78
|
export function mintPromptId(activationId, paneId, parsed) {
|
|
63
79
|
const sig = JSON.stringify({
|
|
64
80
|
q: parsed.question,
|
|
@@ -186,13 +202,26 @@ export async function deliverAnsweredPrompts(opts) {
|
|
|
186
202
|
return [];
|
|
187
203
|
const reported = [];
|
|
188
204
|
for (const p of answered) {
|
|
189
|
-
|
|
205
|
+
const localPaneId = opts.trackedPanes?.get(p.activationId);
|
|
206
|
+
if (!localPaneId)
|
|
207
|
+
continue;
|
|
208
|
+
if (p.paneId && p.paneId !== localPaneId)
|
|
190
209
|
continue;
|
|
191
210
|
if (!opts.injected.has(p.promptId)) {
|
|
192
|
-
const sent = injectAnswer(
|
|
211
|
+
const sent = injectAnswer(localPaneId, p.answerIndex, p.answerText, opts.spawnImpl);
|
|
193
212
|
if (!sent)
|
|
194
213
|
continue;
|
|
195
214
|
opts.injected.add(p.promptId);
|
|
215
|
+
try {
|
|
216
|
+
const spawn = opts.spawnImpl ?? nodeSpawnSync;
|
|
217
|
+
spawn('tmux', ['set-option', '-p', '-t', localPaneId, '@nb_state', 'running'], {
|
|
218
|
+
encoding: 'utf-8',
|
|
219
|
+
timeout: 1000,
|
|
220
|
+
windowsHide: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
}
|
|
196
225
|
}
|
|
197
226
|
const confirmed = await confirmDelivered({
|
|
198
227
|
baseUrl: opts.baseUrl,
|
|
@@ -202,10 +231,9 @@ export async function deliverAnsweredPrompts(opts) {
|
|
|
202
231
|
});
|
|
203
232
|
if (confirmed) {
|
|
204
233
|
reported.push(p.promptId);
|
|
205
|
-
opts.log?.(`✓ ${p.activationId} · answer delivered to pane ${
|
|
234
|
+
opts.log?.(`✓ ${p.activationId} · answer delivered to pane ${localPaneId}\n`);
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
|
-
|
|
209
|
-
opts.injected.clear();
|
|
237
|
+
evictOldestToCap(opts.injected, 500);
|
|
210
238
|
return reported;
|
|
211
239
|
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.9.
|
|
1
|
+
export const VERSION = '0.9.7';
|
package/package.json
CHANGED