mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
Merge pull request #376 from AutoMaker-Org/electron-with-docker-api
feat: add support for external server mode with Docker integration
This commit is contained in:
@@ -131,6 +131,39 @@ export const isElectronMode = (): boolean => {
|
|||||||
return api?.isElectron === true || !!api?.getApiKey;
|
return api?.isElectron === true || !!api?.getApiKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cached external server mode flag
|
||||||
|
let cachedExternalServerMode: boolean | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if running in external server mode (Docker API)
|
||||||
|
* In this mode, Electron uses session-based auth like web mode
|
||||||
|
*/
|
||||||
|
export const checkExternalServerMode = async (): Promise<boolean> => {
|
||||||
|
if (cachedExternalServerMode !== null) {
|
||||||
|
return cachedExternalServerMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const api = window.electronAPI as any;
|
||||||
|
if (api?.isExternalServerMode) {
|
||||||
|
try {
|
||||||
|
cachedExternalServerMode = await api.isExternalServerMode();
|
||||||
|
return cachedExternalServerMode;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to check external server mode:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedExternalServerMode = false;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached external server mode (synchronous, returns null if not yet checked)
|
||||||
|
*/
|
||||||
|
export const isExternalServerMode = (): boolean | null => cachedExternalServerMode;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize API key and server URL for Electron mode authentication.
|
* Initialize API key and server URL for Electron mode authentication.
|
||||||
* In web mode, authentication uses HTTP-only cookies instead.
|
* In web mode, authentication uses HTTP-only cookies instead.
|
||||||
|
|||||||
@@ -132,6 +132,9 @@ let saveWindowBoundsTimeout: ReturnType<typeof setTimeout> | null = null;
|
|||||||
// API key for CSRF protection
|
// API key for CSRF protection
|
||||||
let apiKey: string | null = null;
|
let apiKey: string | null = null;
|
||||||
|
|
||||||
|
// Track if we're using an external server (Docker API mode)
|
||||||
|
let isExternalServerMode = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the relative path to API key file within userData
|
* Get the relative path to API key file within userData
|
||||||
*/
|
*/
|
||||||
@@ -688,15 +691,36 @@ app.whenReady().then(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if we should skip the embedded server (for Docker API mode)
|
||||||
|
const skipEmbeddedServer = process.env.SKIP_EMBEDDED_SERVER === 'true';
|
||||||
|
isExternalServerMode = skipEmbeddedServer;
|
||||||
|
|
||||||
|
if (skipEmbeddedServer) {
|
||||||
|
// Use the default server port (Docker container runs on 3008)
|
||||||
|
serverPort = DEFAULT_SERVER_PORT;
|
||||||
|
logger.info('SKIP_EMBEDDED_SERVER=true, using external server at port', 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)
|
// Generate or load API key for CSRF protection (before starting server)
|
||||||
ensureApiKey();
|
ensureApiKey();
|
||||||
|
|
||||||
try {
|
|
||||||
// Find available ports (prevents conflicts with other apps using same ports)
|
// Find available ports (prevents conflicts with other apps using same ports)
|
||||||
serverPort = await findAvailablePort(DEFAULT_SERVER_PORT);
|
serverPort = await findAvailablePort(DEFAULT_SERVER_PORT);
|
||||||
if (serverPort !== DEFAULT_SERVER_PORT) {
|
if (serverPort !== DEFAULT_SERVER_PORT) {
|
||||||
logger.info('Default server port', DEFAULT_SERVER_PORT, 'in use, using port', serverPort);
|
logger.info('Default server port', DEFAULT_SERVER_PORT, 'in use, using port', serverPort);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
staticPort = await findAvailablePort(DEFAULT_STATIC_PORT);
|
staticPort = await findAvailablePort(DEFAULT_STATIC_PORT);
|
||||||
if (staticPort !== DEFAULT_STATIC_PORT) {
|
if (staticPort !== DEFAULT_STATIC_PORT) {
|
||||||
@@ -708,8 +732,10 @@ app.whenReady().then(async () => {
|
|||||||
await startStaticServer();
|
await startStaticServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start backend server
|
// Start backend server (unless using external server)
|
||||||
|
if (!skipEmbeddedServer) {
|
||||||
await startServer();
|
await startServer();
|
||||||
|
}
|
||||||
|
|
||||||
// Create window
|
// Create window
|
||||||
createWindow();
|
createWindow();
|
||||||
@@ -909,10 +935,20 @@ ipcMain.handle('server:getUrl', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Get API key for authentication
|
// Get API key for authentication
|
||||||
|
// Returns null in external server mode to trigger session-based auth
|
||||||
ipcMain.handle('auth:getApiKey', () => {
|
ipcMain.handle('auth:getApiKey', () => {
|
||||||
|
if (isExternalServerMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return apiKey;
|
return apiKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if running in external server mode (Docker API)
|
||||||
|
// Used by renderer to determine auth flow
|
||||||
|
ipcMain.handle('auth:isExternalServerMode', () => {
|
||||||
|
return isExternalServerMode;
|
||||||
|
});
|
||||||
|
|
||||||
// Window management - update minimum width based on sidebar state
|
// Window management - update minimum width based on sidebar state
|
||||||
// Now uses a fixed small minimum since horizontal scrolling handles overflow
|
// Now uses a fixed small minimum since horizontal scrolling handles overflow
|
||||||
ipcMain.handle('window:updateMinWidth', (_, _sidebarExpanded: boolean) => {
|
ipcMain.handle('window:updateMinWidth', (_, _sidebarExpanded: boolean) => {
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
// Get API key for authentication
|
// Get API key for authentication
|
||||||
getApiKey: (): Promise<string | null> => ipcRenderer.invoke('auth:getApiKey'),
|
getApiKey: (): Promise<string | null> => ipcRenderer.invoke('auth:getApiKey'),
|
||||||
|
|
||||||
|
// Check if running in external server mode (Docker API)
|
||||||
|
isExternalServerMode: (): Promise<boolean> => ipcRenderer.invoke('auth:isExternalServerMode'),
|
||||||
|
|
||||||
// Native dialogs - better UX than prompt()
|
// Native dialogs - better UX than prompt()
|
||||||
openDirectory: (): Promise<Electron.OpenDialogReturnValue> =>
|
openDirectory: (): Promise<Electron.OpenDialogReturnValue> =>
|
||||||
ipcRenderer.invoke('dialog:openDirectory'),
|
ipcRenderer.invoke('dialog:openDirectory'),
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import {
|
|||||||
verifySession,
|
verifySession,
|
||||||
checkSandboxEnvironment,
|
checkSandboxEnvironment,
|
||||||
getServerUrlSync,
|
getServerUrlSync,
|
||||||
|
checkExternalServerMode,
|
||||||
|
isExternalServerMode,
|
||||||
} from '@/lib/http-api-client';
|
} from '@/lib/http-api-client';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||||
@@ -188,13 +190,16 @@ function RootLayoutContent() {
|
|||||||
// Initialize API key for Electron mode
|
// Initialize API key for Electron mode
|
||||||
await initApiKey();
|
await initApiKey();
|
||||||
|
|
||||||
// In Electron mode, we're always authenticated via header
|
// Check if running in external server mode (Docker API)
|
||||||
if (isElectronMode()) {
|
const externalMode = await checkExternalServerMode();
|
||||||
|
|
||||||
|
// In Electron mode (but NOT external server mode), we're always authenticated via header
|
||||||
|
if (isElectronMode() && !externalMode) {
|
||||||
useAuthStore.getState().setAuthState({ isAuthenticated: true, authChecked: true });
|
useAuthStore.getState().setAuthState({ isAuthenticated: true, authChecked: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In web mode, verify the session cookie is still valid
|
// In web mode OR external server mode, verify the session cookie is still valid
|
||||||
// by making a request to an authenticated endpoint
|
// by making a request to an authenticated endpoint
|
||||||
const isValid = await verifySession();
|
const isValid = await verifySession();
|
||||||
|
|
||||||
@@ -235,17 +240,20 @@ function RootLayoutContent() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Routing rules (web mode):
|
// Routing rules (web mode and external server mode):
|
||||||
// - If not authenticated: force /login (even /setup is protected)
|
// - If not authenticated: force /login (even /setup is protected)
|
||||||
// - If authenticated but setup incomplete: force /setup
|
// - If authenticated but setup incomplete: force /setup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!setupHydrated) return;
|
if (!setupHydrated) return;
|
||||||
|
|
||||||
|
// Check if we need session-based auth (web mode OR external server mode)
|
||||||
|
const needsSessionAuth = !isElectronMode() || isExternalServerMode() === true;
|
||||||
|
|
||||||
// Wait for auth check to complete before enforcing any redirects
|
// Wait for auth check to complete before enforcing any redirects
|
||||||
if (!isElectronMode() && !authChecked) return;
|
if (needsSessionAuth && !authChecked) return;
|
||||||
|
|
||||||
// Unauthenticated -> force /login
|
// Unauthenticated -> force /login
|
||||||
if (!isElectronMode() && !isAuthenticated) {
|
if (needsSessionAuth && !isAuthenticated) {
|
||||||
if (location.pathname !== '/login') {
|
if (location.pathname !== '/login') {
|
||||||
navigate({ to: '/login' });
|
navigate({ to: '/login' });
|
||||||
}
|
}
|
||||||
@@ -351,8 +359,11 @@ function RootLayoutContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for auth check before rendering protected routes (web mode only)
|
// Check if we need session-based auth (web mode OR external server mode)
|
||||||
if (!isElectronMode() && !authChecked) {
|
const needsSessionAuth = !isElectronMode() || isExternalServerMode() === true;
|
||||||
|
|
||||||
|
// Wait for auth check before rendering protected routes (web mode and external server mode)
|
||||||
|
if (needsSessionAuth && !authChecked) {
|
||||||
return (
|
return (
|
||||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||||
<LoadingState message="Loading..." />
|
<LoadingState message="Loading..." />
|
||||||
@@ -360,9 +371,9 @@ function RootLayoutContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to login if not authenticated (web mode)
|
// Redirect to login if not authenticated (web mode and external server mode)
|
||||||
// Show loading state while navigation to login is in progress
|
// Show loading state while navigation to login is in progress
|
||||||
if (!isElectronMode() && !isAuthenticated) {
|
if (needsSessionAuth && !isAuthenticated) {
|
||||||
return (
|
return (
|
||||||
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||||
<LoadingState message="Redirecting to login..." />
|
<LoadingState message="Redirecting to login..." />
|
||||||
|
|||||||
9
dev.mjs
9
dev.mjs
@@ -27,6 +27,7 @@ import {
|
|||||||
ensureDependencies,
|
ensureDependencies,
|
||||||
prompt,
|
prompt,
|
||||||
launchDockerDevContainers,
|
launchDockerDevContainers,
|
||||||
|
launchDockerDevServerContainer,
|
||||||
} from './scripts/launcher-utils.mjs';
|
} from './scripts/launcher-utils.mjs';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
@@ -96,7 +97,7 @@ async function main() {
|
|||||||
|
|
||||||
// Prompt for choice
|
// Prompt for choice
|
||||||
while (true) {
|
while (true) {
|
||||||
const choice = await prompt('Enter your choice (1, 2, or 3): ');
|
const choice = await prompt('Enter your choice (1, 2, 3, or 4): ');
|
||||||
|
|
||||||
if (choice === '1') {
|
if (choice === '1') {
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -172,8 +173,12 @@ async function main() {
|
|||||||
console.log('');
|
console.log('');
|
||||||
await launchDockerDevContainers({ baseDir: __dirname, processes });
|
await launchDockerDevContainers({ baseDir: __dirname, processes });
|
||||||
break;
|
break;
|
||||||
|
} else if (choice === '4') {
|
||||||
|
console.log('');
|
||||||
|
await launchDockerDevServerContainer({ baseDir: __dirname, processes });
|
||||||
|
break;
|
||||||
} else {
|
} else {
|
||||||
log('Invalid choice. Please enter 1, 2, or 3.', 'red');
|
log('Invalid choice. Please enter 1, 2, 3, or 4.', 'red');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
103
docker-compose.dev-server.yml
Normal file
103
docker-compose.dev-server.yml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Automaker Docker Compose - Server Only (Development Mode)
|
||||||
|
# Runs only the backend API in a container for use with local Electron.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker compose -f docker-compose.dev-server.yml up
|
||||||
|
# Then run Electron locally which connects to http://localhost:3008
|
||||||
|
#
|
||||||
|
# This mode:
|
||||||
|
# - Runs only the backend server in a container
|
||||||
|
# - Mounts source code as volumes (live reload)
|
||||||
|
# - Server runs with tsx watch for TypeScript changes
|
||||||
|
# - Electron runs locally on host machine
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Development server (backend API only)
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
container_name: automaker-dev-server-only
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- '3008:3008'
|
||||||
|
environment:
|
||||||
|
# Required
|
||||||
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||||
|
|
||||||
|
# Optional - Claude CLI OAuth credentials
|
||||||
|
- CLAUDE_OAUTH_CREDENTIALS=${CLAUDE_OAUTH_CREDENTIALS:-}
|
||||||
|
|
||||||
|
# Optional - Cursor CLI OAuth token
|
||||||
|
- CURSOR_AUTH_TOKEN=${CURSOR_AUTH_TOKEN:-}
|
||||||
|
|
||||||
|
# Optional - authentication
|
||||||
|
- AUTOMAKER_API_KEY=${AUTOMAKER_API_KEY:-}
|
||||||
|
|
||||||
|
# Development settings
|
||||||
|
- NODE_ENV=development
|
||||||
|
- PORT=3008
|
||||||
|
- CORS_ORIGIN=http://localhost:3007
|
||||||
|
|
||||||
|
# Optional - restrict to specific directory within container
|
||||||
|
- ALLOWED_ROOT_DIRECTORY=${ALLOWED_ROOT_DIRECTORY:-/projects}
|
||||||
|
- DATA_DIR=/data
|
||||||
|
|
||||||
|
# Internal - indicates containerized environment
|
||||||
|
- IS_CONTAINERIZED=true
|
||||||
|
volumes:
|
||||||
|
# Mount source code for live reload
|
||||||
|
- .:/app:cached
|
||||||
|
|
||||||
|
# Use named volume for node_modules to avoid platform conflicts
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Persist CLI configurations
|
||||||
|
- automaker-claude-config:/home/automaker/.claude
|
||||||
|
- automaker-cursor-config:/home/automaker/.cursor
|
||||||
|
|
||||||
|
# Note: Workspace mount (/projects) comes from docker-compose.override.yml
|
||||||
|
|
||||||
|
# Install deps, build packages, then start server in watch mode
|
||||||
|
# Note: We override the entrypoint to handle permissions properly
|
||||||
|
entrypoint: /bin/sh
|
||||||
|
command:
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
# Fix permissions on node_modules (created as root by Docker volume)
|
||||||
|
echo 'Fixing node_modules permissions...'
|
||||||
|
chown -R automaker:automaker /app/node_modules 2>/dev/null || true
|
||||||
|
|
||||||
|
# Run the rest as automaker user
|
||||||
|
exec gosu automaker sh -c "
|
||||||
|
echo 'Installing dependencies...' &&
|
||||||
|
npm install &&
|
||||||
|
echo 'Building shared packages...' &&
|
||||||
|
npm run build:packages &&
|
||||||
|
echo 'Starting server in development mode...' &&
|
||||||
|
npm run _dev:server
|
||||||
|
"
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD', 'curl', '-f', 'http://localhost:3008/api/health']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
automaker-dev-node-modules:
|
||||||
|
name: automaker-dev-node-modules
|
||||||
|
# Named volume for container-specific node_modules
|
||||||
|
|
||||||
|
automaker-data:
|
||||||
|
name: automaker-data
|
||||||
|
|
||||||
|
automaker-claude-config:
|
||||||
|
name: automaker-claude-config
|
||||||
|
|
||||||
|
automaker-cursor-config:
|
||||||
|
name: automaker-cursor-config
|
||||||
@@ -500,6 +500,7 @@ export function printModeMenu({ isDev = false } = {}) {
|
|||||||
console.log(' 2) Desktop Application (Electron)');
|
console.log(' 2) Desktop Application (Electron)');
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
console.log(' 3) Docker Container (Dev with Live Reload)');
|
console.log(' 3) Docker Container (Dev with Live Reload)');
|
||||||
|
console.log(' 4) Electron + Docker API (Local Electron, Container API)');
|
||||||
} else {
|
} else {
|
||||||
console.log(' 3) Docker Container (Isolated)');
|
console.log(' 3) Docker Container (Isolated)');
|
||||||
}
|
}
|
||||||
@@ -883,6 +884,123 @@ export async function launchDockerDevContainers({ baseDir, processes }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch only the Docker server container for use with local Electron
|
||||||
|
* Uses docker-compose.dev-server.yml which only runs the backend API
|
||||||
|
* Also includes docker-compose.override.yml if it exists (for workspace mounts)
|
||||||
|
* Automatically launches Electron once the server is healthy.
|
||||||
|
* @param {object} options - Configuration options
|
||||||
|
* @param {string} options.baseDir - Base directory containing docker-compose.dev-server.yml
|
||||||
|
* @param {object} options.processes - Processes object to track docker process
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function launchDockerDevServerContainer({ baseDir, processes }) {
|
||||||
|
log('Launching Docker Server Container + Local Electron...', 'blue');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Check if ANTHROPIC_API_KEY is set
|
||||||
|
if (!process.env.ANTHROPIC_API_KEY) {
|
||||||
|
log('Warning: ANTHROPIC_API_KEY environment variable is not set.', 'yellow');
|
||||||
|
log('The server will require an API key to function.', 'yellow');
|
||||||
|
log('Set it with: export ANTHROPIC_API_KEY=your-key', 'yellow');
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Starting server container...', 'yellow');
|
||||||
|
log('Source code is volume mounted for live reload', 'yellow');
|
||||||
|
log('Running npm install inside container (this may take a moment on first run)...', 'yellow');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Build compose file arguments
|
||||||
|
// Start with dev-server compose file, then add override if it exists
|
||||||
|
const composeArgs = ['compose', '-f', 'docker-compose.dev-server.yml'];
|
||||||
|
|
||||||
|
// Check if docker-compose.override.yml exists and include it for workspace mounts
|
||||||
|
const overridePath = path.join(baseDir, 'docker-compose.override.yml');
|
||||||
|
if (fsNative.existsSync(overridePath)) {
|
||||||
|
composeArgs.push('-f', 'docker-compose.override.yml');
|
||||||
|
log('Using docker-compose.override.yml for workspace mount', 'yellow');
|
||||||
|
}
|
||||||
|
|
||||||
|
composeArgs.push('up', '--build');
|
||||||
|
|
||||||
|
// Use docker-compose.dev-server.yml for server-only development
|
||||||
|
// Run with piped stdio so we can still see output but also run Electron
|
||||||
|
processes.docker = crossSpawn('docker', composeArgs, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: baseDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
log('Server container starting...', 'blue');
|
||||||
|
log('API will be available at: http://localhost:3008', 'green');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Wait for the server to become healthy
|
||||||
|
log('Waiting for server to be ready...', 'yellow');
|
||||||
|
const serverPort = 3008;
|
||||||
|
const maxRetries = 120; // 2 minutes (first run may need npm install + build)
|
||||||
|
let serverReady = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
if (await checkHealth(serverPort)) {
|
||||||
|
serverReady = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await sleep(1000);
|
||||||
|
// Show progress dots every 5 seconds
|
||||||
|
if (i > 0 && i % 5 === 0) {
|
||||||
|
process.stdout.write('.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serverReady) {
|
||||||
|
console.log('');
|
||||||
|
log('Error: Server container failed to become healthy', 'red');
|
||||||
|
log('Check the Docker logs above for errors', 'red');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
log('Server is ready! Launching Electron...', 'green');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Build shared packages before launching Electron
|
||||||
|
log('Building shared packages...', 'blue');
|
||||||
|
try {
|
||||||
|
await runNpmAndWait(['run', 'build:packages'], { stdio: 'inherit' }, baseDir);
|
||||||
|
} catch (error) {
|
||||||
|
log('Failed to build packages: ' + error.message, 'red');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch Electron with SKIP_EMBEDDED_SERVER=true
|
||||||
|
// This tells Electron to connect to the external Docker server instead of starting its own
|
||||||
|
processes.electron = crossSpawn('npm', ['run', '_dev:electron'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
cwd: baseDir,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
SKIP_EMBEDDED_SERVER: 'true',
|
||||||
|
PORT: '3008',
|
||||||
|
VITE_SERVER_URL: 'http://localhost:3008',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
log('Electron launched with SKIP_EMBEDDED_SERVER=true', 'green');
|
||||||
|
log('Changes to server source files will automatically reload.', 'yellow');
|
||||||
|
log('Press Ctrl+C to stop both Electron and the container.', 'yellow');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Wait for either process to exit
|
||||||
|
await Promise.race([
|
||||||
|
new Promise((resolve) => processes.docker.on('close', resolve)),
|
||||||
|
new Promise((resolve) => processes.electron.on('close', resolve)),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launch Docker containers with docker-compose (production mode)
|
* Launch Docker containers with docker-compose (production mode)
|
||||||
* Uses git commit SHA to determine if rebuild is needed
|
* Uses git commit SHA to determine if rebuild is needed
|
||||||
|
|||||||
Reference in New Issue
Block a user