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 HOST = process.env.HOST || '0.0.0.0';
const HOSTNAME = process.env.HOSTNAME || 'localhost'; const HOSTNAME = process.env.HOSTNAME || 'localhost';
const DATA_DIR = process.env.DATA_DIR || './data'; 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 const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
// Runtime-configurable request logging flag (can be changed via settings) // Runtime-configurable request logging flag (can be changed via settings)
@@ -175,14 +178,25 @@ app.use(
return; return;
} }
// For local development, allow localhost origins // For local development, allow all localhost/loopback origins (any port)
if ( try {
origin.startsWith('http://localhost:') || const url = new URL(origin);
origin.startsWith('http://127.0.0.1:') || const hostname = url.hostname;
origin.startsWith('http://[::1]:')
) { if (
callback(null, origin); hostname === 'localhost' ||
return; 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 // 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. // 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)
const projectsLen = Array.isArray((updates as any).projects) ? (updates as any).projects.length
? (updates as any).projects.length : undefined;
: undefined; const trashedLen = Array.isArray((updates as any).trashedProjects)
logger.info( ? (updates as any).trashedProjects.length
`Update global settings request: projects=${projectsLen ?? 'n/a'}, theme=${ : undefined;
(updates as any).theme ?? 'n/a' logger.info(
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}` `[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); 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 // Apply server log level if it was updated
if ('serverLogLevel' in updates && updates.serverLogLevel) { 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; 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 ( if (
Array.isArray(sanitizedUpdates.projects) && Array.isArray(sanitizedUpdates.projects) &&
sanitizedUpdates.projects.length === 0 && sanitizedUpdates.projects.length === 0 &&
currentProjectsLen > 0 currentProjectsLen > 0
) { ) {
attemptedProjectWipe = true; // Only treat as accidental wipe if trashedProjects is also empty
delete sanitizedUpdates.projects; // (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'); ignoreEmptyArrayOverwrite('trashedProjects');

View File

@@ -124,6 +124,19 @@ export function DashboardView() {
const initResult = await initializeProject(path); const initResult = await initializeProject(path);
if (!initResult.success) { 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', { toast.error('Failed to initialize project', {
description: initResult.error || 'Unknown error occurred', description: initResult.error || 'Unknown error occurred',
}); });
@@ -151,7 +164,15 @@ export function DashboardView() {
setIsOpening(false); setIsOpening(false);
} }
}, },
[trashedProjects, currentProject, globalTheme, upsertAndSetCurrentProject, navigate] [
projects,
trashedProjects,
currentProject,
globalTheme,
upsertAndSetCurrentProject,
navigate,
moveProjectToTrash,
]
); );
const handleOpenProject = useCallback(async () => { const handleOpenProject = useCallback(async () => {

View File

@@ -111,9 +111,34 @@ export function resetMigrationState(): void {
/** /**
* Parse localStorage data into settings object * 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 { export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
try { 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'); const automakerStorage = getItem('automaker-storage');
if (!automakerStorage) { if (!automakerStorage) {
return null; return null;
@@ -186,7 +211,14 @@ export function parseLocalStorageSettings(): Partial<GlobalSettings> | null {
/** /**
* Check if localStorage has more complete data than server * 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( export function localStorageHasMoreData(
localSettings: Partial<GlobalSettings> | null, localSettings: Partial<GlobalSettings> | null,
@@ -209,7 +241,15 @@ export function localStorageHasMoreData(
/** /**
* Merge localStorage settings with server settings * 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( export function mergeSettings(
serverSettings: GlobalSettings, serverSettings: GlobalSettings,
@@ -291,20 +331,33 @@ export function mergeSettings(
* This is the core migration logic extracted for use outside of React hooks. * This is the core migration logic extracted for use outside of React hooks.
* Call this from __root.tsx during app initialization. * Call this from __root.tsx during app initialization.
* *
* @param serverSettings - Settings fetched from the server API * Flow:
* @returns Promise resolving to the final settings to use (merged if migration needed) * 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( export async function performSettingsMigration(
serverSettings: GlobalSettings serverSettings: GlobalSettings
): Promise<{ settings: GlobalSettings; migrated: boolean }> { ): Promise<{ settings: GlobalSettings; migrated: boolean }> {
// Get localStorage data // Get localStorage data
const localSettings = parseLocalStorageSettings(); const localSettings = parseLocalStorageSettings();
logger.info(`localStorage has ${localSettings?.projects?.length ?? 0} projects`); const localProjects = localSettings?.projects?.length ?? 0;
logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`); 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 // Check if migration has already been completed
if (serverSettings.localStorageMigrated) { 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 }; return { settings: serverSettings, migrated: false };
} }
@@ -412,6 +465,15 @@ export function useSettingsMigration(): MigrationState {
if (global.success && global.settings) { if (global.success && global.settings) {
serverSettings = global.settings as unknown as GlobalSettings; serverSettings = global.settings as unknown as GlobalSettings;
logger.info(`Server has ${serverSettings.projects?.length ?? 0} projects`); 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) { } catch (error) {
logger.error('Failed to fetch server settings:', 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 * 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( function getSettingsFieldValue(
field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], field: (typeof SETTINGS_FIELDS_TO_SYNC)[number],
@@ -98,6 +106,16 @@ function getSettingsFieldValue(
/** /**
* Helper to check if a settings field changed between states * 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( function hasSettingsFieldChanged(
field: (typeof SETTINGS_FIELDS_TO_SYNC)[number], field: (typeof SETTINGS_FIELDS_TO_SYNC)[number],
@@ -172,14 +190,18 @@ export function useSettingsSync(): SettingsSyncState {
// Never sync when not authenticated or settings not loaded // Never sync when not authenticated or settings not loaded
// The settingsLoaded flag ensures we don't sync default empty state before hydration // The settingsLoaded flag ensures we don't sync default empty state before hydration
const auth = useAuthStore.getState(); const auth = useAuthStore.getState();
logger.debug('syncToServer check:', { logger.debug('[SYNC_CHECK] Auth state:', {
authChecked: auth.authChecked, authChecked: auth.authChecked,
isAuthenticated: auth.isAuthenticated, isAuthenticated: auth.isAuthenticated,
settingsLoaded: auth.settingsLoaded, settingsLoaded: auth.settingsLoaded,
projectsCount: useAppStore.getState().projects?.length ?? 0, projectsCount: useAppStore.getState().projects?.length ?? 0,
}); });
if (!auth.authChecked || !auth.isAuthenticated || !auth.settingsLoaded) { 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; return;
} }
@@ -187,7 +209,9 @@ export function useSettingsSync(): SettingsSyncState {
const api = getHttpApiClient(); const api = getHttpApiClient();
const appState = useAppStore.getState(); 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 // Build updates object from current state
const updates: Record<string, unknown> = {}; const updates: Record<string, unknown> = {};
@@ -204,17 +228,30 @@ export function useSettingsSync(): SettingsSyncState {
// Create a hash of the updates to avoid redundant syncs // Create a hash of the updates to avoid redundant syncs
const updateHash = JSON.stringify(updates); const updateHash = JSON.stringify(updates);
if (updateHash === lastSyncedRef.current) { 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 })); setState((s) => ({ ...s, syncing: false }));
return; 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); const result = await api.settings.updateGlobal(updates);
logger.info('[SYNC_RESPONSE] Server response:', { success: result.success });
if (result.success) { if (result.success) {
lastSyncedRef.current = updateHash; lastSyncedRef.current = updateHash;
logger.debug('Settings synced to server'); 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 { } else {
logger.error('Failed to sync settings:', result.error); logger.error('Failed to sync settings:', result.error);
} }
@@ -340,9 +377,24 @@ export function useSettingsSync(): SettingsSyncState {
return; 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; let changed = false;
for (const field of SETTINGS_FIELDS_TO_SYNC) { for (const field of SETTINGS_FIELDS_TO_SYNC) {
if (field === 'projects') continue; // Already handled above
if (hasSettingsFieldChanged(field, newState, prevState)) { if (hasSettingsFieldChanged(field, newState, prevState)) {
changed = true; changed = true;
break; break;

View File

@@ -185,7 +185,13 @@ export function getAuthenticatedImageUrl(
if (apiKey) { if (apiKey) {
params.set('apiKey', 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()}`; return `${serverUrl}/api/fs/image?${params.toString()}`;
} }

View File

@@ -156,6 +156,12 @@ const getServerUrl = (): string => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const envUrl = import.meta.env.VITE_SERVER_URL; const envUrl = import.meta.env.VITE_SERVER_URL;
if (envUrl) return envUrl; 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 // Use VITE_HOSTNAME if set, otherwise default to localhost
const hostname = import.meta.env.VITE_HOSTNAME || 'localhost'; const hostname = import.meta.env.VITE_HOSTNAME || 'localhost';
@@ -173,8 +179,24 @@ let apiKeyInitialized = false;
let apiKeyInitPromise: Promise<void> | null = null; let apiKeyInitPromise: Promise<void> | null = null;
// Cached session token for authentication (Web mode - explicit header auth) // 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; 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) // Get API key for Electron mode (returns cached value after initialization)
// Exported for use in WebSocket connections that need auth // 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) // Get session token for Web mode (returns cached value after login)
export const getSessionToken = (): string | null => cachedSessionToken; 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 => { export const setSessionToken = (token: string | null): void => {
cachedSessionToken = token; 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) // Clear session token (called on logout)
export const clearSessionToken = (): void => { export const clearSessionToken = (): void => {
cachedSessionToken = null; 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(process.resourcesPath, 'server')
: path.join(__dirname, '../../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) // Build enhanced PATH that includes Node.js directory (cross-platform)
const enhancedPath = buildEnhancedPath(command, process.env.PATH || ''); const enhancedPath = buildEnhancedPath(command, process.env.PATH || '');
if (enhancedPath !== process.env.PATH) { if (enhancedPath !== process.env.PATH) {
@@ -484,7 +495,7 @@ async function startServer(): Promise<void> {
...process.env, ...process.env,
PATH: enhancedPath, PATH: enhancedPath,
PORT: serverPort.toString(), PORT: serverPort.toString(),
DATA_DIR: app.getPath('userData'), DATA_DIR: dataDir,
NODE_PATH: serverNodeModules, NODE_PATH: serverNodeModules,
// Pass API key to server for CSRF protection // Pass API key to server for CSRF protection
AUTOMAKER_API_KEY: apiKey!, AUTOMAKER_API_KEY: apiKey!,
@@ -496,6 +507,7 @@ async function startServer(): Promise<void> {
}; };
logger.info('Server will use port', serverPort); 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('Starting backend server...');
logger.info('Server path:', serverPath); logger.info('Server path:', serverPath);
@@ -647,20 +659,44 @@ function createWindow(): void {
// App lifecycle // App lifecycle
app.whenReady().then(async () => { app.whenReady().then(async () => {
// Ensure userData path is consistent across dev/prod so files land in Automaker dir // In production, use Automaker dir in appData for app isolation
try { // In development, use project root for shared data between Electron and web mode
const desiredUserDataPath = path.join(app.getPath('appData'), 'Automaker'); let userDataPathToUse: string;
if (app.getPath('userData') !== desiredUserDataPath) {
app.setPath('userData', desiredUserDataPath); if (app.isPackaged) {
logger.info('userData path set to:', desiredUserDataPath); // 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 // Initialize centralized path helpers for Electron
// This must be done before any file operations // 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 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 // 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 // Initialize security settings for path validation
// Set DATA_DIR before initializing so it's available for security checks // 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 // 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) // (it will be passed to server process, but we also need it in main process for dialog validation)
initAllowedPaths(); initAllowedPaths();

View File

@@ -1504,7 +1504,16 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
moveProjectToTrash: (projectId) => { moveProjectToTrash: (projectId) => {
const project = get().projects.find((p) => p.id === 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 remainingProjects = get().projects.filter((p) => p.id !== projectId);
const existingTrash = get().trashedProjects.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 isCurrent = get().currentProject?.id === projectId;
const nextCurrentProject = isCurrent ? null : get().currentProject; const nextCurrentProject = isCurrent ? null : get().currentProject;
console.log('[MOVE_TO_TRASH] Updating store with new state:', {
newProjectCount: remainingProjects.length,
newTrashedCount: [trashedProject, ...existingTrash].length,
});
set({ set({
projects: remainingProjects, projects: remainingProjects,
trashedProjects: [trashedProject, ...existingTrash], trashedProjects: [trashedProject, ...existingTrash],

View File

@@ -85,7 +85,17 @@ test.describe('Open Project', () => {
// AND inject our test project into the projects list // AND inject our test project into the projects list
await page.route('**/api/settings/global', async (route) => { await page.route('**/api/settings/global', async (route) => {
const response = await route.fetch(); 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) { if (json.settings) {
// Remove currentProjectId to prevent restoring a project // Remove currentProjectId to prevent restoring a project
json.settings.currentProjectId = null; json.settings.currentProjectId = null;
@@ -106,8 +116,8 @@ test.describe('Open Project', () => {
} }
} }
await route.fulfill({ await route.fulfill({
status: response.status(), status: status,
headers: response.headers(), headers: headers,
json, json,
}); });
}); });

View File

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

View File

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

88
package-lock.json generated
View File

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

View File

@@ -1075,7 +1075,8 @@ case $MODE in
export TEST_PORT="$WEB_PORT" export TEST_PORT="$WEB_PORT"
export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT" export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT"
export PORT="$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" export VITE_APP_MODE="1"
if [ "$PRODUCTION_MODE" = true ]; then 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"
}