mirror of
https://github.com/lotsoftick/openclaw_client.git
synced 2026-08-14 00:48:07 +00:00
227 lines
7.8 KiB
JavaScript
227 lines
7.8 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { cpSync, existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
|
import crypto from 'node:crypto';
|
|
import path from 'node:path';
|
|
import os from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { portEnv, readPorts } from './ports.mjs';
|
|
import { NPM_BIN } from './proc.mjs';
|
|
|
|
const SERVE_MJS = `
|
|
import http from 'node:http';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), 'dist');
|
|
const PORT = Number(process.env.CLIENT_PORT) || Number(process.env.PORT) || 18800;
|
|
const API_PORT = Number(process.env.API_PORT) || 18802;
|
|
|
|
const MIME = {
|
|
'.html': 'text/html', '.js': 'application/javascript', '.mjs': 'application/javascript',
|
|
'.css': 'text/css', '.json': 'application/json',
|
|
'.webmanifest': 'application/manifest+json',
|
|
'.map': 'application/json',
|
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon',
|
|
'.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf',
|
|
'.webp': 'image/webp',
|
|
};
|
|
|
|
// Service-worker files must not be cached long-term or users get stuck on
|
|
// old bundles. Everything else can use the default (immutable hashed names).
|
|
const NO_CACHE_FILES = new Set(['sw.js', 'registerSW.js', 'workbox-window.prod.es5.mjs']);
|
|
|
|
function injectRuntimeConfig(html, hostHeader) {
|
|
const hostname = (hostHeader || '').split(':')[0] || 'localhost';
|
|
const apiBaseUrl = 'http://' + hostname + ':' + API_PORT + '/api';
|
|
const cfg = JSON.stringify({ apiBaseUrl, apiPort: API_PORT });
|
|
const tag = '<script>window.__OPENCLAW_CONFIG__=' + cfg + ';</script>';
|
|
if (html.includes('</head>')) return html.replace('</head>', ' ' + tag + '\\n </head>');
|
|
return tag + html;
|
|
}
|
|
|
|
http.createServer((req, res) => {
|
|
let filePath = path.join(DIST, req.url === '/' ? 'index.html' : req.url);
|
|
if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
|
|
filePath = path.join(DIST, 'index.html');
|
|
}
|
|
const ext = path.extname(filePath);
|
|
const mime = MIME[ext] || 'application/octet-stream';
|
|
try {
|
|
if (mime === 'text/html') {
|
|
const html = fs.readFileSync(filePath, 'utf-8');
|
|
const out = injectRuntimeConfig(html, req.headers.host);
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/html; charset=utf-8',
|
|
'Cache-Control': 'no-store',
|
|
});
|
|
res.end(out);
|
|
return;
|
|
}
|
|
const data = fs.readFileSync(filePath);
|
|
const headers = { 'Content-Type': mime };
|
|
if (NO_CACHE_FILES.has(path.basename(filePath))) {
|
|
headers['Cache-Control'] = 'no-cache';
|
|
headers['Service-Worker-Allowed'] = '/';
|
|
}
|
|
res.writeHead(200, headers);
|
|
res.end(data);
|
|
} catch {
|
|
res.writeHead(404);
|
|
res.end('Not found');
|
|
}
|
|
}).listen(PORT, '0.0.0.0', () => {
|
|
console.log('client listening on port ' + PORT);
|
|
});
|
|
`.trimStart();
|
|
|
|
export function deploy() {
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const API_SRC = path.join(root, 'api');
|
|
const CLIENT_SRC = path.join(root, 'client');
|
|
|
|
const dist = path.join(os.homedir(), '.openclaw_client');
|
|
const apiDist = path.join(dist, 'api');
|
|
const clientDist = path.join(dist, 'client');
|
|
const dataDir = path.join(dist, 'data');
|
|
|
|
function run(cmd, args = [], cwd = root) {
|
|
try {
|
|
execFileSync(cmd, args, { cwd, stdio: 'pipe' });
|
|
} catch (err) {
|
|
const output = err.stdout?.toString() || '';
|
|
const stderr = err.stderr?.toString() || '';
|
|
if (output) process.stderr.write(output);
|
|
if (stderr) process.stderr.write(stderr);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
const { apiPort, clientPort } = readPorts();
|
|
const buildEnv = { ...process.env, ...portEnv() };
|
|
|
|
process.stdout.write('📦 Installing dependencies...\n');
|
|
run(NPM_BIN, ['ci', '--include=dev'], API_SRC);
|
|
run(NPM_BIN, ['ci', '--include=dev'], CLIENT_SRC);
|
|
|
|
process.stdout.write('🔨 Building...\n');
|
|
run(NPM_BIN, ['run', 'build'], API_SRC);
|
|
// VITE_API_BASE_URL is embedded into the bundle at build time
|
|
try {
|
|
execFileSync(NPM_BIN, ['run', 'build'], { cwd: CLIENT_SRC, stdio: 'pipe', env: buildEnv });
|
|
} catch (err) {
|
|
const output = err.stdout?.toString() || '';
|
|
const stderr = err.stderr?.toString() || '';
|
|
if (output) process.stderr.write(output);
|
|
if (stderr) process.stderr.write(stderr);
|
|
throw err;
|
|
}
|
|
|
|
mkdirSync(apiDist, { recursive: true });
|
|
mkdirSync(clientDist, { recursive: true });
|
|
|
|
cpSync(path.join(API_SRC, 'build'), path.join(apiDist, 'build'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
const ptyBridgeSrc = path.join(API_SRC, 'pty-bridge.py');
|
|
if (existsSync(ptyBridgeSrc)) {
|
|
cpSync(ptyBridgeSrc, path.join(apiDist, 'build', 'pty-bridge.py'), { force: true });
|
|
}
|
|
cpSync(path.join(API_SRC, 'package.json'), path.join(apiDist, 'package.json'));
|
|
cpSync(path.join(API_SRC, 'package-lock.json'), path.join(apiDist, 'package-lock.json'));
|
|
|
|
cpSync(path.join(CLIENT_SRC, 'dist'), path.join(clientDist, 'dist'), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
|
|
mkdirSync(dataDir, { recursive: true });
|
|
const canonicalDbPath = path.join(dataDir, 'openclaw.sqlite');
|
|
|
|
const envDist = path.join(apiDist, '.env');
|
|
const allowedDomain = `http://localhost:${clientPort}`;
|
|
const apiPublicUrl = `http://localhost:${apiPort}`;
|
|
|
|
if (!existsSync(envDist)) {
|
|
writeFileSync(
|
|
envDist,
|
|
[
|
|
'NODE_ENV=production',
|
|
`JWT_SECRET=${crypto.randomBytes(32).toString('hex')}`,
|
|
`DB_PATH=${canonicalDbPath}`,
|
|
`PORT=${apiPort}`,
|
|
`ALLOWED_DOMAIN=${allowedDomain}`,
|
|
`API_PUBLIC_URL=${apiPublicUrl}`,
|
|
'',
|
|
].join('\n')
|
|
);
|
|
} else {
|
|
const overrides = {
|
|
DB_PATH: canonicalDbPath,
|
|
PORT: String(apiPort),
|
|
ALLOWED_DOMAIN: allowedDomain,
|
|
API_PUBLIC_URL: apiPublicUrl,
|
|
};
|
|
const seen = new Set();
|
|
const lines = readFileSync(envDist, 'utf-8').split('\n');
|
|
const updated = lines.map((line) => {
|
|
for (const [k, v] of Object.entries(overrides)) {
|
|
if (line.startsWith(`${k}=`)) {
|
|
seen.add(k);
|
|
return `${k}=${v}`;
|
|
}
|
|
}
|
|
return line;
|
|
});
|
|
for (const [k, v] of Object.entries(overrides)) {
|
|
if (!seen.has(k)) updated.push(`${k}=${v}`);
|
|
}
|
|
writeFileSync(envDist, updated.join('\n'));
|
|
}
|
|
|
|
if (!existsSync(canonicalDbPath)) {
|
|
const legacyDb = [
|
|
path.join(apiDist, 'build', 'data', 'openclaw.sqlite'),
|
|
path.join(apiDist, 'data', 'openclaw.sqlite'),
|
|
].find((p) => existsSync(p));
|
|
if (legacyDb) cpSync(legacyDb, canonicalDbPath);
|
|
}
|
|
|
|
writeFileSync(path.join(clientDist, 'serve.mjs'), SERVE_MJS);
|
|
|
|
const runnerSrc = path.join(root, 'scripts', 'service-runner.mjs');
|
|
if (existsSync(runnerSrc)) {
|
|
cpSync(runnerSrc, path.join(dist, 'service-runner.mjs'), { force: true });
|
|
}
|
|
|
|
const rootPkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf-8'));
|
|
const updateDir = path.join(dist, 'update');
|
|
|
|
if (!existsSync(path.join(updateDir, '.git'))) {
|
|
process.stdout.write('📥 Setting up update source...\n');
|
|
try {
|
|
execFileSync(
|
|
'git',
|
|
['clone', '--depth', '1', 'https://github.com/lotsoftick/openclaw_client.git', updateDir],
|
|
{ stdio: 'pipe', env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } }
|
|
);
|
|
} catch {
|
|
process.stdout.write(' ⚠️ Could not clone update source (updates will use local repo)\n');
|
|
}
|
|
}
|
|
|
|
const sourceRepo = existsSync(path.join(updateDir, 'package.json')) ? updateDir : root;
|
|
writeFileSync(
|
|
path.join(dist, 'meta.json'),
|
|
JSON.stringify({
|
|
version: rootPkg.version,
|
|
sourceRepo,
|
|
})
|
|
);
|
|
|
|
process.stdout.write('📦 Installing production dependencies...\n');
|
|
run(NPM_BIN, ['ci', '--omit=dev'], apiDist);
|
|
}
|