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: 1 addition & 4 deletions .superset/setup.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
{
"copy": [
"**/.env*"
],
"commands": [
"bun i"
"./superset-setup.sh"
]
}
15 changes: 15 additions & 0 deletions apps/desktop/src/lib/trpc/routers/terminal/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const createTerminalRouter = () => {
cols: z.number().optional(),
rows: z.number().optional(),
cwd: z.string().optional(),
initialCommands: z.array(z.string()).optional(),
}),
)
.mutation(async ({ input }) => {
Expand All @@ -41,6 +42,7 @@ export const createTerminalRouter = () => {
cols,
rows,
cwd: cwdOverride,
initialCommands,
} = input;

// Get workspace to determine cwd and workspace name
Expand All @@ -50,16 +52,29 @@ export const createTerminalRouter = () => {
cwdOverride ||
(workspace ? getWorktreePath(workspace.worktreeId) : undefined);

// Get project to get root path for setup scripts
const project = workspace
? db.data.projects.find((p) => p.id === workspace.projectId)
: undefined;
const rootPath = project?.mainRepoPath;

const result = await terminalManager.createOrAttach({
tabId,
workspaceId,
tabTitle,
workspaceName,
rootPath,
cwd,
cols,
rows,
});

// Run initial commands on new terminals
if (result.isNew && initialCommands && initialCommands.length > 0) {
const commandString = `${initialCommands.join(" && ")}\n`;
terminalManager.write({ tabId, data: commandString });
}

return {
tabId,
isNew: result.isNew,
Expand Down
74 changes: 3 additions & 71 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { copySetupFiles, loadSetupConfig } from "./setup";
import { loadSetupConfig } from "./setup";

const TEST_DIR = join(__dirname, ".test-tmp");
const MAIN_REPO = join(TEST_DIR, "main-repo");
const WORKTREE = join(TEST_DIR, "worktree");

describe("loadSetupConfig", () => {
beforeEach(() => {
Expand All @@ -27,7 +26,6 @@ describe("loadSetupConfig", () => {

test("loads valid setup config", () => {
const setupConfig = {
copy: ["*.env", "package.json"],
commands: ["npm install", "npm run build"],
};

Expand All @@ -47,79 +45,13 @@ describe("loadSetupConfig", () => {
expect(config).toBeNull();
});

test("validates copy field must be an array", () => {
test("validates commands field must be an array", () => {
writeFileSync(
join(MAIN_REPO, ".superset", "setup.json"),
JSON.stringify({ copy: "not-an-array" }),
JSON.stringify({ commands: "not-an-array" }),
);

const config = loadSetupConfig(MAIN_REPO);
expect(config).toBeNull();
});
});

describe("copySetupFiles", () => {
beforeEach(() => {
// Create test directories
mkdirSync(MAIN_REPO, { recursive: true });
mkdirSync(WORKTREE, { recursive: true });
});

afterEach(() => {
// Clean up
if (existsSync(TEST_DIR)) {
rmSync(TEST_DIR, { recursive: true, force: true });
}
});

test("returns empty result for empty patterns", async () => {
const result = await copySetupFiles(MAIN_REPO, WORKTREE, []);
expect(result.copied).toEqual([]);
expect(result.errors).toEqual([]);
});

test("copies matching files", async () => {
// Create test files
writeFileSync(join(MAIN_REPO, "test.txt"), "test content");
writeFileSync(join(MAIN_REPO, "README.md"), "readme");

const result = await copySetupFiles(MAIN_REPO, WORKTREE, ["*.txt"]);

expect(result.copied).toContain("test.txt");
expect(result.errors).toEqual([]);
expect(existsSync(join(WORKTREE, "test.txt"))).toBe(true);
});

test("creates nested directories", async () => {
// Create nested file
mkdirSync(join(MAIN_REPO, "src"), { recursive: true });
writeFileSync(join(MAIN_REPO, "src", "index.ts"), "export {}");

const result = await copySetupFiles(MAIN_REPO, WORKTREE, ["src/**/*.ts"]);

expect(result.copied).toContain("src/index.ts");
expect(existsSync(join(WORKTREE, "src", "index.ts"))).toBe(true);
});

test("reports errors for files that don't match", async () => {
const result = await copySetupFiles(MAIN_REPO, WORKTREE, [
"nonexistent.txt",
]);

expect(result.copied).toEqual([]);
expect(result.errors.length).toBeGreaterThan(0);
});

test("copies multiple files matching glob pattern", async () => {
writeFileSync(join(MAIN_REPO, "file1.txt"), "content1");
writeFileSync(join(MAIN_REPO, "file2.txt"), "content2");
writeFileSync(join(MAIN_REPO, "file.md"), "markdown");

const result = await copySetupFiles(MAIN_REPO, WORKTREE, ["*.txt"]);

expect(result.copied).toContain("file1.txt");
expect(result.copied).toContain("file2.txt");
expect(result.copied).not.toContain("file.md");
expect(result.errors).toEqual([]);
});
});
55 changes: 1 addition & 54 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { existsSync, readFileSync } from "node:fs";
import { copyFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import fg from "fast-glob";
import { join } from "node:path";
import type { SetupConfig } from "shared/types";

export function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
Expand All @@ -15,10 +13,6 @@ export function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
const content = readFileSync(configPath, "utf-8");
const parsed = JSON.parse(content) as SetupConfig;

if (parsed.copy && !Array.isArray(parsed.copy)) {
throw new Error("'copy' field must be an array of strings");
}

if (parsed.commands && !Array.isArray(parsed.commands)) {
throw new Error("'commands' field must be an array of strings");
}
Expand All @@ -31,50 +25,3 @@ export function loadSetupConfig(mainRepoPath: string): SetupConfig | null {
return null;
}
}

export async function copySetupFiles(
mainRepoPath: string,
worktreePath: string,
patterns: string[],
): Promise<{ copied: string[]; errors: string[] }> {
const copied: string[] = [];
const errors: string[] = [];

for (const pattern of patterns) {
try {
const matches = await fg(pattern, {
cwd: mainRepoPath,
dot: true,
followSymbolicLinks: false,
onlyFiles: true,
ignore: [".superset/**"],
});

if (matches.length === 0) {
errors.push(`No files matched pattern: ${pattern}`);
continue;
}

for (const relativePath of matches) {
const sourcePath = join(mainRepoPath, relativePath);
const destinationPath = join(worktreePath, relativePath);

try {
await mkdir(dirname(destinationPath), { recursive: true });
await copyFile(sourcePath, destinationPath);
copied.push(relativePath);
} catch (copyError) {
errors.push(
`Failed to copy ${relativePath}: ${copyError instanceof Error ? copyError.message : String(copyError)}`,
);
}
}
} catch (globError) {
errors.push(
`Failed to process pattern '${pattern}': ${globError instanceof Error ? globError.message : String(globError)}`,
);
}
}

return { copied, errors };
}
27 changes: 2 additions & 25 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
removeWorktree,
worktreeExists,
} from "./utils/git";
import { copySetupFiles, loadSetupConfig } from "./utils/setup";
import { loadSetupConfig } from "./utils/setup";
import { getWorktreePath } from "./utils/worktree";

export const createWorkspacesRouter = () => {
Expand Down Expand Up @@ -92,33 +92,10 @@ export const createWorkspacesRouter = () => {

// Load setup configuration
const setupConfig = loadSetupConfig(project.mainRepoPath);
let setupCopyResults: { copied: string[]; errors: string[] } | null =
null;

// Copy setup files if config exists and has copy patterns
if (setupConfig?.copy && setupConfig.copy.length > 0) {
try {
setupCopyResults = await copySetupFiles(
project.mainRepoPath,
worktreePath,
setupConfig.copy,
);
} catch (error) {
console.error("Failed to copy setup files:", error);
// Non-fatal: return error info but continue
setupCopyResults = {
copied: [],
errors: [
`Setup file copy failed: ${error instanceof Error ? error.message : String(error)}`,
],
};
}
}

return {
workspace,
setupConfig: setupConfig?.commands || null,
setupCopyResults,
initialCommands: setupConfig?.commands || null,
worktreePath,
};
}),
Expand Down
15 changes: 13 additions & 2 deletions apps/desktop/src/main/lib/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class TerminalManager extends EventEmitter {
workspaceId: string;
tabTitle: string;
workspaceName: string;
rootPath?: string;
cwd?: string;
cols?: number;
rows?: number;
Expand All @@ -53,8 +54,16 @@ export class TerminalManager extends EventEmitter {
scrollback: string;
wasRecovered: boolean;
}> {
const { tabId, workspaceId, tabTitle, workspaceName, cwd, cols, rows } =
params;
const {
tabId,
workspaceId,
tabTitle,
workspaceName,
rootPath,
cwd,
cols,
rows,
} = params;

const existing = this.sessions.get(tabId);
if (existing?.isAlive) {
Expand Down Expand Up @@ -85,6 +94,8 @@ export class TerminalManager extends EventEmitter {
SUPERSET_TAB_TITLE: tabTitle,
SUPERSET_WORKSPACE_NAME: workspaceName,
SUPERSET_WORKSPACE_ID: workspaceId,
SUPERSET_WORKSPACE_PATH: workingDir,
SUPERSET_ROOT_PATH: rootPath || "",
SUPERSET_PORT: String(PORTS.NOTIFICATIONS),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,35 @@ import { useTabsStore } from "renderer/stores/tabs/store";
/**
* Mutation hook for creating a new workspace
* Automatically invalidates all workspace queries on success
* Creates a setup tab if setup commands are present
* Creates a terminal tab with setup commands if present
*/
export function useCreateWorkspace(
options?: Parameters<typeof trpc.workspaces.create.useMutation>[0],
) {
const utils = trpc.useUtils();
const addSetupTab = useTabsStore((state) => state.addSetupTab);
const addTab = useTabsStore((state) => state.addTab);
const createOrAttach = trpc.terminal.createOrAttach.useMutation();

return trpc.workspaces.create.useMutation({
...options,
onSuccess: async (data, ...rest) => {
// Auto-invalidate all workspace queries
await utils.workspaces.invalidate();

// Create setup tab if setup commands are present and is an array
if (Array.isArray(data.setupConfig) && data.setupConfig.length > 0) {
addSetupTab(
data.workspace.id,
data.setupConfig,
data.worktreePath,
data.setupCopyResults ?? undefined,
);
// Create terminal tab with setup commands if present
if (
Array.isArray(data.initialCommands) &&
data.initialCommands.length > 0
) {
const tabId = addTab(data.workspace.id);
// Pre-create terminal session with initial commands
// Terminal component will attach to this session when it mounts
createOrAttach.mutate({
tabId,
workspaceId: data.workspace.id,
tabTitle: "Terminal",
initialCommands: data.initialCommands,
});
}

// Call user's onSuccess if provided
Expand Down

This file was deleted.

Loading