borgmcp 3.9.0 → 3.11.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/SECURITY.md +1 -1
- package/dist/assimilate-cmd.d.ts +14 -1
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +19 -12
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +25 -3
- 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 +33 -6
- package/dist/cli-help.js.map +1 -1
- package/dist/clone-cmd.d.ts +28 -0
- package/dist/clone-cmd.d.ts.map +1 -0
- package/dist/clone-cmd.js +227 -0
- package/dist/clone-cmd.js.map +1 -0
- package/dist/clone-security.d.ts +9 -0
- package/dist/clone-security.d.ts.map +1 -0
- package/dist/clone-security.js +41 -0
- package/dist/clone-security.js.map +1 -0
- package/dist/cubes.js +2 -2
- package/dist/cubes.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/launch-all-cmd.d.ts +11 -0
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +24 -2
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/log-stream.d.ts +6 -2
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +36 -11
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +7 -2
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +291 -64
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-plugin.d.ts.map +1 -1
- package/dist/opencode-plugin.js +12 -2
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/parse-clone-args.d.ts +17 -0
- package/dist/parse-clone-args.d.ts.map +1 -0
- package/dist/parse-clone-args.js +38 -0
- package/dist/parse-clone-args.js.map +1 -0
- package/dist/parse-quickstart-args.d.ts +18 -0
- package/dist/parse-quickstart-args.d.ts.map +1 -0
- package/dist/parse-quickstart-args.js +43 -0
- package/dist/parse-quickstart-args.js.map +1 -0
- package/dist/quickstart-cmd.d.ts +23 -0
- package/dist/quickstart-cmd.d.ts.map +1 -0
- package/dist/quickstart-cmd.js +357 -0
- package/dist/quickstart-cmd.js.map +1 -0
- package/dist/remote-client.d.ts +13 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +25 -0
- package/dist/remote-client.js.map +1 -1
- package/dist/seat-commands.js +12 -12
- package/dist/seat-commands.js.map +1 -1
- package/dist/stream-status.d.ts.map +1 -1
- package/dist/stream-status.js +1 -0
- package/dist/stream-status.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 +3 -1
- package/dist/unknown-subcommand.js.map +1 -1
- package/docs/RELEASING.md +94 -23
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +35 -12
- package/src/claude.ts +25 -3
- package/src/cli-help.ts +39 -6
- package/src/clone-cmd.ts +243 -0
- package/src/clone-security.ts +40 -0
- package/src/cubes.ts +2 -2
- package/src/index.ts +4 -1
- package/src/launch-all-cmd.ts +37 -2
- package/src/log-stream.ts +49 -12
- package/src/opencode-drone.ts +328 -65
- package/src/opencode-plugin.ts +13 -1
- package/src/parse-clone-args.ts +44 -0
- package/src/parse-quickstart-args.ts +54 -0
- package/src/quickstart-cmd.ts +396 -0
- package/src/remote-client.ts +36 -0
- package/src/seat-commands.ts +12 -12
- package/src/stream-status.ts +3 -0
- package/src/unknown-subcommand.ts +3 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { NEW_CUBE_TEMPLATE_PRESENTATIONS } from 'borgmcp-shared/templates';
|
|
2
|
+
|
|
3
|
+
export interface QuickstartRoleRequest {
|
|
4
|
+
slug: string;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface QuickstartArgs {
|
|
9
|
+
template?: string;
|
|
10
|
+
roles: QuickstartRoleRequest[];
|
|
11
|
+
yes: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ParseQuickstartResult =
|
|
15
|
+
| { ok: true; args: QuickstartArgs }
|
|
16
|
+
| { ok: false; error: string };
|
|
17
|
+
|
|
18
|
+
const templates = new Set<string>(NEW_CUBE_TEMPLATE_PRESENTATIONS.map(({ name }) => name));
|
|
19
|
+
|
|
20
|
+
function parseRole(value: string): QuickstartRoleRequest | null {
|
|
21
|
+
const match = /^([a-z0-9]+(?:-[a-z0-9]+)*)(?::([1-9]\d*))?$/.exec(value);
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
const count = match[2] === undefined ? 1 : Number(match[2]);
|
|
24
|
+
return Number.isSafeInteger(count) ? { slug: match[1], count } : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function parseQuickstartArgs(rawArgs: readonly string[]): ParseQuickstartResult {
|
|
28
|
+
const args: QuickstartArgs = { roles: [], yes: false };
|
|
29
|
+
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
30
|
+
const arg = rawArgs[i];
|
|
31
|
+
if (arg === '--yes' || arg === '-y') {
|
|
32
|
+
args.yes = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (arg === '--template') {
|
|
36
|
+
const value = rawArgs[++i];
|
|
37
|
+
if (!value) return { ok: false, error: '--template requires software-dev, starter, or local-model' };
|
|
38
|
+
if (!templates.has(value)) return { ok: false, error: `unknown template '${value}'; choose software-dev, starter, or local-model` };
|
|
39
|
+
if (args.template !== undefined) return { ok: false, error: '--template was provided more than once' };
|
|
40
|
+
args.template = value;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (arg === '--role') {
|
|
44
|
+
const value = rawArgs[++i];
|
|
45
|
+
if (!value) return { ok: false, error: '--role requires <slug>[:<count>]' };
|
|
46
|
+
const role = parseRole(value);
|
|
47
|
+
if (!role) return { ok: false, error: `invalid role '${value}'; use <slug>[:<positive-count>]` };
|
|
48
|
+
args.roles.push(role);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
return { ok: false, error: `unknown option: ${arg}. Supported: --template, --role, --yes/-y` };
|
|
52
|
+
}
|
|
53
|
+
return { ok: true, args };
|
|
54
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NEW_CUBE_TEMPLATE_PRESENTATIONS,
|
|
3
|
+
getTemplate,
|
|
4
|
+
type TemplateRole,
|
|
5
|
+
} from 'borgmcp-shared/templates';
|
|
6
|
+
import type { AssimilateDeps, PreparedAssimilation } from './assimilate-cmd.js';
|
|
7
|
+
import { runAssimilate } from './assimilate-cmd.js';
|
|
8
|
+
import { buildDefaultAssimilateDeps } from './assimilate-deps.js';
|
|
9
|
+
import type { ActiveCube } from './cubes.js';
|
|
10
|
+
import { readAllProjectIdentities } from './cubes.js';
|
|
11
|
+
import { buildDefaultLaunchAllDeps, type LaunchAllDeps } from './launch-all-deps.js';
|
|
12
|
+
import { LAUNCH_ALL_NO_DISPATCH_EXIT_CODE, runLaunchAll } from './launch-all-cmd.js';
|
|
13
|
+
import type { QuickstartArgs } from './parse-quickstart-args.js';
|
|
14
|
+
import { roleSlug, type Role } from './role-resolver.js';
|
|
15
|
+
import { DEFAULT_LOCAL_SERVER_ORIGIN } from './server-handshake.js';
|
|
16
|
+
|
|
17
|
+
interface PlannedRole {
|
|
18
|
+
name: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
isHumanSeat: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ExistingPlan {
|
|
24
|
+
cubeId: string;
|
|
25
|
+
cubeName: string;
|
|
26
|
+
template: string;
|
|
27
|
+
roles: PlannedRole[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface QuickstartDeps {
|
|
31
|
+
buildAssimilateDeps: () => AssimilateDeps;
|
|
32
|
+
buildLaunchAllDeps: () => LaunchAllDeps;
|
|
33
|
+
readAllProjectIdentities: () => Promise<Array<{ projectPath: string; cube: ActiveCube }>>;
|
|
34
|
+
isTTY: () => boolean;
|
|
35
|
+
prompt: (message: string) => Promise<string>;
|
|
36
|
+
stdout: (text: string) => void;
|
|
37
|
+
stderr: (text: string) => void;
|
|
38
|
+
runAssimilate?: typeof runAssimilate;
|
|
39
|
+
runLaunchAll?: typeof runLaunchAll;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildDefaultQuickstartDeps(): QuickstartDeps {
|
|
43
|
+
const io = buildDefaultAssimilateDeps();
|
|
44
|
+
return {
|
|
45
|
+
buildAssimilateDeps: buildDefaultAssimilateDeps,
|
|
46
|
+
buildLaunchAllDeps: buildDefaultLaunchAllDeps,
|
|
47
|
+
readAllProjectIdentities,
|
|
48
|
+
isTTY: io.isTTY,
|
|
49
|
+
prompt: io.prompt,
|
|
50
|
+
stdout: io.stdout,
|
|
51
|
+
stderr: io.stderr,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function plannedTemplateRoles(templateName: string): PlannedRole[] {
|
|
56
|
+
const template = getTemplate(templateName);
|
|
57
|
+
if (!template) return [];
|
|
58
|
+
return template.roles.map((role: TemplateRole) => ({
|
|
59
|
+
name: role.name,
|
|
60
|
+
slug: roleSlug(role.name),
|
|
61
|
+
isHumanSeat: role.is_human_seat === true,
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function plannedServerRoles(roles: readonly Role[]): PlannedRole[] {
|
|
66
|
+
return roles.map((role) => ({
|
|
67
|
+
name: role.name,
|
|
68
|
+
slug: roleSlug(role.name),
|
|
69
|
+
isHumanSeat: role.is_human_seat,
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function templatePresentation(name: string): { label: string; short_description: string } {
|
|
74
|
+
const found = NEW_CUBE_TEMPLATE_PRESENTATIONS.find((candidate) => candidate.name === name);
|
|
75
|
+
return found ?? { label: name, short_description: '' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function renderTemplateMenu(): string {
|
|
79
|
+
const rows = NEW_CUBE_TEMPLATE_PRESENTATIONS.flatMap((presentation, index) => {
|
|
80
|
+
const words = presentation.short_description.split(/\s+/);
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
for (const word of words) {
|
|
83
|
+
const last = lines.at(-1);
|
|
84
|
+
if (!last || `${last} ${word}`.length > 42) lines.push(word);
|
|
85
|
+
else lines[lines.length - 1] = `${last} ${word}`;
|
|
86
|
+
}
|
|
87
|
+
return lines.map((line, lineIndex) => lineIndex === 0
|
|
88
|
+
? ` ${index + 1}) ${presentation.label.padEnd(22)}${line}`
|
|
89
|
+
: ` ${line}`);
|
|
90
|
+
});
|
|
91
|
+
rows[0] = `Template ${rows[0].trimStart()}`;
|
|
92
|
+
return `${rows.join('\n')}\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function selectTemplate(args: QuickstartArgs, deps: QuickstartDeps): Promise<string | null> {
|
|
96
|
+
if (args.template) return args.template;
|
|
97
|
+
if (!deps.isTTY()) return NEW_CUBE_TEMPLATE_PRESENTATIONS[0].name;
|
|
98
|
+
deps.stdout(renderTemplateMenu());
|
|
99
|
+
while (true) {
|
|
100
|
+
let answer: string;
|
|
101
|
+
try {
|
|
102
|
+
answer = (await deps.prompt('Choose [1]: ')).trim();
|
|
103
|
+
} catch {
|
|
104
|
+
deps.stderr('\nborg quickstart: cancelled before anything was created.\n');
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const index = answer === '' ? 0 : /^\d+$/.test(answer) ? Number(answer) - 1 : -1;
|
|
108
|
+
const selected = NEW_CUBE_TEMPLATE_PRESENTATIONS[index];
|
|
109
|
+
if (selected) return selected.name;
|
|
110
|
+
deps.stdout(`Choose 1-${NEW_CUBE_TEMPLATE_PRESENTATIONS.length}.\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function aggregateRequestedRoles(args: QuickstartArgs, available: readonly PlannedRole[]): PlannedRole[] | string {
|
|
115
|
+
if (args.roles.length === 0) return [...available];
|
|
116
|
+
const bySlug = new Map(available.map((role) => [role.slug, role]));
|
|
117
|
+
const requested: PlannedRole[] = [];
|
|
118
|
+
for (const request of args.roles) {
|
|
119
|
+
const role = bySlug.get(request.slug);
|
|
120
|
+
if (!role) return `no role '${request.slug}' exists in this cube; available: ${available.map((item) => item.slug).join(', ')}`;
|
|
121
|
+
for (let i = 0; i < request.count; i += 1) requested.push(role);
|
|
122
|
+
}
|
|
123
|
+
return requested;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function renderRoleList(label: string, roles: readonly PlannedRole[]): string {
|
|
127
|
+
const prefix = label.padEnd(12);
|
|
128
|
+
const continuation = ' '.repeat(12);
|
|
129
|
+
const lines: string[] = [];
|
|
130
|
+
for (let index = 0; index < roles.length; index += 1) {
|
|
131
|
+
const token = `${roles[index].slug}${index === roles.length - 1 ? '' : ','}`;
|
|
132
|
+
const candidate = lines.length === 0 ? token : `${lines.at(-1)} ${token}`;
|
|
133
|
+
if (lines.length === 0 || candidate.length <= 64) {
|
|
134
|
+
if (lines.length === 0) lines.push(token);
|
|
135
|
+
else lines[lines.length - 1] = candidate;
|
|
136
|
+
} else {
|
|
137
|
+
lines.push(token);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return lines.map((line, index) => `${index === 0 ? prefix : continuation}${line}`).join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function affirmative(value: string): boolean {
|
|
144
|
+
const answer = value.trim().toLowerCase();
|
|
145
|
+
return answer === '' || answer === 'y' || answer === 'yes';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function runQuickstart(args: QuickstartArgs, deps: QuickstartDeps): Promise<number> {
|
|
149
|
+
const assimilate = deps.buildAssimilateDeps();
|
|
150
|
+
let context;
|
|
151
|
+
try {
|
|
152
|
+
context = await assimilate.resolveRepositoryContext(assimilate.cwd());
|
|
153
|
+
} catch {
|
|
154
|
+
context = null;
|
|
155
|
+
}
|
|
156
|
+
if (!context) {
|
|
157
|
+
deps.stderr('borg quickstart: run this command inside a non-bare Git repository.\n');
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let serverOrigin: string | null = null;
|
|
162
|
+
try {
|
|
163
|
+
serverOrigin = await assimilate.detectLocalServer();
|
|
164
|
+
} catch {
|
|
165
|
+
serverOrigin = null;
|
|
166
|
+
}
|
|
167
|
+
if (!serverOrigin) {
|
|
168
|
+
deps.stderr(
|
|
169
|
+
`borg quickstart: no Borg server is running at ${DEFAULT_LOCAL_SERVER_ORIGIN}.\n` +
|
|
170
|
+
'Start it in another terminal and leave it open:\n' +
|
|
171
|
+
' borg server start\n' +
|
|
172
|
+
'Then run `borg quickstart` again.\n',
|
|
173
|
+
);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let connection;
|
|
178
|
+
try {
|
|
179
|
+
connection = await assimilate.connectServer(serverOrigin);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
deps.stderr(`borg quickstart: could not use the Borg server at ${serverOrigin}: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let existing: ExistingPlan | null = null;
|
|
186
|
+
try {
|
|
187
|
+
const repository = await assimilate.getRepositoryIdentity(context);
|
|
188
|
+
const saved = await assimilate.getRepositoryAssociation(connection.trustIdentity, repository);
|
|
189
|
+
if (saved) {
|
|
190
|
+
const cube = await assimilate.getCube(serverOrigin, connection.token, saved.cubeId, connection.trustIdentity);
|
|
191
|
+
existing = { cubeId: cube.id, cubeName: cube.name, template: saved.template, roles: plannedServerRoles(cube.roles) };
|
|
192
|
+
} else {
|
|
193
|
+
const resolved = await assimilate.resolveRepositoryCube(
|
|
194
|
+
serverOrigin,
|
|
195
|
+
connection.token,
|
|
196
|
+
{ repository, workingRepoName: context.derivedName },
|
|
197
|
+
connection.trustIdentity,
|
|
198
|
+
);
|
|
199
|
+
if (resolved.result === 'resolved') {
|
|
200
|
+
const cube = await assimilate.getCube(serverOrigin, connection.token, resolved.cube_id, connection.trustIdentity);
|
|
201
|
+
existing = { cubeId: cube.id, cubeName: cube.name, template: resolved.template, roles: plannedServerRoles(cube.roles) };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
deps.stderr(`borg quickstart: could not inspect this repository's cube: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
deps.stdout(`Repository ${context.derivedName}${context.publicRepository ? ` (origin: ${context.publicRepository.value})` : ''}\n`);
|
|
210
|
+
const template = existing?.template ?? await selectTemplate(args, deps);
|
|
211
|
+
if (!template) return 130;
|
|
212
|
+
const availableRoles = existing?.roles ?? plannedTemplateRoles(template);
|
|
213
|
+
const humanSeatRole = availableRoles.find((role) => role.isHumanSeat);
|
|
214
|
+
const requested = aggregateRequestedRoles(args, availableRoles);
|
|
215
|
+
if (typeof requested === 'string') {
|
|
216
|
+
deps.stderr(`borg quickstart: ${requested}.\n`);
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Resolve any multi-CLI choice before the whole-plan confirmation. Every
|
|
221
|
+
// assimilate call then receives the explicit choice and cannot prompt after
|
|
222
|
+
// the operator has approved the plan.
|
|
223
|
+
let selectedCli;
|
|
224
|
+
try {
|
|
225
|
+
selectedCli = await assimilate.resolveCli(undefined);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
deps.stderr(`borg quickstart: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
228
|
+
return 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let identities: Array<{ projectPath: string; cube: ActiveCube }> = [];
|
|
232
|
+
try {
|
|
233
|
+
identities = existing ? await deps.readAllProjectIdentities() : [];
|
|
234
|
+
} catch (error) {
|
|
235
|
+
deps.stderr(`borg quickstart: could not read the local drone registry: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
const existingByRole = new Map<string, Array<{ projectPath: string; cube: ActiveCube }>>();
|
|
239
|
+
for (const identity of identities) {
|
|
240
|
+
if (identity.cube.cubeId !== existing?.cubeId || !identity.cube.roleName) continue;
|
|
241
|
+
const slug = roleSlug(identity.cube.roleName);
|
|
242
|
+
const list = existingByRole.get(slug) ?? [];
|
|
243
|
+
list.push(identity);
|
|
244
|
+
existingByRole.set(slug, list);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const usedByRole = new Map<string, number>();
|
|
248
|
+
const targets: Array<{ role: PlannedRole; existing?: { projectPath: string; cube: ActiveCube } }> = [];
|
|
249
|
+
for (const role of requested) {
|
|
250
|
+
const used = usedByRole.get(role.slug) ?? 0;
|
|
251
|
+
const match = existingByRole.get(role.slug)?.[used];
|
|
252
|
+
targets.push({ role, ...(match ? { existing: match } : {}) });
|
|
253
|
+
usedByRole.set(role.slug, used + 1);
|
|
254
|
+
}
|
|
255
|
+
const have = targets.filter((target) => target.existing).map((target) => target.role);
|
|
256
|
+
const missing = targets.filter((target) => !target.existing).map((target) => target.role);
|
|
257
|
+
|
|
258
|
+
if (existing) {
|
|
259
|
+
deps.stdout(`Cube ${existing.cubeName} (existing)\n`);
|
|
260
|
+
if (have.length > 0) deps.stdout(`${renderRoleList('Have', have)}\n`);
|
|
261
|
+
deps.stdout(missing.length > 0
|
|
262
|
+
? `${renderRoleList('Will create', missing)}\n`
|
|
263
|
+
: 'Will create nothing; every requested drone already exists\n');
|
|
264
|
+
} else {
|
|
265
|
+
deps.stdout(`Cube ${context.derivedName} (new, template: ${templatePresentation(template).label})\n`);
|
|
266
|
+
deps.stdout(`${renderRoleList('Drones', requested)}\n`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (!args.yes) {
|
|
270
|
+
if (!deps.isTTY()) {
|
|
271
|
+
deps.stderr('borg quickstart: confirmation requires an interactive terminal; rerun with --yes.\n');
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
274
|
+
const prompt = existing
|
|
275
|
+
? 'Continue? [Y/n] '
|
|
276
|
+
: `Create and launch these ${requested.length} ${requested.length === 1 ? 'drone' : 'drones'}? [Y/n] `;
|
|
277
|
+
let answer: string;
|
|
278
|
+
try {
|
|
279
|
+
answer = await deps.prompt(prompt);
|
|
280
|
+
} catch {
|
|
281
|
+
deps.stderr('\nborg quickstart: cancelled before anything was created.\n');
|
|
282
|
+
return 130;
|
|
283
|
+
}
|
|
284
|
+
if (!affirmative(answer)) {
|
|
285
|
+
deps.stdout('Cancelled. Nothing was created.\n');
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const runAssimilateImpl = deps.runAssimilate ?? runAssimilate;
|
|
291
|
+
for (const target of targets) {
|
|
292
|
+
if (target.existing) continue;
|
|
293
|
+
let prepared: PreparedAssimilation | undefined;
|
|
294
|
+
let diagnostic = '';
|
|
295
|
+
const inner = deps.buildAssimilateDeps();
|
|
296
|
+
let code = 1;
|
|
297
|
+
try {
|
|
298
|
+
code = await runAssimilateImpl({
|
|
299
|
+
role: target.role.slug,
|
|
300
|
+
flags: {
|
|
301
|
+
server: serverOrigin,
|
|
302
|
+
yes: true,
|
|
303
|
+
cubeName: context.derivedName,
|
|
304
|
+
template,
|
|
305
|
+
cli: selectedCli,
|
|
306
|
+
},
|
|
307
|
+
}, {
|
|
308
|
+
...inner,
|
|
309
|
+
stdout: (text) => { diagnostic += text; },
|
|
310
|
+
stderr: (text) => { diagnostic += text; },
|
|
311
|
+
}, {
|
|
312
|
+
launch: false,
|
|
313
|
+
onPrepared: (value) => { prepared = value; },
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
diagnostic += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
317
|
+
}
|
|
318
|
+
const assignedRoleSlug = prepared ? roleSlug(prepared.roleName) : null;
|
|
319
|
+
const roleMismatch = assignedRoleSlug !== null && assignedRoleSlug !== target.role.slug;
|
|
320
|
+
if (code !== 0 || !prepared || roleMismatch) {
|
|
321
|
+
deps.stderr(`✗ ${target.role.slug}\n`);
|
|
322
|
+
if (diagnostic) deps.stderr(diagnostic);
|
|
323
|
+
if (roleMismatch) {
|
|
324
|
+
deps.stderr(
|
|
325
|
+
`borg quickstart: requested ${target.role.slug}, but the server assigned ${assignedRoleSlug}. ` +
|
|
326
|
+
`The assigned drone was kept; it does not fill the requested ${target.role.slug} slot.\n`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const completed = targets.filter((item) => item.existing).length;
|
|
330
|
+
const remaining = targets.filter((item) => !item.existing).map((item) => item.role);
|
|
331
|
+
deps.stderr(
|
|
332
|
+
`Stopped. ${completed} of ${requested.length} drones exist; ${remaining.map((role) => role.slug).join(', ')} ${remaining.length === 1 ? 'is' : 'are'} missing.\n` +
|
|
333
|
+
`Fix the cause above, then run \`borg quickstart\` again — it continues from here and does not touch the drones that already exist.\n`,
|
|
334
|
+
);
|
|
335
|
+
return 1;
|
|
336
|
+
}
|
|
337
|
+
target.existing = {
|
|
338
|
+
projectPath: prepared.worktree,
|
|
339
|
+
cube: {
|
|
340
|
+
cubeId: prepared.cubeId,
|
|
341
|
+
droneId: prepared.droneId,
|
|
342
|
+
name: prepared.cubeName,
|
|
343
|
+
droneLabel: prepared.droneLabel,
|
|
344
|
+
apiUrl: serverOrigin,
|
|
345
|
+
sessionToken: '',
|
|
346
|
+
roleName: prepared.roleName,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
deps.stdout(`✓ ${target.role.slug.padEnd(20)}${prepared.worktree}\n`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const droneIds = targets.flatMap((target) => target.existing ? [target.existing.cube.droneId] : []);
|
|
353
|
+
const firstTarget = targets.find((target) => target.existing)?.existing?.cube;
|
|
354
|
+
const cubeName = firstTarget?.name ?? existing?.cubeName ?? context.derivedName;
|
|
355
|
+
const cubeId = firstTarget?.cubeId ?? existing?.cubeId;
|
|
356
|
+
if (!cubeId) {
|
|
357
|
+
deps.stderr('borg quickstart: the staffed cube identity could not be confirmed; run `borg quickstart` again.\n');
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
deps.stdout(`Launching ${droneIds.length} sessions.\n`);
|
|
361
|
+
let launchOutput = '';
|
|
362
|
+
const launchDeps = deps.buildLaunchAllDeps();
|
|
363
|
+
let launchCode = 1;
|
|
364
|
+
try {
|
|
365
|
+
launchCode = await (deps.runLaunchAll ?? runLaunchAll)(
|
|
366
|
+
{ cubeName, flags: { yes: true } },
|
|
367
|
+
{
|
|
368
|
+
...launchDeps,
|
|
369
|
+
stdout: (text) => { launchOutput += text; },
|
|
370
|
+
stderr: (text) => { launchOutput += text; },
|
|
371
|
+
},
|
|
372
|
+
{ droneIds, requireAllRequested: true, targetCube: { cubeId, name: cubeName } },
|
|
373
|
+
);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
launchOutput += `${error instanceof Error ? error.message : String(error)}\n`;
|
|
376
|
+
}
|
|
377
|
+
if (launchCode !== 0) {
|
|
378
|
+
if (launchOutput) deps.stderr(launchOutput);
|
|
379
|
+
deps.stderr(launchCode === LAUNCH_ALL_NO_DISPATCH_EXIT_CODE
|
|
380
|
+
? 'No sessions were launched: this environment has no terminal or tmux backend. Run `borg launch-all` to print the commands, then paste them.\n'
|
|
381
|
+
: 'The drones were created, but one or more sessions did not launch. Fix the cause above, then run `borg launch-all`.\n');
|
|
382
|
+
return 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
deps.stdout(`✓ Cube \`${cubeName}\` is staffed. ${droneIds.length} ${droneIds.length === 1 ? 'drone' : 'drones'} launched.\n`);
|
|
386
|
+
const human = targets.find((target) => target.role.isHumanSeat && target.existing);
|
|
387
|
+
if (human?.existing) {
|
|
388
|
+
deps.stdout(
|
|
389
|
+
`Start in the ${human.role.slug} session (\`${human.existing.cube.droneLabel}\`) and tell it what\n` +
|
|
390
|
+
'you want built. It dispatches the rest.\n',
|
|
391
|
+
);
|
|
392
|
+
} else if (humanSeatRole) {
|
|
393
|
+
deps.stdout(`Start a ${humanSeatRole.name} session later with: borg assimilate ${humanSeatRole.slug}\n`);
|
|
394
|
+
}
|
|
395
|
+
return 0;
|
|
396
|
+
}
|
package/src/remote-client.ts
CHANGED
|
@@ -564,6 +564,7 @@ async function localReadLogPage(
|
|
|
564
564
|
|
|
565
565
|
interface PendingWakePage {
|
|
566
566
|
entries: Array<{
|
|
567
|
+
id?: unknown;
|
|
567
568
|
drone_id?: unknown;
|
|
568
569
|
message?: unknown;
|
|
569
570
|
visibility?: unknown;
|
|
@@ -622,6 +623,41 @@ export async function hasPendingWakeActivity(
|
|
|
622
623
|
}
|
|
623
624
|
}
|
|
624
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Inspect the durable unread window for one specific SSE entry without
|
|
628
|
+
* advancing the agent-owned cursor. Wake nonces are retries of that entry, so
|
|
629
|
+
* the entry itself remains the authority for whether another wake is owed.
|
|
630
|
+
*/
|
|
631
|
+
export async function hasPendingWakeEntry(
|
|
632
|
+
active: ActiveCube,
|
|
633
|
+
entryId: string,
|
|
634
|
+
deps: {
|
|
635
|
+
getCursor?: typeof getLocalServerCursor;
|
|
636
|
+
readPage?: (active: ActiveCube, opts: { cursor: LocalServerCursor | null; limit: number }) => Promise<PendingWakePage>;
|
|
637
|
+
} = {},
|
|
638
|
+
): Promise<boolean> {
|
|
639
|
+
if (!active.serverTrustIdentity) {
|
|
640
|
+
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const getCursor = deps.getCursor ?? getLocalServerCursor;
|
|
644
|
+
const readPage = deps.readPage ?? localReadLogPage;
|
|
645
|
+
let cursor = await getCursor(localCursorBinding(active));
|
|
646
|
+
|
|
647
|
+
for (;;) {
|
|
648
|
+
const page: PendingWakePage = await readPage(active, { cursor, limit: 500 });
|
|
649
|
+
if (page.entries.some((entry) =>
|
|
650
|
+
entry.id === entryId && isPendingWakeEntry(entry, active.droneId)
|
|
651
|
+
)) return true;
|
|
652
|
+
if (!page.has_more) return false;
|
|
653
|
+
if (!page.cursor ||
|
|
654
|
+
(cursor && page.cursor.id === cursor.id && page.cursor.created_at === cursor.created_at)) {
|
|
655
|
+
throw new Error('Local Borg server returned a non-advancing log cursor');
|
|
656
|
+
}
|
|
657
|
+
cursor = page.cursor;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
625
661
|
async function resolveLocalLogCursor(
|
|
626
662
|
active: ActiveCube,
|
|
627
663
|
since: string,
|
package/src/seat-commands.ts
CHANGED
|
@@ -143,7 +143,7 @@ function oneLine(value: string): string {
|
|
|
143
143
|
|
|
144
144
|
export function formatLocalSeatRows(rows: readonly LocalSeatRow[]): string {
|
|
145
145
|
if (rows.length === 0) {
|
|
146
|
-
return 'No
|
|
146
|
+
return 'No drones are registered on this machine. Run `borg assimilate` in a project repository to create one.\n';
|
|
147
147
|
}
|
|
148
148
|
const headings = ['DRONE', 'CUBE', 'STATE', 'CLI', 'WORKTREE'];
|
|
149
149
|
const values = rows.map((row) => [
|
|
@@ -176,7 +176,7 @@ export async function runSeats(deps: SeatCommandDeps): Promise<number> {
|
|
|
176
176
|
deps.stdout(formatLocalSeatRows(await readLocalSeatRows(deps)));
|
|
177
177
|
return 0;
|
|
178
178
|
} catch (error) {
|
|
179
|
-
deps.stderr(`borg
|
|
179
|
+
deps.stderr(`borg drones: could not read the local registry: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
180
180
|
return 1;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -198,7 +198,7 @@ export async function runLaunchSeat(
|
|
|
198
198
|
if (cubeIds.size === 0) {
|
|
199
199
|
deps.stderr(
|
|
200
200
|
`borg launch: no cube named '${args.cube}' is registered on this machine. ` +
|
|
201
|
-
`Run \`borg
|
|
201
|
+
`Run \`borg drones\` to list this machine's cubes and drones.\n`,
|
|
202
202
|
);
|
|
203
203
|
return 1;
|
|
204
204
|
}
|
|
@@ -212,19 +212,19 @@ export async function runLaunchSeat(
|
|
|
212
212
|
if (matches.length === 0) {
|
|
213
213
|
if (rows.length === 0) {
|
|
214
214
|
deps.stderr(
|
|
215
|
-
`borg launch: drone '${args.target}' is not in this machine's
|
|
215
|
+
`borg launch: drone '${args.target}' is not in this machine's registry. ` +
|
|
216
216
|
`The registry is local to each machine and lists only drones assimilated here. ` +
|
|
217
|
-
`Run \`borg
|
|
217
|
+
`Run \`borg drones\` on the machine where the drone was created.\n`,
|
|
218
218
|
);
|
|
219
219
|
} else if (args.cube) {
|
|
220
220
|
deps.stderr(
|
|
221
221
|
`borg launch: no drone matches '${args.target}' in cube '${args.cube}'. ` +
|
|
222
|
-
`Run \`borg
|
|
222
|
+
`Run \`borg drones\` to list the drones you can launch.\n`,
|
|
223
223
|
);
|
|
224
224
|
} else {
|
|
225
225
|
deps.stderr(
|
|
226
226
|
`borg launch: no drone matches '${args.target}' on this machine. ` +
|
|
227
|
-
`Run \`borg
|
|
227
|
+
`Run \`borg drones\` to list the drones you can launch.\n`,
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
return 1;
|
|
@@ -248,9 +248,9 @@ export async function runLaunchSeat(
|
|
|
248
248
|
}
|
|
249
249
|
if (selected.state !== 'active') {
|
|
250
250
|
deps.stderr(
|
|
251
|
-
`borg launch: drone '${selected.droneLabel}'
|
|
251
|
+
`borg launch: drone '${selected.droneLabel}' is not fully assimilated (shown as \`pending\` in \`borg drones\`) — ` +
|
|
252
252
|
`its assimilation did not complete, so launching now would start an unattached session. ` +
|
|
253
|
-
`To
|
|
253
|
+
`To finish it, run \`borg assimilate\` in ${selected.worktree}, then run ` +
|
|
254
254
|
`\`borg launch ${selected.droneLabel}\` again.\n`,
|
|
255
255
|
);
|
|
256
256
|
return 1;
|
|
@@ -264,8 +264,8 @@ export async function runLaunchSeat(
|
|
|
264
264
|
}
|
|
265
265
|
if (!preferred) {
|
|
266
266
|
deps.stderr(
|
|
267
|
-
`borg launch: did not launch '${selected.droneLabel}' — its
|
|
268
|
-
`Run \`borg
|
|
267
|
+
`borg launch: did not launch '${selected.droneLabel}' — its registration changed before the launch could start. ` +
|
|
268
|
+
`Run \`borg drones\` to see the current state, then try again.\n`,
|
|
269
269
|
);
|
|
270
270
|
return 1;
|
|
271
271
|
}
|
|
@@ -277,7 +277,7 @@ export async function runLaunchSeat(
|
|
|
277
277
|
deps.stderr(
|
|
278
278
|
`borg launch: did not launch '${selected.droneLabel}' — the worktree at ${selected.worktree} would resume ` +
|
|
279
279
|
`'${preferred.droneLabel}' (cube '${preferred.name}') instead. To resume '${preferred.droneLabel}', run \`borg\` ` +
|
|
280
|
-
`in that worktree. To review this machine's
|
|
280
|
+
`in that worktree. To review this machine's drones, run \`borg drones\`.\n`,
|
|
281
281
|
);
|
|
282
282
|
return 1;
|
|
283
283
|
}
|
package/src/stream-status.ts
CHANGED
|
@@ -272,6 +272,9 @@ export function renderStreamStatus(inputs: RenderInputs): string {
|
|
|
272
272
|
);
|
|
273
273
|
lines.push(`- **OpenCode retried**: ${delivery.retried}`);
|
|
274
274
|
lines.push(`- **OpenCode failed**: ${delivery.failed}`);
|
|
275
|
+
lines.push(
|
|
276
|
+
'- **OpenCode delivery-state meaning**: delivered-unconfirmed counts entries that may have been submitted but are not visible in session history; failed counts entries that could not be submitted or were rejected, until the durable source entry is observed consumed.'
|
|
277
|
+
);
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
// Runtime-specific wake-path warning. The wire-down case takes
|
|
@@ -16,12 +16,14 @@ export const KNOWN_SUBCOMMANDS = [
|
|
|
16
16
|
'setup',
|
|
17
17
|
'update',
|
|
18
18
|
'doctor',
|
|
19
|
+
'clone',
|
|
20
|
+
'quickstart',
|
|
19
21
|
'assimilate',
|
|
20
22
|
'reset-local-connection',
|
|
21
23
|
'recover-enrollment',
|
|
22
24
|
'spawn',
|
|
23
25
|
'cleanup',
|
|
24
|
-
'
|
|
26
|
+
'drones',
|
|
25
27
|
'launch',
|
|
26
28
|
'launch-all',
|
|
27
29
|
'server',
|