* Delete workspace: removes SSH files AND deletes Coder workspace (if Mux-managed). * * IMPORTANT: Only delete the Coder workspace once we're confident mux will commit * the deletion. In the non-force path, WorkspaceService.remove() aborts and keeps * workspace metadata when runtime.dele
(
projectPath: string,
workspaceName: string,
force: boolean,
abortSignal?: AbortSignal,
trusted?: boolean
)
| 577 | * workspace metadata when runtime.deleteWorkspace() fails. |
| 578 | */ |
| 579 | override async deleteWorkspace( |
| 580 | projectPath: string, |
| 581 | workspaceName: string, |
| 582 | force: boolean, |
| 583 | abortSignal?: AbortSignal, |
| 584 | trusted?: boolean |
| 585 | ): Promise<{ success: true; deletedPath: string } | { success: false; error: string }> { |
| 586 | // Deleting a Coder workspace is dangerous; CoderService refuses to delete workspaces |
| 587 | // without the mux- prefix to avoid accidentally deleting user-owned Coder workspaces. |
| 588 | |
| 589 | // If this workspace is an existing Coder workspace that mux didn't create, just do SSH cleanup. |
| 590 | if (this.coderConfig.existingWorkspace) { |
| 591 | return super.deleteWorkspace(projectPath, workspaceName, force, abortSignal, trusted); |
| 592 | } |
| 593 | |
| 594 | const coderWorkspaceName = this.coderConfig.workspaceName; |
| 595 | |
| 596 | if (!coderWorkspaceName) { |
| 597 | log.warn("Coder workspace name not set, falling back to SSH-only deletion"); |
| 598 | return super.deleteWorkspace(projectPath, workspaceName, force, abortSignal, trusted); |
| 599 | } |
| 600 | |
| 601 | // For force deletes ("cancel creation"), skip SSH cleanup and focus on deleting the |
| 602 | // underlying Coder workspace. During provisioning, the SSH host may not be reachable yet. |
| 603 | if (force) { |
| 604 | const deleteResult = await this.coderService.deleteWorkspaceEventually(coderWorkspaceName, { |
| 605 | timeoutMs: 60_000, |
| 606 | signal: abortSignal, |
| 607 | // Avoid races where coder create finishes server-side after we abort the local CLI. |
| 608 | waitForExistence: true, |
| 609 | // If the workspace never appears on the server within 10s, assume it was never created |
| 610 | // and return early instead of waiting the full 60s timeout. |
| 611 | waitForExistenceTimeoutMs: 10_000, |
| 612 | }); |
| 613 | |
| 614 | if (!deleteResult.success) { |
| 615 | return { success: false, error: `Failed to delete Coder workspace: ${deleteResult.error}` }; |
| 616 | } |
| 617 | |
| 618 | return { success: true, deletedPath: this.getWorkspacePath(projectPath, workspaceName) }; |
| 619 | } |
| 620 | |
| 621 | // Check if Coder workspace still exists before attempting SSH operations. |
| 622 | // If it's already gone, skip SSH cleanup (would hang trying to connect to non-existent host). |
| 623 | const statusResult = await this.coderService.getWorkspaceStatus(coderWorkspaceName, { |
| 624 | signal: abortSignal, |
| 625 | }); |
| 626 | if (statusResult.kind === "not_found") { |
| 627 | log.debug("Coder workspace already deleted, skipping SSH cleanup", { coderWorkspaceName }); |
| 628 | return { success: true, deletedPath: this.getWorkspacePath(projectPath, workspaceName) }; |
| 629 | } |
| 630 | if (statusResult.kind === "error") { |
| 631 | // API errors (auth, network): fall through to SSH cleanup, let it fail naturally |
| 632 | log.warn("Could not check Coder workspace status, proceeding with SSH cleanup", { |
| 633 | coderWorkspaceName, |
| 634 | error: statusResult.error, |
| 635 | }); |
| 636 | } |
nothing calls this directly
no test coverage detected