Merge pull request #573 from DhanushSantosh/patchcraft

fix: resolve data directory persistence between Electron and Web modes
This commit is contained in:
Dhanush Santosh
2026-01-18 19:36:09 +05:30
committed by GitHub
18 changed files with 412 additions and 107 deletions

View File

@@ -91,6 +91,9 @@ const PORT = parseInt(process.env.PORT || '3008', 10);
const HOST = process.env.HOST || '0.0.0.0';
const HOSTNAME = process.env.HOSTNAME || 'localhost';
const DATA_DIR = process.env.DATA_DIR || './data';
logger.info('[SERVER_STARTUP] process.env.DATA_DIR:', process.env.DATA_DIR);
logger.info('[SERVER_STARTUP] Resolved DATA_DIR:', DATA_DIR);
logger.info('[SERVER_STARTUP] process.cwd():', process.cwd());
const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
// Runtime-configurable request logging flag (can be changed via settings)
@@ -175,14 +178,25 @@ app.use(
return;
}
// For local development, allow localhost origins
if (
origin.startsWith('http://localhost:') ||
origin.startsWith('http://127.0.0.1:') ||
origin.startsWith('http://[::1]:')
) {
callback(null, origin);
return;
// For local development, allow all localhost/loopback origins (any port)
try {
const url = new URL(origin);
const hostname = url.hostname;
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
callback(null, origin);
return;
}
} catch (err) {
// Ignore URL parsing errors
}
// Reject other origins by default for security

View File

