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..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();
@@ -22,11 +23,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 +41,13 @@ 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 +151,11 @@ ipcMainService.register(
mockWindow as unknown as BrowserWindow
);
+// Add custom endpoint for launch project (only for server mode)
+httpIpcMain.handle("server:getLaunchProject", () => {
+ return Promise.resolve(launchProjectPath);
+});
+
// Serve static files from dist directory (built renderer)
app.use(express.static(path.join(__dirname, ".")));
@@ -247,6 +265,85 @@ 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 {
+ 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 validation = await validateProjectPath(projectPath);
+ if (!validation.valid) {
+ 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) {
+ 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 (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}`);
+ 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) {
+ 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");
+ }
+ } catch (error) {
+ console.error(`Error initializing project:`, error);
+ }
+}
+
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}`);
+ void 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)