152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
type EnvValueType = 'string' | 'boolean' | 'number' | 'json';
|
|
|
|
interface EnvSpec {
|
|
path: string;
|
|
type: EnvValueType;
|
|
}
|
|
|
|
function normalizeEnvSegment(segment: string): string {
|
|
return segment
|
|
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
.replace(/_+/g, '_')
|
|
.replace(/^_|_$/g, '')
|
|
.toUpperCase();
|
|
}
|
|
|
|
export function envVarNameFromPath(path: string): string {
|
|
const key = path
|
|
.split('.')
|
|
.map(normalizeEnvSegment)
|
|
.filter((segment) => segment.length > 0)
|
|
.join('_');
|
|
return `TAKT_${key}`;
|
|
}
|
|
|
|
function parseEnvValue(envKey: string, raw: string, type: EnvValueType): unknown {
|
|
if (type === 'string') {
|
|
return raw;
|
|
}
|
|
if (type === 'boolean') {
|
|
const normalized = raw.trim().toLowerCase();
|
|
if (normalized === 'true') return true;
|
|
if (normalized === 'false') return false;
|
|
throw new Error(`${envKey} must be one of: true, false`);
|
|
}
|
|
if (type === 'number') {
|
|
const trimmed = raw.trim();
|
|
const value = Number(trimmed);
|
|
if (!Number.isFinite(value)) {
|
|
throw new Error(`${envKey} must be a number`);
|
|
}
|
|
return value;
|
|
}
|
|
try {
|
|
return JSON.parse(raw);
|
|
} catch {
|
|
throw new Error(`${envKey} must be valid JSON`);
|
|
}
|
|
}
|
|
|
|
function setNested(target: Record<string, unknown>, path: string, value: unknown): void {
|
|
const parts = path.split('.');
|
|
let current: Record<string, unknown> = target;
|
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
const part = parts[i];
|
|
if (!part) continue;
|
|
const next = current[part];
|
|
if (typeof next !== 'object' || next === null || Array.isArray(next)) {
|
|
current[part] = {};
|
|
}
|
|
current = current[part] as Record<string, unknown>;
|
|
}
|
|
const leaf = parts[parts.length - 1];
|
|
if (!leaf) return;
|
|
current[leaf] = value;
|
|
}
|
|
|
|
function applyEnvOverrides(target: Record<string, unknown>, specs: readonly EnvSpec[]): void {
|
|
for (const spec of specs) {
|
|
const envKey = envVarNameFromPath(spec.path);
|
|
const raw = process.env[envKey];
|
|
if (raw === undefined) continue;
|
|
const parsedValue = parseEnvValue(envKey, raw, spec.type);
|
|
setNested(target, spec.path, parsedValue);
|
|
}
|
|
}
|
|
|
|
const GLOBAL_ENV_SPECS: readonly EnvSpec[] = [
|
|
{ path: 'language', type: 'string' },
|
|
{ path: 'log_level', type: 'string' },
|
|
{ path: 'provider', type: 'string' },
|
|
{ path: 'model', type: 'string' },
|
|
{ path: 'observability', type: 'json' },
|
|
{ path: 'observability.provider_events', type: 'boolean' },
|
|
{ path: 'analytics', type: 'json' },
|
|
{ path: 'analytics.enabled', type: 'boolean' },
|
|
{ path: 'analytics.events_path', type: 'string' },
|
|
{ path: 'analytics.retention_days', type: 'number' },
|
|
{ path: 'worktree_dir', type: 'string' },
|
|
{ path: 'auto_pr', type: 'boolean' },
|
|
{ path: 'draft_pr', type: 'boolean' },
|
|
{ path: 'disabled_builtins', type: 'json' },
|
|
{ path: 'enable_builtin_pieces', type: 'boolean' },
|
|
{ path: 'anthropic_api_key', type: 'string' },
|
|
{ path: 'openai_api_key', type: 'string' },
|
|
{ path: 'codex_cli_path', type: 'string' },
|
|
{ path: 'opencode_api_key', type: 'string' },
|
|
{ path: 'pipeline', type: 'json' },
|
|
{ path: 'pipeline.default_branch_prefix', type: 'string' },
|
|
{ path: 'pipeline.commit_message_template', type: 'string' },
|
|
{ path: 'pipeline.pr_body_template', type: 'string' },
|
|
{ path: 'minimal_output', type: 'boolean' },
|
|
{ path: 'bookmarks_file', type: 'string' },
|
|
{ path: 'piece_categories_file', type: 'string' },
|
|
{ path: 'persona_providers', type: 'json' },
|
|
{ path: 'provider_options', type: 'json' },
|
|
{ path: 'provider_options.codex.network_access', type: 'boolean' },
|
|
{ path: 'provider_options.opencode.network_access', type: 'boolean' },
|
|
{ path: 'provider_options.claude.sandbox.allow_unsandboxed_commands', type: 'boolean' },
|
|
{ path: 'provider_options.claude.sandbox.excluded_commands', type: 'json' },
|
|
{ path: 'provider_profiles', type: 'json' },
|
|
{ path: 'runtime', type: 'json' },
|
|
{ path: 'runtime.prepare', type: 'json' },
|
|
{ path: 'branch_name_strategy', type: 'string' },
|
|
{ path: 'prevent_sleep', type: 'boolean' },
|
|
{ path: 'notification_sound', type: 'boolean' },
|
|
{ path: 'notification_sound_events', type: 'json' },
|
|
{ path: 'notification_sound_events.iteration_limit', type: 'boolean' },
|
|
{ path: 'notification_sound_events.piece_complete', type: 'boolean' },
|
|
{ path: 'notification_sound_events.piece_abort', type: 'boolean' },
|
|
{ path: 'notification_sound_events.run_complete', type: 'boolean' },
|
|
{ path: 'notification_sound_events.run_abort', type: 'boolean' },
|
|
{ path: 'interactive_preview_movements', type: 'number' },
|
|
{ path: 'verbose', type: 'boolean' },
|
|
{ path: 'concurrency', type: 'number' },
|
|
{ path: 'task_poll_interval_ms', type: 'number' },
|
|
];
|
|
|
|
const PROJECT_ENV_SPECS: readonly EnvSpec[] = [
|
|
{ path: 'piece', type: 'string' },
|
|
{ path: 'provider', type: 'string' },
|
|
{ path: 'verbose', type: 'boolean' },
|
|
{ path: 'analytics', type: 'json' },
|
|
{ path: 'analytics.enabled', type: 'boolean' },
|
|
{ path: 'analytics.events_path', type: 'string' },
|
|
{ path: 'analytics.retention_days', type: 'number' },
|
|
{ path: 'provider_options', type: 'json' },
|
|
{ path: 'provider_options.codex.network_access', type: 'boolean' },
|
|
{ path: 'provider_options.opencode.network_access', type: 'boolean' },
|
|
{ path: 'provider_options.claude.sandbox.allow_unsandboxed_commands', type: 'boolean' },
|
|
{ path: 'provider_options.claude.sandbox.excluded_commands', type: 'json' },
|
|
{ path: 'provider_profiles', type: 'json' },
|
|
];
|
|
|
|
export function applyGlobalConfigEnvOverrides(target: Record<string, unknown>): void {
|
|
applyEnvOverrides(target, GLOBAL_ENV_SPECS);
|
|
}
|
|
|
|
export function applyProjectConfigEnvOverrides(target: Record<string, unknown>): void {
|
|
applyEnvOverrides(target, PROJECT_ENV_SPECS);
|
|
}
|