@mosaic/mosaic is now the single package providing both: - 'mosaic' binary (CLI: yolo, coord, prdy, tui, gateway, etc.) - 'mosaic-wizard' binary (installation wizard) Changes: - Move packages/cli/src/* into packages/mosaic/src/ - Convert dynamic @mosaic/mosaic imports to static relative imports - Add CLI deps (ink, react, socket.io-client, @mosaic/config) to mosaic - Add jsx: react-jsx to mosaic's tsconfig - Exclude packages/cli from workspace (pnpm-workspace.yaml) - Update install.sh to install @mosaic/mosaic instead of @mosaic/cli - Bump version to 0.0.17 This eliminates the circular dependency between @mosaic/cli and @mosaic/mosaic that was blocking the build graph.
144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
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<string, string> {
|
|
const map = new Map<string, string>();
|
|
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<string, string>): 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<void> {
|
|
// 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');
|
|
}
|
|
}
|