diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts index 100c7028f48..83bfab7102a 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts @@ -1,7 +1,10 @@ import { existsSync, realpathSync } from "node:fs"; import { resolve } from "node:path"; import type { SelectWorktree } from "@superset/local-db"; +import { worktrees } from "@superset/local-db"; +import { eq } from "drizzle-orm"; import { track } from "main/lib/analytics"; +import { localDb } from "main/lib/local-db"; import { workspaceInitManager } from "main/lib/workspace-init-manager"; import { getWorkspaceRuntimeRegistry } from "main/lib/workspace-runtime"; import { z } from "zod"; @@ -267,43 +270,51 @@ export const createDeleteProcedures = () => { await workspaceInitManager.acquireProjectLock(project.id); try { - // Only delete from disk if this worktree was created by Superset - // External worktrees should only have their DB records removed - if (worktree.createdBySuperset) { - // Safety: Double-check it's not actually external (catches race conditions) - const externalWorktrees = await listExternalWorktrees( - project.mainRepoPath, - ); - const worktreePathNorm = normalizePath(worktree.path); - const isActuallyExternal = externalWorktrees.some( - (wt) => normalizePath(wt.path) === worktreePathNorm, - ); + // Safety: Check if this worktree is tracked in our database + // Prevents deletion of worktrees that were never properly imported + // Get all git worktrees + const allGitWorktrees = await listExternalWorktrees( + project.mainRepoPath, + ); - if (isActuallyExternal) { - console.warn( - `[workspace/delete] Worktree at ${worktree.path} marked as created by Superset but found in external list - preserving as safety measure`, - ); - track("worktree_delete_safety_trigger", { - workspace_id: input.id, - worktree_id: worktree.id, - worktree_path: worktree.path, - reason: "external_detection_mismatch", - }); - } else { - // Confirmed safe to delete - const removeResult = await removeWorktreeFromDisk({ - mainRepoPath: project.mainRepoPath, - worktreePath: worktree.path, - }); - if (!removeResult.success) { - clearWorkspaceDeletingStatus(input.id); - return removeResult; - } - } - } else { - console.log( - `[workspace/delete] Skipping disk deletion for external worktree at ${worktree.path}`, + // Get all tracked worktree paths from database for this project + const trackedWorktrees = localDb + .select({ path: worktrees.path }) + .from(worktrees) + .where(eq(worktrees.projectId, project.id)) + .all(); + const trackedPaths = new Set( + trackedWorktrees.map((wt) => normalizePath(wt.path)), + ); + + // Check if this worktree exists in git but is NOT tracked in our DB + const worktreePathNorm = normalizePath(worktree.path); + const existsInGit = allGitWorktrees.some( + (wt) => normalizePath(wt.path) === worktreePathNorm, + ); + const isActuallyExternal = + existsInGit && !trackedPaths.has(worktreePathNorm); + + if (isActuallyExternal) { + console.warn( + `[workspace/delete] Worktree at ${worktree.path} exists in git but not tracked in database - preserving as safety measure`, ); + track("worktree_delete_safety_trigger", { + workspace_id: input.id, + worktree_id: worktree.id, + worktree_path: worktree.path, + reason: "untracked_worktree_detected", + }); + } else { + // Safe to delete - worktree is tracked in our database + const removeResult = await removeWorktreeFromDisk({ + mainRepoPath: project.mainRepoPath, + worktreePath: worktree.path, + }); + if (!removeResult.success) { + clearWorkspaceDeletingStatus(input.id); + return removeResult; + } } } finally { workspaceInitManager.releaseProjectLock(project.id); @@ -486,68 +497,78 @@ export const createDeleteProcedures = () => { worktree.path, ); - // Only delete from disk if this worktree was created by Superset - if (worktree.createdBySuperset) { - // Safety: Double-check it's not actually external (catches race conditions) - const externalWorktrees = await listExternalWorktrees( - project.mainRepoPath, - ); - const isActuallyExternal = externalWorktrees.some( - (wt) => wt.path === worktree.path, - ); + // Safety: Check if this worktree is tracked in our database + // Prevents deletion of worktrees that were never properly imported + // Get all git worktrees + const allGitWorktrees = await listExternalWorktrees( + project.mainRepoPath, + ); - if (isActuallyExternal) { - console.warn( - `[worktree/delete] Worktree at ${worktree.path} marked as created by Superset but found in external list - preserving as safety measure`, - ); - track("worktree_delete_safety_trigger", { - worktree_id: input.worktreeId, - worktree_path: worktree.path, - reason: "external_detection_mismatch", + // Get all tracked worktree paths from database for this project + const trackedWorktrees = localDb + .select({ path: worktrees.path }) + .from(worktrees) + .where(eq(worktrees.projectId, project.id)) + .all(); + const trackedPaths = new Set( + trackedWorktrees.map((wt) => normalizePath(wt.path)), + ); + + // Check if this worktree exists in git but is NOT tracked in our DB + const worktreePathNorm = normalizePath(worktree.path); + const existsInGit = allGitWorktrees.some( + (wt) => normalizePath(wt.path) === worktreePathNorm, + ); + const isActuallyExternal = + existsInGit && !trackedPaths.has(worktreePathNorm); + + if (isActuallyExternal) { + console.warn( + `[worktree/delete] Worktree at ${worktree.path} exists in git but not tracked in database - preserving as safety measure`, + ); + track("worktree_delete_safety_trigger", { + worktree_id: input.worktreeId, + worktree_path: worktree.path, + reason: "untracked_worktree_detected", + }); + } else { + // Safe to delete - worktree is tracked in our database + if (exists) { + const teardownResult = await runTeardown({ + mainRepoPath: project.mainRepoPath, + worktreePath: worktree.path, + workspaceName: worktree.branch, + projectId: project.id, }); - } else { - // Confirmed safe to delete - if (exists) { - const teardownResult = await runTeardown({ - mainRepoPath: project.mainRepoPath, - worktreePath: worktree.path, - workspaceName: worktree.branch, - projectId: project.id, - }); - if (!teardownResult.success) { - if (input.force) { - console.warn( - `[worktree/delete] Teardown failed but force=true, continuing deletion:`, - teardownResult.error, - ); - } else { - return { - success: false, - error: `Teardown failed: ${teardownResult.error}`, - output: teardownResult.output, - }; - } + if (!teardownResult.success) { + if (input.force) { + console.warn( + `[worktree/delete] Teardown failed but force=true, continuing deletion:`, + teardownResult.error, + ); + } else { + return { + success: false, + error: `Teardown failed: ${teardownResult.error}`, + output: teardownResult.output, + }; } } + } - if (exists) { - const removeResult = await removeWorktreeFromDisk({ - mainRepoPath: project.mainRepoPath, - worktreePath: worktree.path, - }); - if (!removeResult.success) { - return removeResult; - } - } else { - console.warn( - `Worktree ${worktree.path} not found in git, skipping removal`, - ); + if (exists) { + const removeResult = await removeWorktreeFromDisk({ + mainRepoPath: project.mainRepoPath, + worktreePath: worktree.path, + }); + if (!removeResult.success) { + return removeResult; } + } else { + console.warn( + `Worktree ${worktree.path} not found in git, skipping removal`, + ); } - } else { - console.log( - `[worktree/delete] Skipping disk deletion for external worktree at ${worktree.path}`, - ); } } finally { workspaceInitManager.releaseProjectLock(project.id); diff --git a/bun.lock b/bun.lock index 5a792dc7516..6e11476d3c4 100644 --- a/bun.lock +++ b/bun.lock @@ -110,7 +110,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "1.4.0", + "version": "1.4.3", "dependencies": { "@ai-sdk/anthropic": "^3.0.43", "@ai-sdk/openai": "3.0.36", @@ -779,10 +779,6 @@ "react": "19.2.0", }, }, - "packages/scripts": { - "name": "@superset/scripts", - "version": "0.1.0", - }, "packages/shared": { "name": "@superset/shared", "version": "0.1.0", @@ -2342,8 +2338,6 @@ "@superset/pane-layout": ["@superset/pane-layout@workspace:packages/pane-layout"], - "@superset/scripts": ["@superset/scripts@workspace:packages/scripts"], - "@superset/shared": ["@superset/shared@workspace:packages/shared"], "@superset/trpc": ["@superset/trpc@workspace:packages/trpc"],