Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .superset/setup.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"commands": [
"./superset-setup.sh"
]
"setup": ["./superset-setup.sh"],
"teardown": ["./superset-teardown.sh"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
74 changes: 74 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/teardown.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
23 changes: 19 additions & 4 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -95,7 +96,7 @@ export const createWorkspacesRouter = () => {

return {
workspace,
initialCommands: setupConfig?.commands || null,
initialCommands: setupConfig?.setup || null,
worktreePath,
};
}),
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -350,7 +365,7 @@ export const createWorkspacesRouter = () => {
}
});

return { success: true };
return { success: true, teardownError };
}),

setActive: publicProcedure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions superset-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions superset-teardown.sh
Original file line number Diff line number Diff line change
@@ -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!"