import { loadConfig } from '../config/index.js'; import type { Config } from '../config/index.js'; import { resolve, dirname, join } from 'path'; import { homedir } from 'os'; import { existsSync, readFileSync } from 'fs'; function loadEnvFileIfPresent(): void { const envFile = process.env.FLYNN_ENV_FILE ?? resolve(homedir(), '.config/flynn/cloud.env'); if (!existsSync(envFile)) { return; } const raw = readFileSync(envFile, 'utf-8'); for (const line of raw.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) { continue; } const idx = trimmed.indexOf('='); if (idx <= 0) { continue; } const key = trimmed.slice(0, idx).trim(); const value = trimmed.slice(idx + 1); if (!key) { continue; } // Don't override existing env vars. if (process.env[key] === undefined) { process.env[key] = value; } } } /** Get the config file path from env or default location. */ export function getConfigPath(): string { return process.env.FLYNN_CONFIG ?? resolve(homedir(), '.config/flynn/config.yaml'); } /** Get the data directory path (FLYNN_DATA_DIR overrides default for Docker/custom deployments). */ export function getDataDir(): string { return process.env.FLYNN_DATA_DIR ?? resolve(homedir(), '.local/share/flynn'); } /** * Resolve overlay config path from FLYNN_ENV. * If FLYNN_ENV is set, returns {configDir}/{FLYNN_ENV}.yaml relative to the base config file. * Returns undefined if FLYNN_ENV is not set. * Does NOT check if the file exists — caller decides error handling. */ export function resolveOverlayPath(basePath: string): string | undefined { const env = process.env.FLYNN_ENV; if (!env) {return undefined;} const configDir = dirname(basePath); return join(configDir, `${env}.yaml`); } /** Load config without throwing. Returns { config } or { error }. */ export function loadConfigSafe(configPath?: string): { config?: Config; error?: string } { const path = configPath ?? getConfigPath(); try { loadEnvFileIfPresent(); const overlayPath = resolveOverlayPath(path); const config = loadConfig(path, overlayPath); return { config }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { error: `Failed to load config from ${path}: ${message}` }; } } /** Deep-clone config and replace sensitive fields with '***'. */ export function redactSecrets(config: Record): Record { const sensitiveKeys = ['bot_token', 'api_key', 'auth_token', 'access_token']; function redact(obj: unknown): unknown { if (obj === null || obj === undefined) {return obj;} if (Array.isArray(obj)) {return obj.map(redact);} if (typeof obj === 'object') { const result: Record = {}; for (const [key, value] of Object.entries(obj as Record)) { if (sensitiveKeys.includes(key) && typeof value === 'string') { result[key] = '***'; } else { result[key] = redact(value); } } return result; } return obj; } return redact(config) as Record; } /** Format output for terminal display. */ export function formatStatus( status: 'pass' | 'fail' | 'warn' | 'skip', label: string, detail?: string, ): string { const icons: Record = { pass: '\x1b[32m[PASS]\x1b[0m', fail: '\x1b[31m[FAIL]\x1b[0m', warn: '\x1b[33m[WARN]\x1b[0m', skip: '\x1b[2m[SKIP]\x1b[0m', }; const suffix = detail ? ` ${detail}` : ''; return `${icons[status]} ${label}${suffix}`; }