mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
Merge pull request #65 from AutoMaker-Org/fix-electron-build
feat: enhance Electron build process and server preparation
This commit is contained in:
1
apps/app/.gitignore
vendored
1
apps/app/.gitignore
vendored
@@ -48,3 +48,4 @@ next-env.d.ts
|
|||||||
|
|
||||||
# Electron
|
# Electron
|
||||||
/dist/
|
/dist/
|
||||||
|
/server-bundle/
|
||||||
|
|||||||
@@ -8,21 +8,114 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { spawn } = require("child_process");
|
const { spawn } = require("child_process");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const http = require("http");
|
||||||
// Load environment variables from .env file
|
|
||||||
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
||||||
|
|
||||||
|
// Load environment variables from .env file (development only)
|
||||||
|
if (!app.isPackaged) {
|
||||||
|
try {
|
||||||
|
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Electron] dotenv not available:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
let serverProcess = null;
|
let serverProcess = null;
|
||||||
|
let staticServer = null;
|
||||||
const SERVER_PORT = 3008;
|
const SERVER_PORT = 3008;
|
||||||
|
const STATIC_PORT = 3007;
|
||||||
|
|
||||||
// Get icon path - works in both dev and production
|
// Get icon path - works in both dev and production, cross-platform
|
||||||
function getIconPath() {
|
function getIconPath() {
|
||||||
return app.isPackaged
|
// Different icon formats for different platforms
|
||||||
? path.join(process.resourcesPath, "app", "public", "logo.png")
|
let iconFile;
|
||||||
: path.join(__dirname, "../public/logo.png");
|
if (process.platform === "win32") {
|
||||||
|
iconFile = "icon.ico";
|
||||||
|
} else if (process.platform === "darwin") {
|
||||||
|
iconFile = "logo_larger.png";
|
||||||
|
} else {
|
||||||
|
// Linux
|
||||||
|
iconFile = "logo_larger.png";
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconPath = path.join(__dirname, "../public", iconFile);
|
||||||
|
|
||||||
|
// Verify the icon exists
|
||||||
|
if (!fs.existsSync(iconPath)) {
|
||||||
|
console.warn(`[Electron] Icon not found at: ${iconPath}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iconPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start static file server for production builds
|
||||||
|
*/
|
||||||
|
async function startStaticServer() {
|
||||||
|
const staticPath = path.join(__dirname, "../out");
|
||||||
|
|
||||||
|
staticServer = http.createServer((request, response) => {
|
||||||
|
// Parse the URL and remove query string
|
||||||
|
let filePath = path.join(staticPath, request.url.split("?")[0]);
|
||||||
|
|
||||||
|
// Default to index.html for directory requests
|
||||||
|
if (filePath.endsWith("/")) {
|
||||||
|
filePath = path.join(filePath, "index.html");
|
||||||
|
} else if (!path.extname(filePath)) {
|
||||||
|
filePath += ".html";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
fs.stat(filePath, (err, stats) => {
|
||||||
|
if (err || !stats.isFile()) {
|
||||||
|
// Try index.html for SPA fallback
|
||||||
|
filePath = path.join(staticPath, "index.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and serve the file
|
||||||
|
fs.readFile(filePath, (error, content) => {
|
||||||
|
if (error) {
|
||||||
|
response.writeHead(500);
|
||||||
|
response.end("Server Error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set content type based on file extension
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const contentTypes = {
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "application/javascript",
|
||||||
|
".css": "text/css",
|
||||||
|
".json": "application/json",
|
||||||
|
".png": "image/png",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".ico": "image/x-icon",
|
||||||
|
".woff": "font/woff",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
".ttf": "font/ttf",
|
||||||
|
".eot": "application/vnd.ms-fontobject",
|
||||||
|
};
|
||||||
|
|
||||||
|
response.writeHead(200, { "Content-Type": contentTypes[ext] || "application/octet-stream" });
|
||||||
|
response.end(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
staticServer.listen(STATIC_PORT, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
} else {
|
||||||
|
console.log(`[Electron] Static server running at http://localhost:${STATIC_PORT}`);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,18 +156,45 @@ async function startServer() {
|
|||||||
command = "node";
|
command = "node";
|
||||||
serverPath = path.join(process.resourcesPath, "server", "index.js");
|
serverPath = path.join(process.resourcesPath, "server", "index.js");
|
||||||
args = [serverPath];
|
args = [serverPath];
|
||||||
|
|
||||||
|
// Verify server files exist
|
||||||
|
if (!fs.existsSync(serverPath)) {
|
||||||
|
throw new Error(`Server not found at: ${serverPath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set environment variables for server
|
// Set environment variables for server
|
||||||
|
const serverNodeModules = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, "server", "node_modules")
|
||||||
|
: path.join(__dirname, "../../server/node_modules");
|
||||||
|
|
||||||
|
// Set default workspace directory to user's Documents/Automaker
|
||||||
|
const defaultWorkspaceDir = path.join(app.getPath("documents"), "Automaker");
|
||||||
|
|
||||||
|
// Ensure workspace directory exists
|
||||||
|
if (!fs.existsSync(defaultWorkspaceDir)) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(defaultWorkspaceDir, { recursive: true });
|
||||||
|
console.log("[Electron] Created workspace directory:", defaultWorkspaceDir);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Electron] Failed to create workspace directory:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
...process.env,
|
...process.env,
|
||||||
PORT: SERVER_PORT.toString(),
|
PORT: SERVER_PORT.toString(),
|
||||||
DATA_DIR: app.getPath("userData"),
|
DATA_DIR: app.getPath("userData"),
|
||||||
|
NODE_PATH: serverNodeModules,
|
||||||
|
WORKSPACE_DIR: process.env.WORKSPACE_DIR || defaultWorkspaceDir,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[Electron] Starting backend server...");
|
console.log("[Electron] Starting backend server...");
|
||||||
|
console.log("[Electron] Server path:", serverPath);
|
||||||
|
console.log("[Electron] NODE_PATH:", serverNodeModules);
|
||||||
|
|
||||||
serverProcess = spawn(command, args, {
|
serverProcess = spawn(command, args, {
|
||||||
|
cwd: path.dirname(serverPath),
|
||||||
env,
|
env,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
@@ -92,6 +212,11 @@ async function startServer() {
|
|||||||
serverProcess = null;
|
serverProcess = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
serverProcess.on("error", (err) => {
|
||||||
|
console.error(`[Server] Failed to start server process:`, err);
|
||||||
|
serverProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
// Wait for server to be ready
|
// Wait for server to be ready
|
||||||
await waitForServer();
|
await waitForServer();
|
||||||
}
|
}
|
||||||
@@ -132,12 +257,12 @@ async function waitForServer(maxAttempts = 30) {
|
|||||||
* Create the main window
|
* Create the main window
|
||||||
*/
|
*/
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
const iconPath = getIconPath();
|
||||||
|
const windowOptions = {
|
||||||
width: 1400,
|
width: 1400,
|
||||||
height: 900,
|
height: 900,
|
||||||
minWidth: 1024,
|
minWidth: 1024,
|
||||||
minHeight: 700,
|
minHeight: 700,
|
||||||
icon: getIconPath(),
|
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, "preload.js"),
|
preload: path.join(__dirname, "preload.js"),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
@@ -145,17 +270,20 @@ function createWindow() {
|
|||||||
},
|
},
|
||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: "hiddenInset",
|
||||||
backgroundColor: "#0a0a0a",
|
backgroundColor: "#0a0a0a",
|
||||||
});
|
};
|
||||||
|
|
||||||
// Load Next.js dev server in development or production build
|
// Only set icon if it exists
|
||||||
|
if (iconPath) {
|
||||||
|
windowOptions.icon = iconPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow = new BrowserWindow(windowOptions);
|
||||||
|
|
||||||
|
// Load Next.js dev server in development or static server in production
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
if (isDev) {
|
mainWindow.loadURL(`http://localhost:${STATIC_PORT}`);
|
||||||
mainWindow.loadURL("http://localhost:3007");
|
if (isDev && process.env.OPEN_DEVTOOLS === "true") {
|
||||||
if (process.env.OPEN_DEVTOOLS === "true") {
|
mainWindow.webContents.openDevTools();
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
mainWindow.on("closed", () => {
|
||||||
@@ -173,10 +301,22 @@ function createWindow() {
|
|||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
// Set app icon (dock icon on macOS)
|
// Set app icon (dock icon on macOS)
|
||||||
if (process.platform === "darwin" && app.dock) {
|
if (process.platform === "darwin" && app.dock) {
|
||||||
app.dock.setIcon(getIconPath());
|
const iconPath = getIconPath();
|
||||||
|
if (iconPath) {
|
||||||
|
try {
|
||||||
|
app.dock.setIcon(iconPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Electron] Failed to set dock icon:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Start static file server in production
|
||||||
|
if (app.isPackaged) {
|
||||||
|
await startStaticServer();
|
||||||
|
}
|
||||||
|
|
||||||
// Start backend server
|
// Start backend server
|
||||||
await startServer();
|
await startServer();
|
||||||
|
|
||||||
@@ -207,6 +347,13 @@ app.on("before-quit", () => {
|
|||||||
serverProcess.kill();
|
serverProcess.kill();
|
||||||
serverProcess = null;
|
serverProcess = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close static server
|
||||||
|
if (staticServer) {
|
||||||
|
console.log("[Electron] Stopping static server...");
|
||||||
|
staticServer.close();
|
||||||
|
staticServer = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
output: "export",
|
||||||
env: {
|
env: {
|
||||||
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
CLAUDE_CODE_OAUTH_TOKEN: process.env.CLAUDE_CODE_OAUTH_TOKEN || "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,7 +20,10 @@
|
|||||||
"dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"",
|
"dev:electron": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && electron .\"",
|
||||||
"dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"",
|
"dev:electron:debug": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && OPEN_DEVTOOLS=true electron .\"",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"build:electron": "next build && electron-builder",
|
"build:electron": "node scripts/prepare-server.js && next build && electron-builder",
|
||||||
|
"build:electron:win": "node scripts/prepare-server.js && next build && electron-builder --win",
|
||||||
|
"build:electron:mac": "node scripts/prepare-server.js && next build && electron-builder --mac",
|
||||||
|
"build:electron:linux": "node scripts/prepare-server.js && next build && electron-builder --linux",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint",
|
"lint": "eslint",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
@@ -79,35 +82,46 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"electron": "^39.2.6",
|
"electron": "39.2.7",
|
||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.0.7",
|
"eslint-config-next": "16.0.7",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5",
|
"typescript": "5.9.3",
|
||||||
"wait-on": "^9.0.3"
|
"wait-on": "^9.0.3"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "com.automaker.app",
|
"appId": "com.automaker.app",
|
||||||
"productName": "Automaker",
|
"productName": "Automaker",
|
||||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||||
|
"afterPack": "./scripts/rebuild-server-natives.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist"
|
"output": "dist"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"electron/**/*",
|
"electron/**/*",
|
||||||
".next/**/*",
|
"out/**/*",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
"!node_modules/**/*"
|
"!node_modules/**/*"
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": ".env",
|
"from": "server-bundle/dist",
|
||||||
|
"to": "server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "server-bundle/node_modules",
|
||||||
|
"to": "server/node_modules"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "server-bundle/package.json",
|
||||||
|
"to": "server/package.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "../../.env",
|
||||||
"to": ".env",
|
"to": ".env",
|
||||||
"filter": [
|
"filter": ["**/*"]
|
||||||
"**/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
@@ -139,7 +153,7 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "public/logo_larger.png"
|
"icon": "public/icon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"target": [
|
||||||
|
|||||||
BIN
apps/app/public/icon.ico
Normal file
BIN
apps/app/public/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 279 KiB |
81
apps/app/scripts/prepare-server.js
Normal file
81
apps/app/scripts/prepare-server.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script prepares the server for bundling with Electron.
|
||||||
|
* It copies the server dist and installs production dependencies
|
||||||
|
* in a way that works with npm workspaces.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const APP_DIR = join(__dirname, '..');
|
||||||
|
const SERVER_DIR = join(APP_DIR, '..', 'server');
|
||||||
|
const BUNDLE_DIR = join(APP_DIR, 'server-bundle');
|
||||||
|
|
||||||
|
console.log('🔧 Preparing server for Electron bundling...\n');
|
||||||
|
|
||||||
|
// Step 1: Clean up previous bundle
|
||||||
|
if (existsSync(BUNDLE_DIR)) {
|
||||||
|
console.log('🗑️ Cleaning previous server-bundle...');
|
||||||
|
rmSync(BUNDLE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
mkdirSync(BUNDLE_DIR, { recursive: true });
|
||||||
|
|
||||||
|
// Step 2: Build the server TypeScript
|
||||||
|
console.log('📦 Building server TypeScript...');
|
||||||
|
execSync('npm run build', { cwd: SERVER_DIR, stdio: 'inherit' });
|
||||||
|
|
||||||
|
// Step 3: Copy server dist
|
||||||
|
console.log('📋 Copying server dist...');
|
||||||
|
cpSync(join(SERVER_DIR, 'dist'), join(BUNDLE_DIR, 'dist'), { recursive: true });
|
||||||
|
|
||||||
|
// Step 4: Create a minimal package.json for the server
|
||||||
|
console.log('📝 Creating server package.json...');
|
||||||
|
const serverPkg = JSON.parse(readFileSync(join(SERVER_DIR, 'package.json'), 'utf-8'));
|
||||||
|
|
||||||
|
const bundlePkg = {
|
||||||
|
name: '@automaker/server-bundle',
|
||||||
|
version: serverPkg.version,
|
||||||
|
type: 'module',
|
||||||
|
main: 'dist/index.js',
|
||||||
|
dependencies: serverPkg.dependencies
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(BUNDLE_DIR, 'package.json'),
|
||||||
|
JSON.stringify(bundlePkg, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 5: Install production dependencies
|
||||||
|
console.log('📥 Installing server production dependencies...');
|
||||||
|
execSync('npm install --omit=dev', {
|
||||||
|
cwd: BUNDLE_DIR,
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Prevent npm from using workspace resolution
|
||||||
|
npm_config_workspace: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 6: Rebuild native modules for current architecture
|
||||||
|
// This is critical for modules like node-pty that have native bindings
|
||||||
|
console.log('🔨 Rebuilding native modules for current architecture...');
|
||||||
|
try {
|
||||||
|
execSync('npm rebuild', {
|
||||||
|
cwd: BUNDLE_DIR,
|
||||||
|
stdio: 'inherit'
|
||||||
|
});
|
||||||
|
console.log('✅ Native modules rebuilt successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Warning: Failed to rebuild native modules. Terminal functionality may not work.');
|
||||||
|
console.warn(' Error:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✅ Server prepared for bundling at:', BUNDLE_DIR);
|
||||||
66
apps/app/scripts/rebuild-server-natives.js
Normal file
66
apps/app/scripts/rebuild-server-natives.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Electron-builder afterPack hook
|
||||||
|
* Rebuilds native modules in the server bundle for the target architecture
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
exports.default = async function(context) {
|
||||||
|
const { appOutDir, electronPlatformName, arch, packager } = context;
|
||||||
|
const electronVersion = packager.config.electronVersion;
|
||||||
|
|
||||||
|
// Convert arch to string if it's a number (electron-builder sometimes passes indices)
|
||||||
|
const archNames = ['ia32', 'x64', 'armv7l', 'arm64', 'universal'];
|
||||||
|
const archStr = typeof arch === 'number' ? archNames[arch] : arch;
|
||||||
|
|
||||||
|
console.log(`\n🔨 Rebuilding server native modules for ${electronPlatformName}-${archStr}...`);
|
||||||
|
|
||||||
|
// Path to server node_modules in the packaged app
|
||||||
|
let serverNodeModulesPath;
|
||||||
|
if (electronPlatformName === 'darwin') {
|
||||||
|
serverNodeModulesPath = path.join(
|
||||||
|
appOutDir,
|
||||||
|
`${packager.appInfo.productName}.app`,
|
||||||
|
'Contents',
|
||||||
|
'Resources',
|
||||||
|
'server',
|
||||||
|
'node_modules'
|
||||||
|
);
|
||||||
|
} else if (electronPlatformName === 'win32') {
|
||||||
|
serverNodeModulesPath = path.join(
|
||||||
|
appOutDir,
|
||||||
|
'resources',
|
||||||
|
'server',
|
||||||
|
'node_modules'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
serverNodeModulesPath = path.join(
|
||||||
|
appOutDir,
|
||||||
|
'resources',
|
||||||
|
'server',
|
||||||
|
'node_modules'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Rebuild native modules for the target architecture
|
||||||
|
const rebuildCmd = `npx --yes @electron/rebuild --version=${electronVersion} --arch=${archStr} --force --module-dir="${serverNodeModulesPath}/.."`;
|
||||||
|
|
||||||
|
console.log(` Command: ${rebuildCmd}`);
|
||||||
|
|
||||||
|
const { stdout, stderr } = await execAsync(rebuildCmd);
|
||||||
|
if (stdout) console.log(stdout);
|
||||||
|
if (stderr) console.error(stderr);
|
||||||
|
|
||||||
|
console.log(`✅ Server native modules rebuilt successfully for ${archStr}\n`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to rebuild server native modules:`, error.message);
|
||||||
|
// Don't fail the build, just warn
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -113,8 +113,8 @@ export function FileBrowserDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh]">
|
<DialogContent className="bg-popover border-border max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
|
||||||
<DialogHeader>
|
<DialogHeader className="pb-2">
|
||||||
<DialogTitle className="flex items-center gap-2">
|
<DialogTitle className="flex items-center gap-2">
|
||||||
<FolderOpen className="w-5 h-5 text-brand-500" />
|
<FolderOpen className="w-5 h-5 text-brand-500" />
|
||||||
{title}
|
{title}
|
||||||
@@ -124,7 +124,7 @@ export function FileBrowserDialog({
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 min-h-[400px]">
|
<div className="flex flex-col gap-3 min-h-[400px] flex-1 overflow-hidden py-2">
|
||||||
{/* Drives selector (Windows only) */}
|
{/* Drives selector (Windows only) */}
|
||||||
{drives.length > 0 && (
|
{drives.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
<div className="flex flex-wrap gap-2 p-3 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
||||||
@@ -216,7 +216,7 @@ export function FileBrowserDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
<DialogFooter className="border-t border-border pt-4 gap-2">
|
||||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -239,6 +239,25 @@ export function Sidebar() {
|
|||||||
// Ref for project search input
|
// Ref for project search input
|
||||||
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
const projectSearchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Auto-collapse sidebar on small screens
|
||||||
|
useEffect(() => {
|
||||||
|
const mediaQuery = window.matchMedia('(max-width: 1024px)'); // lg breakpoint
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (mediaQuery.matches && sidebarOpen) {
|
||||||
|
// Auto-collapse on small screens
|
||||||
|
toggleSidebar();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check on mount
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
// Listen for changes
|
||||||
|
mediaQuery.addEventListener('change', handleResize);
|
||||||
|
return () => mediaQuery.removeEventListener('change', handleResize);
|
||||||
|
}, [sidebarOpen, toggleSidebar]);
|
||||||
|
|
||||||
// Filtered projects based on search query
|
// Filtered projects based on search query
|
||||||
const filteredProjects = useMemo(() => {
|
const filteredProjects = useMemo(() => {
|
||||||
if (!projectSearchQuery.trim()) {
|
if (!projectSearchQuery.trim()) {
|
||||||
|
|||||||
@@ -198,7 +198,10 @@ export function NewProjectModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectPath = workspaceDir && projectName ? `${workspaceDir}/${projectName}` : "";
|
// Use platform-specific path separator
|
||||||
|
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
||||||
|
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
||||||
|
const projectPath = workspaceDir && projectName ? `${workspaceDir}${pathSep}${projectName}` : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
|||||||
@@ -305,7 +305,10 @@ export function InterviewView() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
const fullProjectPath = `${projectPath}/${projectName}`;
|
// Use platform-specific path separator
|
||||||
|
const pathSep = typeof window !== 'undefined' && (window as any).electronAPI ?
|
||||||
|
(navigator.platform.indexOf('Win') !== -1 ? '\\' : '/') : '/';
|
||||||
|
const fullProjectPath = `${projectPath}${pathSep}${projectName}`;
|
||||||
|
|
||||||
// Create project directory
|
// Create project directory
|
||||||
await api.mkdir(fullProjectPath);
|
await api.mkdir(fullProjectPath);
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export function createTemplatesRoutes(): Router {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[Templates] Clone request - Repo: ${repoUrl}, Project: ${projectName}, Parent: ${parentDir}`);
|
||||||
|
|
||||||
// Validate repo URL is a valid GitHub URL
|
// Validate repo URL is a valid GitHub URL
|
||||||
const githubUrlPattern = /^https:\/\/github\.com\/[\w-]+\/[\w.-]+$/;
|
const githubUrlPattern = /^https:\/\/github\.com\/[\w-]+\/[\w.-]+$/;
|
||||||
if (!githubUrlPattern.test(repoUrl)) {
|
if (!githubUrlPattern.test(repoUrl)) {
|
||||||
@@ -79,12 +81,32 @@ export function createTemplatesRoutes(): Router {
|
|||||||
|
|
||||||
// Ensure parent directory exists
|
// Ensure parent directory exists
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(parentDir, { recursive: true });
|
// Check if parentDir is a root path (Windows: C:\, D:\, etc. or Unix: /)
|
||||||
|
const isWindowsRoot = /^[A-Za-z]:\\?$/.test(parentDir);
|
||||||
|
const isUnixRoot = parentDir === '/' || parentDir === '';
|
||||||
|
const isRoot = isWindowsRoot || isUnixRoot;
|
||||||
|
|
||||||
|
if (isRoot) {
|
||||||
|
// Root paths always exist, just verify access
|
||||||
|
console.log(`[Templates] Using root path: ${parentDir}`);
|
||||||
|
await fs.access(parentDir);
|
||||||
|
} else {
|
||||||
|
// Check if parent directory exists
|
||||||
|
const parentExists = await fs.access(parentDir).then(() => true).catch(() => false);
|
||||||
|
|
||||||
|
if (!parentExists) {
|
||||||
|
console.log(`[Templates] Creating parent directory: ${parentDir}`);
|
||||||
|
await fs.mkdir(parentDir, { recursive: true });
|
||||||
|
} else {
|
||||||
|
console.log(`[Templates] Parent directory exists: ${parentDir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Templates] Failed to create parent directory:", error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error("[Templates] Failed to access parent directory:", parentDir, error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Failed to create parent directory",
|
error: `Failed to access parent directory: ${errorMessage}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
1478
package-lock.json
generated
1478
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,9 @@
|
|||||||
"build": "npm run build --workspace=apps/app",
|
"build": "npm run build --workspace=apps/app",
|
||||||
"build:server": "npm run build --workspace=apps/server",
|
"build:server": "npm run build --workspace=apps/server",
|
||||||
"build:electron": "npm run build:electron --workspace=apps/app",
|
"build:electron": "npm run build:electron --workspace=apps/app",
|
||||||
|
"build:electron:win": "npm run build:electron:win --workspace=apps/app",
|
||||||
|
"build:electron:mac": "npm run build:electron:mac --workspace=apps/app",
|
||||||
|
"build:electron:linux": "npm run build:electron:linux --workspace=apps/app",
|
||||||
"start": "npm run start --workspace=apps/app",
|
"start": "npm run start --workspace=apps/app",
|
||||||
"start:server": "npm run start --workspace=apps/server",
|
"start:server": "npm run start --workspace=apps/server",
|
||||||
"lint": "npm run lint --workspace=apps/app",
|
"lint": "npm run lint --workspace=apps/app",
|
||||||
|
|||||||
Reference in New Issue
Block a user