mirror of
https://github.com/daggerhashimoto/openclaw-nerve
synced 2026-04-21 10:37:17 +00:00
168 lines
5.1 KiB
TypeScript
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
|
|
}
|
|
}
|