File size: 8,904 Bytes
1dbc34b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
/**
 * Electron main process entry point
 *
 * Handles app lifecycle, initialization, and coordination of modular components.
 *
 * Architecture:
 * - electron/constants.ts      - Window sizing, port defaults, filenames
 * - electron/state.ts          - Shared state container
 * - electron/utils/            - Port and icon utilities
 * - electron/security/         - API key management
 * - electron/windows/          - Window bounds and main window creation
 * - electron/server/           - Backend and static server management
 * - electron/ipc/              - IPC handlers (dialog, shell, app, auth, window, server)
 *
 * SECURITY: All file system access uses centralized methods from @automaker/platform.
 */

import path from 'path';
import { app, BrowserWindow, dialog } from 'electron';
import {
  setElectronUserDataPath,
  setElectronAppPaths,
  initAllowedPaths,
} from '@automaker/platform';
import { createLogger } from '@automaker/utils/logger';
import { DEFAULT_SERVER_PORT, DEFAULT_STATIC_PORT } from './electron/constants';
import { state } from './electron/state';
import { findAvailablePort } from './electron/utils/port-manager';
import { getIconPath } from './electron/utils/icon-manager';
import { ensureApiKey } from './electron/security/api-key-manager';
import { createWindow } from './electron/windows/main-window';
import { startStaticServer, stopStaticServer } from './electron/server/static-server';
import { startServer, waitForServer, stopServer } from './electron/server/backend-server';
import { registerAllHandlers } from './electron/ipc';

const logger = createLogger('Electron');

// Development environment
const isDev = !app.isPackaged;

// Load environment variables from .env file (development only)
if (isDev) {
  try {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    require('dotenv').config({ path: path.join(__dirname, '../.env') });
  } catch (error) {
    logger.warn('dotenv not available:', (error as Error).message);
  }
}

// On Linux, auto-detect X11 vs Wayland so the app launches correctly from
// desktop entries where the display protocol isn't guaranteed to be X11.
// Must be set before app.whenReady() — has no effect on macOS/Windows.
if (process.platform === 'linux') {
  app.commandLine.appendSwitch('ozone-platform-hint', 'auto');
}

// Register IPC handlers
registerAllHandlers();

// App lifecycle
app.whenReady().then(handleAppReady);
app.on('window-all-closed', handleWindowAllClosed);
app.on('before-quit', handleBeforeQuit);

/**
 * Handle app.whenReady()
 */
async function handleAppReady(): Promise<void> {
  // 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');
    }
  }

  // Initialize centralized path helpers for Electron
  // This must be done before any file operations
  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
  if (isDev) {
    // __dirname is apps/ui/dist-electron, so go up 3 levels to get project root
    const projectRoot = path.join(__dirname, '../../..');
    setElectronAppPaths([__dirname, projectRoot]);
  } else {
    setElectronAppPaths(__dirname, process.resourcesPath);
  }

  logger.info('Initialized path security helpers');

  // Initialize security settings for path validation
  // Set DATA_DIR before initializing so it's available for security checks
  // 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();

  if (process.platform === 'darwin' && app.dock) {
    const iconPath = getIconPath();
    if (iconPath) {
      try {
        app.dock.setIcon(iconPath);
      } catch (error) {
        logger.warn('Failed to set dock icon:', (error as Error).message);
      }
    }
  }

  try {
    // Check if we should skip the embedded server (for Docker API mode)
    const skipEmbeddedServer = process.env.SKIP_EMBEDDED_SERVER === 'true';
    state.isExternalServerMode = skipEmbeddedServer;

    if (skipEmbeddedServer) {
      // Use the default server port (Docker container runs on 3008)
      state.serverPort = DEFAULT_SERVER_PORT;
      logger.info('SKIP_EMBEDDED_SERVER=true, using external server at port', state.serverPort);

      // Wait for external server to be ready
      logger.info('Waiting for external server...');
      await waitForServer(60); // Give Docker container more time to start
      logger.info('External server is ready');

      // In external server mode, we don't set an API key here.
      // The renderer will detect external server mode and use session-based
      // auth like web mode, redirecting to /login where the user enters
      // the API key from the Docker container logs.
      logger.info('External server mode: using session-based authentication');
    } else {
      // Generate or load API key for CSRF protection (before starting server)
      ensureApiKey();

      // Find available ports (prevents conflicts with other apps using same ports)
      state.serverPort = await findAvailablePort(DEFAULT_SERVER_PORT);
      if (state.serverPort !== DEFAULT_SERVER_PORT) {
        logger.info(
          'Default server port',
          DEFAULT_SERVER_PORT,
          'in use, using port',
          state.serverPort
        );
      }
    }

    state.staticPort = await findAvailablePort(DEFAULT_STATIC_PORT);
    if (state.staticPort !== DEFAULT_STATIC_PORT) {
      logger.info(
        'Default static port',
        DEFAULT_STATIC_PORT,
        'in use, using port',
        state.staticPort
      );
    }

    // Start static file server in production
    if (app.isPackaged) {
      await startStaticServer();
    }

    // Start backend server (unless using external server)
    if (!skipEmbeddedServer) {
      await startServer();
    }

    // Create window
    createWindow();
  } catch (error) {
    logger.error('Failed to start:', error);

    const errorMessage = (error as Error).message;
    const isNodeError = errorMessage.includes('Node.js');

    dialog.showErrorBox(
      'Automaker Failed to Start',
      `The application failed to start.\n\n${errorMessage}\n\n${
        isNodeError
          ? 'Please install Node.js from https://nodejs.org or via a package manager (Homebrew, nvm, fnm).'
          : 'Please check the application logs for more details.'
      }`
    );
    app.quit();
  }

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
}

/**
 * Handle window-all-closed event
 */
function handleWindowAllClosed(): void {
  // On macOS, keep the app and servers running when all windows are closed
  // (standard macOS behavior). On other platforms, stop servers and quit.
  if (process.platform !== 'darwin') {
    stopServer();
    stopStaticServer();
    app.quit();
  }
}

/**
 * Handle before-quit event
 */
function handleBeforeQuit(): void {
  stopServer();
  stopStaticServer();
}