diff --git a/apps/server/.env.example b/apps/server/.env.example index a844ae33..9fbb4cbd 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -22,11 +22,6 @@ AUTOMAKER_API_KEY= # Example: ALLOWED_ROOT_DIRECTORY=/projects ALLOWED_ROOT_DIRECTORY= -# (Legacy) Restrict file operations to these directories (comma-separated) -# DEPRECATED: Use ALLOWED_ROOT_DIRECTORY instead for simpler configuration -# This is kept for backward compatibility -# ALLOWED_PROJECT_DIRS=/home/user/projects,/var/www - # CORS origin - which domains can access the API # Use "*" for development, set specific origin for production CORS_ORIGIN=* diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index 8c019a2f..8bfa8980 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -26,6 +26,14 @@ RUN npm run build --workspace=apps/server # Production stage FROM node:20-alpine +# Install git, curl, and GitHub CLI +RUN apk add --no-cache git curl && \ + GH_VERSION=$(curl -s https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d '"' -f 4 | sed 's/v//') && \ + curl -L "https://github.com/cli/cli/releases/download/v${GH_VERSION}/gh_${GH_VERSION}_linux_amd64.tar.gz" -o gh.tar.gz && \ + tar -xzf gh.tar.gz && \ + mv gh_*_linux_amd64/bin/gh /usr/local/bin/gh && \ + rm -rf gh.tar.gz gh_*_linux_amd64 + WORKDIR /app # Create non-root user diff --git a/apps/server/src/lib/security.ts b/apps/server/src/lib/security.ts index fc7e0077..58dbf628 100644 --- a/apps/server/src/lib/security.ts +++ b/apps/server/src/lib/security.ts @@ -23,21 +23,27 @@ let allowedRootDirectory: string | null = null; // Data directory - always allowed for settings/credentials let dataDirectory: string | null = null; -// Allowed project directories - kept for backward compatibility and API compatibility +// Allowed paths set - stores ALLOWED_ROOT_DIRECTORY and DATA_DIR const allowedPaths = new Set(); /** * Initialize security settings from environment variables * - ALLOWED_ROOT_DIRECTORY: main security boundary * - DATA_DIR: appData exception, always allowed - * - ALLOWED_PROJECT_DIRS: legacy variable, stored for compatibility */ export function initAllowedPaths(): void { - // Load ALLOWED_ROOT_DIRECTORY (new single variable) + // Load ALLOWED_ROOT_DIRECTORY const rootDir = process.env.ALLOWED_ROOT_DIRECTORY; if (rootDir) { allowedRootDirectory = path.resolve(rootDir); allowedPaths.add(allowedRootDirectory); + console.log( + `[Security] ✓ ALLOWED_ROOT_DIRECTORY configured: ${allowedRootDirectory}` + ); + } else { + console.log( + "[Security] ⚠️ ALLOWED_ROOT_DIRECTORY not set - allowing access to all paths" + ); } // Load DATA_DIR (appData exception - always allowed) @@ -45,17 +51,7 @@ export function initAllowedPaths(): void { if (dataDir) { dataDirectory = path.resolve(dataDir); allowedPaths.add(dataDirectory); - } - - // Load legacy ALLOWED_PROJECT_DIRS for backward compatibility during transition - const dirs = process.env.ALLOWED_PROJECT_DIRS; - if (dirs) { - for (const dir of dirs.split(",")) { - const trimmed = dir.trim(); - if (trimmed) { - allowedPaths.add(path.resolve(trimmed)); - } - } + console.log(`[Security] ✓ DATA_DIR configured: ${dataDirectory}`); } } @@ -68,19 +64,13 @@ export function addAllowedPath(filePath: string): void { } /** - * Check if a path is allowed based on ALLOWED_ROOT_DIRECTORY and legacy ALLOWED_PROJECT_DIRS + * Check if a path is allowed based on ALLOWED_ROOT_DIRECTORY * Returns true if: * - Path is within ALLOWED_ROOT_DIRECTORY, OR - * - Path is within any legacy allowed path (ALLOWED_PROJECT_DIRS), OR * - Path is within DATA_DIR (appData exception), OR * - No restrictions are configured (backward compatibility) */ export function isPathAllowed(filePath: string): boolean { - // If no restrictions are configured, allow all paths (backward compatibility) - if (!allowedRootDirectory && allowedPaths.size === 0) { - return true; - } - const resolvedPath = path.resolve(filePath); // Always allow appData directory (settings, credentials) @@ -88,19 +78,21 @@ export function isPathAllowed(filePath: string): boolean { return true; } - // Allow if within ALLOWED_ROOT_DIRECTORY - if (allowedRootDirectory && isPathWithinDirectory(resolvedPath, allowedRootDirectory)) { + // If no ALLOWED_ROOT_DIRECTORY restriction is configured, allow all paths + // Note: DATA_DIR is checked above as an exception, but doesn't restrict other paths + if (!allowedRootDirectory) { return true; } - // Check legacy allowed paths (ALLOWED_PROJECT_DIRS) - for (const allowedPath of allowedPaths) { - if (isPathWithinDirectory(resolvedPath, allowedPath)) { - return true; - } + // Allow if within ALLOWED_ROOT_DIRECTORY + if ( + allowedRootDirectory && + isPathWithinDirectory(resolvedPath, allowedRootDirectory) + ) { + return true; } - // If any restrictions are configured but path doesn't match, deny + // If restrictions are configured but path doesn't match, deny return false; } @@ -132,10 +124,7 @@ export function isPathWithinDirectory( // If relative path starts with "..", it's outside the directory // If relative path is absolute, it's outside the directory // If relative path is empty or ".", it's the directory itself - return ( - !relativePath.startsWith("..") && - !path.isAbsolute(relativePath) - ); + return !relativePath.startsWith("..") && !path.isAbsolute(relativePath); } /** diff --git a/apps/server/src/routes/workspace/routes/config.ts b/apps/server/src/routes/workspace/routes/config.ts index 04b8b9a9..46a0ac67 100644 --- a/apps/server/src/routes/workspace/routes/config.ts +++ b/apps/server/src/routes/workspace/routes/config.ts @@ -5,18 +5,25 @@ import type { Request, Response } from "express"; import fs from "fs/promises"; import path from "path"; -import { addAllowedPath, getAllowedRootDirectory } from "../../../lib/security.js"; +import { + addAllowedPath, + getAllowedRootDirectory, + getDataDirectory, +} from "../../../lib/security.js"; import { getErrorMessage, logError } from "../common.js"; export function createConfigHandler() { return async (_req: Request, res: Response): Promise => { try { const allowedRootDirectory = getAllowedRootDirectory(); + const dataDirectory = getDataDirectory(); if (!allowedRootDirectory) { + // When ALLOWED_ROOT_DIRECTORY is not set, return DATA_DIR as default directory res.json({ success: true, configured: false, + defaultDir: dataDirectory || null, }); return; } @@ -41,6 +48,7 @@ export function createConfigHandler() { success: true, configured: true, workspaceDir: resolvedWorkspaceDir, + defaultDir: resolvedWorkspaceDir, }); } catch { res.json({ diff --git a/apps/server/tests/setup.ts b/apps/server/tests/setup.ts index 3ac88134..2b00c614 100644 --- a/apps/server/tests/setup.ts +++ b/apps/server/tests/setup.ts @@ -8,7 +8,6 @@ import { vi, beforeEach } from "vitest"; // Set test environment variables process.env.NODE_ENV = "test"; process.env.DATA_DIR = "/tmp/test-data"; -process.env.ALLOWED_PROJECT_DIRS = "/tmp/test-projects"; // Reset all mocks before each test beforeEach(() => { diff --git a/apps/server/tests/unit/lib/security.test.ts b/apps/server/tests/unit/lib/security.test.ts index 7f0f718f..2928fefd 100644 --- a/apps/server/tests/unit/lib/security.test.ts +++ b/apps/server/tests/unit/lib/security.test.ts @@ -11,81 +11,49 @@ describe("security.ts", () => { }); describe("initAllowedPaths", () => { - it("should parse comma-separated directories from environment", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/path1,/path2,/path3"; + it("should load ALLOWED_ROOT_DIRECTORY if set", async () => { + process.env.ALLOWED_ROOT_DIRECTORY = "/projects"; process.env.DATA_DIR = ""; - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, getAllowedPaths, getAllowedRootDirectory } = + await import("@/lib/security.js"); initAllowedPaths(); const allowed = getAllowedPaths(); - expect(allowed).toContain(path.resolve("/path1")); - expect(allowed).toContain(path.resolve("/path2")); - expect(allowed).toContain(path.resolve("/path3")); - }); - - it("should trim whitespace from paths", async () => { - process.env.ALLOWED_PROJECT_DIRS = " /path1 , /path2 , /path3 "; - process.env.DATA_DIR = ""; - - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - const allowed = getAllowedPaths(); - expect(allowed).toContain(path.resolve("/path1")); - expect(allowed).toContain(path.resolve("/path2")); + expect(allowed).toContain(path.resolve("/projects")); + expect(getAllowedRootDirectory()).toBe(path.resolve("/projects")); }); it("should always include DATA_DIR if set", async () => { - process.env.ALLOWED_PROJECT_DIRS = ""; + delete process.env.ALLOWED_ROOT_DIRECTORY; process.env.DATA_DIR = "/data/dir"; - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, getAllowedPaths } = + await import("@/lib/security.js"); initAllowedPaths(); const allowed = getAllowedPaths(); expect(allowed).toContain(path.resolve("/data/dir")); }); - it("should handle empty ALLOWED_PROJECT_DIRS", async () => { - process.env.ALLOWED_PROJECT_DIRS = ""; + it("should handle both ALLOWED_ROOT_DIRECTORY and DATA_DIR", async () => { + process.env.ALLOWED_ROOT_DIRECTORY = "/projects"; process.env.DATA_DIR = "/data"; - delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, getAllowedPaths } = + await import("@/lib/security.js"); initAllowedPaths(); const allowed = getAllowedPaths(); - expect(allowed).toHaveLength(1); - expect(allowed[0]).toBe(path.resolve("/data")); - }); - - it("should skip empty entries in comma list", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/path1,,/path2, ,/path3"; - process.env.DATA_DIR = ""; - delete process.env.ALLOWED_ROOT_DIRECTORY; - - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); - initAllowedPaths(); - - const allowed = getAllowedPaths(); - expect(allowed).toHaveLength(3); + expect(allowed).toContain(path.resolve("/projects")); + expect(allowed).toContain(path.resolve("/data")); + expect(allowed).toHaveLength(2); }); }); describe("addAllowedPath", () => { it("should add path to allowed list", async () => { - process.env.ALLOWED_PROJECT_DIRS = ""; + delete process.env.ALLOWED_ROOT_DIRECTORY; process.env.DATA_DIR = ""; const { initAllowedPaths, addAllowedPath, getAllowedPaths } = @@ -99,7 +67,7 @@ describe("security.ts", () => { }); it("should resolve relative paths before adding", async () => { - process.env.ALLOWED_PROJECT_DIRS = ""; + delete process.env.ALLOWED_ROOT_DIRECTORY; process.env.DATA_DIR = ""; const { initAllowedPaths, addAllowedPath, getAllowedPaths } = @@ -115,14 +83,12 @@ describe("security.ts", () => { }); describe("isPathAllowed", () => { - it("should allow paths within configured allowed directories", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed/project"; + it("should allow paths within ALLOWED_ROOT_DIRECTORY", async () => { + process.env.ALLOWED_ROOT_DIRECTORY = "/allowed/project"; process.env.DATA_DIR = ""; - delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, isPathAllowed } = + await import("@/lib/security.js"); initAllowedPaths(); // Paths within allowed directory should be allowed @@ -136,13 +102,11 @@ describe("security.ts", () => { }); it("should allow all paths when no restrictions are configured", async () => { - delete process.env.ALLOWED_PROJECT_DIRS; delete process.env.DATA_DIR; delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, isPathAllowed } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, isPathAllowed } = + await import("@/lib/security.js"); initAllowedPaths(); // All paths should be allowed when no restrictions are configured @@ -152,17 +116,33 @@ describe("security.ts", () => { expect(isPathAllowed("/etc/passwd")).toBe(true); expect(isPathAllowed("/any/path")).toBe(true); }); + + it("should allow all paths when DATA_DIR is set but ALLOWED_ROOT_DIRECTORY is not", async () => { + process.env.DATA_DIR = "/data"; + delete process.env.ALLOWED_ROOT_DIRECTORY; + + const { initAllowedPaths, isPathAllowed } = + await import("@/lib/security.js"); + initAllowedPaths(); + + // DATA_DIR should be allowed + expect(isPathAllowed("/data/settings.json")).toBe(true); + // But all other paths should also be allowed when ALLOWED_ROOT_DIRECTORY is not set + expect(isPathAllowed("/allowed/project/file.txt")).toBe(true); + expect(isPathAllowed("/not/allowed/file.txt")).toBe(true); + expect(isPathAllowed("/tmp/file.txt")).toBe(true); + expect(isPathAllowed("/etc/passwd")).toBe(true); + expect(isPathAllowed("/any/path")).toBe(true); + }); }); describe("validatePath", () => { it("should return resolved path for allowed paths", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed"; + process.env.ALLOWED_ROOT_DIRECTORY = "/allowed"; process.env.DATA_DIR = ""; - delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, validatePath } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, validatePath } = + await import("@/lib/security.js"); initAllowedPaths(); const result = validatePath("/allowed/file.txt"); @@ -170,13 +150,11 @@ describe("security.ts", () => { }); it("should throw error for paths outside allowed directories", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/allowed"; + process.env.ALLOWED_ROOT_DIRECTORY = "/allowed"; process.env.DATA_DIR = ""; - delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, validatePath } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, validatePath } = + await import("@/lib/security.js"); initAllowedPaths(); // Disallowed paths should throw PathNotAllowedError @@ -184,13 +162,11 @@ describe("security.ts", () => { }); it("should not throw error for any path when no restrictions are configured", async () => { - delete process.env.ALLOWED_PROJECT_DIRS; delete process.env.DATA_DIR; delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, validatePath } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, validatePath } = + await import("@/lib/security.js"); initAllowedPaths(); // All paths are allowed when no restrictions configured @@ -202,13 +178,11 @@ describe("security.ts", () => { it("should resolve relative paths within allowed directory", async () => { const cwd = process.cwd(); - process.env.ALLOWED_PROJECT_DIRS = cwd; + process.env.ALLOWED_ROOT_DIRECTORY = cwd; process.env.DATA_DIR = ""; - delete process.env.ALLOWED_ROOT_DIRECTORY; - const { initAllowedPaths, validatePath } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, validatePath } = + await import("@/lib/security.js"); initAllowedPaths(); const result = validatePath("./file.txt"); @@ -218,26 +192,26 @@ describe("security.ts", () => { describe("getAllowedPaths", () => { it("should return array of allowed paths", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/path1,/path2"; + process.env.ALLOWED_ROOT_DIRECTORY = "/projects"; process.env.DATA_DIR = "/data"; - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, getAllowedPaths } = + await import("@/lib/security.js"); initAllowedPaths(); const result = getAllowedPaths(); expect(Array.isArray(result)).toBe(true); - expect(result.length).toBeGreaterThan(0); + expect(result.length).toBe(2); + expect(result).toContain(path.resolve("/projects")); + expect(result).toContain(path.resolve("/data")); }); it("should return resolved paths", async () => { - process.env.ALLOWED_PROJECT_DIRS = "/test"; + process.env.ALLOWED_ROOT_DIRECTORY = "/test"; process.env.DATA_DIR = ""; - const { initAllowedPaths, getAllowedPaths } = await import( - "@/lib/security.js" - ); + const { initAllowedPaths, getAllowedPaths } = + await import("@/lib/security.js"); initAllowedPaths(); const result = getAllowedPaths(); diff --git a/apps/ui/playwright.config.ts b/apps/ui/playwright.config.ts index 65ab32cb..c2a4ac46 100644 --- a/apps/ui/playwright.config.ts +++ b/apps/ui/playwright.config.ts @@ -40,8 +40,7 @@ export default defineConfig({ PORT: String(serverPort), // Enable mock agent in CI to avoid real API calls AUTOMAKER_MOCK_AGENT: mockAgent ? "true" : "false", - // Allow access to test directories and common project paths - ALLOWED_PROJECT_DIRS: "/Users,/home,/tmp,/var/folders", + // No ALLOWED_ROOT_DIRECTORY restriction - allow all paths for testing }, }, // Frontend Vite dev server @@ -54,7 +53,8 @@ export default defineConfig({ ...process.env, VITE_SKIP_SETUP: "true", // Skip electron plugin in CI - no display available for Electron - VITE_SKIP_ELECTRON: process.env.CI === "true" ? "true" : undefined, + VITE_SKIP_ELECTRON: + process.env.CI === "true" ? "true" : undefined, }, }, ], diff --git a/apps/ui/src/components/dialogs/file-browser-dialog.tsx b/apps/ui/src/components/dialogs/file-browser-dialog.tsx index 1687218a..b6a05ab0 100644 --- a/apps/ui/src/components/dialogs/file-browser-dialog.tsx +++ b/apps/ui/src/components/dialogs/file-browser-dialog.tsx @@ -1,4 +1,3 @@ - import { useState, useEffect, useRef, useCallback } from "react"; import { FolderOpen, @@ -21,6 +20,11 @@ import { } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { getJSON, setJSON } from "@/lib/storage"; +import { + getDefaultWorkspaceDirectory, + saveLastProjectDirectory, +} from "@/lib/workspace-config"; interface DirectoryEntry { name: string; @@ -50,38 +54,22 @@ const RECENT_FOLDERS_KEY = "file-browser-recent-folders"; const MAX_RECENT_FOLDERS = 5; function getRecentFolders(): string[] { - if (typeof window === "undefined") return []; - try { - const stored = localStorage.getItem(RECENT_FOLDERS_KEY); - return stored ? JSON.parse(stored) : []; - } catch { - return []; - } + return getJSON(RECENT_FOLDERS_KEY) ?? []; } function addRecentFolder(path: string): void { - if (typeof window === "undefined") return; - try { - const recent = getRecentFolders(); - // Remove if already exists, then add to front - const filtered = recent.filter((p) => p !== path); - const updated = [path, ...filtered].slice(0, MAX_RECENT_FOLDERS); - localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(updated)); - } catch { - // Ignore localStorage errors - } + const recent = getRecentFolders(); + // Remove if already exists, then add to front + const filtered = recent.filter((p) => p !== path); + const updated = [path, ...filtered].slice(0, MAX_RECENT_FOLDERS); + setJSON(RECENT_FOLDERS_KEY, updated); } function removeRecentFolder(path: string): string[] { - if (typeof window === "undefined") return []; - try { - const recent = getRecentFolders(); - const updated = recent.filter((p) => p !== path); - localStorage.setItem(RECENT_FOLDERS_KEY, JSON.stringify(updated)); - return updated; - } catch { - return []; - } + const recent = getRecentFolders(); + const updated = recent.filter((p) => p !== path); + setJSON(RECENT_FOLDERS_KEY, updated); + return updated; } export function FileBrowserDialog({ @@ -110,17 +98,16 @@ export function FileBrowserDialog({ } }, [open]); - const handleRemoveRecent = useCallback((e: React.MouseEvent, path: string) => { - e.stopPropagation(); - const updated = removeRecentFolder(path); - setRecentFolders(updated); - }, []); + const handleRemoveRecent = useCallback( + (e: React.MouseEvent, path: string) => { + e.stopPropagation(); + const updated = removeRecentFolder(path); + setRecentFolders(updated); + }, + [] + ); - const handleSelectRecent = useCallback((path: string) => { - browseDirectory(path); - }, []); - - const browseDirectory = async (dirPath?: string) => { + const browseDirectory = useCallback(async (dirPath?: string) => { setLoading(true); setError(""); setWarning(""); @@ -155,7 +142,14 @@ export function FileBrowserDialog({ } finally { setLoading(false); } - }; + }, []); + + const handleSelectRecent = useCallback( + (path: string) => { + browseDirectory(path); + }, + [browseDirectory] + ); // Reset current path when dialog closes useEffect(() => { @@ -169,12 +163,46 @@ export function FileBrowserDialog({ } }, [open]); - // Load initial path or home directory when dialog opens + // Load initial path or workspace directory when dialog opens useEffect(() => { if (open && !currentPath) { - browseDirectory(initialPath); + // Priority order: + // 1. Last selected directory from this file browser (from localStorage) + // 2. initialPath prop (from parent component) + // 3. Default workspace directory + // 4. Home directory + const loadInitialPath = async () => { + try { + // First, check for last selected directory from getDefaultWorkspaceDirectory + // which already implements the priority: last used > Documents/Automaker > DATA_DIR + const defaultDir = await getDefaultWorkspaceDirectory(); + + // If we have a default directory, use it (unless initialPath is explicitly provided and different) + const pathToUse = initialPath || defaultDir; + + if (pathToUse) { + // Pre-fill the path input immediately + setPathInput(pathToUse); + // Then browse to that directory + browseDirectory(pathToUse); + } else { + // No default directory, browse home directory + browseDirectory(); + } + } catch (err) { + // If config fetch fails, try initialPath or fall back to home directory + if (initialPath) { + setPathInput(initialPath); + browseDirectory(initialPath); + } else { + browseDirectory(); + } + } + }; + + loadInitialPath(); } - }, [open, initialPath]); + }, [open, initialPath, currentPath, browseDirectory]); const handleSelectDirectory = (dir: DirectoryEntry) => { browseDirectory(dir.path); @@ -211,6 +239,8 @@ export function FileBrowserDialog({ const handleSelect = useCallback(() => { if (currentPath) { addRecentFolder(currentPath); + // Save to last project directory so it's used as default next time + saveLastProjectDirectory(currentPath); onSelect(currentPath); onOpenChange(false); } @@ -296,7 +326,9 @@ export function FileBrowserDialog({ title={folder} > - {getFolderName(folder)} + + {getFolderName(folder)} + - diff --git a/apps/ui/src/components/new-project-modal.tsx b/apps/ui/src/components/new-project-modal.tsx index 0af03a5d..93eef763 100644 --- a/apps/ui/src/components/new-project-modal.tsx +++ b/apps/ui/src/components/new-project-modal.tsx @@ -1,4 +1,3 @@ - import { useState, useEffect } from "react"; import { Dialog, @@ -26,11 +25,12 @@ import { } from "lucide-react"; import { starterTemplates, type StarterTemplate } from "@/lib/templates"; import { getElectronAPI } from "@/lib/electron"; -import { getHttpApiClient } from "@/lib/http-api-client"; import { cn } from "@/lib/utils"; import { useFileBrowser } from "@/contexts/file-browser-context"; - -const LAST_PROJECT_DIR_KEY = "automaker:lastProjectDir"; +import { + getDefaultWorkspaceDirectory, + saveLastProjectDirectory, +} from "@/lib/workspace-config"; interface ValidationErrors { projectName?: boolean; @@ -81,25 +81,15 @@ export function NewProjectModal({ // Fetch workspace directory when modal opens useEffect(() => { if (open) { - // First, check localStorage for last used directory - const lastUsedDir = localStorage.getItem(LAST_PROJECT_DIR_KEY); - if (lastUsedDir) { - setWorkspaceDir(lastUsedDir); - return; - } - - // Fall back to server config if no saved directory setIsLoadingWorkspace(true); - const httpClient = getHttpApiClient(); - httpClient.workspace - .getConfig() - .then((result) => { - if (result.success && result.workspaceDir) { - setWorkspaceDir(result.workspaceDir); + getDefaultWorkspaceDirectory() + .then((defaultDir) => { + if (defaultDir) { + setWorkspaceDir(defaultDir); } }) .catch((error) => { - console.error("Failed to get workspace config:", error); + console.error("Failed to get default workspace directory:", error); }) .finally(() => { setIsLoadingWorkspace(false); @@ -211,7 +201,7 @@ export function NewProjectModal({ if (selectedPath) { setWorkspaceDir(selectedPath); // Save to localStorage for next time - localStorage.setItem(LAST_PROJECT_DIR_KEY, selectedPath); + saveLastProjectDirectory(selectedPath); // Clear any workspace error when a valid directory is selected if (errors.workspaceDir) { setErrors((prev) => ({ ...prev, workspaceDir: false })); @@ -296,9 +286,7 @@ export function NewProjectModal({ {projectPath || workspaceDir} - ) : ( - No workspace configured - )} + ) : null}