Make workspace archive self-healing during setup (#2987)
* fix(server): make workspace archive self-healing Background setup could keep writing after its workspace record was archived, causing disk cleanup to fail and the initiating client to restore a local ghost. Track setup per daemon so archive can stop it, and let repeated archive requests finish residual cleanup after the record has committed. * fix(server): await setup process termination Archive must not proceed from setup cancellation until the process-tree terminator has completed. Join that promise to the streamed command's existing finish path so child close cannot release workspace cleanup early.
This commit is contained in:
@@ -117,6 +117,7 @@ export async function fanOutReconciledWorkspaceUpdates(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
|
import { VoiceAssistantWebSocketServer } from "./websocket-server.js";
|
||||||
|
import { WorkspaceSetupRuntime } from "./workspace-setup-runtime.js";
|
||||||
import { createGitHubService } from "../services/github-service.js";
|
import { createGitHubService } from "../services/github-service.js";
|
||||||
import { createPaseoWorktree as createRegisteredPaseoWorktree } from "./paseo-worktree-service.js";
|
import { createPaseoWorktree as createRegisteredPaseoWorktree } from "./paseo-worktree-service.js";
|
||||||
import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js";
|
import { createWorkspaceProvisioningService } from "./session/workspace-provisioning/workspace-provisioning-service.js";
|
||||||
@@ -605,6 +606,7 @@ export async function createPaseoDaemon(
|
|||||||
publicBaseUrl: serviceProxyPublicBaseUrl,
|
publicBaseUrl: serviceProxyPublicBaseUrl,
|
||||||
});
|
});
|
||||||
const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
|
const scriptRuntimeStore = new WorkspaceScriptRuntimeStore();
|
||||||
|
const workspaceSetupRuntime = new WorkspaceSetupRuntime();
|
||||||
const configuredHostnames = config.hostnames ?? config.allowedHosts;
|
const configuredHostnames = config.hostnames ?? config.allowedHosts;
|
||||||
let wsServer: VoiceAssistantWebSocketServer | null = null;
|
let wsServer: VoiceAssistantWebSocketServer | null = null;
|
||||||
let serviceProxyListenTarget: ListenTarget | null = null;
|
let serviceProxyListenTarget: ListenTarget | null = null;
|
||||||
@@ -1039,6 +1041,8 @@ export async function createPaseoDaemon(
|
|||||||
await emitWorkspaceUpdatesExternal([workspaceId]);
|
await emitWorkspaceUpdatesExternal([workspaceId]);
|
||||||
},
|
},
|
||||||
cacheWorkspaceSetupSnapshot: () => {},
|
cacheWorkspaceSetupSnapshot: () => {},
|
||||||
|
startWorkspaceSetup: (workspaceId, operation) =>
|
||||||
|
workspaceSetupRuntime.start(workspaceId, operation),
|
||||||
emit: emitExternalSessionMessage,
|
emit: emitExternalSessionMessage,
|
||||||
sessionLogger: logger,
|
sessionLogger: logger,
|
||||||
terminalManager,
|
terminalManager,
|
||||||
@@ -1079,12 +1083,14 @@ export async function createPaseoDaemon(
|
|||||||
agentStorage,
|
agentStorage,
|
||||||
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
|
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
|
||||||
listActiveWorkspaces: listActiveWorkspacesExternal,
|
listActiveWorkspaces: listActiveWorkspacesExternal,
|
||||||
|
getWorkspace: (workspaceIdToGet) => workspaceRegistry.get(workspaceIdToGet),
|
||||||
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
||||||
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
||||||
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
||||||
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
|
clearWorkspaceArchiving: clearWorkspaceArchivingExternal,
|
||||||
killTerminalsForWorkspace: (workspaceIdToKill) =>
|
killTerminalsForWorkspace: (workspaceIdToKill) =>
|
||||||
killTerminalsForWorkspace({ terminalManager, sessionLogger: logger }, workspaceIdToKill),
|
killTerminalsForWorkspace({ terminalManager, sessionLogger: logger }, workspaceIdToKill),
|
||||||
|
stopWorkspaceSetup: (workspaceIdToStop) => workspaceSetupRuntime.stop(workspaceIdToStop),
|
||||||
sessionLogger: logger,
|
sessionLogger: logger,
|
||||||
},
|
},
|
||||||
{ scope: { kind: "workspace", workspaceId }, requestId },
|
{ scope: { kind: "workspace", workspaceId }, requestId },
|
||||||
@@ -1187,6 +1193,7 @@ export async function createPaseoDaemon(
|
|||||||
agentStorage,
|
agentStorage,
|
||||||
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
|
findWorkspaceIdForCwd: findWorkspaceIdForCwdExternal,
|
||||||
listActiveWorkspaces: listActiveWorkspacesExternal,
|
listActiveWorkspaces: listActiveWorkspacesExternal,
|
||||||
|
getWorkspace: (workspaceIdToGet) => workspaceRegistry.get(workspaceIdToGet),
|
||||||
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
archiveWorkspaceRecord: archiveWorkspaceRecordExternal,
|
||||||
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
emitWorkspaceUpdatesForWorkspaceIds: emitWorkspaceUpdatesExternal,
|
||||||
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
markWorkspaceArchiving: markWorkspaceArchivingExternal,
|
||||||
@@ -1199,6 +1206,7 @@ export async function createPaseoDaemon(
|
|||||||
},
|
},
|
||||||
workspaceIdToKill,
|
workspaceIdToKill,
|
||||||
),
|
),
|
||||||
|
stopWorkspaceSetup: (workspaceIdToStop) => workspaceSetupRuntime.stop(workspaceIdToStop),
|
||||||
sessionLogger: logger,
|
sessionLogger: logger,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1564,6 +1572,7 @@ export async function createPaseoDaemon(
|
|||||||
serviceProxyPublicBaseUrl,
|
serviceProxyPublicBaseUrl,
|
||||||
browserToolsBroker,
|
browserToolsBroker,
|
||||||
hubRelationships,
|
hubRelationships,
|
||||||
|
workspaceSetupRuntime,
|
||||||
);
|
);
|
||||||
relayRuntime = createRelayRuntime({
|
relayRuntime = createRelayRuntime({
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -173,7 +173,6 @@ import { PushTokenStore } from "./push/token-store.js";
|
|||||||
import {
|
import {
|
||||||
archivePersistedWorkspaceRecord,
|
archivePersistedWorkspaceRecord,
|
||||||
archiveWorkspaceContents,
|
archiveWorkspaceContents,
|
||||||
requireActiveWorkspaceForArchive,
|
|
||||||
} from "./workspace-archive-service.js";
|
} from "./workspace-archive-service.js";
|
||||||
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
||||||
import { renameCurrentBranch as renameCurrentBranchDefault } from "../utils/checkout-git.js";
|
import { renameCurrentBranch as renameCurrentBranchDefault } from "../utils/checkout-git.js";
|
||||||
@@ -240,6 +239,13 @@ import {
|
|||||||
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
|
handleWorkspaceSetupStatusRequest as handleWorkspaceSetupStatusRequestMessage,
|
||||||
} from "./worktree-session.js";
|
} from "./worktree-session.js";
|
||||||
import { archiveByScope, type ActiveWorkspaceRef } from "./workspace-archive-service.js";
|
import { archiveByScope, type ActiveWorkspaceRef } from "./workspace-archive-service.js";
|
||||||
|
import { WorkspaceSetupRuntime } from "./workspace-setup-runtime.js";
|
||||||
|
|
||||||
|
function resolveWorkspaceSetupRuntime(
|
||||||
|
runtime: WorkspaceSetupRuntime | undefined,
|
||||||
|
): WorkspaceSetupRuntime {
|
||||||
|
return runtime ?? new WorkspaceSetupRuntime();
|
||||||
|
}
|
||||||
import { WorktreeRequestError, toWorktreeWireError } from "./worktree-errors.js";
|
import { WorktreeRequestError, toWorktreeWireError } from "./worktree-errors.js";
|
||||||
import { parseGitRemoteLocation } from "@getpaseo/protocol/git-remote";
|
import { parseGitRemoteLocation } from "@getpaseo/protocol/git-remote";
|
||||||
import {
|
import {
|
||||||
@@ -458,6 +464,7 @@ export interface SessionOptions {
|
|||||||
serviceProxy?: ServiceProxySubsystem;
|
serviceProxy?: ServiceProxySubsystem;
|
||||||
scriptRuntimeStore?: WorkspaceScriptRuntimeStore;
|
scriptRuntimeStore?: WorkspaceScriptRuntimeStore;
|
||||||
workspaceSetupSnapshots?: Map<string, WorkspaceSetupSnapshot>;
|
workspaceSetupSnapshots?: Map<string, WorkspaceSetupSnapshot>;
|
||||||
|
workspaceSetupRuntime?: WorkspaceSetupRuntime;
|
||||||
onBranchChanged?: (
|
onBranchChanged?: (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
oldBranch: string | null,
|
oldBranch: string | null,
|
||||||
@@ -649,6 +656,7 @@ export class Session {
|
|||||||
private inflightRequests = 0;
|
private inflightRequests = 0;
|
||||||
private peakInflightRequests = 0;
|
private peakInflightRequests = 0;
|
||||||
private readonly workspaceSetupSnapshots: Map<string, WorkspaceSetupSnapshot>;
|
private readonly workspaceSetupSnapshots: Map<string, WorkspaceSetupSnapshot>;
|
||||||
|
private readonly workspaceSetupRuntime: WorkspaceSetupRuntime;
|
||||||
private readonly workspaceGitObserver: WorkspaceGitObserverService;
|
private readonly workspaceGitObserver: WorkspaceGitObserverService;
|
||||||
private readonly workspaceDirectory: WorkspaceDirectory;
|
private readonly workspaceDirectory: WorkspaceDirectory;
|
||||||
private readonly voiceSession: VoiceSession;
|
private readonly voiceSession: VoiceSession;
|
||||||
@@ -704,6 +712,7 @@ export class Session {
|
|||||||
serviceProxy,
|
serviceProxy,
|
||||||
scriptRuntimeStore,
|
scriptRuntimeStore,
|
||||||
workspaceSetupSnapshots,
|
workspaceSetupSnapshots,
|
||||||
|
workspaceSetupRuntime,
|
||||||
onBranchChanged,
|
onBranchChanged,
|
||||||
getDaemonTcpPort,
|
getDaemonTcpPort,
|
||||||
getDaemonTcpHost,
|
getDaemonTcpHost,
|
||||||
@@ -955,6 +964,7 @@ export class Session {
|
|||||||
this.serviceProxy = serviceProxy ?? null;
|
this.serviceProxy = serviceProxy ?? null;
|
||||||
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
|
this.scriptRuntimeStore = scriptRuntimeStore ?? null;
|
||||||
this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
|
this.workspaceSetupSnapshots = workspaceSetupSnapshots ?? new Map();
|
||||||
|
this.workspaceSetupRuntime = resolveWorkspaceSetupRuntime(workspaceSetupRuntime);
|
||||||
this.getDaemonTcpPort = getDaemonTcpPort ?? null;
|
this.getDaemonTcpPort = getDaemonTcpPort ?? null;
|
||||||
this.getDaemonTcpHost = getDaemonTcpHost ?? null;
|
this.getDaemonTcpHost = getDaemonTcpHost ?? null;
|
||||||
this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
|
this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null;
|
||||||
@@ -5890,6 +5900,8 @@ export class Session {
|
|||||||
this.workspaceAutoName.scheduleForWorktree(autoNameInput, {
|
this.workspaceAutoName.scheduleForWorktree(autoNameInput, {
|
||||||
currentSelection: this.getFocusedAgentSelectionForCwd(autoNameInput.workspace.cwd),
|
currentSelection: this.getFocusedAgentSelectionForCwd(autoNameInput.workspace.cwd),
|
||||||
}),
|
}),
|
||||||
|
startWorkspaceSetup: (workspaceId, operation) =>
|
||||||
|
this.workspaceSetupRuntime.start(workspaceId, operation),
|
||||||
emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
|
emitWorkspaceUpdateForWorkspaceId: (workspaceId) =>
|
||||||
this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
|
this.emitWorkspaceUpdateForWorkspaceId(workspaceId),
|
||||||
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
|
cacheWorkspaceSetupSnapshot: (workspaceId, snapshot) => {
|
||||||
@@ -5929,10 +5941,10 @@ export class Session {
|
|||||||
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
|
request: Extract<SessionInboundMessage, { type: "archive_workspace_request" }>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const existing = await requireActiveWorkspaceForArchive(
|
const existing = await this.workspaceRegistry.get(request.workspaceId);
|
||||||
{ listActiveWorkspaces: () => this.listActiveWorkspaceRefs() },
|
if (!existing) {
|
||||||
request.workspaceId,
|
throw new Error(`Workspace not found: ${request.workspaceId}`);
|
||||||
);
|
}
|
||||||
|
|
||||||
await archiveByScope(
|
await archiveByScope(
|
||||||
{
|
{
|
||||||
@@ -5943,6 +5955,7 @@ export class Session {
|
|||||||
agentManager: this.agentManager,
|
agentManager: this.agentManager,
|
||||||
agentStorage: this.agentStorage,
|
agentStorage: this.agentStorage,
|
||||||
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
findWorkspaceIdForCwd: (cwd) => this.findWorkspaceIdForCwd(cwd),
|
||||||
|
getWorkspace: (workspaceId) => this.workspaceRegistry.get(workspaceId),
|
||||||
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
listActiveWorkspaces: () => this.listActiveWorkspaceRefs(),
|
||||||
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
archiveWorkspaceRecord: (workspaceId) => this.archiveWorkspaceRecord(workspaceId),
|
||||||
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
|
emitWorkspaceUpdatesForWorkspaceIds: (workspaceIds) =>
|
||||||
@@ -5952,6 +5965,7 @@ export class Session {
|
|||||||
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
clearWorkspaceArchiving: (workspaceIds) => this.clearWorkspaceArchiving(workspaceIds),
|
||||||
killTerminalsForWorkspace: (workspaceId) =>
|
killTerminalsForWorkspace: (workspaceId) =>
|
||||||
this.terminalController.killTerminalsForWorkspace(workspaceId),
|
this.terminalController.killTerminalsForWorkspace(workspaceId),
|
||||||
|
stopWorkspaceSetup: (workspaceId) => this.workspaceSetupRuntime.stop(workspaceId),
|
||||||
sessionLogger: this.sessionLogger,
|
sessionLogger: this.sessionLogger,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import type { HostnamesConfig } from "./hostnames.js";
|
|||||||
import { isHostnameAllowed } from "./hostnames.js";
|
import { isHostnameAllowed } from "./hostnames.js";
|
||||||
import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js";
|
import { Session, type SessionLifecycleIntent, type SessionRuntimeMetrics } from "./session.js";
|
||||||
import type { HubRelationshipManagement } from "./hub/relationship-controller.js";
|
import type { HubRelationshipManagement } from "./hub/relationship-controller.js";
|
||||||
|
import { WorkspaceSetupRuntime } from "./workspace-setup-runtime.js";
|
||||||
import type { HubExecutionAgents } from "./hub/daemon-executions.js";
|
import type { HubExecutionAgents } from "./hub/daemon-executions.js";
|
||||||
import type { AgentProvider } from "./agent/agent-sdk-types.js";
|
import type { AgentProvider } from "./agent/agent-sdk-types.js";
|
||||||
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
import { ProviderSnapshotManager } from "./agent/provider-snapshot-manager.js";
|
||||||
@@ -537,6 +538,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
private readonly voiceSpeakHandlers = new Map<string, VoiceSpeakHandler>();
|
private readonly voiceSpeakHandlers = new Map<string, VoiceSpeakHandler>();
|
||||||
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
|
private readonly voiceCallerContexts = new Map<string, VoiceCallerContext>();
|
||||||
private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>();
|
private readonly workspaceSetupSnapshots = new Map<string, WorkspaceSetupSnapshot>();
|
||||||
|
private readonly workspaceSetupRuntime: WorkspaceSetupRuntime;
|
||||||
private readonly providerSnapshotManager: ProviderSnapshotManager;
|
private readonly providerSnapshotManager: ProviderSnapshotManager;
|
||||||
private onLifecycleIntent!: ((intent: SessionLifecycleIntent) => void) | null;
|
private onLifecycleIntent!: ((intent: SessionLifecycleIntent) => void) | null;
|
||||||
private onBranchChanged!:
|
private onBranchChanged!:
|
||||||
@@ -604,8 +606,10 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
serviceProxyPublicBaseUrl?: string | null,
|
serviceProxyPublicBaseUrl?: string | null,
|
||||||
browserToolsBroker?: BrowserToolsBroker | null,
|
browserToolsBroker?: BrowserToolsBroker | null,
|
||||||
hubRelationships?: HubRelationshipManagement | null,
|
hubRelationships?: HubRelationshipManagement | null,
|
||||||
|
workspaceSetupRuntime: WorkspaceSetupRuntime = new WorkspaceSetupRuntime(),
|
||||||
) {
|
) {
|
||||||
this.logger = logger.child({ module: "websocket-server" });
|
this.logger = logger.child({ module: "websocket-server" });
|
||||||
|
this.workspaceSetupRuntime = workspaceSetupRuntime;
|
||||||
this.advertiseDaemonStatusRpc = wsConfig.daemonStatusRpc !== false;
|
this.advertiseDaemonStatusRpc = wsConfig.daemonStatusRpc !== false;
|
||||||
this.advertiseRelayConfig = wsConfig.relayConfig !== false;
|
this.advertiseRelayConfig = wsConfig.relayConfig !== false;
|
||||||
this.serverId = serverId;
|
this.serverId = serverId;
|
||||||
@@ -1348,6 +1352,7 @@ export class VoiceAssistantWebSocketServer {
|
|||||||
serviceProxy: this.serviceProxy ?? undefined,
|
serviceProxy: this.serviceProxy ?? undefined,
|
||||||
scriptRuntimeStore: this.scriptRuntimeStore ?? undefined,
|
scriptRuntimeStore: this.scriptRuntimeStore ?? undefined,
|
||||||
workspaceSetupSnapshots: this.workspaceSetupSnapshots,
|
workspaceSetupSnapshots: this.workspaceSetupSnapshots,
|
||||||
|
workspaceSetupRuntime: this.workspaceSetupRuntime,
|
||||||
onBranchChanged: this.onBranchChanged ?? undefined,
|
onBranchChanged: this.onBranchChanged ?? undefined,
|
||||||
getDaemonTcpPort: this.getDaemonTcpPort ?? undefined,
|
getDaemonTcpPort: this.getDaemonTcpPort ?? undefined,
|
||||||
getDaemonTcpHost: this.getDaemonTcpHost ?? undefined,
|
getDaemonTcpHost: this.getDaemonTcpHost ?? undefined,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync, spawn } from "node:child_process";
|
||||||
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, beforeEach, expect, test } from "vitest";
|
import { afterEach, beforeEach, expect, test } from "vitest";
|
||||||
@@ -300,6 +300,118 @@ test("archiving the last reference to a worktree removes it from disk regardless
|
|||||||
await expect.poll(() => existsSync(deleteDir), { timeout: 10000, interval: 100 }).toBe(false);
|
await expect.poll(() => existsSync(deleteDir), { timeout: 10000, interval: 100 }).toBe(false);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
|
|
||||||
|
test.skipIf(process.platform === "win32")(
|
||||||
|
"archiving a worktree is repeatable while background setup is still writing into it",
|
||||||
|
async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const setupStartedPath = path.join(repoDir, "setup-started");
|
||||||
|
const stopSetupPath = path.join(repoDir, "stop-setup");
|
||||||
|
writeFileSync(
|
||||||
|
path.join(repoDir, "paseo.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
worktree: {
|
||||||
|
setup: [
|
||||||
|
`node -e "const fs=require('fs'),path=require('path');const source=process.env.PASEO_SOURCE_CHECKOUT_PATH;const worktree=process.env.PASEO_WORKTREE_PATH;const target=path.join(worktree,'node_modules/react-native-svg/lib/typescript');fs.writeFileSync(path.join(source,'setup-started'),'started');while(!fs.existsSync(path.join(source,'stop-setup'))){try{fs.mkdirSync(target,{recursive:true});fs.writeFileSync(path.join(target,'active'),String(Date.now()))}catch{}}"`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
execFileSync("git", ["add", "paseo.json"], { cwd: repoDir, stdio: "pipe" });
|
||||||
|
execFileSync(
|
||||||
|
"git",
|
||||||
|
["-c", "commit.gpgsign=false", "commit", "-m", "add active worktree setup"],
|
||||||
|
{ cwd: repoDir, stdio: "pipe" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await ctx.client.createWorkspace({
|
||||||
|
source: {
|
||||||
|
kind: "worktree",
|
||||||
|
cwd: repoDir,
|
||||||
|
worktreeSlug: "archive-during-setup",
|
||||||
|
baseBranch: "main",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const workspace = result.workspace;
|
||||||
|
if (!workspace?.workspaceDirectory) {
|
||||||
|
throw new Error(result.error ?? "Failed to create worktree workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect.poll(() => existsSync(setupStartedPath), { timeout: 10000 }).toBe(true);
|
||||||
|
|
||||||
|
const archive = await ctx.client.archiveWorkspace(workspace.id);
|
||||||
|
const retry = await ctx.client.archiveWorkspace(workspace.id);
|
||||||
|
|
||||||
|
expect({ archiveError: archive.error, retryError: retry.error }).toEqual({
|
||||||
|
archiveError: null,
|
||||||
|
retryError: null,
|
||||||
|
});
|
||||||
|
expect((await activeWorkspaceIds()).has(workspace.id)).toBe(false);
|
||||||
|
expect(existsSync(workspace.workspaceDirectory)).toBe(false);
|
||||||
|
} finally {
|
||||||
|
writeFileSync(stopSetupPath, "stop\n");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
60000,
|
||||||
|
);
|
||||||
|
|
||||||
|
test.skipIf(process.platform === "win32")(
|
||||||
|
"repeating archive removes a residual worktree for an already-archived workspace",
|
||||||
|
async () => {
|
||||||
|
const repoDir = createGitRepo();
|
||||||
|
const result = await ctx.client.createWorkspace({
|
||||||
|
source: {
|
||||||
|
kind: "worktree",
|
||||||
|
cwd: repoDir,
|
||||||
|
worktreeSlug: "self-heal-archive",
|
||||||
|
baseBranch: "main",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const workspace = result.workspace;
|
||||||
|
if (!workspace?.workspaceDirectory) {
|
||||||
|
throw new Error(result.error ?? "Failed to create worktree workspace");
|
||||||
|
}
|
||||||
|
|
||||||
|
const writerStartedPath = path.join(repoDir, "writer-started");
|
||||||
|
const stopWriterPath = path.join(repoDir, "stop-writer");
|
||||||
|
const target = path.join(
|
||||||
|
workspace.workspaceDirectory,
|
||||||
|
"node_modules/react-native-svg/lib/typescript",
|
||||||
|
);
|
||||||
|
const writer = spawn(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
"-e",
|
||||||
|
`const fs=require('fs'),path=require('path');const target=${JSON.stringify(target)};const started=${JSON.stringify(writerStartedPath)};const stop=${JSON.stringify(stopWriterPath)};fs.writeFileSync(started,'started');while(!fs.existsSync(stop)){try{fs.mkdirSync(target,{recursive:true});fs.writeFileSync(path.join(target,'active'),String(Date.now()))}catch{}}`,
|
||||||
|
],
|
||||||
|
{ stdio: "ignore" },
|
||||||
|
);
|
||||||
|
const writerExit = new Promise<void>((resolve) => writer.once("exit", () => resolve()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect.poll(() => existsSync(writerStartedPath), { timeout: 10000 }).toBe(true);
|
||||||
|
|
||||||
|
const firstArchive = await ctx.client.archiveWorkspace(workspace.id);
|
||||||
|
|
||||||
|
expect(firstArchive.error).toBeNull();
|
||||||
|
expect((await activeWorkspaceIds()).has(workspace.id)).toBe(false);
|
||||||
|
expect(existsSync(workspace.workspaceDirectory)).toBe(true);
|
||||||
|
|
||||||
|
writeFileSync(stopWriterPath, "stop\n");
|
||||||
|
await writerExit;
|
||||||
|
|
||||||
|
const retry = await ctx.client.archiveWorkspace(workspace.id);
|
||||||
|
|
||||||
|
expect(retry.error).toBeNull();
|
||||||
|
expect(existsSync(workspace.workspaceDirectory)).toBe(false);
|
||||||
|
} finally {
|
||||||
|
writeFileSync(stopWriterPath, "stop\n");
|
||||||
|
await writerExit;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
60000,
|
||||||
|
);
|
||||||
|
|
||||||
test("worktree archive targets the explicit workspaceId when a directory backs multiple workspaces", async () => {
|
test("worktree archive targets the explicit workspaceId when a directory backs multiple workspaces", async () => {
|
||||||
const repoDir = createGitRepo();
|
const repoDir = createGitRepo();
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface ArchiveDependencies {
|
|||||||
// path uniquely identifies a worktree workspace; this is a directory lookup for
|
// path uniquely identifies a worktree workspace; this is a directory lookup for
|
||||||
// the archive target, not status/ownership.
|
// the archive target, not status/ownership.
|
||||||
findWorkspaceIdForCwd: (cwd: string) => Promise<string | null>;
|
findWorkspaceIdForCwd: (cwd: string) => Promise<string | null>;
|
||||||
|
getWorkspace?: (workspaceId: string) => Promise<PersistedWorkspaceRecord | null>;
|
||||||
// Active (non-archived) workspaces, used to decide whether the workspace being
|
// Active (non-archived) workspaces, used to decide whether the workspace being
|
||||||
// archived is the last reference to its backing worktree directory, and to
|
// archived is the last reference to its backing worktree directory, and to
|
||||||
// break a same-cwd tie in favor of the worktree-kind record when archiving by
|
// break a same-cwd tie in favor of the worktree-kind record when archiving by
|
||||||
@@ -48,6 +49,7 @@ export interface ArchiveDependencies {
|
|||||||
markWorkspaceArchiving: (workspaceIds: Iterable<string>, archivingAt: string) => void;
|
markWorkspaceArchiving: (workspaceIds: Iterable<string>, archivingAt: string) => void;
|
||||||
clearWorkspaceArchiving: (workspaceIds: Iterable<string>) => void;
|
clearWorkspaceArchiving: (workspaceIds: Iterable<string>) => void;
|
||||||
killTerminalsForWorkspace: (workspaceId: string) => Promise<void>;
|
killTerminalsForWorkspace: (workspaceId: string) => Promise<void>;
|
||||||
|
stopWorkspaceSetup?: (workspaceId: string) => Promise<void>;
|
||||||
sessionLogger?: Logger;
|
sessionLogger?: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ interface BackingDirectory {
|
|||||||
interface ArchiveTarget {
|
interface ArchiveTarget {
|
||||||
backing: BackingDirectory | null;
|
backing: BackingDirectory | null;
|
||||||
teardownTargets: Array<{ workspaceId: string | null; cwd: string }>;
|
teardownTargets: Array<{ workspaceId: string | null; cwd: string }>;
|
||||||
|
setupWorkspaceIds: string[];
|
||||||
workspaceIds: string[];
|
workspaceIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +132,8 @@ async function archiveByScopeWithPriority(
|
|||||||
const target = await resolveArchiveTarget(dependencies, request.scope);
|
const target = await resolveArchiveTarget(dependencies, request.scope);
|
||||||
const targetWorkspaceIds = target.workspaceIds;
|
const targetWorkspaceIds = target.workspaceIds;
|
||||||
|
|
||||||
|
await stopWorkspaceSetups(dependencies, target.setupWorkspaceIds, request.requestId);
|
||||||
|
|
||||||
if (targetWorkspaceIds.length > 0) {
|
if (targetWorkspaceIds.length > 0) {
|
||||||
dependencies.markWorkspaceArchiving(targetWorkspaceIds, new Date().toISOString());
|
dependencies.markWorkspaceArchiving(targetWorkspaceIds, new Date().toISOString());
|
||||||
}
|
}
|
||||||
@@ -190,18 +195,22 @@ async function resolveArchiveTarget(
|
|||||||
|
|
||||||
if (scope.kind === "workspace") {
|
if (scope.kind === "workspace") {
|
||||||
const workspaceId = scope.workspaceId;
|
const workspaceId = scope.workspaceId;
|
||||||
const record = activeWorkspaces.find((workspace) => workspace.workspaceId === workspaceId);
|
const record =
|
||||||
|
(await dependencies.getWorkspace?.(workspaceId)) ??
|
||||||
|
activeWorkspaces.find((workspace) => workspace.workspaceId === workspaceId);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
dependencies.sessionLogger?.warn(
|
dependencies.sessionLogger?.warn(
|
||||||
{ workspaceId },
|
{ workspaceId },
|
||||||
"Workspace not found for archive-by-scope; skipping",
|
"Workspace not found for archive-by-scope; skipping",
|
||||||
);
|
);
|
||||||
return { backing: null, teardownTargets: [], workspaceIds: [] };
|
return { backing: null, teardownTargets: [], setupWorkspaceIds: [], workspaceIds: [] };
|
||||||
}
|
}
|
||||||
|
const isArchived = "archivedAt" in record && Boolean(record.archivedAt);
|
||||||
return {
|
return {
|
||||||
backing: await resolveWorkspaceBackingDirectory(record, dependencies),
|
backing: await resolveWorkspaceBackingDirectory(record, dependencies),
|
||||||
teardownTargets: [{ workspaceId, cwd: record.cwd }],
|
teardownTargets: isArchived ? [] : [{ workspaceId, cwd: record.cwd }],
|
||||||
workspaceIds: [workspaceId],
|
setupWorkspaceIds: [workspaceId],
|
||||||
|
workspaceIds: isArchived ? [] : [workspaceId],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,10 +239,32 @@ async function resolveArchiveTarget(
|
|||||||
cwd: workspace.cwd,
|
cwd: workspace.cwd,
|
||||||
}))
|
}))
|
||||||
: [{ workspaceId: null, cwd: scope.targetPath }],
|
: [{ workspaceId: null, cwd: scope.targetPath }],
|
||||||
|
setupWorkspaceIds: targetWorkspaces.map((workspace) => workspace.workspaceId),
|
||||||
workspaceIds: targetWorkspaces.map((workspace) => workspace.workspaceId),
|
workspaceIds: targetWorkspaces.map((workspace) => workspace.workspaceId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function stopWorkspaceSetups(
|
||||||
|
dependencies: ArchiveDependencies,
|
||||||
|
workspaceIds: string[],
|
||||||
|
requestId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!dependencies.stopWorkspaceSetup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
workspaceIds.map((workspaceId) => dependencies.stopWorkspaceSetup!(workspaceId)),
|
||||||
|
);
|
||||||
|
for (const [index, result] of results.entries()) {
|
||||||
|
if (result?.status === "rejected") {
|
||||||
|
dependencies.sessionLogger?.warn(
|
||||||
|
{ err: result.reason, workspaceId: workspaceIds[index], requestId },
|
||||||
|
"Failed to stop workspace setup during archive; continuing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveWorkspaceBackingDirectory(
|
async function resolveWorkspaceBackingDirectory(
|
||||||
workspace: ActiveWorkspaceRef,
|
workspace: ActiveWorkspaceRef,
|
||||||
dependencies: Pick<ArchiveDependencies, "paseoHome" | "paseoWorktreesBaseRoot">,
|
dependencies: Pick<ArchiveDependencies, "paseoHome" | "paseoWorktreesBaseRoot">,
|
||||||
@@ -379,14 +410,11 @@ async function maybeRemoveDirectory(
|
|||||||
dependencies.github.invalidate({ cwd: backing.path });
|
dependencies.github.invalidate({ cwd: backing.path });
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof WorktreeTeardownError) {
|
dependencies.sessionLogger?.warn(
|
||||||
dependencies.sessionLogger?.warn(
|
{ err: error, targetPath: backing.path, requestId: request.requestId },
|
||||||
{ err: error, targetPath: backing.path, requestId: request.requestId },
|
"Worktree disk removal failed during archive; workspace already archived",
|
||||||
"Worktree disk removal failed during archive; workspace already archived",
|
);
|
||||||
);
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
export type WorkspaceSetupOperation = (signal: AbortSignal) => Promise<void>;
|
||||||
|
|
||||||
|
interface WorkspaceSetupRun {
|
||||||
|
controller: AbortController;
|
||||||
|
completion: Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkspaceSetupRuntime {
|
||||||
|
private readonly runs = new Map<string, WorkspaceSetupRun>();
|
||||||
|
|
||||||
|
start(workspaceId: string, operation: WorkspaceSetupOperation): void {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const run: WorkspaceSetupRun = {
|
||||||
|
controller,
|
||||||
|
completion: Promise.resolve(),
|
||||||
|
};
|
||||||
|
this.runs.set(workspaceId, run);
|
||||||
|
run.completion = Promise.resolve()
|
||||||
|
.then(() => operation(controller.signal))
|
||||||
|
.catch(() => undefined)
|
||||||
|
.finally(() => {
|
||||||
|
if (this.runs.get(workspaceId) === run) {
|
||||||
|
this.runs.delete(workspaceId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop(workspaceId: string): Promise<void> {
|
||||||
|
const run = this.runs.get(workspaceId);
|
||||||
|
if (!run) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
run.controller.abort();
|
||||||
|
await run.completion;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
createPaseoWorktreeCommand,
|
createPaseoWorktreeCommand,
|
||||||
listPaseoWorktreesCommand,
|
listPaseoWorktreesCommand,
|
||||||
} from "./worktree/commands.js";
|
} from "./worktree/commands.js";
|
||||||
|
import type { WorkspaceSetupOperation } from "./workspace-setup-runtime.js";
|
||||||
|
|
||||||
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._/-]+$/;
|
const SAFE_GIT_REF_PATTERN = /^[A-Za-z0-9._/-]+$/;
|
||||||
|
|
||||||
@@ -123,6 +124,7 @@ interface CreatePaseoWorktreeWorkflowDependencies extends CreatePaseoWorktreeInB
|
|||||||
workspace: PersistedWorkspaceRecord;
|
workspace: PersistedWorkspaceRecord;
|
||||||
firstAgentContext: FirstAgentContext;
|
firstAgentContext: FirstAgentContext;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
startWorkspaceSetup?: (workspaceId: string, operation: WorkspaceSetupOperation) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AgentWorktreeSetupContinuationInput {
|
interface AgentWorktreeSetupContinuationInput {
|
||||||
@@ -619,16 +621,26 @@ export async function createPaseoWorktreeWorkflow(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
if (setupContinuation.kind === "workspace") {
|
if (setupContinuation.kind === "workspace") {
|
||||||
void runWorktreeSetupInBackground(dependencies, {
|
const runSetup = (signal: AbortSignal) =>
|
||||||
requestCwd: input.cwd,
|
runWorktreeSetupInBackground(
|
||||||
repoRoot: createdWorktree.repoRoot,
|
dependencies,
|
||||||
workspaceId: workspace.workspaceId,
|
{
|
||||||
worktree: createdWorktree.worktree,
|
requestCwd: input.cwd,
|
||||||
shouldBootstrap: createdWorktree.created,
|
repoRoot: createdWorktree.repoRoot,
|
||||||
slug,
|
workspaceId: workspace.workspaceId,
|
||||||
worktreePath: createdWorktree.worktree.worktreePath,
|
worktree: createdWorktree.worktree,
|
||||||
workspaceCwd: workspace.cwd,
|
shouldBootstrap: createdWorktree.created,
|
||||||
});
|
slug,
|
||||||
|
worktreePath: createdWorktree.worktree.worktreePath,
|
||||||
|
workspaceCwd: workspace.cwd,
|
||||||
|
},
|
||||||
|
signal,
|
||||||
|
);
|
||||||
|
if (dependencies.startWorkspaceSetup) {
|
||||||
|
dependencies.startWorkspaceSetup(workspace.workspaceId, runSetup);
|
||||||
|
} else {
|
||||||
|
void runSetup(new AbortController().signal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@@ -687,6 +699,7 @@ export async function runWorktreeSetupInBackground(
|
|||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
workspaceCwd?: string;
|
workspaceCwd?: string;
|
||||||
},
|
},
|
||||||
|
signal?: AbortSignal,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let worktree: WorktreeConfig = options.worktree;
|
let worktree: WorktreeConfig = options.worktree;
|
||||||
let setupResults: WorktreeSetupCommandResult[] = [];
|
let setupResults: WorktreeSetupCommandResult[] = [];
|
||||||
@@ -746,6 +759,7 @@ export async function runWorktreeSetupInBackground(
|
|||||||
cleanupOnFailure: false,
|
cleanupOnFailure: false,
|
||||||
repoRootPath: options.repoRoot,
|
repoRootPath: options.repoRoot,
|
||||||
runtimeEnv,
|
runtimeEnv,
|
||||||
|
signal,
|
||||||
onEvent: (event) => {
|
onEvent: (event) => {
|
||||||
applyWorktreeSetupProgressEvent(progressAccumulator, event);
|
applyWorktreeSetupProgressEvent(progressAccumulator, event);
|
||||||
emitSetupProgress("running", null);
|
emitSetupProgress("running", null);
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { createExternalProcessEnv } from "../server/paseo-env.js";
|
|||||||
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
|
||||||
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
import { validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
||||||
import { expandTilde, getRealpathAwareRelativePath, isPathInsideRoot } from "./path.js";
|
import { expandTilde, getRealpathAwareRelativePath, isPathInsideRoot } from "./path.js";
|
||||||
|
import { terminateWithTreeKill } from "./tree-kill.js";
|
||||||
|
|
||||||
export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
export { slugify, validateBranchSlug } from "@getpaseo/protocol/branch-slug";
|
||||||
|
|
||||||
@@ -449,6 +450,7 @@ async function execSetupCommandStreamed(options: {
|
|||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
index: number;
|
index: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
signal?: AbortSignal;
|
||||||
onEvent?: (event: WorktreeSetupCommandProgressEvent) => void;
|
onEvent?: (event: WorktreeSetupCommandProgressEvent) => void;
|
||||||
}): Promise<WorktreeSetupCommandResult> {
|
}): Promise<WorktreeSetupCommandResult> {
|
||||||
return new Promise((resolvePromise) => {
|
return new Promise((resolvePromise) => {
|
||||||
@@ -456,6 +458,7 @@ async function execSetupCommandStreamed(options: {
|
|||||||
const stdoutChunks: string[] = [];
|
const stdoutChunks: string[] = [];
|
||||||
const stderrChunks: string[] = [];
|
const stderrChunks: string[] = [];
|
||||||
let settled = false;
|
let settled = false;
|
||||||
|
let termination: Promise<unknown> | null = null;
|
||||||
|
|
||||||
const emitOutput = (stream: "stdout" | "stderr", chunk: string) => {
|
const emitOutput = (stream: "stdout" | "stderr", chunk: string) => {
|
||||||
const text = stripAnsi(chunk);
|
const text = stripAnsi(chunk);
|
||||||
@@ -478,11 +481,13 @@ async function execSetupCommandStreamed(options: {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const finish = (exitCode: number | null) => {
|
const finish = async (exitCode: number | null) => {
|
||||||
if (settled) {
|
if (settled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
settled = true;
|
settled = true;
|
||||||
|
options.signal?.removeEventListener("abort", abort);
|
||||||
|
await termination;
|
||||||
const result: WorktreeSetupCommandResult = {
|
const result: WorktreeSetupCommandResult = {
|
||||||
command: options.command,
|
command: options.command,
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
@@ -521,6 +526,18 @@ async function execSetupCommandStreamed(options: {
|
|||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const abort = () => {
|
||||||
|
termination ??= terminateWithTreeKill(child, {
|
||||||
|
gracefulTimeoutMs: 1000,
|
||||||
|
forceTimeoutMs: 1000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (options.signal?.aborted) {
|
||||||
|
abort();
|
||||||
|
} else {
|
||||||
|
options.signal?.addEventListener("abort", abort, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
child.stdout?.on("data", (chunk: Buffer | string) => {
|
child.stdout?.on("data", (chunk: Buffer | string) => {
|
||||||
emitOutput("stdout", chunk.toString());
|
emitOutput("stdout", chunk.toString());
|
||||||
});
|
});
|
||||||
@@ -531,11 +548,11 @@ async function execSetupCommandStreamed(options: {
|
|||||||
|
|
||||||
child.on("error", (error) => {
|
child.on("error", (error) => {
|
||||||
emitOutput("stderr", error instanceof Error ? error.message : String(error));
|
emitOutput("stderr", error instanceof Error ? error.message : String(error));
|
||||||
finish(null);
|
void finish(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", (code) => {
|
child.on("close", (code) => {
|
||||||
finish(typeof code === "number" ? code : null);
|
void finish(typeof code === "number" ? code : null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -621,6 +638,7 @@ export async function runWorktreeSetupCommands(options: {
|
|||||||
cleanupOnFailure: boolean;
|
cleanupOnFailure: boolean;
|
||||||
repoRootPath?: string;
|
repoRootPath?: string;
|
||||||
runtimeEnv?: WorktreeRuntimeEnv;
|
runtimeEnv?: WorktreeRuntimeEnv;
|
||||||
|
signal?: AbortSignal;
|
||||||
onEvent?: (event: WorktreeSetupCommandProgressEvent) => void;
|
onEvent?: (event: WorktreeSetupCommandProgressEvent) => void;
|
||||||
}): Promise<WorktreeSetupCommandResult[]> {
|
}): Promise<WorktreeSetupCommandResult[]> {
|
||||||
// Read paseo.json from the worktree (it will have the same content as the source repo)
|
// Read paseo.json from the worktree (it will have the same content as the source repo)
|
||||||
@@ -647,6 +665,7 @@ export async function runWorktreeSetupCommands(options: {
|
|||||||
env: setupEnv,
|
env: setupEnv,
|
||||||
index: index + 1,
|
index: index + 1,
|
||||||
total: setupCommands.length,
|
total: setupCommands.length,
|
||||||
|
signal: options.signal,
|
||||||
onEvent: options.onEvent,
|
onEvent: options.onEvent,
|
||||||
})
|
})
|
||||||
: await execSetupCommand(cmd, {
|
: await execSetupCommand(cmd, {
|
||||||
|
|||||||
Reference in New Issue
Block a user