Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .superset/setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"**/.env*"
],
"commands": [
"bun i"
"echo 'Hello world'"
]
Comment on lines 5 to 7
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Revert placeholder command — dependency installation is missing.

Line 6 replaces the meaningful "bun i" (dependency installation) command with "echo 'Hello world'" (a trivial placeholder). This breaks the workspace setup flow and prevents dependencies from being installed in new workspaces.

Revert to the original install command:

 	"commands": [
-		"echo 'Hello world'"
+		"bun i"
 	]

If this change was intentional and the setup flow no longer requires dependency installation, please clarify the updated setup strategy and ensure all downstream setup utilities (file copying, terminal execution) account for this change.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"commands": [
"bun i"
"echo 'Hello world'"
]
"commands": [
"bun i"
]
🤖 Prompt for AI Agents
In .superset/setup.json around lines 5 to 7, the commands array was changed to a
placeholder ("echo 'Hello world'") which removes the dependency installation
step; restore the original dependency install command (e.g., "bun i") in place
of the echo so the workspace setup installs required packages, or if the change
was intentional, update the setup.json to document the new flow and adjust any
downstream setup utilities to handle the absence of an install step.

}
2 changes: 1 addition & 1 deletion apps/desktop/src/lib/trpc/routers/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { observable } from "@trpc/server/observable";
import {
notificationsEmitter,
type AgentCompleteEvent,
notificationsEmitter,
} from "main/lib/notifications/server";
import { publicProcedure, router } from "..";

Expand Down
125 changes: 125 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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";

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

describe("loadSetupConfig", () => {
beforeEach(() => {
// Create test directories
mkdirSync(join(MAIN_REPO, ".superset"), { recursive: true });
});

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

test("returns null when setup.json does not exist", () => {
const config = loadSetupConfig(MAIN_REPO);
expect(config).toBeNull();
});

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

writeFileSync(
join(MAIN_REPO, ".superset", "setup.json"),
JSON.stringify(setupConfig),
);

const config = loadSetupConfig(MAIN_REPO);
expect(config).toEqual(setupConfig);
});

test("returns null for invalid JSON", () => {
writeFileSync(join(MAIN_REPO, ".superset", "setup.json"), "{ invalid json");

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

test("validates copy field must be an array", () => {
writeFileSync(
join(MAIN_REPO, ".superset", "setup.json"),
JSON.stringify({ copy: "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([]);
});
});
80 changes: 80 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 type { SetupConfig } from "shared/types";

export 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.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");
}

return parsed;
} catch (error) {
console.error(
`Failed to read setup config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
);
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 };
}
35 changes: 33 additions & 2 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
generateBranchName,
removeWorktree,
} from "./utils/git";
import { copySetupFiles, loadSetupConfig } from "./utils/setup";

export const createWorkspacesRouter = () => {
return router({
Expand Down Expand Up @@ -80,7 +81,37 @@ export const createWorkspacesRouter = () => {
}
});

return workspace;
// 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,
worktreePath,
};
}),

get: publicProcedure
Expand Down Expand Up @@ -142,7 +173,7 @@ export const createWorkspacesRouter = () => {

for (const workspace of workspaces) {
if (groupsMap.has(workspace.projectId)) {
groupsMap.get(workspace.projectId)!.workspaces.push(workspace);
groupsMap.get(workspace.projectId)?.workspaces.push(workspace);
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/lib/agent-setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { execSync } from "node:child_process";
import { NOTIFICATIONS_PORT } from "shared/constants";

const SUPERSET_DIR = path.join(os.homedir(), ".superset");
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/main/lib/terminal-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { promises as fs } from "node:fs";
import { join } from "node:path";
import {
HistoryReader,
HistoryWriter,
getHistoryDir,
getHistoryFilePath,
getMetadataPath,
HistoryReader,
HistoryWriter,
type SessionMetadata,
} from "./terminal-history";

Expand Down Expand Up @@ -379,7 +379,7 @@ describe("Terminal history integration", () => {
await fs.stat(historyDir);
throw new Error("Directory should not exist");
} catch (error) {
// @ts-ignore
// @ts-expect-error
expect(error.code).toBe("ENOENT");
}
});
Expand Down
14 changes: 14 additions & 0 deletions apps/desktop/src/main/lib/terminal-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ describe("TerminalManager", () => {
const result = await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
cwd: "/test/path",
cols: 80,
rows: 24,
Expand All @@ -102,6 +104,8 @@ describe("TerminalManager", () => {
await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
cwd: "/test/path",
});

Expand All @@ -111,6 +115,8 @@ describe("TerminalManager", () => {
const result = await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
});

expect(result.isNew).toBe(false);
Expand All @@ -124,13 +130,17 @@ describe("TerminalManager", () => {
await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
cols: 80,
rows: 24,
});

await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
cols: 100,
rows: 30,
});
Expand All @@ -144,6 +154,8 @@ describe("TerminalManager", () => {
await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
});

manager.write({
Expand All @@ -169,6 +181,8 @@ describe("TerminalManager", () => {
await manager.createOrAttach({
tabId: "tab-1",
workspaceId: "workspace-1",
tabTitle: "Test Tab",
workspaceName: "Test Workspace",
});

manager.resize({
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/main/lib/terminal-manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { EventEmitter } from "node:events";
import os from "node:os";
import * as pty from "node-pty";
import { getSupersetPath } from "./agent-setup";
import { NOTIFICATIONS_PORT } from "shared/constants";
import { getSupersetPath } from "./agent-setup";
import { HistoryReader, HistoryWriter } from "./terminal-history";

interface TerminalSession {
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/main/windows/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { createIPCHandler } from "trpc-electron/main";
import { productName } from "~/package.json";
import { createApplicationMenu } from "../lib/menu";
import {
type AgentCompleteEvent,
NOTIFICATIONS_PORT,
notificationsApp,
notificationsEmitter,
NOTIFICATIONS_PORT,
type AgentCompleteEvent,
} from "../lib/notifications/server";

export async function MainWindow() {
Expand Down
Loading
Loading