borgmcp 3.4.0 → 3.5.0
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/README.md +20 -2
- package/dist/backends/{launch-all-windows.d.ts → launch-all-terminals.d.ts} +5 -3
- package/dist/backends/launch-all-terminals.d.ts.map +1 -0
- package/dist/backends/{launch-all-windows.js → launch-all-terminals.js} +26 -12
- package/dist/backends/launch-all-terminals.js.map +1 -0
- package/dist/bare-launch-menu.d.ts +53 -16
- package/dist/bare-launch-menu.d.ts.map +1 -1
- package/dist/bare-launch-menu.js +95 -4
- package/dist/bare-launch-menu.js.map +1 -1
- package/dist/claude.d.ts +6 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +83 -8
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts +2 -0
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +31 -2
- package/dist/cli-help.js.map +1 -1
- package/dist/config-utils.d.ts +12 -6
- package/dist/config-utils.d.ts.map +1 -1
- package/dist/config-utils.js +30 -7
- package/dist/config-utils.js.map +1 -1
- package/dist/cubes.d.ts +17 -0
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +87 -1
- package/dist/cubes.js.map +1 -1
- package/dist/ensure-mcp-config.d.ts.map +1 -1
- package/dist/ensure-mcp-config.js +5 -2
- package/dist/ensure-mcp-config.js.map +1 -1
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +20 -10
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/parse-launch-all-args.d.ts +1 -1
- package/dist/parse-launch-all-args.d.ts.map +1 -1
- package/dist/parse-launch-all-args.js +3 -3
- package/dist/parse-launch-all-args.js.map +1 -1
- package/dist/seat-commands.d.ts +58 -0
- package/dist/seat-commands.d.ts.map +1 -0
- package/dist/seat-commands.js +240 -0
- package/dist/seat-commands.js.map +1 -0
- package/dist/seats.d.ts +8 -0
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +14 -0
- package/dist/seats.js.map +1 -1
- package/dist/unknown-subcommand.d.ts +1 -1
- package/dist/unknown-subcommand.d.ts.map +1 -1
- package/dist/unknown-subcommand.js +2 -0
- package/dist/unknown-subcommand.js.map +1 -1
- package/package.json +1 -1
- package/src/backends/{launch-all-windows.ts → launch-all-terminals.ts} +33 -15
- package/src/bare-launch-menu.ts +141 -18
- package/src/claude.ts +127 -7
- package/src/cli-help.ts +37 -2
- package/src/config-utils.ts +32 -7
- package/src/cubes.ts +118 -1
- package/src/ensure-mcp-config.ts +5 -2
- package/src/launch-all-cmd.ts +24 -11
- package/src/parse-launch-all-args.ts +4 -4
- package/src/seat-commands.ts +322 -0
- package/src/seats.ts +15 -0
- package/src/unknown-subcommand.ts +2 -0
- package/dist/backends/launch-all-windows.d.ts.map +0 -1
- package/dist/backends/launch-all-windows.js.map +0 -1
package/src/cubes.ts
CHANGED
|
@@ -76,6 +76,113 @@ export interface ActiveCube {
|
|
|
76
76
|
worktree?: string;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export const BORG_LAUNCH_EXPECTED_SEAT_ENV = 'BORG_LAUNCH_EXPECTED_SEAT';
|
|
80
|
+
|
|
81
|
+
export interface LaunchSeatExpectation {
|
|
82
|
+
credentialRef: string;
|
|
83
|
+
cubeId: string;
|
|
84
|
+
droneId: string;
|
|
85
|
+
worktree: string;
|
|
86
|
+
droneLabel: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class LaunchSeatIdentityChangedError extends Error {
|
|
90
|
+
readonly code = 'LAUNCH_SEAT_IDENTITY_CHANGED';
|
|
91
|
+
|
|
92
|
+
constructor(droneLabel: string) {
|
|
93
|
+
super(
|
|
94
|
+
`borg launch: did not launch '${droneLabel}' — its seat registration changed before the launch could start. ` +
|
|
95
|
+
'Run `borg seats` to see the current state, then try again.',
|
|
96
|
+
);
|
|
97
|
+
this.name = 'LaunchSeatIdentityChangedError';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function withLaunchSeatExpectationEnv(
|
|
102
|
+
env: NodeJS.ProcessEnv,
|
|
103
|
+
expectation: LaunchSeatExpectation,
|
|
104
|
+
): NodeJS.ProcessEnv {
|
|
105
|
+
// The deterministic ref and public identity are sufficient; never copy the
|
|
106
|
+
// stored bearer into a process environment.
|
|
107
|
+
return {
|
|
108
|
+
...env,
|
|
109
|
+
[BORG_LAUNCH_EXPECTED_SEAT_ENV]: JSON.stringify(expectation),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Codex MCP children do not inherit the wrapper environment, so carry the
|
|
114
|
+
* launch-scoped expected seat through the same per-invocation config channel as
|
|
115
|
+
* the Borg-session and state-root markers. */
|
|
116
|
+
export function codexLaunchSeatExpectationConfigArgs(
|
|
117
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
118
|
+
): string[] {
|
|
119
|
+
const expectation = env[BORG_LAUNCH_EXPECTED_SEAT_ENV];
|
|
120
|
+
if (expectation === undefined) return [];
|
|
121
|
+
return [
|
|
122
|
+
'-c',
|
|
123
|
+
`mcp_servers.borg.env.${BORG_LAUNCH_EXPECTED_SEAT_ENV}=${JSON.stringify(expectation)}`,
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function readLaunchSeatExpectation(
|
|
128
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
129
|
+
): LaunchSeatExpectation | null {
|
|
130
|
+
const raw = env[BORG_LAUNCH_EXPECTED_SEAT_ENV];
|
|
131
|
+
// OpenCode substitutes an unset `{env:NAME}` reference with an empty string.
|
|
132
|
+
// That is an ordinary bare launch, not a malformed launch-seat expectation.
|
|
133
|
+
if (raw === undefined || raw === '') return null;
|
|
134
|
+
let parsed: unknown;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(raw);
|
|
137
|
+
} catch {
|
|
138
|
+
throw new LaunchSeatIdentityChangedError('<unknown>');
|
|
139
|
+
}
|
|
140
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
141
|
+
throw new LaunchSeatIdentityChangedError('<unknown>');
|
|
142
|
+
}
|
|
143
|
+
const value = parsed as Record<string, unknown>;
|
|
144
|
+
const droneLabel = typeof value.droneLabel === 'string' ? value.droneLabel : '<unknown>';
|
|
145
|
+
if (
|
|
146
|
+
typeof value.credentialRef !== 'string' ||
|
|
147
|
+
typeof value.cubeId !== 'string' ||
|
|
148
|
+
typeof value.droneId !== 'string' ||
|
|
149
|
+
typeof value.worktree !== 'string' ||
|
|
150
|
+
typeof value.droneLabel !== 'string'
|
|
151
|
+
) {
|
|
152
|
+
throw new LaunchSeatIdentityChangedError(droneLabel);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
credentialRef: value.credentialRef,
|
|
156
|
+
cubeId: value.cubeId,
|
|
157
|
+
droneId: value.droneId,
|
|
158
|
+
worktree: value.worktree,
|
|
159
|
+
droneLabel: value.droneLabel,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function assertLaunchSeatExpectation(
|
|
164
|
+
expectation: LaunchSeatExpectation,
|
|
165
|
+
active: ActiveCube | null | undefined,
|
|
166
|
+
worktree: string,
|
|
167
|
+
currentRecord: SeatRecord | null,
|
|
168
|
+
): void {
|
|
169
|
+
if (
|
|
170
|
+
currentRecord === null ||
|
|
171
|
+
seatRef(currentRecord) !== expectation.credentialRef ||
|
|
172
|
+
currentRecord.cubeId !== expectation.cubeId ||
|
|
173
|
+
currentRecord.droneId !== expectation.droneId ||
|
|
174
|
+
resolve(worktree) !== resolve(expectation.worktree) ||
|
|
175
|
+
(active !== undefined && (
|
|
176
|
+
active === null ||
|
|
177
|
+
active.localSessionCredentialRef !== expectation.credentialRef ||
|
|
178
|
+
active.cubeId !== expectation.cubeId ||
|
|
179
|
+
active.droneId !== expectation.droneId
|
|
180
|
+
))
|
|
181
|
+
) {
|
|
182
|
+
throw new LaunchSeatIdentityChangedError(expectation.droneLabel);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
79
186
|
export type ActiveCubeInput = Omit<ActiveCube, 'sessionToken'> & {
|
|
80
187
|
sessionToken?: string;
|
|
81
188
|
};
|
|
@@ -303,9 +410,19 @@ export async function getActiveCube(): Promise<ActiveCube | null> {
|
|
|
303
410
|
}
|
|
304
411
|
|
|
305
412
|
export async function getActiveCubeForWorktree(worktree: string): Promise<ActiveCube | null> {
|
|
306
|
-
const
|
|
413
|
+
const projectRoot = findProjectRoot(worktree);
|
|
414
|
+
const expectation = readLaunchSeatExpectation();
|
|
415
|
+
const record = await getActiveSeatForWorktree(projectRoot);
|
|
416
|
+
// A launch-by-label child must hydrate the exact record selected by its
|
|
417
|
+
// parent. Check on both sides of bearer hydration so a concurrent preferred-
|
|
418
|
+
// seat replacement fails closed instead of launching a different drone.
|
|
419
|
+
if (expectation) assertLaunchSeatExpectation(expectation, undefined, projectRoot, record);
|
|
307
420
|
if (!record || !record.cubeId || !record.droneId) return null;
|
|
308
421
|
const active = await hydrateActiveCube(record);
|
|
422
|
+
if (expectation) {
|
|
423
|
+
const currentRecord = await getActiveSeatForWorktree(projectRoot);
|
|
424
|
+
assertLaunchSeatExpectation(expectation, active, projectRoot, currentRecord);
|
|
425
|
+
}
|
|
309
426
|
if (active && pinnedMcpSeatIdentity && (
|
|
310
427
|
resolve(active.worktree ?? '') !== pinnedMcpSeatIdentity.worktree ||
|
|
311
428
|
active.cubeId !== pinnedMcpSeatIdentity.cubeId ||
|
package/src/ensure-mcp-config.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
addOpenCodeMcpServer,
|
|
6
6
|
isCodexMcpServerConfigured,
|
|
7
7
|
isMcpServerConfigured,
|
|
8
|
-
|
|
8
|
+
isOpenCodeMcpServerConfiguredForLaunch,
|
|
9
9
|
} from './config-utils.js';
|
|
10
10
|
|
|
11
11
|
export interface EnsureMcpConfigDeps {
|
|
@@ -22,7 +22,7 @@ const defaultDeps: EnsureMcpConfigDeps = {
|
|
|
22
22
|
addClaude: addMcpServer,
|
|
23
23
|
isCodexConfigured: isCodexMcpServerConfigured,
|
|
24
24
|
addCodex: addCodexMcpServer,
|
|
25
|
-
isOpenCodeConfigured:
|
|
25
|
+
isOpenCodeConfigured: isOpenCodeMcpServerConfiguredForLaunch,
|
|
26
26
|
addOpenCode: addOpenCodeMcpServer,
|
|
27
27
|
};
|
|
28
28
|
|
|
@@ -49,6 +49,9 @@ export function ensureCliMcpConfigured(
|
|
|
49
49
|
case 'opencode':
|
|
50
50
|
if (deps.isOpenCodeConfigured()) return false;
|
|
51
51
|
deps.addOpenCode();
|
|
52
|
+
if (!deps.isOpenCodeConfigured()) {
|
|
53
|
+
throw new Error('OpenCode MCP registration could not be verified after setup');
|
|
54
|
+
}
|
|
52
55
|
return true;
|
|
53
56
|
}
|
|
54
57
|
}
|
package/src/launch-all-cmd.ts
CHANGED
|
@@ -11,10 +11,10 @@ import type { SeatStatus } from './seat-probe.js';
|
|
|
11
11
|
import { resolveBorgPath } from './launch-all-command.js';
|
|
12
12
|
import { sweepStaleLocks, isLockLive } from './launch-all-locks.js';
|
|
13
13
|
import { runTmuxBackend } from './backends/launch-all-tmux.js';
|
|
14
|
-
import {
|
|
14
|
+
import { hasMacOSTerminalApp, runTerminalsBackend } from './backends/launch-all-terminals.js';
|
|
15
15
|
import { runPastelistBackend } from './backends/launch-all-pastelist.js';
|
|
16
16
|
|
|
17
|
-
type Backend = 'tmux' | '
|
|
17
|
+
type Backend = 'tmux' | 'terminals' | 'pastelist';
|
|
18
18
|
|
|
19
19
|
const TMUX_INSTALL_HINT =
|
|
20
20
|
'borg launch-all: tmux not found.\n' +
|
|
@@ -82,10 +82,11 @@ async function resolveTargetCube(
|
|
|
82
82
|
return { cubeId: active.cubeId, name: active.name };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/** Backend selection
|
|
85
|
+
/** Backend selection: native-Windows / explicit / platform-specific auto. */
|
|
86
86
|
function selectBackend(args: LaunchAllArgs, deps: LaunchAllDeps): { backend: Backend } | { hardFail: string } {
|
|
87
87
|
const explicit = args.flags.mode;
|
|
88
|
-
const
|
|
88
|
+
const platform = deps.platform();
|
|
89
|
+
const nativeWindows = platform === 'win32' && !isWSL(deps);
|
|
89
90
|
if (nativeWindows) {
|
|
90
91
|
deps.stderr(
|
|
91
92
|
'native Windows is not supported for interactive launch; using pastelist mode instead ' +
|
|
@@ -93,13 +94,23 @@ function selectBackend(args: LaunchAllArgs, deps: LaunchAllDeps): { backend: Bac
|
|
|
93
94
|
);
|
|
94
95
|
return { backend: 'pastelist' };
|
|
95
96
|
}
|
|
96
|
-
if (explicit === '
|
|
97
|
+
if (explicit === 'terminals') return { backend: 'terminals' };
|
|
97
98
|
if (explicit === 'pastelist') return { backend: 'pastelist' };
|
|
98
|
-
|
|
99
|
-
const tmuxAvail = checkTmuxAvailable(deps);
|
|
100
99
|
if (explicit === 'tmux') {
|
|
101
|
-
return
|
|
100
|
+
return checkTmuxAvailable(deps) ? { backend: 'tmux' } : { hardFail: TMUX_INSTALL_HINT };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// macOS defaults to native terminal tabs/windows when either supported app
|
|
104
|
+
// is installed. Explicit modes above always win.
|
|
105
|
+
if (
|
|
106
|
+
platform === 'darwin' &&
|
|
107
|
+
deps.isTTY() &&
|
|
108
|
+
hasMacOSTerminalApp(deps)
|
|
109
|
+
) {
|
|
110
|
+
return { backend: 'terminals' };
|
|
102
111
|
}
|
|
112
|
+
|
|
113
|
+
const tmuxAvail = checkTmuxAvailable(deps);
|
|
103
114
|
// auto (no explicit mode)
|
|
104
115
|
if (tmuxAvail) return { backend: 'tmux' };
|
|
105
116
|
deps.stderr(TMUX_INSTALL_HINT + 'Falling back to pastelist mode (paste the commands below).\n');
|
|
@@ -416,8 +427,8 @@ export async function runLaunchAll(
|
|
|
416
427
|
}
|
|
417
428
|
printCheatSheet(sessionName, deps);
|
|
418
429
|
}
|
|
419
|
-
} else if (sel.backend === '
|
|
420
|
-
await
|
|
430
|
+
} else if (sel.backend === 'terminals') {
|
|
431
|
+
await runTerminalsBackend(launchable, { borgPath, platform: deps.platform(), cubeName, launchedAtISO: launchStartISO, launchDelayMs, sleep }, deps);
|
|
421
432
|
} else {
|
|
422
433
|
runPastelistBackend(launchable, borgPath, deps);
|
|
423
434
|
return 0; // pastelist: nothing to reconcile (operator pastes manually)
|
|
@@ -453,6 +464,8 @@ export async function runLaunchAll(
|
|
|
453
464
|
const status = statuses ? (statuses.get(c.droneId) === 'verified' ? 'VERIFIED' : 'unconfirmed (may still be joining)') : 'launched';
|
|
454
465
|
deps.stdout(` ${c.droneLabel} ${c.worktreeDir} ${status}\n`);
|
|
455
466
|
}
|
|
456
|
-
|
|
467
|
+
if (sel.backend === 'tmux') {
|
|
468
|
+
deps.stdout(`\nAttach: tmux attach -t ${sessionName}\n`);
|
|
469
|
+
}
|
|
457
470
|
return 0;
|
|
458
471
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Pure: rawArgs → validated LaunchAllArgs | error. Mirror of parse-assimilate-args.
|
|
3
3
|
|
|
4
4
|
export interface LaunchAllFlags {
|
|
5
|
-
mode?: 'tmux' | '
|
|
5
|
+
mode?: 'tmux' | 'terminals' | 'pastelist';
|
|
6
6
|
only?: string;
|
|
7
7
|
dryRun?: boolean;
|
|
8
8
|
cli?: 'claude' | 'codex' | 'opencode';
|
|
@@ -28,7 +28,7 @@ export type ParseLaunchAllResult =
|
|
|
28
28
|
| { ok: false; error: string };
|
|
29
29
|
|
|
30
30
|
const SUPPORTED =
|
|
31
|
-
'--mode <tmux|
|
|
31
|
+
'--mode <tmux|terminals|pastelist>, --only <name>, --dry-run, --cli <claude|codex|opencode>, ' +
|
|
32
32
|
'--no-attach, --yes/-y, --force, --launch-delay <ms>';
|
|
33
33
|
|
|
34
34
|
export function parseLaunchAllArgs(rawArgs: string[]): ParseLaunchAllResult {
|
|
@@ -40,8 +40,8 @@ export function parseLaunchAllArgs(rawArgs: string[]): ParseLaunchAllResult {
|
|
|
40
40
|
switch (arg) {
|
|
41
41
|
case '--mode': {
|
|
42
42
|
const v = rawArgs[++i];
|
|
43
|
-
if (v !== 'tmux' && v !== '
|
|
44
|
-
return { ok: false, error: `--mode must be one of tmux|
|
|
43
|
+
if (v !== 'tmux' && v !== 'terminals' && v !== 'pastelist') {
|
|
44
|
+
return { ok: false, error: `--mode must be one of tmux|terminals|pastelist (got: ${v ?? '<missing>'})` };
|
|
45
45
|
}
|
|
46
46
|
flags.mode = v;
|
|
47
47
|
break;
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import {
|
|
4
|
+
getProjectCliPreferenceForPath,
|
|
5
|
+
readAllProjectIdentities,
|
|
6
|
+
withLaunchSeatExpectationEnv,
|
|
7
|
+
type ActiveCube,
|
|
8
|
+
type BorgCli,
|
|
9
|
+
type LaunchSeatExpectation,
|
|
10
|
+
} from './cubes.js';
|
|
11
|
+
import { resolveBorgPath } from './launch-all-command.js';
|
|
12
|
+
import { getActiveSeatForWorktree, readAllBoundSeats, seatRef, type SeatRecord } from './seats.js';
|
|
13
|
+
|
|
14
|
+
export type LocalSeatState = 'active' | 'pending';
|
|
15
|
+
|
|
16
|
+
export interface LocalSeatRow {
|
|
17
|
+
droneLabel: string;
|
|
18
|
+
droneId: string;
|
|
19
|
+
cubeName: string;
|
|
20
|
+
cubeId: string;
|
|
21
|
+
worktree: string;
|
|
22
|
+
canonicalWorktree: string | null;
|
|
23
|
+
credentialRef: string;
|
|
24
|
+
cli: BorgCli | null;
|
|
25
|
+
state: LocalSeatState;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SeatCommandDeps {
|
|
29
|
+
readAllProjectIdentities: () => Promise<Array<{ projectPath: string; cube: ActiveCube }>>;
|
|
30
|
+
readAllBoundSeats: () => Promise<Array<{ worktree: string; record: SeatRecord }>>;
|
|
31
|
+
getActiveSeatForWorktree: (worktree: string) => Promise<SeatRecord | null>;
|
|
32
|
+
getProjectCliPreference: (worktree: string) => Promise<BorgCli | null>;
|
|
33
|
+
pathExists: (path: string) => boolean;
|
|
34
|
+
realpath: (path: string) => string;
|
|
35
|
+
/** Run this same borg executable with no args from the selected worktree,
|
|
36
|
+
* carrying only the expected durable identity for child-side verification. */
|
|
37
|
+
launchBareBorg: (worktree: string, expectation: LaunchSeatExpectation) => Promise<number>;
|
|
38
|
+
stdout: (line: string) => void;
|
|
39
|
+
stderr: (line: string) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ParsedSeatsArgs = { ok: true } | { ok: false; error: string };
|
|
43
|
+
|
|
44
|
+
export function parseSeatsArgs(args: readonly string[]): ParsedSeatsArgs {
|
|
45
|
+
return args.length === 0
|
|
46
|
+
? { ok: true }
|
|
47
|
+
: { ok: false, error: 'takes no arguments' };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ParsedLaunchSeatArgs =
|
|
51
|
+
| { ok: true; target: string; cube?: string }
|
|
52
|
+
| { ok: false; error: string };
|
|
53
|
+
|
|
54
|
+
export function parseLaunchSeatArgs(args: readonly string[]): ParsedLaunchSeatArgs {
|
|
55
|
+
let target: string | undefined;
|
|
56
|
+
let cube: string | undefined;
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
const arg = args[i];
|
|
60
|
+
if (arg === '--cube') {
|
|
61
|
+
const next = args[i + 1];
|
|
62
|
+
if (!next || next.startsWith('-')) {
|
|
63
|
+
return { ok: false, error: '--cube requires a cube name' };
|
|
64
|
+
}
|
|
65
|
+
cube = next;
|
|
66
|
+
i += 1;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg.startsWith('--cube=')) {
|
|
70
|
+
const value = arg.slice('--cube='.length);
|
|
71
|
+
if (!value) return { ok: false, error: '--cube requires a cube name' };
|
|
72
|
+
cube = value;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (arg.startsWith('-')) return { ok: false, error: `unknown option: ${arg}` };
|
|
76
|
+
if (target !== undefined) {
|
|
77
|
+
return { ok: false, error: 'accepts exactly one drone label or id prefix' };
|
|
78
|
+
}
|
|
79
|
+
target = arg;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!target) return { ok: false, error: 'requires a drone label or id prefix' };
|
|
83
|
+
return { ok: true, target, ...(cube ? { cube } : {}) };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeRealpath(deps: SeatCommandDeps, path: string): string | null {
|
|
87
|
+
try {
|
|
88
|
+
return deps.realpath(path);
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function seatKey(cubeId: string, droneId: string): string {
|
|
95
|
+
return `${cubeId}\0${droneId}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function readLocalSeatRows(deps: SeatCommandDeps): Promise<LocalSeatRow[]> {
|
|
99
|
+
const [identities, boundSeats] = await Promise.all([
|
|
100
|
+
deps.readAllProjectIdentities(),
|
|
101
|
+
deps.readAllBoundSeats(),
|
|
102
|
+
]);
|
|
103
|
+
const identityBySeat = new Map(
|
|
104
|
+
identities.map((entry) => [seatKey(entry.cube.cubeId, entry.cube.droneId), entry]),
|
|
105
|
+
);
|
|
106
|
+
const identityByRealpath = new Map<string, { projectPath: string; cube: ActiveCube }>();
|
|
107
|
+
for (const entry of identities) {
|
|
108
|
+
const canonical = safeRealpath(deps, entry.projectPath);
|
|
109
|
+
if (canonical) identityByRealpath.set(canonical, entry);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const rows = await Promise.all(boundSeats.map(async ({ worktree, record }) => {
|
|
113
|
+
const exists = deps.pathExists(worktree);
|
|
114
|
+
const canonicalWorktree = exists ? safeRealpath(deps, worktree) : null;
|
|
115
|
+
const identity = (record.droneId ? identityBySeat.get(seatKey(record.cubeId, record.droneId)) : undefined)
|
|
116
|
+
?? (canonicalWorktree ? identityByRealpath.get(canonicalWorktree) : undefined);
|
|
117
|
+
const cube = identity?.cube;
|
|
118
|
+
const launchPath = identity?.projectPath ?? worktree;
|
|
119
|
+
const launchCanonical = deps.pathExists(launchPath) ? safeRealpath(deps, launchPath) : null;
|
|
120
|
+
return {
|
|
121
|
+
droneLabel: cube?.droneLabel ?? record.droneLabel ?? '<unknown>',
|
|
122
|
+
droneId: cube?.droneId ?? record.droneId ?? '',
|
|
123
|
+
cubeName: cube?.name ?? record.name ?? '<unknown>',
|
|
124
|
+
cubeId: cube?.cubeId ?? record.cubeId,
|
|
125
|
+
worktree: launchPath,
|
|
126
|
+
canonicalWorktree: launchCanonical,
|
|
127
|
+
credentialRef: seatRef(record),
|
|
128
|
+
cli: await deps.getProjectCliPreference(launchPath),
|
|
129
|
+
state: record.state,
|
|
130
|
+
};
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
return rows.sort((a, b) =>
|
|
134
|
+
a.droneLabel.localeCompare(b.droneLabel)
|
|
135
|
+
|| a.cubeName.localeCompare(b.cubeName)
|
|
136
|
+
|| a.worktree.localeCompare(b.worktree)
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function oneLine(value: string): string {
|
|
141
|
+
return value.replace(/[\r\n\t]/g, ' ');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function formatLocalSeatRows(rows: readonly LocalSeatRow[]): string {
|
|
145
|
+
if (rows.length === 0) {
|
|
146
|
+
return 'No drone seats are registered on this machine. Run `borg assimilate` in a project repository to create one.\n';
|
|
147
|
+
}
|
|
148
|
+
const headings = ['DRONE', 'CUBE', 'STATE', 'CLI', 'WORKTREE'];
|
|
149
|
+
const values = rows.map((row) => [
|
|
150
|
+
oneLine(row.droneLabel),
|
|
151
|
+
oneLine(row.cubeName),
|
|
152
|
+
row.state,
|
|
153
|
+
row.cli ?? '-',
|
|
154
|
+
oneLine(row.worktree),
|
|
155
|
+
]);
|
|
156
|
+
const widths = headings.slice(0, -1).map((heading, index) =>
|
|
157
|
+
Math.max(heading.length, ...values.map((row) => row[index].length))
|
|
158
|
+
);
|
|
159
|
+
const render = (row: string[]) =>
|
|
160
|
+
row.slice(0, -1).map((value, index) => value.padEnd(widths[index] + 2)).join('') + row[row.length - 1];
|
|
161
|
+
return `${render(headings)}\n${values.map(render).join('\n')}\n`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function formatAmbiguousMatches(rows: readonly LocalSeatRow[]): string {
|
|
165
|
+
const labels = rows.map((row) => row.droneLabel);
|
|
166
|
+
const cubes = rows.map((row) => row.cubeName);
|
|
167
|
+
const labelWidth = Math.max(...labels.map((label) => label.length));
|
|
168
|
+
const cubeWidth = Math.max(...cubes.map((cube) => cube.length));
|
|
169
|
+
return rows.map((row) =>
|
|
170
|
+
` ${row.droneLabel.padEnd(labelWidth + 2)}${row.cubeName.padEnd(cubeWidth + 2)}id:${row.droneId.slice(0, 8)}`
|
|
171
|
+
).join('\n');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function runSeats(deps: SeatCommandDeps): Promise<number> {
|
|
175
|
+
try {
|
|
176
|
+
deps.stdout(formatLocalSeatRows(await readLocalSeatRows(deps)));
|
|
177
|
+
return 0;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
deps.stderr(`borg seats: could not read the local registry: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function runLaunchSeat(
|
|
185
|
+
args: { target: string; cube?: string },
|
|
186
|
+
deps: SeatCommandDeps,
|
|
187
|
+
): Promise<number> {
|
|
188
|
+
let rows: LocalSeatRow[];
|
|
189
|
+
try {
|
|
190
|
+
rows = await readLocalSeatRows(deps);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
deps.stderr(`borg launch: could not read the local registry: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (args.cube) {
|
|
197
|
+
const cubeIds = new Set(rows.filter((row) => row.cubeName === args.cube).map((row) => row.cubeId));
|
|
198
|
+
if (cubeIds.size === 0) {
|
|
199
|
+
deps.stderr(
|
|
200
|
+
`borg launch: no cube named '${args.cube}' is registered on this machine. ` +
|
|
201
|
+
`Run \`borg seats\` to list this machine's cubes and drones.\n`,
|
|
202
|
+
);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
rows = rows.filter((row) => cubeIds.has(row.cubeId));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const labelMatches = rows.filter((row) => row.droneLabel === args.target);
|
|
209
|
+
const matches = labelMatches.length > 0
|
|
210
|
+
? labelMatches
|
|
211
|
+
: rows.filter((row) => row.droneId.toLowerCase().startsWith(args.target.toLowerCase()));
|
|
212
|
+
if (matches.length === 0) {
|
|
213
|
+
if (rows.length === 0) {
|
|
214
|
+
deps.stderr(
|
|
215
|
+
`borg launch: drone '${args.target}' is not in this machine's seat registry. ` +
|
|
216
|
+
`The registry is local to each machine and lists only drones assimilated here. ` +
|
|
217
|
+
`Run \`borg seats\` on the machine where the drone was created.\n`,
|
|
218
|
+
);
|
|
219
|
+
} else if (args.cube) {
|
|
220
|
+
deps.stderr(
|
|
221
|
+
`borg launch: no drone matches '${args.target}' in cube '${args.cube}'. ` +
|
|
222
|
+
`Run \`borg seats\` to list the drones you can launch.\n`,
|
|
223
|
+
);
|
|
224
|
+
} else {
|
|
225
|
+
deps.stderr(
|
|
226
|
+
`borg launch: no drone matches '${args.target}' on this machine. ` +
|
|
227
|
+
`Run \`borg seats\` to list the drones you can launch.\n`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return 1;
|
|
231
|
+
}
|
|
232
|
+
if (matches.length > 1) {
|
|
233
|
+
deps.stderr(
|
|
234
|
+
`borg launch: '${args.target}' matches ${matches.length} drones:\n` +
|
|
235
|
+
`${formatAmbiguousMatches(matches)}\n` +
|
|
236
|
+
`Add --cube <name>, or use the drone id prefix: borg launch ${matches[0].droneId.slice(0, 8)}.\n`,
|
|
237
|
+
);
|
|
238
|
+
return 1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const selected = matches[0];
|
|
242
|
+
if (!selected.canonicalWorktree) {
|
|
243
|
+
deps.stderr(
|
|
244
|
+
`borg launch: drone '${selected.droneLabel}' is registered at ${selected.worktree}, but that directory does not exist. ` +
|
|
245
|
+
`Restore the directory, or run \`borg cleanup\` to review orphaned worktrees.\n`,
|
|
246
|
+
);
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
if (selected.state !== 'active') {
|
|
250
|
+
deps.stderr(
|
|
251
|
+
`borg launch: drone '${selected.droneLabel}' has a pending seat (shown as \`pending\` in \`borg seats\`) — ` +
|
|
252
|
+
`its assimilation did not complete, so launching now would start an unattached session. ` +
|
|
253
|
+
`To complete the seat, run \`borg assimilate\` in ${selected.worktree}, then run ` +
|
|
254
|
+
`\`borg launch ${selected.droneLabel}\` again.\n`,
|
|
255
|
+
);
|
|
256
|
+
return 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let preferred: SeatRecord | null;
|
|
260
|
+
try {
|
|
261
|
+
preferred = await deps.getActiveSeatForWorktree(selected.canonicalWorktree);
|
|
262
|
+
} catch {
|
|
263
|
+
preferred = null;
|
|
264
|
+
}
|
|
265
|
+
if (!preferred) {
|
|
266
|
+
deps.stderr(
|
|
267
|
+
`borg launch: did not launch '${selected.droneLabel}' — its seat registration changed before the launch could start. ` +
|
|
268
|
+
`Run \`borg seats\` to see the current state, then try again.\n`,
|
|
269
|
+
);
|
|
270
|
+
return 1;
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
seatRef(preferred) !== selected.credentialRef ||
|
|
274
|
+
preferred.cubeId !== selected.cubeId ||
|
|
275
|
+
preferred.droneId !== selected.droneId
|
|
276
|
+
) {
|
|
277
|
+
deps.stderr(
|
|
278
|
+
`borg launch: did not launch '${selected.droneLabel}' — the worktree at ${selected.worktree} would resume ` +
|
|
279
|
+
`'${preferred.droneLabel}' (cube '${preferred.name}') instead. To resume '${preferred.droneLabel}', run \`borg\` ` +
|
|
280
|
+
`in that worktree. To review this machine's seats, run \`borg seats\`.\n`,
|
|
281
|
+
);
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const expectation: LaunchSeatExpectation = {
|
|
286
|
+
credentialRef: selected.credentialRef,
|
|
287
|
+
cubeId: selected.cubeId,
|
|
288
|
+
droneId: selected.droneId,
|
|
289
|
+
worktree: selected.canonicalWorktree,
|
|
290
|
+
droneLabel: selected.droneLabel,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
return await deps.launchBareBorg(selected.canonicalWorktree, expectation);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
deps.stderr(`borg launch: failed to start borg in ${selected.canonicalWorktree}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function buildDefaultSeatCommandDeps(): SeatCommandDeps {
|
|
302
|
+
return {
|
|
303
|
+
readAllProjectIdentities,
|
|
304
|
+
readAllBoundSeats,
|
|
305
|
+
getActiveSeatForWorktree,
|
|
306
|
+
getProjectCliPreference: getProjectCliPreferenceForPath,
|
|
307
|
+
pathExists: existsSync,
|
|
308
|
+
realpath: realpathSync,
|
|
309
|
+
launchBareBorg: (worktree, expectation) => new Promise((resolve, reject) => {
|
|
310
|
+
const child = spawn(resolveBorgPath(), [], {
|
|
311
|
+
cwd: worktree,
|
|
312
|
+
stdio: 'inherit',
|
|
313
|
+
shell: false,
|
|
314
|
+
env: withLaunchSeatExpectationEnv(process.env, expectation),
|
|
315
|
+
});
|
|
316
|
+
child.once('error', reject);
|
|
317
|
+
child.once('exit', (code, signal) => resolve(signal ? 1 : code ?? 1));
|
|
318
|
+
}),
|
|
319
|
+
stdout: (line) => process.stdout.write(line),
|
|
320
|
+
stderr: (line) => process.stderr.write(line),
|
|
321
|
+
};
|
|
322
|
+
}
|
package/src/seats.ts
CHANGED
|
@@ -675,6 +675,21 @@ export async function readAllActiveSeats(): Promise<Array<{ worktree: string; re
|
|
|
675
675
|
return out;
|
|
676
676
|
}
|
|
677
677
|
|
|
678
|
+
/** All valid worktree-bound registry entries, including a PENDING seat whose
|
|
679
|
+
* interrupted finalize preserved its worktree for a later resume. Read-only:
|
|
680
|
+
* pending records remain non-hydratable and getActiveSeatForWorktree stays
|
|
681
|
+
* active-only. */
|
|
682
|
+
export async function readAllBoundSeats(): Promise<Array<{ worktree: string; record: SeatRecord }>> {
|
|
683
|
+
const store = await readStore();
|
|
684
|
+
const out: Array<{ worktree: string; record: SeatRecord }> = [];
|
|
685
|
+
for (const [ref, record] of Object.entries(store.seats)) {
|
|
686
|
+
if (typeof record.worktree === 'string' && seatRef(record) === ref) {
|
|
687
|
+
out.push({ worktree: record.worktree, record });
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return out;
|
|
691
|
+
}
|
|
692
|
+
|
|
678
693
|
// ─── Reset / scrub / metadata (all single-commit) ────────────────────────────
|
|
679
694
|
|
|
680
695
|
export type ResetSeatOutcome =
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"launch-all-windows.d.ts","sourceRoot":"","sources":["../../src/backends/launch-all-windows.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAI3D,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC;AAuED,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,cAAc,EAAE,EAC5B,IAAI,EAAE,WAAW,EACjB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAgBf"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"launch-all-windows.js","sourceRoot":"","sources":["../../src/backends/launch-all-windows.ts"],"names":[],"mappings":"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"}
|