import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { execSync } from 'node:child_process'; import { ENV_FILE, getDaemonPid, readMeta, META_FILE, ensureDirs } from './daemon.js'; // Keys that should be masked in output const SECRET_KEYS = new Set([ 'BETTER_AUTH_SECRET', 'ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'ZAI_API_KEY', 'OPENROUTER_API_KEY', 'DISCORD_BOT_TOKEN', 'TELEGRAM_BOT_TOKEN', ]); function maskValue(key: string, value: string): string { if (SECRET_KEYS.has(key) && value.length > 8) { return value.slice(0, 4) + '…' + value.slice(-4); } return value; } function parseEnvFile(): Map { const map = new Map(); if (!existsSync(ENV_FILE)) return map; const lines = readFileSync(ENV_FILE, 'utf-8').split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eqIdx = trimmed.indexOf('='); if (eqIdx === -1) continue; map.set(trimmed.slice(0, eqIdx), trimmed.slice(eqIdx + 1)); } return map; } function writeEnvFile(entries: Map): void { ensureDirs(); const lines: string[] = []; for (const [key, value] of entries) { lines.push(`${key}=${value}`); } writeFileSync(ENV_FILE, lines.join('\n') + '\n', { mode: 0o600 }); } interface ConfigOpts { set?: string; unset?: string; edit?: boolean; } export async function runConfig(opts: ConfigOpts): Promise { // Set a value if (opts.set) { const eqIdx = opts.set.indexOf('='); if (eqIdx === -1) { console.error('Usage: mosaic gateway config --set KEY=VALUE'); process.exit(1); } const key = opts.set.slice(0, eqIdx); const value = opts.set.slice(eqIdx + 1); const entries = parseEnvFile(); entries.set(key, value); writeEnvFile(entries); console.log(`Set ${key}=${maskValue(key, value)}`); promptRestart(); return; } // Unset a value if (opts.unset) { const entries = parseEnvFile(); if (!entries.has(opts.unset)) { console.error(`Key not found: ${opts.unset}`); process.exit(1); } entries.delete(opts.unset); writeEnvFile(entries); console.log(`Removed ${opts.unset}`); promptRestart(); return; } // Open in editor if (opts.edit) { if (!existsSync(ENV_FILE)) { console.error(`No config file found at ${ENV_FILE}`); console.error('Run `mosaic gateway install` first.'); process.exit(1); } const editor = process.env['EDITOR'] ?? process.env['VISUAL'] ?? 'vi'; try { execSync(`${editor} "${ENV_FILE}"`, { stdio: 'inherit' }); promptRestart(); } catch { console.error('Editor exited with error.'); } return; } // Default: show current config showConfig(); } function showConfig(): void { if (!existsSync(ENV_FILE)) { console.log('No gateway configuration found.'); console.log('Run `mosaic gateway install` to set up.'); return; } const entries = parseEnvFile(); const meta = readMeta(); console.log('Mosaic Gateway Configuration'); console.log('────────────────────────────'); console.log(` Config file: ${ENV_FILE}`); console.log(` Meta file: ${META_FILE}`); console.log(); if (entries.size === 0) { console.log(' (empty)'); return; } const maxKeyLen = Math.max(...[...entries.keys()].map((k) => k.length)); for (const [key, value] of entries) { const padding = ' '.repeat(maxKeyLen - key.length); console.log(` ${key}${padding} ${maskValue(key, value)}`); } if (meta?.adminToken) { console.log(); console.log(` Admin token: ${maskValue('token', meta.adminToken)}`); } } function promptRestart(): void { if (getDaemonPid() !== null) { console.log('\nGateway is running — restart to apply changes: mosaic gateway restart'); } }