114 lines
3.6 KiB
TypeScript
114 lines
3.6 KiB
TypeScript
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<string, unknown>): Record<string, unknown> {
|
|
const sensitiveKeys = ['bot_token', 'api_key', 'auth_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<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
if (sensitiveKeys.includes(key) && typeof value === 'string') {
|
|
result[key] = '***';
|
|
} else {
|
|
result[key] = redact(value);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
return redact(config) as Record<string, unknown>;
|
|
}
|
|
|
|
/** Format output for terminal display. */
|
|
export function formatStatus(
|
|
status: 'pass' | 'fail' | 'warn' | 'skip',
|
|
label: string,
|
|
detail?: string,
|
|
): string {
|
|
const icons: Record<string, string> = {
|
|
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}`;
|
|
}
|