@junctionpanel/server 0.1.24 → 0.1.26
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/server/client/daemon-client.d.ts +1 -0
- package/dist/server/client/daemon-client.d.ts.map +1 -1
- package/dist/server/client/daemon-client.js +3 -0
- package/dist/server/client/daemon-client.js.map +1 -1
- package/dist/server/server/agent/activity-curator.d.ts.map +1 -1
- package/dist/server/server/agent/activity-curator.js +4 -0
- package/dist/server/server/agent/activity-curator.js.map +1 -1
- package/dist/server/server/agent/agent-manager.d.ts +3 -1
- package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
- package/dist/server/server/agent/agent-manager.js +8 -1
- package/dist/server/server/agent/agent-manager.js.map +1 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts +14 -1
- package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +24 -0
- package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
- package/dist/server/server/session.d.ts.map +1 -1
- package/dist/server/server/session.js +36 -17
- package/dist/server/server/session.js.map +1 -1
- package/dist/server/server/worktree-bootstrap.d.ts +6 -0
- package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
- package/dist/server/server/worktree-bootstrap.js +428 -30
- package/dist/server/server/worktree-bootstrap.js.map +1 -1
- package/dist/server/shared/bootstrap-setup.d.ts +18 -0
- package/dist/server/shared/bootstrap-setup.d.ts.map +1 -0
- package/dist/server/shared/bootstrap-setup.js +116 -0
- package/dist/server/shared/bootstrap-setup.js.map +1 -0
- package/dist/server/shared/messages.d.ts +209 -192
- package/dist/server/shared/messages.d.ts.map +1 -1
- package/dist/server/shared/messages.js +19 -0
- package/dist/server/shared/messages.js.map +1 -1
- package/dist/server/shared/tool-call-display.d.ts.map +1 -1
- package/dist/server/shared/tool-call-display.js +4 -0
- package/dist/server/shared/tool-call-display.js.map +1 -1
- package/dist/server/terminal/terminal-manager.d.ts +2 -0
- package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
- package/dist/server/terminal/terminal-manager.js +8 -1
- package/dist/server/terminal/terminal-manager.js.map +1 -1
- package/dist/server/terminal/terminal.d.ts +1 -0
- package/dist/server/terminal/terminal.d.ts.map +1 -1
- package/dist/server/terminal/terminal.js +2 -2
- package/dist/server/terminal/terminal.js.map +1 -1
- package/dist/server/utils/worktree.d.ts +12 -0
- package/dist/server/utils/worktree.d.ts.map +1 -1
- package/dist/server/utils/worktree.js +38 -1
- package/dist/server/utils/worktree.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
|
+
import type { BootstrapSetupOverride } from "../shared/bootstrap-setup.js";
|
|
2
3
|
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
|
3
4
|
import { type WorktreeConfig } from "../utils/worktree.js";
|
|
4
5
|
import type { AgentTimelineItem } from "./agent/agent-sdk-types.js";
|
|
@@ -12,9 +13,14 @@ export interface WorktreeBootstrapTerminalResult {
|
|
|
12
13
|
export interface RunAsyncWorktreeBootstrapOptions {
|
|
13
14
|
agentId: string;
|
|
14
15
|
worktree: WorktreeConfig;
|
|
16
|
+
setupOverride?: BootstrapSetupOverride | null;
|
|
15
17
|
terminalManager: TerminalManager | null;
|
|
16
18
|
appendTimelineItem: (item: AgentTimelineItem) => Promise<boolean>;
|
|
17
19
|
emitLiveTimelineItem?: (item: AgentTimelineItem) => Promise<boolean>;
|
|
20
|
+
onSetupSettled?: (result: {
|
|
21
|
+
setupStatus: "not_required" | "completed" | "failed";
|
|
22
|
+
errorMessage: string | null;
|
|
23
|
+
}) => Promise<void> | void;
|
|
18
24
|
logger?: Logger;
|
|
19
25
|
}
|
|
20
26
|
export interface CreateAgentWorktreeOptions {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worktree-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/server/worktree-bootstrap.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"worktree-bootstrap.d.ts","sourceRoot":"","sources":["../../../src/server/worktree-bootstrap.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AACnC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,8BAA8B,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAEvE,OAAO,EAOL,KAAK,cAAc,EAKpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,MAAM,WAAW,+BAA+B;IAC9C,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC9C,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,kBAAkB,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,oBAAoB,CAAC,EAAE,CAAC,IAAI,EAAE,iBAAiB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE;QACxB,WAAW,EAAE,cAAc,GAAG,WAAW,GAAG,QAAQ,CAAC;QACrD,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;KAC7B,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiJD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,cAAc,CAAC,CASzB;AA0qBD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,IAAI,CAAC,CAqOf"}
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
1
3
|
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import { createWorktree, getWorktreeTerminalSpecs, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
|
|
4
|
+
import { createWorktree, getWorktreeTerminalSpecs, resolveWorktreeBootstrapSetup, resolveWorktreeRuntimeEnv, runWorktreeSetupCommands, WorktreeSetupError, } from "../utils/worktree.js";
|
|
3
5
|
const MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
|
4
6
|
const WORKTREE_SETUP_TRUNCATION_MARKER = "\n...<output truncated in the middle>...\n";
|
|
5
7
|
const WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS = 1500;
|
|
8
|
+
const SETUP_TERMINAL_NAME = "Setup";
|
|
9
|
+
const SETUP_SENTINEL_PREFIX = "__JUNCTION_SETUP_DONE__";
|
|
10
|
+
const SETUP_COMMAND_START_SENTINEL_PREFIX = "__JUNCTION_SETUP_COMMAND_START__";
|
|
11
|
+
const SETUP_COMMAND_DONE_SENTINEL_PREFIX = "__JUNCTION_SETUP_COMMAND_DONE__";
|
|
12
|
+
const ANSI_ESCAPE_REGEX = /\u001B[\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
|
13
|
+
function countCopiedFiles(rootPath) {
|
|
14
|
+
let total = 0;
|
|
15
|
+
const stack = [rootPath];
|
|
16
|
+
while (stack.length > 0) {
|
|
17
|
+
const currentPath = stack.pop();
|
|
18
|
+
if (!currentPath) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const entry of readdirSync(currentPath, { withFileTypes: true })) {
|
|
22
|
+
if (entry.name === ".git") {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const nextPath = join(currentPath, entry.name);
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
stack.push(nextPath);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
total += 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return total;
|
|
34
|
+
}
|
|
6
35
|
function byteLength(text) {
|
|
7
36
|
return Buffer.byteLength(text, "utf8");
|
|
8
37
|
}
|
|
@@ -96,6 +125,44 @@ export async function createAgentWorktree(options) {
|
|
|
96
125
|
function formatDurationMs(durationMs) {
|
|
97
126
|
return `${(durationMs / 1000).toFixed(2)}s`;
|
|
98
127
|
}
|
|
128
|
+
function escapeRegex(value) {
|
|
129
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
130
|
+
}
|
|
131
|
+
function quoteForShell(value) {
|
|
132
|
+
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
133
|
+
}
|
|
134
|
+
function resolveSetupTerminalShells() {
|
|
135
|
+
const interactiveShell = process.env.SHELL || "/bin/sh";
|
|
136
|
+
const interactiveShellName = basename(interactiveShell).toLowerCase();
|
|
137
|
+
const supportsDashLc = interactiveShellName === "sh" || interactiveShellName === "bash" || interactiveShellName === "zsh";
|
|
138
|
+
return {
|
|
139
|
+
runnerShell: supportsDashLc ? interactiveShell : "/bin/sh",
|
|
140
|
+
interactiveShell,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function normalizeTerminalControlText(value) {
|
|
144
|
+
if (!value) {
|
|
145
|
+
return "";
|
|
146
|
+
}
|
|
147
|
+
const stripped = value.replace(ANSI_ESCAPE_REGEX, "");
|
|
148
|
+
let normalized = "";
|
|
149
|
+
for (const char of stripped) {
|
|
150
|
+
if (char === "\b") {
|
|
151
|
+
normalized = normalized.slice(0, -1);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (char === "\r") {
|
|
155
|
+
normalized += "\n";
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
159
|
+
if (codePoint < 0x20 && char !== "\n" && char !== "\t") {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
normalized += char;
|
|
163
|
+
}
|
|
164
|
+
return normalized;
|
|
165
|
+
}
|
|
99
166
|
function commandStatusFromResult(result) {
|
|
100
167
|
if (result.exitCode === null) {
|
|
101
168
|
return "running";
|
|
@@ -184,6 +251,45 @@ function buildSetupTimelineItem(input) {
|
|
|
184
251
|
error: { message: input.errorMessage ?? "Worktree setup failed" },
|
|
185
252
|
};
|
|
186
253
|
}
|
|
254
|
+
function buildWorkspaceBootstrapTimelineItem(input) {
|
|
255
|
+
const detail = {
|
|
256
|
+
type: "workspace_bootstrap",
|
|
257
|
+
workspaceName: input.worktree.workspaceName,
|
|
258
|
+
baseBranch: input.worktree.baseBranch,
|
|
259
|
+
branchName: input.worktree.branchName,
|
|
260
|
+
worktreePath: input.worktree.worktreePath,
|
|
261
|
+
copiedFilesCount: input.copiedFilesCount,
|
|
262
|
+
setupStatus: input.setupStatus,
|
|
263
|
+
};
|
|
264
|
+
if (input.status === "running") {
|
|
265
|
+
return {
|
|
266
|
+
type: "tool_call",
|
|
267
|
+
name: "junction_workspace_bootstrap",
|
|
268
|
+
callId: input.callId,
|
|
269
|
+
status: "running",
|
|
270
|
+
detail,
|
|
271
|
+
error: null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (input.status === "completed") {
|
|
275
|
+
return {
|
|
276
|
+
type: "tool_call",
|
|
277
|
+
name: "junction_workspace_bootstrap",
|
|
278
|
+
callId: input.callId,
|
|
279
|
+
status: "completed",
|
|
280
|
+
detail,
|
|
281
|
+
error: null,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
type: "tool_call",
|
|
286
|
+
name: "junction_workspace_bootstrap",
|
|
287
|
+
callId: input.callId,
|
|
288
|
+
status: "failed",
|
|
289
|
+
detail,
|
|
290
|
+
error: { message: input.errorMessage ?? "Workspace bootstrap failed" },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
187
293
|
function buildTerminalTimelineItem(input) {
|
|
188
294
|
const detailInput = {
|
|
189
295
|
worktreePath: input.worktree.worktreePath,
|
|
@@ -268,6 +374,225 @@ async function waitForTerminalBootstrapReadiness(terminal) {
|
|
|
268
374
|
timeout = setTimeout(finish, WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS);
|
|
269
375
|
});
|
|
270
376
|
}
|
|
377
|
+
function buildSetupTerminalBootstrapCommand(input) {
|
|
378
|
+
const lines = [
|
|
379
|
+
`__junction_setup_exec_shell=${quoteForShell(input.runnerShell)}`,
|
|
380
|
+
`__junction_setup_interactive_shell=${quoteForShell(input.interactiveShell)}`,
|
|
381
|
+
"__junction_setup_status=0",
|
|
382
|
+
"__junction_setup_run_command() {",
|
|
383
|
+
' __junction_setup_index="$1"',
|
|
384
|
+
' __junction_setup_command="$2"',
|
|
385
|
+
` printf '\\n${SETUP_COMMAND_START_SENTINEL_PREFIX}:${input.token}:%s\\n' "$__junction_setup_index"`,
|
|
386
|
+
' "$__junction_setup_exec_shell" -lc "$__junction_setup_command"',
|
|
387
|
+
" __junction_setup_command_status=$?",
|
|
388
|
+
` printf '\\n${SETUP_COMMAND_DONE_SENTINEL_PREFIX}:${input.token}:%s:%s\\n' "$__junction_setup_index" "$__junction_setup_command_status"`,
|
|
389
|
+
' return "$__junction_setup_command_status"',
|
|
390
|
+
"}",
|
|
391
|
+
];
|
|
392
|
+
for (const [index, command] of input.commands.entries()) {
|
|
393
|
+
lines.push('if [ "$__junction_setup_status" -eq 0 ]; then');
|
|
394
|
+
lines.push(` __junction_setup_run_command ${index + 1} ${quoteForShell(command)} || __junction_setup_status=$?`);
|
|
395
|
+
lines.push("fi");
|
|
396
|
+
}
|
|
397
|
+
lines.push(`printf '\\n${SETUP_SENTINEL_PREFIX}:${input.token}:%s\\n' "$__junction_setup_status"`);
|
|
398
|
+
lines.push('exec "$__junction_setup_interactive_shell" -i || exec /bin/sh -i');
|
|
399
|
+
lines.push("");
|
|
400
|
+
return lines.join("\n");
|
|
401
|
+
}
|
|
402
|
+
function stripSetupProtocolOutput(output, token) {
|
|
403
|
+
if (!output) {
|
|
404
|
+
return "";
|
|
405
|
+
}
|
|
406
|
+
const protocolPattern = new RegExp([
|
|
407
|
+
escapeRegex(SETUP_COMMAND_START_SENTINEL_PREFIX),
|
|
408
|
+
escapeRegex(SETUP_COMMAND_DONE_SENTINEL_PREFIX),
|
|
409
|
+
escapeRegex(SETUP_SENTINEL_PREFIX),
|
|
410
|
+
]
|
|
411
|
+
.map((prefix) => `\\r?\\n?${prefix}:${escapeRegex(token)}(?::\\d+){1,2}\\r?\\n?`)
|
|
412
|
+
.join("|"), "g");
|
|
413
|
+
return output.replace(protocolPattern, "\n");
|
|
414
|
+
}
|
|
415
|
+
async function runSetupCommandsInTerminal(options) {
|
|
416
|
+
const token = uuidv4();
|
|
417
|
+
const { runnerShell, interactiveShell } = resolveSetupTerminalShells();
|
|
418
|
+
// Ensure Terminal 1 is provisioned before Setup so both tabs always exist together.
|
|
419
|
+
await options.terminalManager.getTerminals(options.worktree.worktreePath);
|
|
420
|
+
const terminal = await options.terminalManager.createTerminal({
|
|
421
|
+
cwd: options.worktree.worktreePath,
|
|
422
|
+
name: SETUP_TERMINAL_NAME,
|
|
423
|
+
env: options.runtimeEnv,
|
|
424
|
+
shell: runnerShell,
|
|
425
|
+
shellArgs: [
|
|
426
|
+
"-lc",
|
|
427
|
+
buildSetupTerminalBootstrapCommand({
|
|
428
|
+
commands: options.setup.commands,
|
|
429
|
+
token,
|
|
430
|
+
runnerShell,
|
|
431
|
+
interactiveShell,
|
|
432
|
+
}),
|
|
433
|
+
],
|
|
434
|
+
});
|
|
435
|
+
let terminalClosedError = null;
|
|
436
|
+
const commandOutputByIndex = new Map();
|
|
437
|
+
const commandStartedAtByIndex = new Map();
|
|
438
|
+
const resultsByIndex = new Map();
|
|
439
|
+
const startPattern = new RegExp(`^${escapeRegex(SETUP_COMMAND_START_SENTINEL_PREFIX)}:${escapeRegex(token)}:(\\d+)$`);
|
|
440
|
+
const donePattern = new RegExp(`^${escapeRegex(SETUP_COMMAND_DONE_SENTINEL_PREFIX)}:${escapeRegex(token)}:(\\d+):(\\d+)$`);
|
|
441
|
+
const setupDonePattern = new RegExp(`^${escapeRegex(SETUP_SENTINEL_PREFIX)}:${escapeRegex(token)}:(\\d+)$`);
|
|
442
|
+
let activeCommandIndex = null;
|
|
443
|
+
let normalizedBuffer = "";
|
|
444
|
+
const unsubscribeExit = terminal.onExit(() => {
|
|
445
|
+
terminalClosedError = "Setup terminal exited before the setup script completed.";
|
|
446
|
+
});
|
|
447
|
+
const buildOrderedResults = () => Array.from(resultsByIndex.entries())
|
|
448
|
+
.sort((a, b) => a[0] - b[0])
|
|
449
|
+
.map(([, result]) => result);
|
|
450
|
+
try {
|
|
451
|
+
await new Promise((resolve, reject) => {
|
|
452
|
+
let settled = false;
|
|
453
|
+
let rawSubscription = null;
|
|
454
|
+
let exitPoll = null;
|
|
455
|
+
const cleanup = () => {
|
|
456
|
+
if (exitPoll) {
|
|
457
|
+
clearInterval(exitPoll);
|
|
458
|
+
exitPoll = null;
|
|
459
|
+
}
|
|
460
|
+
rawSubscription?.unsubscribe();
|
|
461
|
+
rawSubscription = null;
|
|
462
|
+
};
|
|
463
|
+
const settle = (callback) => {
|
|
464
|
+
if (settled) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
settled = true;
|
|
468
|
+
cleanup();
|
|
469
|
+
callback();
|
|
470
|
+
};
|
|
471
|
+
const handleError = (error) => {
|
|
472
|
+
settle(() => reject(error));
|
|
473
|
+
};
|
|
474
|
+
const processLine = (line) => {
|
|
475
|
+
const startMatch = startPattern.exec(line);
|
|
476
|
+
if (startMatch) {
|
|
477
|
+
const commandIndex = Number.parseInt(startMatch[1] ?? "", 10);
|
|
478
|
+
if (!Number.isFinite(commandIndex) || commandIndex < 1 || commandIndex > options.setup.commands.length) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
activeCommandIndex = commandIndex;
|
|
482
|
+
commandStartedAtByIndex.set(commandIndex, Date.now());
|
|
483
|
+
commandOutputByIndex.set(commandIndex, "");
|
|
484
|
+
options.onEvent?.({
|
|
485
|
+
type: "command_started",
|
|
486
|
+
index: commandIndex,
|
|
487
|
+
total: options.setup.commands.length,
|
|
488
|
+
command: options.setup.commands[commandIndex - 1] ?? "",
|
|
489
|
+
cwd: options.worktree.worktreePath,
|
|
490
|
+
});
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const doneMatch = donePattern.exec(line);
|
|
494
|
+
if (doneMatch) {
|
|
495
|
+
const commandIndex = Number.parseInt(doneMatch[1] ?? "", 10);
|
|
496
|
+
const exitCode = Number.parseInt(doneMatch[2] ?? "", 10);
|
|
497
|
+
if (!Number.isFinite(commandIndex) || commandIndex < 1 || commandIndex > options.setup.commands.length) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const stdout = stripSetupProtocolOutput(commandOutputByIndex.get(commandIndex) ?? "", token);
|
|
501
|
+
const result = {
|
|
502
|
+
command: options.setup.commands[commandIndex - 1] ?? "",
|
|
503
|
+
cwd: options.worktree.worktreePath,
|
|
504
|
+
stdout,
|
|
505
|
+
stderr: "",
|
|
506
|
+
exitCode: Number.isFinite(exitCode) ? exitCode : null,
|
|
507
|
+
durationMs: Math.max(0, Date.now() - (commandStartedAtByIndex.get(commandIndex) ?? Date.now())),
|
|
508
|
+
};
|
|
509
|
+
resultsByIndex.set(commandIndex, result);
|
|
510
|
+
if (activeCommandIndex === commandIndex) {
|
|
511
|
+
activeCommandIndex = null;
|
|
512
|
+
}
|
|
513
|
+
options.onEvent?.({
|
|
514
|
+
type: "command_completed",
|
|
515
|
+
index: commandIndex,
|
|
516
|
+
total: options.setup.commands.length,
|
|
517
|
+
command: result.command,
|
|
518
|
+
cwd: result.cwd,
|
|
519
|
+
exitCode: result.exitCode,
|
|
520
|
+
durationMs: result.durationMs,
|
|
521
|
+
stdout: result.stdout,
|
|
522
|
+
stderr: result.stderr,
|
|
523
|
+
});
|
|
524
|
+
if (result.exitCode !== 0) {
|
|
525
|
+
handleError(new WorktreeSetupError(`Worktree setup command failed: ${result.command}`.trim(), buildOrderedResults()));
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const setupDoneMatch = setupDonePattern.exec(line);
|
|
530
|
+
if (setupDoneMatch) {
|
|
531
|
+
const exitCode = Number.parseInt(setupDoneMatch[1] ?? "", 10);
|
|
532
|
+
if (!Number.isFinite(exitCode) || exitCode !== 0) {
|
|
533
|
+
handleError(new WorktreeSetupError("Worktree setup failed", buildOrderedResults()));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if (resultsByIndex.size !== options.setup.commands.length) {
|
|
537
|
+
handleError(new Error("Setup terminal completed without recording all setup commands."));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
settle(resolve);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (activeCommandIndex === null) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const outputChunk = `${line}\n`;
|
|
547
|
+
commandOutputByIndex.set(activeCommandIndex, `${commandOutputByIndex.get(activeCommandIndex) ?? ""}${outputChunk}`);
|
|
548
|
+
options.onEvent?.({
|
|
549
|
+
type: "output",
|
|
550
|
+
index: activeCommandIndex,
|
|
551
|
+
total: options.setup.commands.length,
|
|
552
|
+
command: options.setup.commands[activeCommandIndex - 1] ?? "",
|
|
553
|
+
cwd: options.worktree.worktreePath,
|
|
554
|
+
stream: "stdout",
|
|
555
|
+
chunk: outputChunk,
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
rawSubscription = terminal.subscribeRaw((chunk) => {
|
|
559
|
+
if (settled) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const normalizedChunk = normalizeTerminalControlText(chunk.data);
|
|
563
|
+
if (!normalizedChunk) {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
normalizedBuffer = `${normalizedBuffer}${normalizedChunk}`;
|
|
567
|
+
let newlineIndex = normalizedBuffer.indexOf("\n");
|
|
568
|
+
while (newlineIndex !== -1) {
|
|
569
|
+
const line = normalizedBuffer.slice(0, newlineIndex);
|
|
570
|
+
normalizedBuffer = normalizedBuffer.slice(newlineIndex + 1);
|
|
571
|
+
processLine(line);
|
|
572
|
+
newlineIndex = normalizedBuffer.indexOf("\n");
|
|
573
|
+
}
|
|
574
|
+
}, { fromOffset: 0 });
|
|
575
|
+
if (terminalClosedError) {
|
|
576
|
+
handleError(new Error(terminalClosedError));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
exitPoll = setInterval(() => {
|
|
580
|
+
if (settled) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (terminalClosedError) {
|
|
584
|
+
handleError(new Error(terminalClosedError));
|
|
585
|
+
}
|
|
586
|
+
}, 25);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
unsubscribeExit();
|
|
591
|
+
}
|
|
592
|
+
return Array.from(resultsByIndex.entries())
|
|
593
|
+
.sort((a, b) => a[0] - b[0])
|
|
594
|
+
.map(([, result]) => result);
|
|
595
|
+
}
|
|
271
596
|
async function runWorktreeTerminalBootstrap(options, runtimeEnv) {
|
|
272
597
|
const terminalSpecs = getWorktreeTerminalSpecs(options.worktree.worktreePath);
|
|
273
598
|
if (terminalSpecs.length === 0) {
|
|
@@ -336,13 +661,44 @@ async function runWorktreeTerminalBootstrap(options, runtimeEnv) {
|
|
|
336
661
|
}));
|
|
337
662
|
}
|
|
338
663
|
export async function runAsyncWorktreeBootstrap(options) {
|
|
664
|
+
const bootstrapCallId = uuidv4();
|
|
339
665
|
const setupCallId = uuidv4();
|
|
340
666
|
let setupResults = [];
|
|
341
667
|
let runtimeEnv = null;
|
|
668
|
+
const copiedFilesCount = countCopiedFiles(options.worktree.worktreePath);
|
|
669
|
+
const resolvedSetup = resolveWorktreeBootstrapSetup({
|
|
670
|
+
repoRoot: options.worktree.worktreePath,
|
|
671
|
+
override: options.setupOverride,
|
|
672
|
+
});
|
|
673
|
+
const hasSetupCommands = Boolean(resolvedSetup && resolvedSetup.commands.length > 0);
|
|
342
674
|
const emitLiveTimelineItem = options.emitLiveTimelineItem;
|
|
343
675
|
const runningResultsByIndex = new Map();
|
|
344
676
|
const outputAccumulatorsByIndex = new Map();
|
|
345
677
|
let liveEmitQueue = Promise.resolve();
|
|
678
|
+
const started = await options.appendTimelineItem(buildWorkspaceBootstrapTimelineItem({
|
|
679
|
+
callId: bootstrapCallId,
|
|
680
|
+
status: "running",
|
|
681
|
+
worktree: options.worktree,
|
|
682
|
+
copiedFilesCount,
|
|
683
|
+
setupStatus: hasSetupCommands ? "running" : "not_required",
|
|
684
|
+
errorMessage: null,
|
|
685
|
+
}));
|
|
686
|
+
if (!started) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
if (hasSetupCommands) {
|
|
690
|
+
const setupStarted = await options.appendTimelineItem(buildSetupTimelineItem({
|
|
691
|
+
callId: setupCallId,
|
|
692
|
+
status: "running",
|
|
693
|
+
worktree: options.worktree,
|
|
694
|
+
results: [],
|
|
695
|
+
outputAccumulatorsByIndex,
|
|
696
|
+
errorMessage: null,
|
|
697
|
+
}));
|
|
698
|
+
if (!setupStarted) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
346
702
|
const queueLiveRunningEmit = () => {
|
|
347
703
|
if (!emitLiveTimelineItem) {
|
|
348
704
|
return;
|
|
@@ -375,12 +731,8 @@ export async function runAsyncWorktreeBootstrap(options) {
|
|
|
375
731
|
cwd: options.worktree.worktreePath,
|
|
376
732
|
env: runtimeEnv,
|
|
377
733
|
});
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
branchName: options.worktree.branchName,
|
|
381
|
-
cleanupOnFailure: false,
|
|
382
|
-
runtimeEnv,
|
|
383
|
-
onEvent: (event) => {
|
|
734
|
+
if (resolvedSetup) {
|
|
735
|
+
const handleSetupEvent = (event) => {
|
|
384
736
|
const existing = runningResultsByIndex.get(event.index);
|
|
385
737
|
const baseResult = existing ?? {
|
|
386
738
|
command: event.command,
|
|
@@ -390,48 +742,80 @@ export async function runAsyncWorktreeBootstrap(options) {
|
|
|
390
742
|
exitCode: null,
|
|
391
743
|
durationMs: 0,
|
|
392
744
|
};
|
|
393
|
-
if (event.type === "output") {
|
|
745
|
+
if (event.type === "output" && event.chunk) {
|
|
394
746
|
const outputAccumulator = outputAccumulatorsByIndex.get(event.index) ??
|
|
395
747
|
createMiddleTruncationAccumulator();
|
|
396
748
|
appendToMiddleTruncationAccumulator(outputAccumulator, event.chunk);
|
|
397
749
|
outputAccumulatorsByIndex.set(event.index, outputAccumulator);
|
|
398
|
-
runningResultsByIndex.set(event.index,
|
|
399
|
-
...baseResult,
|
|
400
|
-
// Keep the timeline command model lightweight; output is carried in
|
|
401
|
-
// outputAccumulatorsByIndex.
|
|
402
|
-
stdout: baseResult.stdout,
|
|
403
|
-
stderr: baseResult.stderr,
|
|
404
|
-
});
|
|
750
|
+
runningResultsByIndex.set(event.index, baseResult);
|
|
405
751
|
queueLiveRunningEmit();
|
|
406
752
|
return;
|
|
407
753
|
}
|
|
408
754
|
if (event.type === "command_completed") {
|
|
755
|
+
const outputAccumulator = outputAccumulatorsByIndex.get(event.index) ??
|
|
756
|
+
createMiddleTruncationAccumulator();
|
|
757
|
+
appendToMiddleTruncationAccumulator(outputAccumulator, `${event.stdout ?? ""}${event.stderr ?? ""}`);
|
|
758
|
+
outputAccumulatorsByIndex.set(event.index, outputAccumulator);
|
|
409
759
|
runningResultsByIndex.set(event.index, {
|
|
410
760
|
...baseResult,
|
|
411
|
-
stdout: event.stdout,
|
|
412
|
-
stderr: event.stderr,
|
|
413
|
-
exitCode: event.exitCode,
|
|
414
|
-
durationMs: event.durationMs,
|
|
761
|
+
stdout: event.stdout ?? "",
|
|
762
|
+
stderr: event.stderr ?? "",
|
|
763
|
+
exitCode: event.exitCode ?? null,
|
|
764
|
+
durationMs: event.durationMs ?? 0,
|
|
415
765
|
});
|
|
416
766
|
queueLiveRunningEmit();
|
|
417
767
|
return;
|
|
418
768
|
}
|
|
419
769
|
runningResultsByIndex.set(event.index, baseResult);
|
|
420
770
|
queueLiveRunningEmit();
|
|
421
|
-
}
|
|
422
|
-
|
|
771
|
+
};
|
|
772
|
+
setupResults =
|
|
773
|
+
options.terminalManager
|
|
774
|
+
? await runSetupCommandsInTerminal({
|
|
775
|
+
terminalManager: options.terminalManager,
|
|
776
|
+
worktree: options.worktree,
|
|
777
|
+
runtimeEnv,
|
|
778
|
+
setup: resolvedSetup,
|
|
779
|
+
onEvent: handleSetupEvent,
|
|
780
|
+
})
|
|
781
|
+
: await runWorktreeSetupCommands({
|
|
782
|
+
worktreePath: options.worktree.worktreePath,
|
|
783
|
+
branchName: options.worktree.branchName,
|
|
784
|
+
cleanupOnFailure: false,
|
|
785
|
+
commands: resolvedSetup.commands,
|
|
786
|
+
runtimeEnv,
|
|
787
|
+
onEvent: handleSetupEvent,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
423
790
|
await liveEmitQueue;
|
|
424
|
-
|
|
425
|
-
|
|
791
|
+
if (hasSetupCommands) {
|
|
792
|
+
const completed = await options.appendTimelineItem(buildSetupTimelineItem({
|
|
793
|
+
callId: setupCallId,
|
|
794
|
+
status: "completed",
|
|
795
|
+
worktree: options.worktree,
|
|
796
|
+
results: setupResults,
|
|
797
|
+
outputAccumulatorsByIndex,
|
|
798
|
+
errorMessage: null,
|
|
799
|
+
}));
|
|
800
|
+
if (!completed) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const bootstrapCompleted = await options.appendTimelineItem(buildWorkspaceBootstrapTimelineItem({
|
|
805
|
+
callId: bootstrapCallId,
|
|
426
806
|
status: "completed",
|
|
427
807
|
worktree: options.worktree,
|
|
428
|
-
|
|
429
|
-
|
|
808
|
+
copiedFilesCount,
|
|
809
|
+
setupStatus: hasSetupCommands ? "completed" : "not_required",
|
|
430
810
|
errorMessage: null,
|
|
431
811
|
}));
|
|
432
|
-
if (!
|
|
812
|
+
if (!bootstrapCompleted) {
|
|
433
813
|
return;
|
|
434
814
|
}
|
|
815
|
+
await options.onSetupSettled?.({
|
|
816
|
+
setupStatus: hasSetupCommands ? "completed" : "not_required",
|
|
817
|
+
errorMessage: null,
|
|
818
|
+
});
|
|
435
819
|
}
|
|
436
820
|
catch (error) {
|
|
437
821
|
if (error instanceof WorktreeSetupError) {
|
|
@@ -439,14 +823,28 @@ export async function runAsyncWorktreeBootstrap(options) {
|
|
|
439
823
|
}
|
|
440
824
|
await liveEmitQueue;
|
|
441
825
|
const message = error instanceof Error ? error.message : String(error);
|
|
442
|
-
|
|
443
|
-
|
|
826
|
+
if (hasSetupCommands) {
|
|
827
|
+
await options.appendTimelineItem(buildSetupTimelineItem({
|
|
828
|
+
callId: setupCallId,
|
|
829
|
+
status: "failed",
|
|
830
|
+
worktree: options.worktree,
|
|
831
|
+
results: setupResults,
|
|
832
|
+
outputAccumulatorsByIndex,
|
|
833
|
+
errorMessage: message,
|
|
834
|
+
}));
|
|
835
|
+
}
|
|
836
|
+
await options.appendTimelineItem(buildWorkspaceBootstrapTimelineItem({
|
|
837
|
+
callId: bootstrapCallId,
|
|
444
838
|
status: "failed",
|
|
445
839
|
worktree: options.worktree,
|
|
446
|
-
|
|
447
|
-
|
|
840
|
+
copiedFilesCount,
|
|
841
|
+
setupStatus: "failed",
|
|
448
842
|
errorMessage: message,
|
|
449
843
|
}));
|
|
844
|
+
await options.onSetupSettled?.({
|
|
845
|
+
setupStatus: "failed",
|
|
846
|
+
errorMessage: message,
|
|
847
|
+
});
|
|
450
848
|
return;
|
|
451
849
|
}
|
|
452
850
|
await runWorktreeTerminalBootstrap(options, runtimeEnv);
|