@@ -45,18 +45,24 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
}
// Minimal debug logging to help diagnose accidental wipes.
if ('projects' in updates || 'theme' in updates || 'localStorageMigrated' in updates) {
const projectsLen = Array.isArray((updates as any).projects)
? (updates as any).projects.length
: undefined;
logger.info(
`Update global settings request: projects=${projectsLen ?? 'n/a'}, theme=${
(updates as any).theme ?? 'n/a'
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}`
);
}
const projectsLen = Array.isArray((updates as any).projects)
? (updates as any).projects.length
: undefined;
const trashedLen = Array.isArray((updates as any).trashedProjects)
? (updates as any).trashedProjects.length
: undefined;
logger.info(
`[SERVER_SETTINGS_UPDATE] Request received: projects=${projectsLen ?? 'n/a'}, trashedProjects=${trashedLen ?? 'n/a'}, theme=${
(updates as any).theme ?? 'n/a'
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}`
);
logger.info('[SERVER_SETTINGS_UPDATE] Calling updateGlobalSettings...');
const settings = await settingsService.updateGlobalSettings(updates);
logger.info(
'[SERVER_SETTINGS_UPDATE] Update complete, projects count:',
settings.projects?.length ?? 0
);
// Apply server log level if it was updated
if ('serverLogLevel' in updates && updates.serverLogLevel) {

View File

@@ -273,13 +273,39 @@ export class SettingsService {
};
const currentProjectsLen = Array.isArray(current.projects) ? current.projects.length : 0;
// Check if this is a legitimate project removal (moved to trash) vs accidental wipe
const newTrashedProjectsLen = Array.isArray(sanitizedUpdates.trashedProjects)
? sanitizedUpdates.trashedProjects.length
: Array.isArray(current.trashedProjects)
? current.trashedProjects.length
: 0;
if (
Array.isArray(sanitizedUpdates.projects) &&
sanitizedUpdates.projects.length === 0 &&
currentProjectsLen > 0
) {
attemptedProjectWipe = true;
delete sanitizedUpdates.projects;
// Only treat as accidental wipe if trashedProjects is also empty
// (If projects are moved to trash, they appear in trashedProjects)
if (newTrashedProjectsLen === 0) {
logger.warn(
'[WIPE_PROTECTION] Attempted to set projects to empty array with no trash! Ignoring update.',
{
currentProjectsLen,
newProjectsLen: 0,
newTrashedProjectsLen,
currentProjects: current.projects?.map((p) => p.name),
}
);
attemptedProjectWipe = true;
delete sanitizedUpdates.projects;
} else {
logger.info('[LEGITIMATE_REMOVAL] Removing all projects to trash', {
currentProjectsLen,
newProjectsLen: 0,
movedToTrash: newTrashedProjectsLen,
});
}
}
ignoreEmptyArrayOverwrite('trashedProjects');

View File

@@ -124,6 +124,19 @@ export function DashboardView() {
const initResult = await initializeProject(path);
if (!initResult.success) {
// If the project directory doesn't exist, automatically remove it from the project list
if (initResult.error?.includes('does not exist')) {
const projectToRemove = projects.find((p) => p.path === path);
if (projectToRemove) {
logger.warn(`[Dashboard] Removing project with non-existent path: ${path}`);
moveProjectToTrash(projectToRemove.id);
toast.error('Project directory not found', {
description: `Removed ${name} from your projects list since the directory no longer exists.`,
});
return;
}
}
toast.error('Failed to initialize project', {
description: initResult.error || 'Unknown error occurred',
});
@@ -151,7 +164,15 @@ export function DashboardView() {
setIsOpening(false);
}
},
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, navigate]
[
projects,
trashedProjects,
currentProject,
globalTheme,
upsertAndSetCurrentProject,
navigate,
moveProjectToTrash,
]
);
const handleOpenProject = useCallback(async () => {

View File

@@ -111,9 +111,34 @@ export function resetMigrationState(): void {
/**
* Parse localStorage data into settings object
*
* Checks for settings in multiple locations:
* 1. automaker-settings-cache: Fresh server settings cached from last fetch
* 2. automaker-storage: Zustand-persisted app store state (legacy)
* 3. automaker-setup: Setup wizard state (legacy)
* 4. Standalone keys: worktree-panel-collapsed, file-browser-recent-folders, etc.
*
* @returns Merged settings object or null if no settings found
*/
export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
try {
// First, check for fresh server settings cache (updated whenever server settings are fetched)
// This prevents stale data when switching between modes
const settingsCache = getItem('automaker-settings-cache');
if (settingsCache) {
try {
const cached = JSON.parse(settingsCache) as GlobalSettings;
const cacheProjectCount = cached?.projects?.length ?? 0;
logger.info(`[CACHE_LOADED] projects=${cacheProjectCount}, theme=${cached?.theme}`);
return cached;
} catch (e) {
logger.warn('Failed to parse settings cache, falling back to old storage');
}
} else {
logger.info('[CACHE_EMPTY] No settings cache found in localStorage');
}
// Fall back to old Zustand persisted storage
const automakerStorage = getItem('automaker-storage');
if (!automakerStorage) {
return null;
@@ -186,7 +211,14 @@ export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
/**
* Check if localStorage has more complete data than server
* Returns true if localStorage has projects but server doesn't
*
* Compares the completeness of data to determine if a migration is needed.
* Returns true if localStorage has projects but server doesn't, indicating
* the localStorage data should be merged with server settings.
*
* @param localSettings Settings loaded from localStorage
* @param serverSettings Settings loaded from server
* @returns true if localStorage has more data that should be preserved
*/
export function localStorageHasMoreData(
localSettings: Partial<GlobalSettings> | null,
@@ -209,7 +241,15 @@ export function localStorageHasMoreData(
/**
* Merge localStorage settings with server settings
* Prefers server data, but uses localStorage for missing arrays/objects
*
* Intelligently combines settings from both sources:
* - Prefers server data as the base
* - Uses localStorage values when server has empty arrays/objects
* - Specific handling for: projects, trashedProjects, mcpServers, recentFolders, etc.
*
* @param serverSettings Settings from server API (base)
* @param localSettings Settings from localStorage (fallback)
* @returns Merged GlobalSettings object ready to hydrate the store
*/
export function mergeSettings(
serverSettings: GlobalSettings,
@@ -291,20 +331,33 @@ export function mergeSettings(
* This is the core migration logic extracted for use outside of React hooks.
* Call this from __root.tsx during app initialization.
*
* @param serverSettings - Settings fetched from the server API
* @returns Promise resolving to the final settings to use (merged if migration needed)
* Flow:
* 1. If server has localStorageMigrated flag, skip migration (already done)
* 2. Check if localStorage has more data than server
* 3. If yes, merge them and sync merged state back to server
* 4. Set localStorageMigrated flag to prevent re-migration
*
* @param serverSettings Settings fetched from the server API
* @returns Promise resolving to {settings, migrated} - final settings and whether migration occurred
*/
export async function performSettingsMigration(
serverSettings: GlobalSettings
): Promise<{ settings: GlobalSettings; migrated: boolean }> {
// Get localStorage data
const localSettings = parseLocalStorageSettings();
logger.info(`localStorage has ${localSettings?.projects?.length ?? 0} projects`);
logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`);
const localProjects = localSettings?.projects?.length ?? 0;
const serverProjects = serverSettings.projects?.length ?? 0;
logger.info('[MIGRATION_CHECK]', {
localStorageProjects: localProjects,
serverProjects: serverProjects,
localStorageMigrated: serverSettings.localStorageMigrated,
dataSourceMismatch: localProjects !== serverProjects,
});
// Check if migration has already been completed
if (serverSettings.localStorageMigrated) {
logger.info('localStorage migration already completed, using server settings only');
logger.info('[MIGRATION_SKIP] Using server settings only (migration already completed)');
return { settings: serverSettings, migrated: false };
}
@@ -412,6 +465,15 @@ export function useSettingsMigration(): MigrationState {
if (global.success && global.settings) {
serverSettings = global.settings as unknown as GlobalSettings;
logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`);
// Update localStorage with fresh server data to keep cache in sync
// This prevents stale localStorage data from being used when switching between modes
try {
setItem('automaker-settings-cache', JSON.stringify(serverSettings));
logger.debug('Updated localStorage with fresh server settings');
} catch (storageError) {
logger.warn('Failed to update localStorage cache:', storageError);
}
}
} catch (error) {
logger.error('Failed to fetch server settings:', error);

View File

@@ -81,7 +81,15 @@ const SETUP_FIELDS_TO_SYNC = ['isFirstRun', 'setupComplete', 'skipClaudeSetup']
/**
* Helper to extract a settings field value from app state
* Handles special cases for nested/mapped fields
*
* Handles special cases where store fields don't map directly to settings:
* - currentProjectId: Extract from currentProject?.id
* - terminalFontFamily: Extract from terminalState.fontFamily
* - Other fields: Direct access
*
* @param field The settings field to extract
* @param appState Current app store state
* @returns The value of the field in the app state
*/
function getSettingsFieldValue(
field: (typeof SETTINGS_FIELDS_TO_SYNC)[number],
@@ -98,6 +106,16 @@ function getSettingsFieldValue(
/**
* Helper to check if a settings field changed between states
*
* Compares field values between old and new state, handling special cases:
* - currentProjectId: Compare currentProject?.id values
* - terminalFontFamily: Compare terminalState.fontFamily values
* - Other fields: Direct reference equality check
*
* @param field The settings field to check
* @param newState New app store state
* @param prevState Previous app store state
* @returns true if the field value changed between states
*/
function hasSettingsFieldChanged(
field: (typeof SETTINGS_FIELDS_TO_SYNC)[number],
@@ -172,14 +190,18 @@ export function useSettingsSync(): SettingsSyncState {
// Never sync when not authenticated or settings not loaded
// The settingsLoaded flag ensures we don't sync default empty state before hydration
const auth = useAuthStore.getState();
logger.debug('syncToServer check:', {
logger.debug('[SYNC_CHECK] Auth state:', {
authChecked: auth.authChecked,
isAuthenticated: auth.isAuthenticated,
settingsLoaded: auth.settingsLoaded,
projectsCount: useAppStore.getState().projects?.length ?? 0,
});
if (!auth.authChecked || !auth.isAuthenticated || !auth.settingsLoaded) {
logger.debug('Sync skipped: not authenticated or settings not loaded');
logger.warn('[SYNC_SKIPPED] Not ready:', {
authChecked: auth.authChecked,
isAuthenticated: auth.isAuthenticated,
settingsLoaded: auth.settingsLoaded,
});
return;
}
@@ -187,7 +209,9 @@ export function useSettingsSync(): SettingsSyncState {
const api = getHttpApiClient();
const appState = useAppStore.getState();
logger.debug('Syncing to server:', { projectsCount: appState.projects?.length ?? 0 });
logger.info('[SYNC_START] Syncing to server:', {
projectsCount: appState.projects?.length ?? 0,
});
// Build updates object from current state
const updates: Record<string, unknown> = {};
@@ -204,17 +228,30 @@ export function useSettingsSync(): SettingsSyncState {
// Create a hash of the updates to avoid redundant syncs
const updateHash = JSON.stringify(updates);
if (updateHash === lastSyncedRef.current) {
logger.debug('Sync skipped: no changes');
logger.debug('[SYNC_SKIP_IDENTICAL] No changes from last sync');
setState((s) => ({ ...s, syncing: false }));
return;
}
logger.info('Sending settings update:', { projects: updates.projects });
logger.info('[SYNC_SEND] Sending settings update to server:', {
projects: (updates.projects as any)?.length ?? 0,
trashedProjects: (updates.trashedProjects as any)?.length ?? 0,
});
const result = await api.settings.updateGlobal(updates);
logger.info('[SYNC_RESPONSE] Server response:', { success: result.success });
if (result.success) {
lastSyncedRef.current = updateHash;
logger.debug('Settings synced to server');
// Update localStorage cache with synced settings to keep it fresh
// This prevents stale data when switching between Electron and web modes
try {
setItem('automaker-settings-cache', JSON.stringify(updates));
logger.debug('Updated localStorage cache after sync');
} catch (storageError) {
logger.warn('Failed to update localStorage cache after sync:', storageError);
}
} else {
logger.error('Failed to sync settings:', result.error);
}
@@ -340,9 +377,24 @@ export function useSettingsSync(): SettingsSyncState {
return;
}
// Check if any synced field changed
// If projects array changed (by reference, meaning content changed), sync immediately
// This is critical - projects list changes must sync right away to prevent loss
// when switching between Electron and web modes or closing the app
if (newState.projects !== prevState.projects) {
logger.info('[PROJECTS_CHANGED] Projects array changed, syncing immediately', {
prevCount: prevState.projects?.length ?? 0,
newCount: newState.projects?.length ?? 0,
prevProjects: prevState.projects?.map((p) => p.name) ?? [],
newProjects: newState.projects?.map((p) => p.name) ?? [],
});
syncNow();
return;
}
// Check if any other synced field changed
let changed = false;
for (const field of SETTINGS_FIELDS_TO_SYNC) {
if (field === 'projects') continue; // Already handled above
if (hasSettingsFieldChanged(field, newState, prevState)) {
changed = true;
break;

View File

@@ -185,7 +185,13 @@ export function getAuthenticatedImageUrl(
if (apiKey) {
params.set('apiKey', apiKey);
}
// Note: Session token auth relies on cookies which are sent automatically by the browser
// Web mode: also add session token as query param for image loads
// This ensures images load correctly even if cookies aren't sent (e.g., cross-origin proxy scenarios)
const sessionToken = getSessionToken();
if (sessionToken) {
params.set('token', sessionToken);
}
return `${serverUrl}/api/fs/image?${params.toString()}`;
}

View File

@@ -156,6 +156,12 @@ const getServerUrl = (): string => {
if (typeof window !== 'undefined') {
const envUrl = import.meta.env.VITE_SERVER_URL;
if (envUrl) return envUrl;
// In web mode (not Electron), use relative URL to leverage Vite proxy
// This avoids CORS issues since requests appear same-origin
if (!window.electron) {
return '';
}
}
// Use VITE_HOSTNAME if set, otherwise default to localhost
const hostname = import.meta.env.VITE_HOSTNAME || 'localhost';
@@ -173,8 +179,24 @@ let apiKeyInitialized = false;
let apiKeyInitPromise: Promise<void> | null = null;
// Cached session token for authentication (Web mode - explicit header auth)
// Only used in-memory after fresh login; on refresh we rely on HTTP-only cookies
// Persisted to localStorage to survive page reloads
let cachedSessionToken: string | null = null;
const SESSION_TOKEN_KEY = 'automaker:sessionToken';
// Initialize cached session token from localStorage on module load
// This ensures web mode survives page reloads with valid authentication
const initSessionToken = (): void => {
if (typeof window === 'undefined') return; // Skip in SSR
try {
cachedSessionToken = window.localStorage.getItem(SESSION_TOKEN_KEY);
} catch {
// localStorage might be disabled or unavailable
cachedSessionToken = null;
}
};
// Initialize on module load
initSessionToken();
// Get API key for Electron mode (returns cached value after initialization)
// Exported for use in WebSocket connections that need auth
@@ -194,14 +216,30 @@ export const waitForApiKeyInit = (): Promise<void> => {
// Get session token for Web mode (returns cached value after login)
export const getSessionToken = (): string | null => cachedSessionToken;
// Set session token (called after login)
// Set session token (called after login) - persists to localStorage for page reload survival
export const setSessionToken = (token: string | null): void => {
cachedSessionToken = token;
if (typeof window === 'undefined') return; // Skip in SSR
try {
if (token) {
window.localStorage.setItem(SESSION_TOKEN_KEY, token);
} else {
window.localStorage.removeItem(SESSION_TOKEN_KEY);
}
} catch {
// localStorage might be disabled; continue with in-memory cache
}
};
// Clear session token (called on logout)
export const clearSessionToken = (): void => {
cachedSessionToken = null;
if (typeof window === 'undefined') return; // Skip in SSR
try {
window.localStorage.removeItem(SESSION_TOKEN_KEY);
} catch {
// localStorage might be disabled
}
};
/**

View File

@@ -474,6 +474,17 @@ async function startServer(): Promise<void> {
? path.join(process.resourcesPath, 'server')
: path.join(__dirname, '../../server');
// IMPORTANT: Use shared data directory (not Electron's user data directory)
// This ensures Electron and web mode share the same settings/projects
// In dev: project root/data (navigate from __dirname which is apps/server/dist or apps/ui/dist-electron)
// In production: same as Electron user data (for app isolation)
const dataDir = app.isPackaged
? app.getPath('userData')
: path.join(__dirname, '../../..', 'data');
logger.info(
`[DATA_DIR] app.isPackaged=${app.isPackaged}, __dirname=${__dirname}, dataDir=${dataDir}`
);
// Build enhanced PATH that includes Node.js directory (cross-platform)
const enhancedPath = buildEnhancedPath(command, process.env.PATH || '');
if (enhancedPath !== process.env.PATH) {
@@ -484,7 +495,7 @@ async function startServer(): Promise<void> {
...process.env,
PATH: enhancedPath,
PORT: serverPort.toString(),
DATA_DIR: app.getPath('userData'),
DATA_DIR: dataDir,
NODE_PATH: serverNodeModules,
// Pass API key to server for CSRF protection
AUTOMAKER_API_KEY: apiKey!,
@@ -496,6 +507,7 @@ async function startServer(): Promise<void> {
};
logger.info('Server will use port', serverPort);
logger.info('[DATA_DIR_SPAWN] env.DATA_DIR=', env.DATA_DIR);
logger.info('Starting backend server...');
logger.info('Server path:', serverPath);
@@ -647,20 +659,44 @@ function createWindow(): void {
// App lifecycle
app.whenReady().then(async () => {
// Ensure userData path is consistent across dev/prod so files land in Automaker dir
try {
const desiredUserDataPath = path.join(app.getPath('appData'), 'Automaker');
if (app.getPath('userData') !== desiredUserDataPath) {
app.setPath('userData', desiredUserDataPath);
logger.info('userData path set to:', desiredUserDataPath);
// In production, use Automaker dir in appData for app isolation
// In development, use project root for shared data between Electron and web mode
let userDataPathToUse: string;
if (app.isPackaged) {
// Production: Ensure userData path is consistent so files land in Automaker dir
try {
const desiredUserDataPath = path.join(app.getPath('appData'), 'Automaker');
if (app.getPath('userData') !== desiredUserDataPath) {
app.setPath('userData', desiredUserDataPath);
logger.info('[PRODUCTION] userData path set to:', desiredUserDataPath);
}
userDataPathToUse = desiredUserDataPath;
} catch (error) {
logger.warn('[PRODUCTION] Failed to set userData path:', (error as Error).message);
userDataPathToUse = app.getPath('userData');
}
} else {
// Development: Explicitly set userData to project root for shared data between Electron and web
// This OVERRIDES Electron's default userData path (~/.config/Automaker)
// __dirname is apps/ui/dist-electron, so go up to get project root
const projectRoot = path.join(__dirname, '../../..');
userDataPathToUse = path.join(projectRoot, 'data');
try {
app.setPath('userData', userDataPathToUse);
logger.info('[DEVELOPMENT] userData path explicitly set to:', userDataPathToUse);
} catch (error) {
logger.warn(
'[DEVELOPMENT] Failed to set userData path, using fallback:',
(error as Error).message
);
userDataPathToUse = path.join(projectRoot, 'data');
}
} catch (error) {
logger.warn('Failed to set userData path:', (error as Error).message);
}
// Initialize centralized path helpers for Electron
// This must be done before any file operations
setElectronUserDataPath(app.getPath('userData'));
setElectronUserDataPath(userDataPathToUse);
// In development mode, allow access to the entire project root (for source files, node_modules, etc.)
// In production, only allow access to the built app directory and resources
@@ -675,7 +711,12 @@ app.whenReady().then(async () => {
// Initialize security settings for path validation
// Set DATA_DIR before initializing so it's available for security checks
process.env.DATA_DIR = app.getPath('userData');
// Use the project's shared data directory in development, userData in production
const mainProcessDataDir = app.isPackaged
? app.getPath('userData')
: path.join(process.cwd(), 'data');
process.env.DATA_DIR = mainProcessDataDir;
logger.info('[MAIN_PROCESS_DATA_DIR]', mainProcessDataDir);
// ALLOWED_ROOT_DIRECTORY should already be in process.env if set by user
// (it will be passed to server process, but we also need it in main process for dialog validation)
initAllowedPaths();

View File

@@ -1504,7 +1504,16 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
moveProjectToTrash: (projectId) => {
const project = get().projects.find((p) => p.id === projectId);
if (!project) return;
if (!project) {
console.warn('[MOVE_TO_TRASH] Project not found:', projectId);
return;
}
console.log('[MOVE_TO_TRASH] Moving project to trash:', {
projectId,
projectName: project.name,
currentProjectCount: get().projects.length,
});
const remainingProjects = get().projects.filter((p) => p.id !== projectId);
const existingTrash = get().trashedProjects.filter((p) => p.id !== projectId);
@@ -1517,6 +1526,11 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
const isCurrent = get().currentProject?.id === projectId;
const nextCurrentProject = isCurrent ? null : get().currentProject;
console.log('[MOVE_TO_TRASH] Updating store with new state:', {
newProjectCount: remainingProjects.length,
newTrashedCount: [trashedProject, ...existingTrash].length,
});
set({
projects: remainingProjects,
trashedProjects: [trashedProject, ...existingTrash],

View File

@@ -85,7 +85,17 @@ test.describe('Open Project', () => {
// AND inject our test project into the projects list
await page.route('**/api/settings/global', async (route) => {
const response = await route.fetch();
const json = await response.json();
// Immediately consume the body to prevent disposal issues
const bodyPromise = response.body();
const status = response.status();
const headers = response.headers();
const body = await bodyPromise;
let json;
try {
json = JSON.parse(body.toString());
} catch {
json = {};
}
if (json.settings) {
// Remove currentProjectId to prevent restoring a project
json.settings.currentProjectId = null;
@@ -106,8 +116,8 @@ test.describe('Open Project', () => {
}
}
await route.fulfill({
status: response.status(),
headers: response.headers(),
status: status,
headers: headers,
json,
});
});

View File

@@ -68,6 +68,13 @@ export default defineConfig(({ command }) => {
host: process.env.HOST || '0.0.0.0',
port: parseInt(process.env.TEST_PORT || '3007', 10),
allowedHosts: true,
proxy: {
'/api': {
target: 'http://localhost:3008',
changeOrigin: true,
ws: true,
},
},
},
build: {
outDir: 'dist',

View File

@@ -59,8 +59,10 @@ services:
# This ensures native modules are built for the container's architecture
- automaker-dev-node-modules:/app/node_modules
# Persist data across restarts
- automaker-data:/data
# IMPORTANT: Mount local ./data directory (not a Docker volume)
# This ensures Electron and web mode share the same data directory
# and projects opened in either mode are visible in both
- ./data:/data
# Persist CLI configurations
- automaker-claude-config:/home/automaker/.claude
@@ -97,9 +99,6 @@ volumes:
name: automaker-dev-node-modules
# Named volume for container-specific node_modules
automaker-data:
name: automaker-data
automaker-claude-config:
name: automaker-claude-config

View File

@@ -60,8 +60,9 @@ services:
# This ensures native modules are built for the container's architecture
- automaker-dev-node-modules:/app/node_modules
# Persist data across restarts
- automaker-data:/data
# IMPORTANT: Mount local ./data directory (not a Docker volume)
# This ensures data is consistent across Electron and web modes
- ./data:/data
# Persist CLI configurations
- automaker-claude-config:/home/automaker/.claude
@@ -141,9 +142,6 @@ volumes:
name: automaker-dev-node-modules
# Named volume for container-specific node_modules
automaker-data:
name: automaker-data
automaker-claude-config:
name: automaker-claude-config

88
package-lock.json generated
View File

@@ -18,7 +18,6 @@
"tree-kill": "1.2.2"
},
"devDependencies": {
"dmg-license": "^1.0.11",
"husky": "9.1.7",
"lint-staged": "16.2.7",
"prettier": "3.7.4",
@@ -26,6 +25,9 @@
},
"engines": {
"node": ">=22.0.0 <23.0.0"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"
}
},
"apps/server": {
@@ -6114,7 +6116,7 @@
"version": "25.0.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
@@ -6124,15 +6126,15 @@
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/plist": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz",
"integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/node": "*",
"xmlbuilder": ">=11.0.1"
@@ -6156,7 +6158,6 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
@@ -6166,7 +6167,7 @@
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -6213,8 +6214,8 @@
"version": "1.10.11",
"resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz",
"integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/@types/ws": {
"version": "8.18.1",
@@ -6719,7 +6720,7 @@
"version": "0.8.11",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
"integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=10.0.0"
@@ -6921,7 +6922,7 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -7003,7 +7004,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -7013,7 +7014,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -7237,8 +7238,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.8"
}
@@ -7289,8 +7290,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=8"
}
@@ -7363,7 +7364,7 @@
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "github",
@@ -7537,7 +7538,7 @@
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "github",
@@ -8033,8 +8034,8 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
@@ -8128,7 +8129,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -8141,7 +8142,7 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/colorette": {
@@ -8309,8 +8310,8 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/cors": {
"version": "2.8.5",
@@ -8329,8 +8330,8 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz",
"integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"buffer": "^5.1.0"
}
@@ -8377,7 +8378,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/d3-color": {
@@ -8792,8 +8792,8 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz",
"integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
@@ -9057,7 +9057,7 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/encodeurl": {
@@ -9682,11 +9682,11 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
"dev": true,
"engines": [
"node >=0.6.0"
],
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
@@ -9698,7 +9698,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
@@ -10648,8 +10648,8 @@
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
"integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
@@ -10678,7 +10678,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"devOptional": true,
"funding": [
{
"type": "github",
@@ -10866,7 +10866,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11132,7 +11132,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/json-schema-typed": {
@@ -11253,6 +11253,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -11318,6 +11319,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">= 12.0.0"
},
@@ -13077,8 +13079,8 @@
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz",
"integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"optional": true
},
"node_modules/node-api-version": {
"version": "0.2.1",
@@ -13677,7 +13679,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz",
"integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.8.8",
@@ -13793,7 +13795,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -14593,8 +14595,8 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
@@ -14608,7 +14610,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
"integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 6.0.0",
@@ -14805,7 +14807,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -14850,7 +14852,7 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -15609,7 +15611,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@@ -15709,8 +15711,8 @@
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
@@ -16153,7 +16155,7 @@
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=8.0"

View File

@@ -1075,7 +1075,8 @@ case $MODE in
export TEST_PORT="$WEB_PORT"
export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT"
export PORT="$SERVER_PORT"
export CORS_ORIGIN="http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT"
export DATA_DIR="$SCRIPT_DIR/data"
export CORS_ORIGIN="http://localhost:$WEB_PORT,http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT"
export VITE_APP_MODE="1"
if [ "$PRODUCTION_MODE" = true ]; then

View File

@@ -0,0 +1,4 @@
{
"name": "test-project-1768743000887",
"version": "1.0.0"
}

View File

@@ -0,0 +1,4 @@
{
"name": "test-project-1768742910934",
"version": "1.0.0"
}