From 73f70f3aa97219b6eba789c184d3379967ba0a59 Mon Sep 17 00:00:00 2001 From: Coder AI <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:25:40 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=A4=96=20feat:=20add=20--add-project?= =?UTF-8?q?=20flag=20to=20cmux=20server=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add idempotent --add-project flag that: - Checks if project exists at specified path - Creates project if it doesn't exist - Sets launch project hint for frontend - Frontend auto-selects first workspace in project Backend changes: - Added --add-project CLI flag to main-server.ts - Added launchProjectPath tracking variable - Added getHandler() method to HttpIpcMainAdapter - Implemented initializeProject() to check/create project - Added server:getLaunchProject IPC endpoint Frontend changes: - Added server.getLaunchProject() to browser API - Added server property to IPCApi type - Added useEffect in AppLoader to check launch project - Auto-navigates to first workspace if available _Generated with `cmux`_ --- src/browser/api.ts | 3 ++ src/components/AppLoader.tsx | 45 +++++++++++++++++++++ src/main-server.ts | 77 +++++++++++++++++++++++++++++++++++- src/types/ipc.ts | 3 ++ 4 files changed, 127 insertions(+), 1 deletion(-) diff --git a/src/browser/api.ts b/src/browser/api.ts index 9f1cc2c8a..d3460b3ca 100644 --- a/src/browser/api.ts +++ b/src/browser/api.ts @@ -260,6 +260,9 @@ const webApi: IPCApi = { return wsManager.on(IPC_CHANNELS.UPDATE_STATUS, callback as (data: unknown) => void); }, }, + server: { + getLaunchProject: () => invokeIPC("server:getLaunchProject"), + }, }; if (typeof window.api === "undefined") { diff --git a/src/components/AppLoader.tsx b/src/components/AppLoader.tsx index 739eee2c7..c6b4ed5ed 100644 --- a/src/components/AppLoader.tsx +++ b/src/components/AppLoader.tsx @@ -96,6 +96,51 @@ export function AppLoader() { setSelectedWorkspace, ]); + // Check for launch project from server (for --add-project flag) + // This only applies in server mode + useEffect(() => { + // Wait until stores are synced and hash restoration is complete + if (!storesSynced || !hasRestoredFromHash) return; + + // Skip if we already have a selected workspace (from localStorage or URL hash) + if (selectedWorkspace) return; + + // Only check once + const checkLaunchProject = async () => { + // Only available in server mode + if (!window.api.server?.getLaunchProject) return; + + const launchProjectPath = await window.api.server.getLaunchProject(); + if (!launchProjectPath) return; + + // Find first workspace in this project + const projectWorkspaces = Array.from(workspaceManagement.workspaceMetadata.values()).filter( + (meta) => meta.projectPath === launchProjectPath + ); + + if (projectWorkspaces.length > 0) { + // Select the first workspace in the project + const metadata = projectWorkspaces[0]; + setSelectedWorkspace({ + workspaceId: metadata.id, + projectPath: metadata.projectPath, + projectName: metadata.projectName, + namedWorkspacePath: metadata.namedWorkspacePath, + }); + } + // If no workspaces exist yet, just leave the project in the sidebar + // The user will need to create a workspace + }; + + void checkLaunchProject(); + }, [ + storesSynced, + hasRestoredFromHash, + selectedWorkspace, + workspaceManagement.workspaceMetadata, + setSelectedWorkspace, + ]); + // Show loading screen until stores are synced if (workspaceManagement.loading || !storesSynced) { return ; diff --git a/src/main-server.ts b/src/main-server.ts index 3be56ecef..bdd5ce6a2 100644 --- a/src/main-server.ts +++ b/src/main-server.ts @@ -22,11 +22,16 @@ program .description("HTTP/WebSocket server for cmux - allows accessing cmux backend from mobile devices") .option("-h, --host ", "bind to specific host", "localhost") .option("-p, --port ", "bind to specific port", "3000") + .option("--add-project ", "add and open project at the specified path (idempotent)") .parse(process.argv); const options = program.opts(); const HOST = options.host as string; const PORT = parseInt(options.port as string, 10); +const ADD_PROJECT_PATH = options.addProject as string | undefined; + +// Track the launch project path for initial navigation +let launchProjectPath: string | null = null; // Mock Electron's ipcMain for HTTP class HttpIpcMainAdapter { @@ -35,6 +40,11 @@ class HttpIpcMainAdapter { constructor(private readonly app: express.Application) {} + // Public method to get a handler (for internal use) + getHandler(channel: string): ((event: unknown, ...args: unknown[]) => Promise) | undefined { + return this.handlers.get(channel); + } + handle(channel: string, handler: (event: unknown, ...args: unknown[]) => Promise): void { this.handlers.set(channel, handler); @@ -138,6 +148,11 @@ ipcMainService.register( mockWindow as unknown as BrowserWindow ); +// Add custom endpoint for launch project (only for server mode) +httpIpcMain.handle("server:getLaunchProject", async () => { + return launchProjectPath; +}); + // Serve static files from dist directory (built renderer) app.use(express.static(path.join(__dirname, "."))); @@ -247,6 +262,66 @@ wss.on("connection", (ws) => { }); }); -server.listen(PORT, HOST, () => { +/** + * Initialize a project from the --add-project flag + * This checks if a project exists at the given path, creates it if not, and opens it + */ +async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAdapter): Promise { + try { + // First, check if project already exists by listing all projects + const handler = ipcAdapter.getHandler(IPC_CHANNELS.PROJECT_LIST); + if (!handler) { + console.error("PROJECT_LIST handler not found"); + return; + } + + const projectsList = await handler(null); + if (!Array.isArray(projectsList)) { + console.error("Unexpected PROJECT_LIST response format"); + return; + } + + // Check if the project already exists + const existingProject = projectsList.find(([path]: [string, unknown]) => path === projectPath); + + if (existingProject) { + console.log(`Project already exists at: ${projectPath}`); + launchProjectPath = projectPath; + return; + } + + // Project doesn't exist, create it + console.log(`Creating new project at: ${projectPath}`); + const createHandler = ipcAdapter.getHandler(IPC_CHANNELS.PROJECT_CREATE); + if (!createHandler) { + console.error("PROJECT_CREATE handler not found"); + return; + } + + const createResult = await createHandler(null, projectPath); + + // Check if creation was successful using the Result type + if (createResult && typeof createResult === "object" && "success" in createResult) { + if (createResult.success) { + console.log(`Successfully created project at: ${projectPath}`); + launchProjectPath = projectPath; + } else if ("error" in createResult) { + console.error(`Failed to create project: ${(createResult as { error: unknown }).error}`); + } + } else { + console.error("Unexpected PROJECT_CREATE response format"); + } + } catch (error) { + console.error(`Error initializing project:`, error); + } +} + +server.listen(PORT, HOST, async () => { console.log(`Server is running on http://${HOST}:${PORT}`); + + // Handle --add-project flag if present + if (ADD_PROJECT_PATH) { + console.log(`Initializing project at: ${ADD_PROJECT_PATH}`); + await initializeProject(ADD_PROJECT_PATH, httpIpcMain); + } }); diff --git a/src/types/ipc.ts b/src/types/ipc.ts index 4a0a46c77..81b10627f 100644 --- a/src/types/ipc.ts +++ b/src/types/ipc.ts @@ -299,6 +299,9 @@ export interface IPCApi { install(): void; onStatus(callback: (status: UpdateStatus) => void): () => void; }; + server?: { + getLaunchProject(): Promise; + }; } // Update status type (matches updater service) From 46151f3b40becce53c629f9ca03a4bddd724f828 Mon Sep 17 00:00:00 2001 From: Coder AI <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:42:26 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20trim=20trailing=20sla?= =?UTF-8?q?shes=20from=20--add-project=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reassign projectPath directly instead of creating unnecessary variable. _Generated with `cmux`_ --- src/main-server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main-server.ts b/src/main-server.ts index bdd5ce6a2..c292d3057 100644 --- a/src/main-server.ts +++ b/src/main-server.ts @@ -268,6 +268,9 @@ wss.on("connection", (ws) => { */ async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAdapter): Promise { try { + // Trim trailing slashes to ensure proper project name extraction + projectPath = projectPath.replace(/\/+$/, ""); + // First, check if project already exists by listing all projects const handler = ipcAdapter.getHandler(IPC_CHANNELS.PROJECT_LIST); if (!handler) { From 9572de30fa0acb54130b47d877e702f29a844be1 Mon Sep 17 00:00:00 2001 From: Coder AI <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:18:04 +0000 Subject: [PATCH 3/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20normalize=20--add-pro?= =?UTF-8?q?ject=20path=20for=20proper=20matching?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validate and normalize path (expand tilde, make absolute) before comparing with existing projects and storing in launchProjectPath. This ensures ~/foo matches /home/user/foo in config and frontend can match normalized paths in workspace metadata. _Generated with `cmux`_ --- src/main-server.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main-server.ts b/src/main-server.ts index c292d3057..c8e3b4ac2 100644 --- a/src/main-server.ts +++ b/src/main-server.ts @@ -271,6 +271,15 @@ async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAda // Trim trailing slashes to ensure proper project name extraction projectPath = projectPath.replace(/\/+$/, ""); + // Normalize path (expand tilde, make absolute) to match how PROJECT_CREATE normalizes paths + const { validateProjectPath } = await import("./utils/pathUtils"); + const validation = await validateProjectPath(projectPath); + if (!validation.valid) { + console.error(`Invalid project path: ${validation.error}`); + return; + } + projectPath = validation.expandedPath!; + // First, check if project already exists by listing all projects const handler = ipcAdapter.getHandler(IPC_CHANNELS.PROJECT_LIST); if (!handler) { From d492fd5ff6baab8e544a6533ff705778cc413031 Mon Sep 17 00:00:00 2001 From: Coder AI <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:23:56 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20fix:=20address=20ESLint=20er?= =?UTF-8?q?rors=20in=20main-server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move validateProjectPath to static import - Remove unnecessary async from getLaunchProject handler - Handle error template literal types properly - Add proper type casting for projectsList - Use void for fire-and-forget initializeProject call _Generated with `cmux`_ --- src/main-server.ts | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/main-server.ts b/src/main-server.ts index c8e3b4ac2..01b54e18f 100644 --- a/src/main-server.ts +++ b/src/main-server.ts @@ -13,6 +13,7 @@ import * as path from "path"; import type { RawData } from "ws"; import { WebSocket, WebSocketServer } from "ws"; import { Command } from "commander"; +import { validateProjectPath } from "./utils/pathUtils"; // Parse command line arguments const program = new Command(); @@ -41,7 +42,9 @@ class HttpIpcMainAdapter { constructor(private readonly app: express.Application) {} // Public method to get a handler (for internal use) - getHandler(channel: string): ((event: unknown, ...args: unknown[]) => Promise) | undefined { + getHandler( + channel: string + ): ((event: unknown, ...args: unknown[]) => Promise) | undefined { return this.handlers.get(channel); } @@ -149,8 +152,8 @@ ipcMainService.register( ); // Add custom endpoint for launch project (only for server mode) -httpIpcMain.handle("server:getLaunchProject", async () => { - return launchProjectPath; +httpIpcMain.handle("server:getLaunchProject", () => { + return Promise.resolve(launchProjectPath); }); // Serve static files from dist directory (built renderer) @@ -266,20 +269,23 @@ wss.on("connection", (ws) => { * Initialize a project from the --add-project flag * This checks if a project exists at the given path, creates it if not, and opens it */ -async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAdapter): Promise { +async function initializeProject( + projectPath: string, + ipcAdapter: HttpIpcMainAdapter +): Promise { try { // Trim trailing slashes to ensure proper project name extraction projectPath = projectPath.replace(/\/+$/, ""); - + // Normalize path (expand tilde, make absolute) to match how PROJECT_CREATE normalizes paths - const { validateProjectPath } = await import("./utils/pathUtils"); const validation = await validateProjectPath(projectPath); if (!validation.valid) { - console.error(`Invalid project path: ${validation.error}`); + const errorMsg = validation.error ?? "Unknown validation error"; + console.error(`Invalid project path: ${errorMsg}`); return; } projectPath = validation.expandedPath!; - + // First, check if project already exists by listing all projects const handler = ipcAdapter.getHandler(IPC_CHANNELS.PROJECT_LIST); if (!handler) { @@ -293,8 +299,10 @@ async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAda return; } - // Check if the project already exists - const existingProject = projectsList.find(([path]: [string, unknown]) => path === projectPath); + // Check if the project already exists (projectsList is Array<[string, ProjectConfig]>) + const existingProject = (projectsList as Array<[string, unknown]>).find( + ([path]) => path === projectPath + ); if (existingProject) { console.log(`Project already exists at: ${projectPath}`); @@ -311,14 +319,16 @@ async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAda } const createResult = await createHandler(null, projectPath); - + // Check if creation was successful using the Result type if (createResult && typeof createResult === "object" && "success" in createResult) { if (createResult.success) { console.log(`Successfully created project at: ${projectPath}`); launchProjectPath = projectPath; } else if ("error" in createResult) { - console.error(`Failed to create project: ${(createResult as { error: unknown }).error}`); + const err = createResult as { error: unknown }; + const errorMsg = err.error instanceof Error ? err.error.message : String(err.error); + console.error(`Failed to create project: ${errorMsg}`); } } else { console.error("Unexpected PROJECT_CREATE response format"); @@ -328,12 +338,12 @@ async function initializeProject(projectPath: string, ipcAdapter: HttpIpcMainAda } } -server.listen(PORT, HOST, async () => { +server.listen(PORT, HOST, () => { console.log(`Server is running on http://${HOST}:${PORT}`); // Handle --add-project flag if present if (ADD_PROJECT_PATH) { console.log(`Initializing project at: ${ADD_PROJECT_PATH}`); - await initializeProject(ADD_PROJECT_PATH, httpIpcMain); + void initializeProject(ADD_PROJECT_PATH, httpIpcMain); } });