openclaw-nerve/scripts/lib/env-writer.ts
2026-02-19 22:28:07 +01:00

168 lines
5.1 KiB
TypeScript

/**
* .env file generator — reads, writes, and backs up .env files.
*/
import { readFileSync, writeFileSync, renameSync, copyFileSync, existsSync, unlinkSync, chmodSync } from 'node:fs';
/** All supported env config keys. */
export interface EnvConfig {
GATEWAY_URL?: string;
GATEWAY_TOKEN?: string;
AGENT_NAME?: string;
PORT?: string;
HOST?: string;
SSL_PORT?: string;
OPENAI_API_KEY?: string;
REPLICATE_API_TOKEN?: string;
ALLOWED_ORIGINS?: string;
CSP_CONNECT_EXTRA?: string;
WS_ALLOWED_HOSTS?: string;
MEMORY_PATH?: string;
MEMORY_DIR?: string;
SESSIONS_DIR?: string;
USAGE_FILE?: string;
TTS_CACHE_TTL_MS?: string;
TTS_CACHE_MAX?: string;
VITE_PORT?: string;
}
/** Default values (matching server/lib/config.ts). */
export const DEFAULTS: Record<string, string> = {
GATEWAY_URL: 'http://127.0.0.1:18789',
AGENT_NAME: 'Agent',
PORT: '3080',
HOST: '127.0.0.1',
SSL_PORT: '3443',
TTS_CACHE_TTL_MS: '3600000',
TTS_CACHE_MAX: '200',
};
/**
* Generate a clean .env file.
* Only writes values that differ from defaults (keeps .env minimal).
* Always writes GATEWAY_TOKEN since it has no default.
*/
export function generateEnvContent(config: EnvConfig): string {
const lines: string[] = [
'# Nerve Configuration',
'# Generated by `npm run setup`',
`# ${new Date().toISOString()}`,
'',
];
// Gateway (always written — most important)
lines.push('# OpenClaw Gateway');
if (config.GATEWAY_URL && config.GATEWAY_URL !== DEFAULTS.GATEWAY_URL) {
lines.push(`GATEWAY_URL=${config.GATEWAY_URL}`);
}
lines.push(`GATEWAY_TOKEN=${config.GATEWAY_TOKEN || ''}`);
lines.push('');
// Agent
if (config.AGENT_NAME && config.AGENT_NAME !== DEFAULTS.AGENT_NAME) {
lines.push('# Agent');
lines.push(`AGENT_NAME=${config.AGENT_NAME}`);
lines.push('');
}
// Server — always write PORT for clarity (even if default)
const serverLines: string[] = [];
serverLines.push(`PORT=${config.PORT || DEFAULTS.PORT}`);
if (config.HOST && config.HOST !== DEFAULTS.HOST) {
serverLines.push(`HOST=${config.HOST}`);
}
if (config.SSL_PORT && config.SSL_PORT !== DEFAULTS.SSL_PORT) {
serverLines.push(`SSL_PORT=${config.SSL_PORT}`);
}
lines.push('# Server');
lines.push(...serverLines);
lines.push('');
// API Keys
const keyLines: string[] = [];
if (config.OPENAI_API_KEY) keyLines.push(`OPENAI_API_KEY=${config.OPENAI_API_KEY}`);
if (config.REPLICATE_API_TOKEN) keyLines.push(`REPLICATE_API_TOKEN=${config.REPLICATE_API_TOKEN}`);
if (keyLines.length > 0) {
lines.push('# API Keys');
lines.push(...keyLines);
lines.push('');
}
// Advanced
const advLines: string[] = [];
if (config.ALLOWED_ORIGINS) advLines.push(`ALLOWED_ORIGINS=${config.ALLOWED_ORIGINS}`);
if (config.CSP_CONNECT_EXTRA) advLines.push(`CSP_CONNECT_EXTRA=${config.CSP_CONNECT_EXTRA}`);
if (config.WS_ALLOWED_HOSTS) advLines.push(`WS_ALLOWED_HOSTS=${config.WS_ALLOWED_HOSTS}`);
if (config.MEMORY_PATH) advLines.push(`MEMORY_PATH=${config.MEMORY_PATH}`);
if (config.MEMORY_DIR) advLines.push(`MEMORY_DIR=${config.MEMORY_DIR}`);
if (config.SESSIONS_DIR) advLines.push(`SESSIONS_DIR=${config.SESSIONS_DIR}`);
if (config.USAGE_FILE) advLines.push(`USAGE_FILE=${config.USAGE_FILE}`);
if (advLines.length > 0) {
lines.push('# Advanced');
lines.push(...advLines);
lines.push('');
}
return lines.join('\n');
}
/**
* Write .env file atomically (write .env.tmp then rename).
*/
export function writeEnvFile(envPath: string, config: EnvConfig): void {
const content = generateEnvContent(config);
const tmpPath = envPath + '.tmp';
writeFileSync(tmpPath, content, 'utf-8');
renameSync(tmpPath, envPath);
try { chmodSync(envPath, 0o600); } catch { /* non-fatal on Windows */ }
}
/**
* Parse an existing .env file into key-value pairs.
*/
export function loadExistingEnv(envPath: string): EnvConfig {
const content = readFileSync(envPath, 'utf-8');
const config: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIdx = trimmed.indexOf('=');
if (eqIdx > 0) {
const key = trimmed.slice(0, eqIdx).trim();
const value = trimmed.slice(eqIdx + 1).trim();
if (value) config[key] = value;
}
}
return config as EnvConfig;
}
/**
* Backup existing .env file before overwriting.
* Uses timestamped backup if .env.backup already exists.
*/
export function backupExistingEnv(envPath: string): string {
const backupPath = `${envPath}.backup`;
if (existsSync(backupPath)) {
const dated = `${backupPath}.${new Date().toISOString().slice(0, 10)}`;
copyFileSync(envPath, dated);
try { chmodSync(dated, 0o600); } catch { /* non-fatal */ }
return dated;
}
copyFileSync(envPath, backupPath);
try { chmodSync(backupPath, 0o600); } catch { /* non-fatal */ }
return backupPath;
}
/**
* Clean up .env.tmp if it exists (interrupted setup).
*/
export function cleanupTmp(envPath: string): void {
const tmpPath = envPath + '.tmp';
try {
if (existsSync(tmpPath)) {
unlinkSync(tmpPath);
}
} catch {
// ignore cleanup failures
}
}