@nonbot/cli 0.9.3 → 0.9.4
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
CHANGED
|
@@ -6,7 +6,8 @@ import { checkCompletions } from '../lib/completion.js';
|
|
|
6
6
|
import { deliverAnsweredPrompts } from '../lib/run-prompt.js';
|
|
7
7
|
import { loadOrCreateMachineId, resolveMachineName } from '../lib/machine.js';
|
|
8
8
|
import { emitRunStage as defaultEmitRunStage, startRunHeartbeat as defaultStartRunHeartbeat, RUN_STAGE, } from '../lib/choir/run-progress.js';
|
|
9
|
-
import { groupBySession, launchCoordinatedSet, } from '../lib/choir/coordinated-set.js';
|
|
9
|
+
import { groupBySession, launchCoordinatedSet, setIsIsolated, } from '../lib/choir/coordinated-set.js';
|
|
10
|
+
import { IsolatedSessionTracker, reconcileAndCleanup as defaultReconcileAndCleanup, resolveBaseBranch as defaultResolveBaseBranch, } from '../lib/choir/isolated-session.js';
|
|
10
11
|
import { applyPaneTitle } from '../lib/pane-title.js';
|
|
11
12
|
import { installService, uninstallService } from '../lib/service.js';
|
|
12
13
|
import { detectTmuxSession, inTmuxSession, nonbotTmuxOptOut, resolveTerminal, } from '../lib/terminal.js';
|
|
@@ -193,6 +194,10 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
193
194
|
const seen = new Set();
|
|
194
195
|
const launchedSessions = new Set();
|
|
195
196
|
const launchCoordinatedSetFn = deps.launchCoordinatedSet ?? launchCoordinatedSet;
|
|
197
|
+
const isolatedSessions = deps.isolatedSessions ?? new IsolatedSessionTracker();
|
|
198
|
+
const reconcileAndCleanupFn = deps.reconcileAndCleanup ?? defaultReconcileAndCleanup;
|
|
199
|
+
const resolveBaseBranchFn = deps.resolveBaseBranch ?? defaultResolveBaseBranch;
|
|
200
|
+
const reconcileBuildCommand = deps.reconcileBuildCommand ?? process.env.NONBOT_RECONCILE_BUILD_CMD ?? '';
|
|
196
201
|
const trackedPanes = new Map();
|
|
197
202
|
const killedByStop = new Set();
|
|
198
203
|
const injectedPrompts = new Set();
|
|
@@ -351,6 +356,11 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
351
356
|
const acts = body?.activations ?? [];
|
|
352
357
|
const { singletons, sets } = groupBySession(acts);
|
|
353
358
|
for (const [choirSessionId, setActs] of sets) {
|
|
359
|
+
if (!setIsIsolated(setActs)) {
|
|
360
|
+
for (const a of setActs)
|
|
361
|
+
singletons.push(a);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
354
364
|
for (const a of setActs)
|
|
355
365
|
if (a?.id)
|
|
356
366
|
seen.add(a.id);
|
|
@@ -396,6 +406,22 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
396
406
|
catch {
|
|
397
407
|
}
|
|
398
408
|
}
|
|
409
|
+
try {
|
|
410
|
+
isolatedSessions.register({
|
|
411
|
+
choirSessionId,
|
|
412
|
+
sessionId: result.sessionId,
|
|
413
|
+
repoRoot: result.repoRoot,
|
|
414
|
+
baseBranch: resolveBaseBranchFn(result.repoRoot, deps.spawnSync),
|
|
415
|
+
buildTestCommand: reconcileBuildCommand,
|
|
416
|
+
panes: result.panes.map((p) => ({
|
|
417
|
+
activationId: p.activationId,
|
|
418
|
+
branch: p.branch,
|
|
419
|
+
worktreePath: p.worktreePath,
|
|
420
|
+
})),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
}
|
|
399
425
|
lastFailureReason = null;
|
|
400
426
|
emitSummary();
|
|
401
427
|
}
|
|
@@ -512,6 +538,20 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
512
538
|
trackedMeta.delete(id);
|
|
513
539
|
safeEmit(id, RUN_STAGE.FINISHED);
|
|
514
540
|
stopHeartbeat(id, 'daemon-exit');
|
|
541
|
+
try {
|
|
542
|
+
const session = isolatedSessions.noteTerminal(id);
|
|
543
|
+
if (session) {
|
|
544
|
+
reconcileAndCleanupFn(session, {
|
|
545
|
+
spawnImpl: deps.spawnSync,
|
|
546
|
+
log,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
catch (e) {
|
|
551
|
+
errLog(statusRow('⚠', 'reconcile failed', e.message, {
|
|
552
|
+
stream: process.stderr,
|
|
553
|
+
}) + '\n');
|
|
554
|
+
}
|
|
515
555
|
}
|
|
516
556
|
emitSummary();
|
|
517
557
|
}
|
|
@@ -25,6 +25,12 @@ export function choirSessionIdOf(act) {
|
|
|
25
25
|
? p.choirSessionId
|
|
26
26
|
: null;
|
|
27
27
|
}
|
|
28
|
+
export function isolateOf(act) {
|
|
29
|
+
return payloadOf(act).isolate === true;
|
|
30
|
+
}
|
|
31
|
+
export function setIsIsolated(activations) {
|
|
32
|
+
return Array.isArray(activations) && activations.some(isolateOf);
|
|
33
|
+
}
|
|
28
34
|
export function groupBySession(activations) {
|
|
29
35
|
const singletons = [];
|
|
30
36
|
const sets = new Map();
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawnSync as nodeSpawnSync } from 'node:child_process';
|
|
2
|
+
import { removeWorktree as defaultRemoveWorktree } from './worktree.js';
|
|
3
|
+
import { runReconcile as defaultRunReconcile } from './reconcile.js';
|
|
4
|
+
import { isValidBranch } from './names.js';
|
|
5
|
+
export class IsolatedSessionTracker {
|
|
6
|
+
sessions = new Map();
|
|
7
|
+
register(a) {
|
|
8
|
+
if (this.sessions.has(a.choirSessionId))
|
|
9
|
+
return;
|
|
10
|
+
this.sessions.set(a.choirSessionId, {
|
|
11
|
+
choirSessionId: a.choirSessionId,
|
|
12
|
+
sessionId: a.sessionId,
|
|
13
|
+
repoRoot: a.repoRoot,
|
|
14
|
+
baseBranch: a.baseBranch,
|
|
15
|
+
buildTestCommand: a.buildTestCommand ?? '',
|
|
16
|
+
panes: a.panes,
|
|
17
|
+
pending: new Set(a.panes.map((p) => p.activationId)),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
tracks(activationId) {
|
|
21
|
+
for (const s of this.sessions.values()) {
|
|
22
|
+
if (s.pending.has(activationId))
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
noteTerminal(activationId) {
|
|
28
|
+
for (const s of this.sessions.values()) {
|
|
29
|
+
if (!s.pending.delete(activationId))
|
|
30
|
+
continue;
|
|
31
|
+
if (s.pending.size === 0) {
|
|
32
|
+
this.sessions.delete(s.choirSessionId);
|
|
33
|
+
return s;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
get size() {
|
|
40
|
+
return this.sessions.size;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function resolveBaseBranch(repoRoot, spawnImpl = nodeSpawnSync) {
|
|
44
|
+
try {
|
|
45
|
+
const r = spawnImpl('git', ['-C', repoRoot, 'rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
46
|
+
encoding: 'utf-8',
|
|
47
|
+
timeout: 5000,
|
|
48
|
+
windowsHide: true,
|
|
49
|
+
});
|
|
50
|
+
const name = typeof r.stdout === 'string' ? r.stdout.trim() : '';
|
|
51
|
+
if (isValidBranch(name))
|
|
52
|
+
return name;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
}
|
|
56
|
+
return 'main';
|
|
57
|
+
}
|
|
58
|
+
export function reconcileAndCleanup(session, deps = {}) {
|
|
59
|
+
const spawnImpl = deps.spawnImpl ?? nodeSpawnSync;
|
|
60
|
+
const runReconcileImpl = deps.runReconcileImpl ?? defaultRunReconcile;
|
|
61
|
+
const removeWorktreeImpl = deps.removeWorktreeImpl ?? defaultRemoveWorktree;
|
|
62
|
+
const log = deps.log;
|
|
63
|
+
const branches = session.panes.map((p) => p.branch);
|
|
64
|
+
const reconcile = runReconcileImpl({
|
|
65
|
+
repoRoot: session.repoRoot,
|
|
66
|
+
baseBranch: session.baseBranch,
|
|
67
|
+
branches,
|
|
68
|
+
buildTestCommand: session.buildTestCommand,
|
|
69
|
+
sessionId: session.sessionId,
|
|
70
|
+
spawnImpl,
|
|
71
|
+
emit: deps.emit,
|
|
72
|
+
});
|
|
73
|
+
const removed = [];
|
|
74
|
+
if (!reconcile.blocked) {
|
|
75
|
+
for (const p of session.panes) {
|
|
76
|
+
try {
|
|
77
|
+
removeWorktreeImpl({ repoRoot: session.repoRoot, worktreePath: p.worktreePath });
|
|
78
|
+
removed.push(p.worktreePath);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
log?.(`[reconcile] ${session.sessionId} — merged ${reconcile.merged.length} ` +
|
|
84
|
+
`branch${reconcile.merged.length === 1 ? '' : 'es'}, ` +
|
|
85
|
+
`removed ${removed.length} worktree${removed.length === 1 ? '' : 's'}\n`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
log?.(`[reconcile] ${session.sessionId} — BLOCKED (${reconcile.reason}); ` +
|
|
89
|
+
`worktrees kept for the human merge gate\n`);
|
|
90
|
+
}
|
|
91
|
+
return { reconcile, removed };
|
|
92
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.9.
|
|
1
|
+
export const VERSION = '0.9.4';
|
package/package.json
CHANGED