@nonbot/cli 0.9.2 → 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
|
@@ -4,8 +4,10 @@ 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
6
|
import { deliverAnsweredPrompts } from '../lib/run-prompt.js';
|
|
7
|
+
import { loadOrCreateMachineId, resolveMachineName } from '../lib/machine.js';
|
|
7
8
|
import { emitRunStage as defaultEmitRunStage, startRunHeartbeat as defaultStartRunHeartbeat, RUN_STAGE, } from '../lib/choir/run-progress.js';
|
|
8
|
-
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';
|
|
9
11
|
import { applyPaneTitle } from '../lib/pane-title.js';
|
|
10
12
|
import { installService, uninstallService } from '../lib/service.js';
|
|
11
13
|
import { detectTmuxSession, inTmuxSession, nonbotTmuxOptOut, resolveTerminal, } from '../lib/terminal.js';
|
|
@@ -177,6 +179,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
177
179
|
}
|
|
178
180
|
const detectTmux = deps.detectTmuxSession ?? detectTmuxSession;
|
|
179
181
|
const tmuxSessionName = detectTmux();
|
|
182
|
+
const machineId = deps.machineId ?? loadOrCreateMachineId();
|
|
183
|
+
const machineName = resolveMachineName();
|
|
180
184
|
if (tmuxSessionName) {
|
|
181
185
|
applyNonbotTmuxConfig({ spawnSync: deps.spawnSync });
|
|
182
186
|
}
|
|
@@ -190,6 +194,10 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
190
194
|
const seen = new Set();
|
|
191
195
|
const launchedSessions = new Set();
|
|
192
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 ?? '';
|
|
193
201
|
const trackedPanes = new Map();
|
|
194
202
|
const killedByStop = new Set();
|
|
195
203
|
const injectedPrompts = new Set();
|
|
@@ -304,6 +312,8 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
304
312
|
'X-CLI-Version': VERSION,
|
|
305
313
|
'X-Terminal-Kind': terminalKind,
|
|
306
314
|
'X-Terminal-Launchable': terminalLaunchable ? 'true' : 'false',
|
|
315
|
+
'X-Machine-Id': machineId,
|
|
316
|
+
'X-Machine-Name': machineName,
|
|
307
317
|
};
|
|
308
318
|
if (tmuxSessionName)
|
|
309
319
|
headers['X-Tmux-Session'] = tmuxSessionName;
|
|
@@ -346,6 +356,11 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
346
356
|
const acts = body?.activations ?? [];
|
|
347
357
|
const { singletons, sets } = groupBySession(acts);
|
|
348
358
|
for (const [choirSessionId, setActs] of sets) {
|
|
359
|
+
if (!setIsIsolated(setActs)) {
|
|
360
|
+
for (const a of setActs)
|
|
361
|
+
singletons.push(a);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
349
364
|
for (const a of setActs)
|
|
350
365
|
if (a?.id)
|
|
351
366
|
seen.add(a.id);
|
|
@@ -391,6 +406,22 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
391
406
|
catch {
|
|
392
407
|
}
|
|
393
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
|
+
}
|
|
394
425
|
lastFailureReason = null;
|
|
395
426
|
emitSummary();
|
|
396
427
|
}
|
|
@@ -465,6 +496,7 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
465
496
|
baseUrl: auth.baseUrl,
|
|
466
497
|
pat: auth.pat,
|
|
467
498
|
injected: injectedPrompts,
|
|
499
|
+
machineId,
|
|
468
500
|
fetchImpl,
|
|
469
501
|
spawnImpl: deps.spawnSync,
|
|
470
502
|
log,
|
|
@@ -506,6 +538,20 @@ export async function runDaemonCommand(args = [], options = {}, deps = {}) {
|
|
|
506
538
|
trackedMeta.delete(id);
|
|
507
539
|
safeEmit(id, RUN_STAGE.FINISHED);
|
|
508
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
|
+
}
|
|
509
555
|
}
|
|
510
556
|
emitSummary();
|
|
511
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
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir, hostname } from 'node:os';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import * as nodeFs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
const NAME_MAX = 64;
|
|
6
|
+
function configDir() {
|
|
7
|
+
const override = process.env.NONBOT_CONFIG_DIR;
|
|
8
|
+
if (override && override.length > 0)
|
|
9
|
+
return override;
|
|
10
|
+
return path.join(homedir(), '.config', 'nonbot');
|
|
11
|
+
}
|
|
12
|
+
export function machineFilePath(dir = configDir()) {
|
|
13
|
+
return path.join(dir, 'machine.json');
|
|
14
|
+
}
|
|
15
|
+
export function loadOrCreateMachineId(deps = {}) {
|
|
16
|
+
const fs = deps.fs ?? nodeFs;
|
|
17
|
+
const uuid = deps.uuid ?? randomUUID;
|
|
18
|
+
const dir = deps.dir ?? configDir();
|
|
19
|
+
const file = path.join(dir, 'machine.json');
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(file)) {
|
|
22
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
23
|
+
if (raw && typeof raw.machineId === 'string' && raw.machineId.length > 0) {
|
|
24
|
+
return raw.machineId;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
}
|
|
30
|
+
const machineId = uuid();
|
|
31
|
+
try {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
33
|
+
fs.writeFileSync(file, JSON.stringify({ machineId, createdAt: Date.now() }), { mode: 0o600 });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
return machineId;
|
|
38
|
+
}
|
|
39
|
+
export function resolveMachineName(env = process.env) {
|
|
40
|
+
const override = env.NONBOT_MACHINE_NAME;
|
|
41
|
+
if (override && override.trim().length > 0)
|
|
42
|
+
return override.trim().slice(0, NAME_MAX);
|
|
43
|
+
try {
|
|
44
|
+
const h = hostname();
|
|
45
|
+
if (h && h.length > 0)
|
|
46
|
+
return h.slice(0, NAME_MAX);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
}
|
|
50
|
+
return 'unknown';
|
|
51
|
+
}
|
package/dist/lib/run-prompt.js
CHANGED
|
@@ -99,13 +99,14 @@ export async function reportPrompt(args) {
|
|
|
99
99
|
export async function pollAnsweredPrompts(args) {
|
|
100
100
|
const fetchImpl = args.fetchImpl ?? fetch;
|
|
101
101
|
try {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
const headers = {
|
|
103
|
+
Authorization: `Bearer ${args.pat}`,
|
|
104
|
+
'X-Requested-With': 'ConradPM-Native',
|
|
105
|
+
'X-CLI-Version': VERSION,
|
|
106
|
+
};
|
|
107
|
+
if (args.machineId)
|
|
108
|
+
headers['X-Machine-Id'] = args.machineId;
|
|
109
|
+
const res = await fetchImpl(`${args.baseUrl}/api/cli/run-prompts/answered`, { headers });
|
|
109
110
|
if (!res.ok)
|
|
110
111
|
return [];
|
|
111
112
|
const data = (await res.json());
|
|
@@ -178,6 +179,7 @@ export async function deliverAnsweredPrompts(opts) {
|
|
|
178
179
|
const answered = await pollAnsweredPrompts({
|
|
179
180
|
baseUrl: opts.baseUrl,
|
|
180
181
|
pat: opts.pat,
|
|
182
|
+
machineId: opts.machineId,
|
|
181
183
|
fetchImpl: opts.fetchImpl,
|
|
182
184
|
});
|
|
183
185
|
if (answered.length === 0)
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.9.
|
|
1
|
+
export const VERSION = '0.9.4';
|
package/package.json
CHANGED