diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..95c7921 --- /dev/null +++ b/electron/main.js @@ -0,0 +1,138 @@ +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); + +const args = process.argv.slice(2); +let port = 9997; +let debugPort = null; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--port' && i + 1 < args.length) { + port = parseInt(args[i + 1], 10); + } else if (args[i] === '--debug-port' && i + 1 < args.length) { + debugPort = args[i + 1]; + } +} + +if (isNaN(port)) { + console.error('Invalid port number:', args[i + 1]); + process.exit(1); +} + +const env = { ...process.env, PORT: port.toString() }; + +const serverPath = path.join(__dirname, '../src/backend/server.js'); +const child = spawn(process.execPath, [serverPath], { + env, + stdio: 'inherit', + detached: false +}); + +child.on('error', (err) => { + console.error('Failed to start server:', err); + process.exit(1); +}); + +child.on('exit', (code, signal) => { + process.exit(code || 0); +}); + +process.on('SIGINT', () => { + child.kill('SIGINT'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + child.kill('SIGTERM'); + process.exit(0); +}); + +const terminals = new Map(); + +wss.on('connection', (ws, req) => { + const urlParams = new URLSearchParams(req.url.split('?')[1]); + const shell = urlParams.get('shell') || SHELL; + + console.log(`[${new Date().toISOString()}] New terminal connection: shell=${shell}`); + + let ptyProcess; + try { + ptyProcess = pty.spawn(shell, [], { + name: 'xterm-256color', + cols: 80, + rows: 24, + cwd: process.env.HOME || process.env.USERPROFILE || '/', + env: process.env + }); + } catch (err) { + console.error('Failed to spawn PTY:', err); + ws.close(1000, 'Failed to start shell'); + return; + } + + terminals.set(ws, ptyProcess); + + ptyProcess.onData((data) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data); + } + }); + + ptyProcess.onExit(({ exitCode, signal }) => { + console.log(`[${new Date().toISOString()}] PTY exited: code=${exitCode}, signal=${signal}`); + if (ws.readyState === WebSocket.OPEN) { + ws.send('\r\n[Process exited]\r\n'); + ws.close(); + } + terminals.delete(ws); + }); + + ws.on('message', (message) => { + ptyProcess.write(message); + }); + + ws.on('close', () => { + console.log(`[${new Date().toISOString()}] Terminal connection closed`); + ptyProcess.kill(); + terminals.delete(ws); + }); + + ws.on('error', (err) => { + console.error('WebSocket error:', err); + ptyProcess.kill(); + terminals.delete(ws); + }); +}); + +process.on('SIGINT', () => { + console.log('\nShutting down...'); + terminals.forEach((proc) => proc.kill()); + process.exit(0); +}); + +app.commandLine.appendSwitch('disable-gpu'); +app.commandLine.appendSwitch('no-sandbox'); + +app.disableHardwareAcceleration(); + +app.whenReady().then(() => { + server.listen(PORT, () => { + console.log(` +╔═══════════════════════════════════════════════════════════╗ +║ XCTerminal (Electron Headless) ║ +╠═══════════════════════════════════════════════════════════╣ +║ Server running at: http://localhost:${PORT} ║ +║ WebSocket endpoint: ws://localhost:${PORT} ║ +║ ║ +║ Usage: electron . --port 9997 ║ +╚═══════════════════════════════════════════════════════════╝ + `); + }); +}); + +app.on('window-all-closed', () => { +}); + +process.on('exit', () => { + terminals.forEach((proc) => proc.kill()); +}); diff --git a/package.json b/package.json index d2a0d76..c0d89c8 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,58 @@ "description": "Web-based terminal using libghostty-vt and xterm.js", "main": "src/backend/server.js", "scripts": { - "start": "node src/backend/server.js", - "dev": "node src/backend/server.js", - "build:frontend": "echo 'Frontend is static, no build needed'" + "start": "electron .", + "dev": "electron . --port 9997", + "start:server": "node src/backend/server.js", + "build": "electron-builder", + "build:dir": "electron-builder --dir" }, - "keywords": ["terminal", "web", "ghostty", "xterm"], + "keywords": [ + "terminal", + "web", + "ghostty", + "xterm" + ], "license": "MIT", + "author": "XCTerminal", "dependencies": { - "ws": "^8.18.0", - "node-pty": "^1.0.0" + "node-pty": "^1.0.0", + "ws": "^8.18.0" }, "devDependencies": { + "electron": "^41.0.3", + "electron-builder": "^26.8.1", "express": "^4.18.2" + }, + "build": { + "appId": "com.xcterminal.app", + "productName": "XCTerminal", + "directories": { + "output": "dist" + }, + "win": { + "target": [ + { + "target": "portable", + "arch": ["x64"] + } + ] + }, + "linux": { + "target": ["AppImage"] + }, + "mac": { + "target": ["dmg"] + }, + "files": [ + "src/**/*", + "node_modules/**/*", + "!node_modules/.cache/**/*" + ], + "extraMetadata": { + "main": "src/backend/server.js" + }, + "nodeGypRebuild": false, + "npmRebuild": false } -} \ No newline at end of file +} diff --git a/src/backend/server.js b/src/backend/server.js index 260f9f6..4f04a73 100644 --- a/src/backend/server.js +++ b/src/backend/server.js @@ -4,7 +4,22 @@ const path = require('path'); const pty = require('node-pty'); const http = require('http'); -const PORT = process.env.PORT || 3000; +const args = process.argv.slice(2); +let PORT = 9997; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--port' && i + 1 < args.length) { + PORT = parseInt(args[i + 1], 10); + if (isNaN(PORT)) { + console.error('Invalid port number:', args[i + 1]); + process.exit(1); + } + } +} + +if (process.env.PORT && !args.includes('--port')) { + PORT = parseInt(process.env.PORT, 10); +} const SHELL = process.platform === 'win32' ? 'powershell.exe' : 'bash'; const app = express();