From b561b108a700f07a9a7a4e18358b28a5b6720c07 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Jun 2026 12:20:55 +0800 Subject: [PATCH] refactor(server): extract the workspace-scripts feature into a deep module (#1716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(server): extract the workspace-scripts feature into a deep module The service-proxy-backed "workspace scripts" feature had no home: building a workspace's scripts payload was duplicated between the descriptor builder and buildWorkspaceScriptPayloadSnapshot (the same 9-input buildWorkspaceScriptPayloads assembly), and the "scripts available on this daemon?" guard appeared a third time in the start handler. Move the feature into session/workspace-scripts/workspace-scripts-service.ts behind createWorkspaceScriptsService(deps): { buildSnapshot, emitStatusUpdate, start }. The payload assembly and availability guard now live in one place; the descriptor builder, the status-emission path, and the start RPC all funnel through it. spawnWorkspaceScript is injected as a port so the feature is testable without a real process. session.ts drops ~98 lines; the two methods existing tests reach stay as thin delegators. New zero-mock unit test covers the guard, status emission, and the start branch matrix with injected fakes, the real service proxy + runtime store, and a fake launcher. * refactor(server): address review — durable comment + documented test stand-in - Drop the transient "#1714" PR reference from the buildWorkspaceScriptPayloadSnapshot accessor comment; state the durable reason instead. - Hoist the opaque terminalManager test stand-in to a named const documenting the non-call guarantee, rather than an inline `as unknown as` cast. --- packages/server/src/server/session.ts | 150 ++--------- .../workspace-scripts-service.test.ts | 244 ++++++++++++++++++ .../workspace-scripts-service.ts | 185 +++++++++++++ .../server/src/server/worktree-bootstrap.ts | 2 +- 4 files changed, 456 insertions(+), 125 deletions(-) create mode 100644 packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts create mode 100644 packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 5e1269d68..799377c5d 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -52,14 +52,13 @@ import { respondToAgentPermission } from "./agent/permission-response.js"; import { experimental_createMCPClient } from "ai"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { VoiceCallerContext, VoiceSpeakHandler } from "./voice-types.js"; -import { - buildWorkspaceScriptPayloads, - readPaseoConfigForProjection, -} from "./script-status-projection.js"; -import { deriveProjectSlug } from "./workspace-git-metadata.js"; import type { ScriptHealthState } from "./script-health-monitor.js"; import { spawnWorkspaceScript } from "./worktree-bootstrap.js"; import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js"; +import { + createWorkspaceScriptsService, + type WorkspaceScriptsService, +} from "./session/workspace-scripts/workspace-scripts-service.js"; import type { DaemonConfigStore } from "./daemon-config-store.js"; import { getErrorMessage, getErrorMessageOr } from "@getpaseo/protocol/error-utils"; import { getAgentStatusPriority } from "@getpaseo/protocol/agent-state-bucket"; @@ -589,6 +588,7 @@ export class Session { private readonly agentConfigSession: AgentConfigSession; private readonly projectConfigSession: ProjectConfigSession; private readonly daemonSession: DaemonSession; + private readonly workspaceScripts: WorkspaceScriptsService; private readonly createAgentLifecycleDispatch: CreateAgentLifecycleDispatch; constructor(options: SessionOptions) { @@ -848,6 +848,20 @@ export class Session { this.getDaemonTcpHost = getDaemonTcpHost ?? null; this.serviceProxyPublicBaseUrl = serviceProxyPublicBaseUrl ?? null; this.resolveScriptHealth = resolveScriptHealth ?? null; + this.workspaceScripts = createWorkspaceScriptsService({ + serviceProxy: this.serviceProxy, + scriptRuntimeStore: this.scriptRuntimeStore, + terminalManager: this.terminalManager, + workspaceRegistry: this.workspaceRegistry, + workspaceGitService: this.workspaceGitService, + getDaemonTcpPort: this.getDaemonTcpPort, + getDaemonTcpHost: this.getDaemonTcpHost, + serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl, + resolveScriptHealth: this.resolveScriptHealth, + logger: this.sessionLogger, + emit: (message) => this.emit(message), + spawnWorkspaceScript, + }); this.subscribeToOptionalManagers(); this.workspaceDirectory = new WorkspaceDirectory({ logger: this.sessionLogger, @@ -3688,20 +3702,7 @@ export class Session { statusEnteredAt: null, activityAt: null, diffStat, - scripts: - this.serviceProxy && this.scriptRuntimeStore - ? buildWorkspaceScriptPayloads({ - workspaceId: workspace.workspaceId, - workspaceDirectory: workspace.cwd, - paseoConfig: readPaseoConfigForProjection(workspace.cwd, this.sessionLogger), - serviceProxy: this.serviceProxy, - runtimeStore: this.scriptRuntimeStore, - daemonPort: this.getDaemonTcpPort?.() ?? null, - serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl, - gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspace.cwd), - resolveHealth: this.resolveScriptHealth ?? undefined, - }) - : [], + scripts: this.buildWorkspaceScriptPayloadSnapshot(workspace.workspaceId, workspace.cwd), ...(resolvedProjectRecord ? { project: await this.buildProjectPlacementForWorkspace(workspace, resolvedProjectRecord), @@ -4862,116 +4863,17 @@ export class Session { } } + // Named accessor: the workspace descriptor builder and the git-watch test both read a workspace's + // scripts snapshot through here; the workspace-scripts module owns the payload assembly. private buildWorkspaceScriptPayloadSnapshot( workspaceId: string, workspaceDirectory: string, ): WorkspaceDescriptorPayload["scripts"] { - if (!this.serviceProxy || !this.scriptRuntimeStore) { - return []; - } - return buildWorkspaceScriptPayloads({ - workspaceId, - workspaceDirectory, - paseoConfig: readPaseoConfigForProjection(workspaceDirectory, this.sessionLogger), - serviceProxy: this.serviceProxy, - runtimeStore: this.scriptRuntimeStore, - daemonPort: this.getDaemonTcpPort?.() ?? null, - serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl, - gitMetadata: this.resolveWorkspaceScriptGitMetadata(workspaceDirectory), - resolveHealth: this.resolveScriptHealth ?? undefined, - }); + return this.workspaceScripts.buildSnapshot(workspaceId, workspaceDirectory); } - private resolveWorkspaceScriptGitMetadata( - workspaceDirectory: string, - ): { projectSlug: string; currentBranch: string | null } | undefined { - const snapshot = this.workspaceGitService.peekSnapshot(workspaceDirectory); - if (!snapshot) { - return undefined; - } - return { - projectSlug: deriveProjectSlug( - workspaceDirectory, - snapshot.git.isGit ? snapshot.git.remoteUrl : null, - ), - currentBranch: snapshot.git.currentBranch, - }; - } - - private emitWorkspaceScriptStatusUpdate(workspaceId: string, workspaceDirectory: string): void { - this.emit({ - type: "script_status_update", - payload: { - workspaceId, - scripts: this.buildWorkspaceScriptPayloadSnapshot(workspaceId, workspaceDirectory), - }, - }); - } - - private async handleStartWorkspaceScriptRequest( - request: StartWorkspaceScriptRequest, - ): Promise { - try { - if (!this.terminalManager || !this.serviceProxy || !this.scriptRuntimeStore) { - throw new Error("Workspace scripts are not available on this daemon"); - } - - const workspace = await this.workspaceRegistry.get(request.workspaceId); - if (!workspace) { - throw new Error(`Workspace not found: ${request.workspaceId}`); - } - const gitMetadata = await this.workspaceGitService.getWorkspaceGitMetadata(workspace.cwd); - - const serviceResult = await spawnWorkspaceScript({ - repoRoot: workspace.cwd, - workspaceId: workspace.workspaceId, - projectSlug: gitMetadata.projectSlug, - branchName: gitMetadata.currentBranch, - scriptName: request.scriptName, - daemonPort: this.getDaemonTcpPort?.() ?? null, - daemonListenHost: this.getDaemonTcpHost?.() ?? null, - serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl, - serviceProxy: this.serviceProxy, - runtimeStore: this.scriptRuntimeStore, - terminalManager: this.terminalManager, - logger: this.sessionLogger, - onLifecycleChanged: () => { - this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd); - }, - }); - - this.emitWorkspaceScriptStatusUpdate(workspace.workspaceId, workspace.cwd); - this.emit({ - type: "start_workspace_script_response", - payload: { - requestId: request.requestId, - workspaceId: request.workspaceId, - scriptName: request.scriptName, - terminalId: serviceResult.terminalId, - error: null, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : "Failed to start workspace script"; - this.sessionLogger.error( - { - err: error, - workspaceId: request.workspaceId, - scriptName: request.scriptName, - }, - "Failed to start workspace script", - ); - this.emit({ - type: "start_workspace_script_response", - payload: { - requestId: request.requestId, - workspaceId: request.workspaceId, - scriptName: request.scriptName, - terminalId: null, - error: message, - }, - }); - } + private handleStartWorkspaceScriptRequest(request: StartWorkspaceScriptRequest): Promise { + return this.workspaceScripts.start(request); } // COMPAT(desktopEditorBridge): added in v0.1.88, remove after 2026-12-03 once old clients no longer call daemon editor RPCs. @@ -5047,7 +4949,7 @@ export class Session { getDaemonTcpHost: this.getDaemonTcpHost, serviceProxyPublicBaseUrl: this.serviceProxyPublicBaseUrl, onScriptsChanged: (workspaceId, workspaceDirectory) => { - this.emitWorkspaceScriptStatusUpdate(workspaceId, workspaceDirectory); + this.workspaceScripts.emitStatusUpdate(workspaceId, workspaceDirectory); }, }, input, diff --git a/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts new file mode 100644 index 000000000..fbb0b78c9 --- /dev/null +++ b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.test.ts @@ -0,0 +1,244 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pino } from "pino"; +import { afterEach, describe, expect, test } from "vitest"; +import type { SessionOutboundMessage, StartWorkspaceScriptRequest } from "../../messages.js"; +import { createServiceProxySubsystem, type ServiceProxySubsystem } from "../../service-proxy.js"; +import type { TerminalManager } from "../../../terminal/terminal-manager.js"; +import type { PersistedWorkspaceRecord, WorkspaceRegistry } from "../../workspace-registry.js"; +import type { WorkspaceGitMetadata } from "../../workspace-git-metadata.js"; +import { WorkspaceScriptRuntimeStore } from "../../workspace-script-runtime-store.js"; +import type { + SpawnWorkspaceScriptOptions, + WorktreeScriptResult, +} from "../../worktree-bootstrap.js"; +import { createWorkspaceScriptsService } from "./workspace-scripts-service.js"; + +// The production module reads only WorkspaceGitService.{peekSnapshot,getWorkspaceGitMetadata}, +// WorkspaceRegistry.get, and forwards the launcher + opaque managers to the injected +// spawnWorkspaceScript port. The fakes below implement exactly that slice; the service proxy and +// runtime store are the real in-memory implementations, and spawning is injected so no process runs. + +const logger = pino({ level: "silent" }); + +const gitMetadata: WorkspaceGitMetadata = { + projectKind: "git", + projectDisplayName: "repo", + workspaceDisplayName: "repo", + gitRemote: null, + isWorktree: false, + projectSlug: "paseo", + repoRoot: "/tmp/repo", + currentBranch: "feature/scripts", + remoteUrl: null, +}; + +function fakeWorkspaceRegistry( + record: PersistedWorkspaceRecord | null, +): Pick { + return { + async get() { + return record; + }, + }; +} + +function fakeGitService(metadata: WorkspaceGitMetadata = gitMetadata) { + return { + peekSnapshot() { + return null; + }, + async getWorkspaceGitMetadata() { + return metadata; + }, + }; +} + +// The service only truthiness-checks terminalManager in its availability guard and then forwards it +// opaquely to the injected spawnWorkspaceScript fake, which ignores it — an empty stand-in is enough. +const availableTerminalManager = {} as unknown as TerminalManager; + +interface BuildOptions { + serviceProxy?: ServiceProxySubsystem | null; + scriptRuntimeStore?: WorkspaceScriptRuntimeStore | null; + terminalManager?: TerminalManager | null; + workspace?: PersistedWorkspaceRecord | null; + spawnThrows?: string; +} + +function buildService(options: BuildOptions = {}) { + const emitted: SessionOutboundMessage[] = []; + const spawnCalls: SpawnWorkspaceScriptOptions[] = []; + const workspace = + options.workspace === undefined + ? ({ workspaceId: "ws-1", cwd: "/tmp/repo" } as PersistedWorkspaceRecord) + : options.workspace; + + const service = createWorkspaceScriptsService({ + serviceProxy: + options.serviceProxy === undefined + ? createServiceProxySubsystem({ logger }) + : options.serviceProxy, + scriptRuntimeStore: + options.scriptRuntimeStore === undefined + ? new WorkspaceScriptRuntimeStore() + : options.scriptRuntimeStore, + terminalManager: + options.terminalManager === undefined ? availableTerminalManager : options.terminalManager, + workspaceRegistry: fakeWorkspaceRegistry(workspace), + workspaceGitService: fakeGitService(), + getDaemonTcpPort: () => 6767, + getDaemonTcpHost: () => "127.0.0.1", + serviceProxyPublicBaseUrl: null, + resolveScriptHealth: null, + logger, + emit: (message) => emitted.push(message), + async spawnWorkspaceScript(spawnOptions): Promise { + spawnCalls.push(spawnOptions); + if (options.spawnThrows) { + throw new Error(options.spawnThrows); + } + spawnOptions.onLifecycleChanged?.(); + return { + scriptName: spawnOptions.scriptName, + hostname: null, + port: null, + terminalId: "terminal-1", + }; + }, + }); + + return { service, emitted, spawnCalls }; +} + +const request: StartWorkspaceScriptRequest = { + type: "start_workspace_script_request", + workspaceId: "ws-1", + scriptName: "app", + requestId: "req-1", +}; + +const tempDirs: string[] = []; +afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) { + rmSync(dir, { recursive: true, force: true }); + } + } +}); + +describe("buildSnapshot", () => { + test("returns no scripts when the service proxy is unavailable", () => { + const { service } = buildService({ serviceProxy: null }); + expect(service.buildSnapshot("ws-1", "/tmp/repo")).toEqual([]); + }); + + test("returns no scripts when the runtime store is unavailable", () => { + const { service } = buildService({ scriptRuntimeStore: null }); + expect(service.buildSnapshot("ws-1", "/tmp/repo")).toEqual([]); + }); + + test("returns no scripts for a workspace without a paseo.json", () => { + const dir = mkdtempSync(join(tmpdir(), "workspace-scripts-")); + tempDirs.push(dir); + const { service } = buildService(); + expect(service.buildSnapshot("ws-1", dir)).toEqual([]); + }); +}); + +describe("emitStatusUpdate", () => { + test("emits one script_status_update carrying the snapshot", () => { + const { service, emitted } = buildService(); + service.emitStatusUpdate("ws-1", "/tmp/repo"); + expect(emitted).toEqual([ + { type: "script_status_update", payload: { workspaceId: "ws-1", scripts: [] } }, + ]); + }); +}); + +describe("start", () => { + test("reports an error when workspace scripts are unavailable", async () => { + const { service, emitted, spawnCalls } = buildService({ terminalManager: null }); + await service.start(request); + expect(spawnCalls).toEqual([]); + expect(emitted).toEqual([ + { + type: "start_workspace_script_response", + payload: { + requestId: "req-1", + workspaceId: "ws-1", + scriptName: "app", + terminalId: null, + error: "Workspace scripts are not available on this daemon", + }, + }, + ]); + }); + + test("reports an error when the workspace is not found", async () => { + const { service, emitted, spawnCalls } = buildService({ workspace: null }); + await service.start(request); + expect(spawnCalls).toEqual([]); + expect(emitted).toEqual([ + { + type: "start_workspace_script_response", + payload: { + requestId: "req-1", + workspaceId: "ws-1", + scriptName: "app", + terminalId: null, + error: "Workspace not found: ws-1", + }, + }, + ]); + }); + + test("spawns the script with resolved git metadata and reports success", async () => { + const { service, emitted, spawnCalls } = buildService(); + await service.start(request); + + expect(spawnCalls).toHaveLength(1); + expect(spawnCalls[0]).toMatchObject({ + repoRoot: "/tmp/repo", + workspaceId: "ws-1", + projectSlug: "paseo", + branchName: "feature/scripts", + scriptName: "app", + daemonPort: 6767, + daemonListenHost: "127.0.0.1", + }); + expect(emitted).toContainEqual({ + type: "script_status_update", + payload: { workspaceId: "ws-1", scripts: [] }, + }); + expect(emitted).toContainEqual({ + type: "start_workspace_script_response", + payload: { + requestId: "req-1", + workspaceId: "ws-1", + scriptName: "app", + terminalId: "terminal-1", + error: null, + }, + }); + }); + + test("reports the launcher error when spawning fails", async () => { + const { service, emitted } = buildService({ spawnThrows: "boom" }); + await service.start(request); + expect(emitted).toEqual([ + { + type: "start_workspace_script_response", + payload: { + requestId: "req-1", + workspaceId: "ws-1", + scriptName: "app", + terminalId: null, + error: "boom", + }, + }, + ]); + }); +}); diff --git a/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts new file mode 100644 index 000000000..044b23c5d --- /dev/null +++ b/packages/server/src/server/session/workspace-scripts/workspace-scripts-service.ts @@ -0,0 +1,185 @@ +import type pino from "pino"; +import type { + SessionOutboundMessage, + StartWorkspaceScriptRequest, + WorkspaceDescriptorPayload, +} from "../../messages.js"; +import type { TerminalManager } from "../../../terminal/terminal-manager.js"; +import type { ServiceProxySubsystem } from "../../service-proxy.js"; +import type { WorkspaceScriptRuntimeStore } from "../../workspace-script-runtime-store.js"; +import type { ScriptHealthState } from "../../script-health-monitor.js"; +import type { WorkspaceGitService } from "../../workspace-git-service.js"; +import type { WorkspaceRegistry } from "../../workspace-registry.js"; +import type { + SpawnWorkspaceScriptOptions, + WorktreeScriptResult, +} from "../../worktree-bootstrap.js"; +import { + buildWorkspaceScriptPayloads, + readPaseoConfigForProjection, +} from "../../script-status-projection.js"; +import { deriveProjectSlug } from "../../workspace-git-metadata.js"; + +type WorkspaceScriptsPayload = WorkspaceDescriptorPayload["scripts"]; + +interface WorkspaceScriptGitMetadata { + projectSlug: string; + currentBranch: string | null; +} + +/** + * The service-proxy-backed scripts a workspace exposes: build the scripts payload + * snapshot, emit a script_status_update to clients, and start a script. + * + * The workspace descriptor builder, the script-status emission path, and the + * start-script RPC all funnel through one assembly of buildWorkspaceScriptPayloads' + * inputs and one "scripts available on this daemon?" guard, instead of duplicating + * that assembly and guard across the session. + */ +export interface WorkspaceScriptsService { + buildSnapshot(workspaceId: string, workspaceDirectory: string): WorkspaceScriptsPayload; + emitStatusUpdate(workspaceId: string, workspaceDirectory: string): void; + start(request: StartWorkspaceScriptRequest): Promise; +} + +type WorkspaceScriptsGitSource = Pick< + WorkspaceGitService, + "peekSnapshot" | "getWorkspaceGitMetadata" +>; + +export function createWorkspaceScriptsService(deps: { + serviceProxy: ServiceProxySubsystem | null; + scriptRuntimeStore: WorkspaceScriptRuntimeStore | null; + terminalManager: TerminalManager | null; + workspaceRegistry: Pick; + workspaceGitService: WorkspaceScriptsGitSource; + getDaemonTcpPort: (() => number | null) | null; + getDaemonTcpHost: (() => string | null) | null; + serviceProxyPublicBaseUrl: string | null; + resolveScriptHealth: ((hostname: string) => ScriptHealthState | null) | null; + logger: pino.Logger; + emit: (message: SessionOutboundMessage) => void; + spawnWorkspaceScript: (options: SpawnWorkspaceScriptOptions) => Promise; +}): WorkspaceScriptsService { + const { + serviceProxy, + scriptRuntimeStore, + terminalManager, + workspaceRegistry, + workspaceGitService, + getDaemonTcpPort, + getDaemonTcpHost, + serviceProxyPublicBaseUrl, + resolveScriptHealth, + logger, + emit, + spawnWorkspaceScript, + } = deps; + + function resolveGitMetadata(workspaceDirectory: string): WorkspaceScriptGitMetadata | undefined { + const snapshot = workspaceGitService.peekSnapshot(workspaceDirectory); + if (!snapshot) { + return undefined; + } + return { + projectSlug: deriveProjectSlug( + workspaceDirectory, + snapshot.git.isGit ? snapshot.git.remoteUrl : null, + ), + currentBranch: snapshot.git.currentBranch, + }; + } + + function buildSnapshot(workspaceId: string, workspaceDirectory: string): WorkspaceScriptsPayload { + if (!serviceProxy || !scriptRuntimeStore) { + return []; + } + return buildWorkspaceScriptPayloads({ + workspaceId, + workspaceDirectory, + paseoConfig: readPaseoConfigForProjection(workspaceDirectory, logger), + serviceProxy, + runtimeStore: scriptRuntimeStore, + daemonPort: getDaemonTcpPort?.() ?? null, + serviceProxyPublicBaseUrl, + gitMetadata: resolveGitMetadata(workspaceDirectory), + resolveHealth: resolveScriptHealth ?? undefined, + }); + } + + function emitStatusUpdate(workspaceId: string, workspaceDirectory: string): void { + emit({ + type: "script_status_update", + payload: { + workspaceId, + scripts: buildSnapshot(workspaceId, workspaceDirectory), + }, + }); + } + + async function start(request: StartWorkspaceScriptRequest): Promise { + try { + if (!terminalManager || !serviceProxy || !scriptRuntimeStore) { + throw new Error("Workspace scripts are not available on this daemon"); + } + + const workspace = await workspaceRegistry.get(request.workspaceId); + if (!workspace) { + throw new Error(`Workspace not found: ${request.workspaceId}`); + } + const gitMetadata = await workspaceGitService.getWorkspaceGitMetadata(workspace.cwd); + + const serviceResult = await spawnWorkspaceScript({ + repoRoot: workspace.cwd, + workspaceId: workspace.workspaceId, + projectSlug: gitMetadata.projectSlug, + branchName: gitMetadata.currentBranch, + scriptName: request.scriptName, + daemonPort: getDaemonTcpPort?.() ?? null, + daemonListenHost: getDaemonTcpHost?.() ?? null, + serviceProxyPublicBaseUrl, + serviceProxy, + runtimeStore: scriptRuntimeStore, + terminalManager, + logger, + onLifecycleChanged: () => { + emitStatusUpdate(workspace.workspaceId, workspace.cwd); + }, + }); + + emitStatusUpdate(workspace.workspaceId, workspace.cwd); + emit({ + type: "start_workspace_script_response", + payload: { + requestId: request.requestId, + workspaceId: request.workspaceId, + scriptName: request.scriptName, + terminalId: serviceResult.terminalId, + error: null, + }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to start workspace script"; + logger.error( + { + err: error, + workspaceId: request.workspaceId, + scriptName: request.scriptName, + }, + "Failed to start workspace script", + ); + emit({ + type: "start_workspace_script_response", + payload: { + requestId: request.requestId, + workspaceId: request.workspaceId, + scriptName: request.scriptName, + terminalId: null, + error: message, + }, + }); + } + } + + return { buildSnapshot, emitStatusUpdate, start }; +} diff --git a/packages/server/src/server/worktree-bootstrap.ts b/packages/server/src/server/worktree-bootstrap.ts index 0aad34c2b..8cb880675 100644 --- a/packages/server/src/server/worktree-bootstrap.ts +++ b/packages/server/src/server/worktree-bootstrap.ts @@ -694,7 +694,7 @@ export interface WorktreeScriptResult { terminalId: string; } -interface SpawnWorkspaceScriptOptions { +export interface SpawnWorkspaceScriptOptions { repoRoot: string; workspaceId: string; projectSlug: string;