Replace electron with Bun compile for single-file exe distribution
This commit is contained in:
@@ -1,111 +0,0 @@
|
||||
const { app, Tray, Menu, nativeImage } = require('electron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
let tray = null;
|
||||
let serverPort = '3000';
|
||||
|
||||
function startServer() {
|
||||
console.log('Starting OpenChamber server...');
|
||||
|
||||
let serverPath;
|
||||
let cwdPath;
|
||||
let execPath;
|
||||
|
||||
if (app.isPackaged) {
|
||||
serverPath = path.join(process.resourcesPath, 'server', 'index.js');
|
||||
cwdPath = path.join(process.resourcesPath);
|
||||
execPath = path.join(process.resourcesPath, 'nodejs', 'node.exe');
|
||||
} else {
|
||||
serverPath = path.join(__dirname, '..', 'server', 'index.js');
|
||||
cwdPath = path.join(__dirname, '..');
|
||||
execPath = 'node';
|
||||
}
|
||||
|
||||
console.log('Server path:', serverPath);
|
||||
console.log('CWD:', cwdPath);
|
||||
console.log('Exec:', execPath);
|
||||
|
||||
// 支持命令行参数或环境变量指定端口
|
||||
const port = process.argv.find(arg => arg.startsWith('--port='))?.split('=')[1]
|
||||
|| process.env.OPENCHAMBER_PORT
|
||||
|| '3000';
|
||||
|
||||
serverPort = port;
|
||||
console.log('Port:', port);
|
||||
|
||||
try {
|
||||
// 设置环境变量
|
||||
const env = { ...process.env };
|
||||
env.OPENCHAMBER_PORT = port;
|
||||
env.OPENCODE_SKIP_START = 'true';
|
||||
|
||||
// 直接 spawn,用命令行参数指定端口
|
||||
const child = spawn(execPath, [serverPath, '--port=' + port], {
|
||||
cwd: cwdPath,
|
||||
stdio: 'inherit',
|
||||
env: env,
|
||||
windowsHide: true
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error('Server error:', err);
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
console.log('Server exited:', code);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to start:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function createTray(port) {
|
||||
try {
|
||||
let iconPath;
|
||||
if (app.isPackaged) {
|
||||
iconPath = path.join(process.resourcesPath, 'public', 'favicon.png');
|
||||
} else {
|
||||
iconPath = path.join(__dirname, '..', 'public', 'favicon.png');
|
||||
}
|
||||
|
||||
let icon;
|
||||
try {
|
||||
icon = nativeImage.createFromPath(iconPath);
|
||||
if (icon.isEmpty()) {
|
||||
icon = nativeImage.createEmpty();
|
||||
}
|
||||
} catch (e) {
|
||||
icon = nativeImage.createEmpty();
|
||||
}
|
||||
|
||||
tray = new Tray(icon);
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: `OpenChamber Server (:${port})`, enabled: false },
|
||||
{ type: 'separator' },
|
||||
{ label: `Open http://localhost:${port}`, click: () => {
|
||||
require('electron').shell.openExternal(`http://localhost:${port}`);
|
||||
}},
|
||||
{ type: 'separator' },
|
||||
{ label: 'Quit', click: () => {
|
||||
app.quit();
|
||||
}}
|
||||
]);
|
||||
|
||||
tray.setToolTip(`OpenChamber Server (:${port})`);
|
||||
tray.setContextMenu(contextMenu);
|
||||
} catch (e) {
|
||||
console.error('Failed to create tray:', e);
|
||||
}
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
startServer();
|
||||
createTray(serverPort);
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.quit();
|
||||
});
|
||||
4885
web/electron/package-lock.json
generated
4885
web/electron/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
{
|
||||
"name": "openchamber",
|
||||
"version": "1.8.5",
|
||||
"description": "OpenChamber - AI Agent Web UI",
|
||||
"author": "OpenChamber",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"electron:dev": "electron .",
|
||||
"electron:build": "electron-builder --win --x64"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.openchamber.app",
|
||||
"productName": "OpenChamber",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "../dist",
|
||||
"to": "dist",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../server",
|
||||
"to": "server",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../bin",
|
||||
"to": "bin",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "../public",
|
||||
"to": "public",
|
||||
"filter": ["**/*"]
|
||||
},
|
||||
{
|
||||
"from": "D:\\Program Files\\nodejs",
|
||||
"to": "nodejs",
|
||||
"filter": ["node.exe"]
|
||||
}
|
||||
],
|
||||
"win": {
|
||||
"target": "portable"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "OpenChamber.exe"
|
||||
},
|
||||
"asar": true,
|
||||
"npmRebuild": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^41.0.0",
|
||||
"electron-builder": "^26.8.1"
|
||||
}
|
||||
}
|
||||
33
web/embed-static.js
Normal file
33
web/embed-static.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
const distDir = './dist';
|
||||
const outputFile = './embedded-static.js';
|
||||
|
||||
function walkDir(dir, baseDir, result = {}) {
|
||||
const items = readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const fullPath = join(dir, item);
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
walkDir(fullPath, baseDir, result);
|
||||
} else {
|
||||
const relPath = relative(baseDir, fullPath).replace(/\\/g, '/');
|
||||
const content = readFileSync(fullPath);
|
||||
result[relPath] = content.toString('base64');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log('Embedding static files...');
|
||||
const files = walkDir(distDir, distDir);
|
||||
console.log('Found', Object.keys(files).length, 'files in dist');
|
||||
|
||||
const jsContent = `// Auto-generated - embedded static files
|
||||
export const staticFiles = ${JSON.stringify(files, null, 0)};
|
||||
export const fileCount = ${Object.keys(files).length};
|
||||
`;
|
||||
|
||||
writeFileSync(outputFile, jsContent);
|
||||
console.log('Written to', outputFile, 'size:', statSync(outputFile).size);
|
||||
61
web/entry-singlefile.js
Normal file
61
web/entry-singlefile.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
// Import embedded static files
|
||||
import { staticFiles } from './embedded-static.js';
|
||||
|
||||
// Import server statically
|
||||
import * as serverModule from './server/index.js';
|
||||
|
||||
// Parse args
|
||||
const args = process.argv.slice(2);
|
||||
let port = 3000;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--port' || args[i] === '-p') {
|
||||
port = parseInt(args[++i]) || 3000;
|
||||
} else if (args[i].startsWith('--port=')) {
|
||||
port = parseInt(args[i].split('=')[1]) || 3000;
|
||||
} else if (args[i] === '--help' || args[i] === '-h') {
|
||||
console.log(`OpenChamber - Web UI for OpenCode
|
||||
Usage: OpenChamber [options]
|
||||
Options:
|
||||
--port, -p Port (default: 3000)
|
||||
--help, -h Show help`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract static files to temp directory
|
||||
const extractDir = join(tmpdir(), 'openchamber-static-' + process.pid);
|
||||
const distPath = join(extractDir, 'dist');
|
||||
|
||||
console.log('Extracting static files...');
|
||||
mkdirSync(distPath, { recursive: true });
|
||||
|
||||
let extractedCount = 0;
|
||||
for (const [relPath, base64Content] of Object.entries(staticFiles)) {
|
||||
const fullPath = join(distPath, relPath);
|
||||
const dir = dirname(fullPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
const content = Buffer.from(base64Content, 'base64');
|
||||
writeFileSync(fullPath, content);
|
||||
extractedCount++;
|
||||
}
|
||||
console.log(`Extracted ${extractedCount} files to ${distPath}`);
|
||||
|
||||
// Set environment
|
||||
process.env.OPENCHAMBER_DIST_DIR = distPath;
|
||||
process.env.OPENCHAMBER_PORT = String(port);
|
||||
|
||||
console.log(`Starting OpenChamber on port ${port}...`);
|
||||
|
||||
// Start server
|
||||
if (typeof serverModule.startWebUiServer === 'function') {
|
||||
await serverModule.startWebUiServer({ port, attachSignals: true, exitOnShutdown: true });
|
||||
} else {
|
||||
console.error('Error: startWebUiServer not found in server module');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -20,7 +20,8 @@
|
||||
"type-check": "tsc --noEmit",
|
||||
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../eslint.config.js",
|
||||
"start": "node bin/cli.js serve",
|
||||
"bundle": "pkg bin/cli.js --public --targets node18-win-x64 --output ../OpenChamber.exe"
|
||||
"embed:static": "node embed-static.js",
|
||||
"build:exe": "vite build && node embed-static.js && bun build entry-singlefile.js --compile --outfile OpenChamber.exe"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/lang-cpp": "^6.0.3",
|
||||
|
||||
Reference in New Issue
Block a user