diff --git a/.superset/setup.json b/.superset/setup.json index 936f2ddf3b6..5d1709aa006 100644 --- a/.superset/setup.json +++ b/.superset/setup.json @@ -1,5 +1,4 @@ { - "commands": [ - "./superset-setup.sh" - ] + "setup": ["./superset-setup.sh"], + "teardown": ["./superset-teardown.sh"] } \ No newline at end of file diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts index 9e572242d61..b3c63955d72 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts @@ -26,7 +26,7 @@ describe("loadSetupConfig", () => { test("loads valid setup config", () => { const setupConfig = { - commands: ["npm install", "npm run build"], + setup: ["npm install", "npm run build"], }; writeFileSync( @@ -45,10 +45,10 @@ describe("loadSetupConfig", () => { expect(config).toBeNull(); }); - test("validates commands field must be an array", () => { + test("validates setup field must be an array", () => { writeFileSync( join(MAIN_REPO, ".superset", "setup.json"), - JSON.stringify({ commands: "not-an-array" }), + JSON.stringify({ setup: "not-an-array" }), ); const config = loadSetupConfig(MAIN_REPO); diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts index ebbcac90aec..d2cbfe6a8d5 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts @@ -13,8 +13,8 @@ export function loadSetupConfig(mainRepoPath: string): SetupConfig | null { const content = readFileSync(configPath, "utf-8"); const parsed = JSON.parse(content) as SetupConfig; - if (parsed.commands && !Array.isArray(parsed.commands)) { - throw new Error("'commands' field must be an array of strings"); + if (parsed.setup && !Array.isArray(parsed.setup)) { + throw new Error("'setup' field must be an array of strings"); } return parsed; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts new file mode 100644 index 00000000000..26bef181460 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts @@ -0,0 +1,74 @@ +import { execSync } from "node:child_process"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import type { SetupConfig } from "shared/types"; + +const TEARDOWN_TIMEOUT_MS = 60_000; // 60 seconds + +export interface TeardownResult { + success: boolean; + error?: string; +} + +function loadSetupConfig(mainRepoPath: string): SetupConfig | null { + const configPath = join(mainRepoPath, ".superset", "setup.json"); + + if (!existsSync(configPath)) { + return null; + } + + try { + const content = readFileSync(configPath, "utf-8"); + const parsed = JSON.parse(content) as SetupConfig; + + if (parsed.teardown && !Array.isArray(parsed.teardown)) { + throw new Error("'teardown' field must be an array of strings"); + } + + return parsed; + } catch (error) { + console.error( + `Failed to read setup config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`, + ); + return null; + } +} + +export function runTeardown( + mainRepoPath: string, + worktreePath: string, + workspaceName: string, +): TeardownResult { + const config = loadSetupConfig(mainRepoPath); + + if (!config?.teardown || config.teardown.length === 0) { + return { success: true }; + } + + const command = config.teardown.join(" && "); + + try { + execSync(command, { + cwd: worktreePath, + timeout: TEARDOWN_TIMEOUT_MS, + env: { + ...process.env, + SUPERSET_WORKSPACE_NAME: workspaceName, + SUPERSET_ROOT_PATH: mainRepoPath, + }, + stdio: "pipe", + }); + + return { success: true }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error( + `Teardown failed for workspace ${workspaceName}:`, + errorMessage, + ); + return { + success: false, + error: errorMessage, + }; + } +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index c3a4bed44a7..6d8346ed9b9 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -12,6 +12,7 @@ import { worktreeExists, } from "./utils/git"; import { loadSetupConfig } from "./utils/setup"; +import { runTeardown } from "./utils/teardown"; import { getWorktreePath } from "./utils/worktree"; export const createWorkspacesRouter = () => { @@ -95,7 +96,7 @@ export const createWorkspacesRouter = () => { return { workspace, - initialCommands: setupConfig?.commands || null, + initialCommands: setupConfig?.setup || null, worktreePath, }; }), @@ -295,13 +296,27 @@ export const createWorkspacesRouter = () => { (p) => p.id === workspace.projectId, ); + let teardownError: string | undefined; + if (worktree && project) { - try { - const exists = await worktreeExists( + // Run teardown scripts before removing worktree + const exists = await worktreeExists( + project.mainRepoPath, + worktree.path, + ); + + if (exists) { + const teardownResult = runTeardown( project.mainRepoPath, worktree.path, + workspace.name, ); + if (!teardownResult.success) { + teardownError = teardownResult.error; + } + } + try { if (exists) { await removeWorktree(project.mainRepoPath, worktree.path); } else { @@ -350,7 +365,7 @@ export const createWorkspacesRouter = () => { } }); - return { success: true }; + return { success: true, teardownError }; }), setActive: publicProcedure diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/DeleteWorkspaceDialog.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/DeleteWorkspaceDialog.tsx index bbe640cebb0..d34991ab5a0 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/DeleteWorkspaceDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceTabs/DeleteWorkspaceDialog.tsx @@ -8,6 +8,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@superset/ui/alert-dialog"; +import { toast } from "@superset/ui/sonner"; import { useState } from "react"; import { trpc } from "renderer/lib/trpc"; import { useDeleteWorkspace } from "renderer/react-query/workspaces"; @@ -37,7 +38,12 @@ export function DeleteWorkspaceDialog({ const handleDelete = async () => { setIsDeleting(true); try { - await deleteWorkspace.mutateAsync({ id: workspaceId }); + const result = await deleteWorkspace.mutateAsync({ id: workspaceId }); + if (result.teardownError) { + toast.warning("Workspace deleted with teardown warning", { + description: result.teardownError, + }); + } onOpenChange(false); } catch (error) { console.error("Failed to delete workspace:", error); diff --git a/apps/desktop/src/shared/types.ts b/apps/desktop/src/shared/types.ts index efd54242694..ebac5467e81 100644 --- a/apps/desktop/src/shared/types.ts +++ b/apps/desktop/src/shared/types.ts @@ -128,7 +128,8 @@ export interface UpdateWorkspaceInput { // Setup script configuration export interface SetupConfig { - commands?: string[]; // Shell commands to run in worktree directory + setup?: string[]; // Shell commands to run when workspace is created + teardown?: string[]; // Shell commands to run when workspace is deleted } // Port detection types diff --git a/superset-setup.sh b/superset-setup.sh index 889f7a37aa4..5110432e533 100755 --- a/superset-setup.sh +++ b/superset-setup.sh @@ -24,6 +24,9 @@ success "Dependencies installed" if [ -n "$SUPERSET_ROOT_PATH" ] && [ -f "$SUPERSET_ROOT_PATH/.envrc" ]; then echo "๐Ÿ”ง Linking .envrc..." ln -sf "$SUPERSET_ROOT_PATH/.envrc" .envrc + if command -v direnv &> /dev/null; then + direnv allow + fi success "direnv configured" fi diff --git a/superset-teardown.sh b/superset-teardown.sh new file mode 100755 index 00000000000..063fd74aab6 --- /dev/null +++ b/superset-teardown.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +error() { echo -e "${RED}โœ—${NC} $1"; exit 1; } +success() { echo -e "${GREEN}โœ“${NC} $1"; } + +echo "๐Ÿงน Tearing down Superset workspace..." + +# Check dependencies +command -v neonctl &> /dev/null || error "Neon CLI not installed. Run: npm install -g neonctl" + +# Delete Neon branch for this workspace +WORKSPACE_NAME="${SUPERSET_WORKSPACE_NAME:-$(basename "$PWD")}" + +echo "๐Ÿ—„๏ธ Deleting Neon branch: $WORKSPACE_NAME" +if neonctl branches delete "$WORKSPACE_NAME" --project-id tiny-cherry-82420694 --force 2>/dev/null; then + success "Neon branch deleted: $WORKSPACE_NAME" +else + echo "โš ๏ธ Neon branch '$WORKSPACE_NAME' not found or already deleted" +fi + +echo "โœจ Teardown complete!"