diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index d90c7a36..259a1900 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -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 diff --git a/apps/server/src/routes/settings/routes/update-global.ts b/apps/server/src/routes/settings/routes/update-global.ts index a04227d8..b45e9965 100644 --- a/apps/server/src/routes/settings/routes/update-global.ts +++ b/apps/server/src/routes/settings/routes/update-global.ts @@ -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) { diff --git a/apps/server/src/services/settings-service.ts b/apps/server/src/services/settings-service.ts index e63b075c..8726bba0 100644 --- a/apps/server/src/services/settings-service.ts +++ b/apps/server/src/services/settings-service.ts @@ -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'); diff --git a/apps/ui/src/components/views/dashboard-view.tsx b/apps/ui/src/components/views/dashboard-view.tsx index f9582d00..80f9624b 100644 --- a/apps/ui/src/components/views/dashboard-view.tsx +++ b/apps/ui/src/components/views/dashboard-view.tsx @@ -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 () => { diff --git a/apps/ui/src/hooks/use-settings-migration.ts b/apps/ui/src/hooks/use-settings-migration.ts index 07119b85..63e62c50 100644 --- a/apps/ui/src/hooks/use-settings-migration.ts +++ b/apps/ui/src/hooks/use-settings-migration.ts @@ -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 | 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 | 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 | 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); diff --git a/apps/ui/src/hooks/use-settings-sync.ts b/apps/ui/src/hooks/use-settings-sync.ts index ea865566..349c4ac7 100644 --- a/apps/ui/src/hooks/use-settings-sync.ts +++ b/apps/ui/src/hooks/use-settings-sync.ts @@ -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 = {}; @@ -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; diff --git a/apps/ui/src/lib/api-fetch.ts b/apps/ui/src/lib/api-fetch.ts index b544c993..f8959c8f 100644 --- a/apps/ui/src/lib/api-fetch.ts +++ b/apps/ui/src/lib/api-fetch.ts @@ -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()}`; } diff --git a/apps/ui/src/lib/http-api-client.ts b/apps/ui/src/lib/http-api-client.ts index cd0e6739..2943f3e2 100644 --- a/apps/ui/src/lib/http-api-client.ts +++ b/apps/ui/src/lib/http-api-client.ts @@ -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 | 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 => { // 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 + } }; /** diff --git a/apps/ui/src/main.ts b/apps/ui/src/main.ts index 8930d664..4d093106 100644 --- a/apps/ui/src/main.ts +++ b/apps/ui/src/main.ts @@ -474,6 +474,17 @@ async function startServer(): Promise { ? 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 { ...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 { }; 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(); diff --git a/apps/ui/src/store/app-store.ts b/apps/ui/src/store/app-store.ts index a23c17c4..ee8ca98a 100644 --- a/apps/ui/src/store/app-store.ts +++ b/apps/ui/src/store/app-store.ts @@ -1504,7 +1504,16 @@ export const useAppStore = create()((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()((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], diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 51772a25..cd9beb98 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -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, }); }); diff --git a/apps/ui/vite.config.mts b/apps/ui/vite.config.mts index 0d18997e..1a378d56 100644 --- a/apps/ui/vite.config.mts +++ b/apps/ui/vite.config.mts @@ -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', diff --git a/docker-compose.dev-server.yml b/docker-compose.dev-server.yml index 9ff0972e..ea44fffc 100644 --- a/docker-compose.dev-server.yml +++ b/docker-compose.dev-server.yml @@ -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 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index de4ebb11..d9cf830f 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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 diff --git a/package-lock.json b/package-lock.json index 8fc7b149..97a2c4fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/start-automaker.sh b/start-automaker.sh index a2d3e54c..ef7b1172 100755 --- a/start-automaker.sh +++ b/start-automaker.sh @@ -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 diff --git a/test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json b/test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json new file mode 100644 index 00000000..68258c5b --- /dev/null +++ b/test/agent-session-test-115699-vyk2nk2/test-project-1768743000887/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-project-1768743000887", + "version": "1.0.0" +} diff --git a/test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json b/test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json new file mode 100644 index 00000000..4ea81845 --- /dev/null +++ b/test/feature-backlog-test-114171-aysp86y/test-project-1768742910934/package.json @@ -0,0 +1,4 @@ +{ + "name": "test-project-1768742910934", + "version": "1.0.0" +}