@nonbot/cli 0.9.4 → 0.9.6
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 +180 -15
- 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/coordinated-set.js +3 -1
- package/dist/lib/choir/isolated-session.js +21 -3
- package/dist/lib/choir/worktree.js +3 -2
- package/dist/lib/completion.js +12 -8
- package/dist/lib/machine.js +6 -0
- package/dist/lib/output.js +109 -0
- 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, pollTick, activationCard, runSummary, formatElapsed, c, } from '../lib/output.js';
|
|
16
|
+
import { errorBlock, statusRow, daemonOpener, daemonCloser, pollTick, activationCard, runSummary, formatElapsed, liveFooter, needsYouBanner, resumedLine, buildPaneBorderFormat, buildTmuxStatusLeft, buildTmuxStatusRight, 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=#0b0b12,fg=#8b949e');
|
|
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=#0b0b12,bg=#4fc3f7,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,144 @@ 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);
|
|
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 stdoutIsTTY = deps.stdoutIsTTY ?? (process.stdout?.isTTY === true && !process.env.VITEST);
|
|
293
|
+
const footerEnabled = stdoutIsTTY && !headless;
|
|
294
|
+
let footerActive = false;
|
|
295
|
+
let lastPollOk = true;
|
|
296
|
+
const noBell = process.env.NONBOT_NO_BELL === '1' || process.env.NONBOT_NO_BELL === 'true';
|
|
297
|
+
const ERASE = '\r\x1b[K';
|
|
298
|
+
const footerHealth = () => {
|
|
299
|
+
if (openPrompts.size > 0)
|
|
300
|
+
return 'waiting';
|
|
301
|
+
if (!lastPollOk)
|
|
302
|
+
return 'reconnecting';
|
|
303
|
+
if (fixedInterval === undefined && currentInterval > POLL_FAST_MS)
|
|
304
|
+
return 'backoff';
|
|
305
|
+
return 'live';
|
|
306
|
+
};
|
|
307
|
+
const renderFooter = () => liveFooter({
|
|
308
|
+
running: trackedPanes.size,
|
|
309
|
+
waiting: openPrompts.size,
|
|
310
|
+
done: doneCount,
|
|
311
|
+
failed: failedCount,
|
|
312
|
+
nextPollMs: fixedInterval ?? currentInterval,
|
|
313
|
+
version: `v${VERSION}`,
|
|
314
|
+
machine: machineName,
|
|
315
|
+
health: footerHealth(),
|
|
316
|
+
});
|
|
317
|
+
const refreshFooter = () => {
|
|
318
|
+
if (footerEnabled && footerActive)
|
|
319
|
+
rawLog(ERASE + renderFooter());
|
|
320
|
+
};
|
|
321
|
+
const startFooter = () => {
|
|
322
|
+
if (!footerEnabled)
|
|
323
|
+
return;
|
|
324
|
+
footerActive = true;
|
|
325
|
+
rawLog(ERASE + renderFooter());
|
|
326
|
+
};
|
|
327
|
+
const log = (s) => {
|
|
328
|
+
if (!footerEnabled || !footerActive) {
|
|
329
|
+
rawLog(s);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
rawLog(ERASE);
|
|
333
|
+
rawLog(s);
|
|
334
|
+
rawLog(ERASE + renderFooter());
|
|
335
|
+
};
|
|
336
|
+
const dropFooter = () => {
|
|
337
|
+
if (footerActive) {
|
|
338
|
+
footerActive = false;
|
|
339
|
+
rawLog(ERASE);
|
|
340
|
+
}
|
|
242
341
|
};
|
|
243
342
|
const emitSummary = () => {
|
|
244
343
|
log(runSummary({ running: trackedPanes.size, done: doneCount, failed: failedCount }) + '\n');
|
|
344
|
+
pushTmuxStatus();
|
|
345
|
+
};
|
|
346
|
+
const sweepPanePrompts = () => {
|
|
347
|
+
if (!paneTitlingEnabled || trackedPanes.size === 0)
|
|
348
|
+
return;
|
|
349
|
+
let changed = false;
|
|
350
|
+
for (const [activationId, paneId] of trackedPanes) {
|
|
351
|
+
let menu;
|
|
352
|
+
try {
|
|
353
|
+
menu = parsePromptMenu(capturePane(paneId, spawnForTmux), '');
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const meta = trackedMeta.get(activationId);
|
|
359
|
+
if (menu.options.length > 0) {
|
|
360
|
+
const promptId = mintPromptId(activationId, paneId, menu);
|
|
361
|
+
if (openPrompts.get(activationId) === promptId)
|
|
362
|
+
continue;
|
|
363
|
+
openPrompts.set(activationId, promptId);
|
|
364
|
+
log('\n' +
|
|
365
|
+
needsYouBanner({
|
|
366
|
+
activationId,
|
|
367
|
+
story: meta?.story,
|
|
368
|
+
paneId,
|
|
369
|
+
provider: meta?.provider,
|
|
370
|
+
options: menu.options,
|
|
371
|
+
elapsedMs: meta ? Date.now() - meta.startedAt : undefined,
|
|
372
|
+
}) +
|
|
373
|
+
'\n');
|
|
374
|
+
retitlePane('waiting', paneId, meta?.story ?? '');
|
|
375
|
+
if (!noBell && footerEnabled)
|
|
376
|
+
rawLog('\x07');
|
|
377
|
+
changed = true;
|
|
378
|
+
}
|
|
379
|
+
else if (openPrompts.has(activationId)) {
|
|
380
|
+
openPrompts.delete(activationId);
|
|
381
|
+
log(resumedLine(activationId) + '\n');
|
|
382
|
+
retitlePane('running', paneId, meta?.story ?? '');
|
|
383
|
+
changed = true;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (const id of [...openPrompts.keys()]) {
|
|
387
|
+
if (!trackedPanes.has(id)) {
|
|
388
|
+
openPrompts.delete(id);
|
|
389
|
+
changed = true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (changed) {
|
|
393
|
+
pushTmuxStatus();
|
|
394
|
+
refreshFooter();
|
|
395
|
+
}
|
|
245
396
|
};
|
|
246
397
|
let running = true;
|
|
247
398
|
let sigHandler;
|
|
@@ -250,7 +401,9 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
250
401
|
running = false;
|
|
251
402
|
for (const id of [...runHeartbeats.keys()])
|
|
252
403
|
stopHeartbeat(id);
|
|
253
|
-
|
|
404
|
+
dropFooter();
|
|
405
|
+
pushTmuxStatus(0, true);
|
|
406
|
+
rawLog('\n' + statusRow('✓', 'daemon stopped', 'Ctrl-C received') + '\n');
|
|
254
407
|
process.exit(0);
|
|
255
408
|
};
|
|
256
409
|
process.on('SIGINT', sigHandler);
|
|
@@ -286,6 +439,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
286
439
|
log(`${headerMarker} ${c.bold(headerLabel)}\n`);
|
|
287
440
|
const ctxRows = [
|
|
288
441
|
['YOU', auth.email],
|
|
442
|
+
['MACHINE', `${machineName} · ${shortMachineId(machineId)}`],
|
|
289
443
|
['LISTENING', `${hostUrl}/api/cli/activations/pending`],
|
|
290
444
|
['PROFILE', profileName],
|
|
291
445
|
['LANDING', terminalLabel],
|
|
@@ -303,6 +457,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
303
457
|
inTmux: tmuxSessionName !== null,
|
|
304
458
|
}) + '\n');
|
|
305
459
|
log('\n');
|
|
460
|
+
pushTmuxStatus();
|
|
461
|
+
startFooter();
|
|
306
462
|
while (running) {
|
|
307
463
|
let firedThisPoll = false;
|
|
308
464
|
try {
|
|
@@ -329,6 +485,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
329
485
|
return 1;
|
|
330
486
|
}
|
|
331
487
|
if (res.ok) {
|
|
488
|
+
lastPollOk = true;
|
|
332
489
|
const body = (await res.json());
|
|
333
490
|
const pendingKills = body?.pendingKills ?? [];
|
|
334
491
|
if (pendingKills.length > 0) {
|
|
@@ -496,6 +653,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
496
653
|
baseUrl: auth.baseUrl,
|
|
497
654
|
pat: auth.pat,
|
|
498
655
|
injected: injectedPrompts,
|
|
656
|
+
trackedPanes,
|
|
499
657
|
machineId,
|
|
500
658
|
fetchImpl,
|
|
501
659
|
spawnImpl: deps.spawnSync,
|
|
@@ -504,6 +662,11 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
504
662
|
}
|
|
505
663
|
catch {
|
|
506
664
|
}
|
|
665
|
+
try {
|
|
666
|
+
sweepPanePrompts();
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
}
|
|
507
670
|
if (trackedPanes.size > 0) {
|
|
508
671
|
const reported = await checkCompletions({
|
|
509
672
|
tracked: trackedPanes,
|
|
@@ -537,7 +700,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
537
700
|
doneCount++;
|
|
538
701
|
trackedMeta.delete(id);
|
|
539
702
|
safeEmit(id, RUN_STAGE.FINISHED);
|
|
540
|
-
stopHeartbeat(id, '
|
|
703
|
+
stopHeartbeat(id, 'finished');
|
|
541
704
|
try {
|
|
542
705
|
const session = isolatedSessions.noteTerminal(id);
|
|
543
706
|
if (session) {
|
|
@@ -556,28 +719,30 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
556
719
|
emitSummary();
|
|
557
720
|
}
|
|
558
721
|
}
|
|
559
|
-
if (!firedThisPoll && pendingKills.length === 0) {
|
|
722
|
+
if (!firedThisPoll && pendingKills.length === 0 && !footerEnabled) {
|
|
560
723
|
log(pollTick({
|
|
561
724
|
pending: 0,
|
|
562
725
|
sleepMs: fixedInterval ?? currentInterval,
|
|
563
726
|
backingOff: fixedInterval === undefined && currentInterval > POLL_FAST_MS,
|
|
564
727
|
}) + '\n');
|
|
565
728
|
}
|
|
566
|
-
|
|
567
|
-
seen.clear();
|
|
729
|
+
evictOldestToCap(seen, 500);
|
|
568
730
|
}
|
|
569
731
|
}
|
|
570
732
|
catch (e) {
|
|
733
|
+
lastPollOk = false;
|
|
571
734
|
errLog(statusRow('⚠', 'poll error', e.message, { stream: process.stderr }) + '\n');
|
|
572
735
|
}
|
|
573
736
|
if (options.oneShot)
|
|
574
737
|
break;
|
|
575
738
|
if (fixedInterval !== undefined) {
|
|
739
|
+
refreshFooter();
|
|
576
740
|
await sleep(fixedInterval);
|
|
577
741
|
}
|
|
578
742
|
else {
|
|
579
743
|
if (firedThisPoll)
|
|
580
744
|
currentInterval = POLL_FAST_MS;
|
|
745
|
+
refreshFooter();
|
|
581
746
|
await sleep(currentInterval);
|
|
582
747
|
if (!firedThisPoll) {
|
|
583
748
|
currentInterval = Math.min(currentInterval * 2, maxIntervalMs);
|
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,7 @@ import { addWorktree as defaultAddWorktree } from './worktree.js';
|
|
|
7
7
|
import { createHub as defaultCreateHub } from './hub.js';
|
|
8
8
|
import { PROVIDER_PROFILES } from '../command-builders.js';
|
|
9
9
|
import { validateRepoPath } from '../payload-validator.js';
|
|
10
|
+
import { resolveBaseBranch as defaultResolveBaseBranch } from './isolated-session.js';
|
|
10
11
|
const defaultFs = {
|
|
11
12
|
mkdirSync: (p, opts) => nodeFs.mkdirSync(p, opts),
|
|
12
13
|
writeFileSync: (p, data, opts) => nodeFs.writeFileSync(p, data, opts),
|
|
@@ -122,7 +123,8 @@ export async function launchCoordinatedSet(args) {
|
|
|
122
123
|
throw new Error('coordinated set token minting produced an empty token');
|
|
123
124
|
}
|
|
124
125
|
const sessionId = `choir_${sessionName}_${token.slice(0, 8)}`;
|
|
125
|
-
const
|
|
126
|
+
const resolveBaseBranchFn = deps.resolveBaseBranchImpl ?? defaultResolveBaseBranch;
|
|
127
|
+
const baseBranch = resolveBaseBranchFn(repoRoot);
|
|
126
128
|
const hub = createHubImpl({
|
|
127
129
|
sessionId,
|
|
128
130
|
repoRoot,
|
|
@@ -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,9 +83,14 @@ 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
|
-
removeWorktreeImpl({ repoRoot: session.repoRoot, worktreePath: p.worktreePath });
|
|
93
|
+
removeWorktreeImpl({ repoRoot: session.repoRoot, worktreePath: p.worktreePath, force: true });
|
|
78
94
|
removed.push(p.worktreePath);
|
|
79
95
|
}
|
|
80
96
|
catch {
|
|
@@ -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}); ` +
|
|
@@ -84,8 +84,9 @@ export function listWorktrees(repoRoot, spawnImpl = nodeSpawnSync) {
|
|
|
84
84
|
return entries;
|
|
85
85
|
}
|
|
86
86
|
export function removeWorktree(args) {
|
|
87
|
-
const { repoRoot, worktreePath, spawnImpl = nodeSpawnSync } = args;
|
|
88
|
-
const
|
|
87
|
+
const { repoRoot, worktreePath, spawnImpl = nodeSpawnSync, force = false } = args;
|
|
88
|
+
const flags = force ? ['--force'] : [];
|
|
89
|
+
const r = runGit(spawnImpl, repoRoot, ['worktree', 'remove', ...flags, worktreePath]);
|
|
89
90
|
assertOk(r, 'worktree remove');
|
|
90
91
|
}
|
|
91
92
|
export function ensureChoirGitignored(repoRoot, fsImpl = defaultFs) {
|
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
|
@@ -390,3 +390,112 @@ export function daemonCloser(opts = {}) {
|
|
|
390
390
|
}
|
|
391
391
|
return parts.join('');
|
|
392
392
|
}
|
|
393
|
+
function stripControlBytes(s) {
|
|
394
|
+
return (s ?? '')
|
|
395
|
+
.replace(/[\x00-\x1f\x7f]/g, ' ')
|
|
396
|
+
.replace(/ {2,}/g, ' ')
|
|
397
|
+
.trim();
|
|
398
|
+
}
|
|
399
|
+
const FOOTER_DOT_COLOR = {
|
|
400
|
+
live: 'green',
|
|
401
|
+
backoff: 'amber',
|
|
402
|
+
reconnecting: 'red',
|
|
403
|
+
waiting: 'red',
|
|
404
|
+
};
|
|
405
|
+
export function liveFooter(args) {
|
|
406
|
+
const stream = args.stream ?? process.stdout;
|
|
407
|
+
const failed = args.failed ?? 0;
|
|
408
|
+
const health = args.health ?? (args.waiting > 0 ? 'waiting' : 'live');
|
|
409
|
+
const dotGlyph = asciiOnly() ? '*' : '●';
|
|
410
|
+
const dotColor = FOOTER_DOT_COLOR[health];
|
|
411
|
+
const dot = colorize(dotGlyph, dotColor, stream);
|
|
412
|
+
const healthLabel = colorize(health, dotColor, stream);
|
|
413
|
+
const sep = c.muted('·', stream);
|
|
414
|
+
const segs = [`${dot} ${healthLabel}`];
|
|
415
|
+
segs.push(c.green(`${args.running} running`, stream));
|
|
416
|
+
if (args.waiting > 0)
|
|
417
|
+
segs.push(c.bold(c.amber(`${args.waiting} waiting`, stream), stream));
|
|
418
|
+
segs.push(c.muted(`${args.done} done`, stream));
|
|
419
|
+
if (failed > 0)
|
|
420
|
+
segs.push(c.red(`${failed} failed`, stream));
|
|
421
|
+
if (args.nextPollMs !== undefined) {
|
|
422
|
+
segs.push(c.muted(`next poll ${formatMsToS(args.nextPollMs)}`, stream));
|
|
423
|
+
}
|
|
424
|
+
if (args.version)
|
|
425
|
+
segs.push(c.muted(args.version, stream));
|
|
426
|
+
if (args.machine)
|
|
427
|
+
segs.push(c.cyan(args.machine, stream));
|
|
428
|
+
return segs.join(` ${sep} `);
|
|
429
|
+
}
|
|
430
|
+
export function needsYouBanner(args) {
|
|
431
|
+
const stream = args.stream ?? process.stdout;
|
|
432
|
+
const ascii = asciiOnly();
|
|
433
|
+
const bang = ascii ? '!!' : '‼';
|
|
434
|
+
const ptr = ascii ? '>' : '❯';
|
|
435
|
+
const tl = ascii ? '+' : '╔';
|
|
436
|
+
const tr = ascii ? '+' : '╗';
|
|
437
|
+
const bl = ascii ? '+' : '╚';
|
|
438
|
+
const br = ascii ? '+' : '╝';
|
|
439
|
+
const h = ascii ? '-' : '═';
|
|
440
|
+
const v = ascii ? '|' : '║';
|
|
441
|
+
const inner = Math.max(40, Math.min(args.width ?? 72, 100)) - 4;
|
|
442
|
+
const titleRaw = `${bang} NEEDS YOU · a run is waiting on your answer`;
|
|
443
|
+
const title = titleRaw.length > inner ? titleRaw.slice(0, inner - 1) + '…' : titleRaw.padEnd(inner, ' ');
|
|
444
|
+
const sep = c.muted('·', stream);
|
|
445
|
+
const lines = [];
|
|
446
|
+
lines.push(c.amber(` ${tl}${h.repeat(inner + 2)}${tr}`, stream));
|
|
447
|
+
lines.push(` ${c.amber(v, stream)} ${c.amber(c.bold(title, stream), stream)} ${c.amber(v, stream)}`);
|
|
448
|
+
lines.push(c.amber(` ${bl}${h.repeat(inner + 2)}${br}`, stream));
|
|
449
|
+
const story = stripControlBytes(args.story ?? '');
|
|
450
|
+
const idCell = c.bold(c.white(args.activationId, stream), stream);
|
|
451
|
+
lines.push(` ${idCell}${story ? ` ${sep} ${c.white(story, stream)}` : ''}`);
|
|
452
|
+
const metaParts = [c.amber('WAITING', stream)];
|
|
453
|
+
if (args.elapsedMs !== undefined)
|
|
454
|
+
metaParts.push(c.muted(formatElapsed(args.elapsedMs), stream));
|
|
455
|
+
metaParts.push(c.muted(`pane ${args.paneId}`, stream));
|
|
456
|
+
if (args.provider)
|
|
457
|
+
metaParts.push(c.muted(args.provider, stream));
|
|
458
|
+
lines.push(` ${metaParts.join(` ${sep} `)}`);
|
|
459
|
+
if (args.options && args.options.length > 0) {
|
|
460
|
+
const opts = args.options.slice(0, 4).map((o, i) => {
|
|
461
|
+
const label = stripControlBytes(o.label).slice(0, 40);
|
|
462
|
+
const lead = i === 0 ? `${c.cyan(ptr, stream)} ` : '';
|
|
463
|
+
return `${lead}${c.white(`${o.index}. ${label}`, stream)}`;
|
|
464
|
+
});
|
|
465
|
+
lines.push(` ${opts.join(' ')}`);
|
|
466
|
+
}
|
|
467
|
+
lines.push(` ${c.muted('answer it → on the canvas, on your phone, or', stream)} ` +
|
|
468
|
+
c.cyan(`tmux select-window -t ${args.paneId}`, stream));
|
|
469
|
+
return lines.join('\n');
|
|
470
|
+
}
|
|
471
|
+
export function resumedLine(activationId, stream = process.stdout) {
|
|
472
|
+
const check = asciiOnly() ? 'OK' : '✓';
|
|
473
|
+
return (` ${c.green(check, stream)} ${c.white(activationId, stream)} ` +
|
|
474
|
+
c.muted('· resumed · answer received', stream));
|
|
475
|
+
}
|
|
476
|
+
export function buildPaneBorderFormat(opts = {}) {
|
|
477
|
+
const warn = opts.ascii ? '!! ' : '⚠ ';
|
|
478
|
+
return (`#{?#{==:#{@nb_state},waiting},#[fg=colour203 bold]${warn},}` +
|
|
479
|
+
'#[fg=' +
|
|
480
|
+
'#{?#{==:#{@nb_state},waiting},colour203,' +
|
|
481
|
+
'#{?#{==:#{@nb_state},failed},colour203,' +
|
|
482
|
+
'#{?#{==:#{@nb_state},completed},colour78,' +
|
|
483
|
+
'#{?#{==:#{@nb_state},stopping},colour214,colour45}}}}' +
|
|
484
|
+
']' +
|
|
485
|
+
' #{pane_title} ');
|
|
486
|
+
}
|
|
487
|
+
export function buildTmuxStatusLeft(opts = {}) {
|
|
488
|
+
const dot = opts.ascii ? '* ' : '● ';
|
|
489
|
+
return (' #[fg=#ffffff,bold]non.bot#[default] ' +
|
|
490
|
+
'#{?#{==:#{@nb_halted},1},#[fg=#ff4c51]HALTED,' +
|
|
491
|
+
`#[fg=#28c76f]${dot}CONNECTED}#[default] `);
|
|
492
|
+
}
|
|
493
|
+
export function buildTmuxStatusRight(opts = {}) {
|
|
494
|
+
const warn = opts.ascii ? '!!' : '⚠';
|
|
495
|
+
const dot = '·';
|
|
496
|
+
return ('#[fg=#28c76f]#{@nb_running} run#[default] ' +
|
|
497
|
+
`#[fg=#8b949e]${dot} #{@nb_done} done` +
|
|
498
|
+
`#{?#{@nb_failed}, #[fg=#ff4c51]${dot} #{@nb_failed} fail,}#[default] ` +
|
|
499
|
+
`#{?#{@nb_needsyou},#[fg=#ff9f43,bold]${dot} ${warn} #{@nb_needsyou} NEEDS YOU ,}` +
|
|
500
|
+
`#[fg=#4fc3f7]${dot} #{host_short} ${dot} poll #{@nb_poll}s `);
|
|
501
|
+
}
|
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.6';
|
package/package.json
CHANGED