276 lines
8.2 KiB
TypeScript
276 lines
8.2 KiB
TypeScript
/**
|
|
* Global configuration loader
|
|
*
|
|
* Manages ~/.takt/config.yaml and project-level debug settings.
|
|
* GlobalConfigManager encapsulates the config cache as a singleton.
|
|
*/
|
|
|
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
import { GlobalConfigSchema } from '../../../core/models/index.js';
|
|
import type { GlobalConfig, DebugConfig, Language } from '../../../core/models/index.js';
|
|
import { getGlobalConfigPath, getProjectConfigPath } from '../paths.js';
|
|
import { DEFAULT_LANGUAGE } from '../../../shared/constants.js';
|
|
|
|
/** Create default global configuration (fresh instance each call) */
|
|
function createDefaultGlobalConfig(): GlobalConfig {
|
|
return {
|
|
language: DEFAULT_LANGUAGE,
|
|
defaultPiece: 'default',
|
|
logLevel: 'info',
|
|
provider: 'claude',
|
|
enableBuiltinPieces: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Manages global configuration loading and caching.
|
|
* Singleton — use GlobalConfigManager.getInstance().
|
|
*/
|
|
export class GlobalConfigManager {
|
|
private static instance: GlobalConfigManager | null = null;
|
|
private cachedConfig: GlobalConfig | null = null;
|
|
|
|
private constructor() {}
|
|
|
|
static getInstance(): GlobalConfigManager {
|
|
if (!GlobalConfigManager.instance) {
|
|
GlobalConfigManager.instance = new GlobalConfigManager();
|
|
}
|
|
return GlobalConfigManager.instance;
|
|
}
|
|
|
|
/** Reset singleton for testing */
|
|
static resetInstance(): void {
|
|
GlobalConfigManager.instance = null;
|
|
}
|
|
|
|
/** Invalidate the cached configuration */
|
|
invalidateCache(): void {
|
|
this.cachedConfig = null;
|
|
}
|
|
|
|
/** Load global configuration (cached) */
|
|
load(): GlobalConfig {
|
|
if (this.cachedConfig !== null) {
|
|
return this.cachedConfig;
|
|
}
|
|
const configPath = getGlobalConfigPath();
|
|
if (!existsSync(configPath)) {
|
|
const defaultConfig = createDefaultGlobalConfig();
|
|
this.cachedConfig = defaultConfig;
|
|
return defaultConfig;
|
|
}
|
|
const content = readFileSync(configPath, 'utf-8');
|
|
const raw = parseYaml(content);
|
|
const parsed = GlobalConfigSchema.parse(raw);
|
|
const config: GlobalConfig = {
|
|
language: parsed.language,
|
|
defaultPiece: parsed.default_piece,
|
|
logLevel: parsed.log_level,
|
|
provider: parsed.provider,
|
|
model: parsed.model,
|
|
debug: parsed.debug ? {
|
|
enabled: parsed.debug.enabled,
|
|
logFile: parsed.debug.log_file,
|
|
} : undefined,
|
|
worktreeDir: parsed.worktree_dir,
|
|
disabledBuiltins: parsed.disabled_builtins,
|
|
enableBuiltinPieces: parsed.enable_builtin_pieces,
|
|
anthropicApiKey: parsed.anthropic_api_key,
|
|
openaiApiKey: parsed.openai_api_key,
|
|
pipeline: parsed.pipeline ? {
|
|
defaultBranchPrefix: parsed.pipeline.default_branch_prefix,
|
|
commitMessageTemplate: parsed.pipeline.commit_message_template,
|
|
prBodyTemplate: parsed.pipeline.pr_body_template,
|
|
} : undefined,
|
|
minimalOutput: parsed.minimal_output,
|
|
bookmarksFile: parsed.bookmarks_file,
|
|
pieceCategoriesFile: parsed.piece_categories_file,
|
|
branchNameStrategy: parsed.branch_name_strategy,
|
|
};
|
|
this.cachedConfig = config;
|
|
return config;
|
|
}
|
|
|
|
/** Save global configuration to disk and invalidate cache */
|
|
save(config: GlobalConfig): void {
|
|
const configPath = getGlobalConfigPath();
|
|
const raw: Record<string, unknown> = {
|
|
language: config.language,
|
|
default_piece: config.defaultPiece,
|
|
log_level: config.logLevel,
|
|
provider: config.provider,
|
|
};
|
|
if (config.model) {
|
|
raw.model = config.model;
|
|
}
|
|
if (config.debug) {
|
|
raw.debug = {
|
|
enabled: config.debug.enabled,
|
|
log_file: config.debug.logFile,
|
|
};
|
|
}
|
|
if (config.worktreeDir) {
|
|
raw.worktree_dir = config.worktreeDir;
|
|
}
|
|
if (config.disabledBuiltins && config.disabledBuiltins.length > 0) {
|
|
raw.disabled_builtins = config.disabledBuiltins;
|
|
}
|
|
if (config.enableBuiltinPieces !== undefined) {
|
|
raw.enable_builtin_pieces = config.enableBuiltinPieces;
|
|
}
|
|
if (config.anthropicApiKey) {
|
|
raw.anthropic_api_key = config.anthropicApiKey;
|
|
}
|
|
if (config.openaiApiKey) {
|
|
raw.openai_api_key = config.openaiApiKey;
|
|
}
|
|
if (config.pipeline) {
|
|
const pipelineRaw: Record<string, unknown> = {};
|
|
if (config.pipeline.defaultBranchPrefix) pipelineRaw.default_branch_prefix = config.pipeline.defaultBranchPrefix;
|
|
if (config.pipeline.commitMessageTemplate) pipelineRaw.commit_message_template = config.pipeline.commitMessageTemplate;
|
|
if (config.pipeline.prBodyTemplate) pipelineRaw.pr_body_template = config.pipeline.prBodyTemplate;
|
|
if (Object.keys(pipelineRaw).length > 0) {
|
|
raw.pipeline = pipelineRaw;
|
|
}
|
|
}
|
|
if (config.minimalOutput !== undefined) {
|
|
raw.minimal_output = config.minimalOutput;
|
|
}
|
|
if (config.bookmarksFile) {
|
|
raw.bookmarks_file = config.bookmarksFile;
|
|
}
|
|
if (config.pieceCategoriesFile) {
|
|
raw.piece_categories_file = config.pieceCategoriesFile;
|
|
}
|
|
if (config.branchNameStrategy) {
|
|
raw.branch_name_strategy = config.branchNameStrategy;
|
|
}
|
|
writeFileSync(configPath, stringifyYaml(raw), 'utf-8');
|
|
this.invalidateCache();
|
|
}
|
|
}
|
|
|
|
export function invalidateGlobalConfigCache(): void {
|
|
GlobalConfigManager.getInstance().invalidateCache();
|
|
}
|
|
|
|
export function loadGlobalConfig(): GlobalConfig {
|
|
return GlobalConfigManager.getInstance().load();
|
|
}
|
|
|
|
export function saveGlobalConfig(config: GlobalConfig): void {
|
|
GlobalConfigManager.getInstance().save(config);
|
|
}
|
|
|
|
export function getDisabledBuiltins(): string[] {
|
|
try {
|
|
const config = loadGlobalConfig();
|
|
return config.disabledBuiltins ?? [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function getBuiltinPiecesEnabled(): boolean {
|
|
try {
|
|
const config = loadGlobalConfig();
|
|
return config.enableBuiltinPieces !== false;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function getLanguage(): Language {
|
|
try {
|
|
const config = loadGlobalConfig();
|
|
return config.language;
|
|
} catch {
|
|
return DEFAULT_LANGUAGE;
|
|
}
|
|
}
|
|
|
|
export function setLanguage(language: Language): void {
|
|
const config = loadGlobalConfig();
|
|
config.language = language;
|
|
saveGlobalConfig(config);
|
|
}
|
|
|
|
export function setProvider(provider: 'claude' | 'codex'): void {
|
|
const config = loadGlobalConfig();
|
|
config.provider = provider;
|
|
saveGlobalConfig(config);
|
|
}
|
|
|
|
/**
|
|
* Resolve the Anthropic API key.
|
|
* Priority: TAKT_ANTHROPIC_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
|
*/
|
|
export function resolveAnthropicApiKey(): string | undefined {
|
|
const envKey = process.env['TAKT_ANTHROPIC_API_KEY'];
|
|
if (envKey) return envKey;
|
|
|
|
try {
|
|
const config = loadGlobalConfig();
|
|
return config.anthropicApiKey;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve the OpenAI API key.
|
|
* Priority: TAKT_OPENAI_API_KEY env var > config.yaml > undefined (CLI auth fallback)
|
|
*/
|
|
export function resolveOpenaiApiKey(): string | undefined {
|
|
const envKey = process.env['TAKT_OPENAI_API_KEY'];
|
|
if (envKey) return envKey;
|
|
|
|
try {
|
|
const config = loadGlobalConfig();
|
|
return config.openaiApiKey;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/** Load project-level debug configuration (from .takt/config.yaml) */
|
|
export function loadProjectDebugConfig(projectDir: string): DebugConfig | undefined {
|
|
const configPath = getProjectConfigPath(projectDir);
|
|
if (!existsSync(configPath)) {
|
|
return undefined;
|
|
}
|
|
try {
|
|
const content = readFileSync(configPath, 'utf-8');
|
|
const raw = parseYaml(content);
|
|
if (raw && typeof raw === 'object' && 'debug' in raw) {
|
|
const debug = raw.debug;
|
|
if (debug && typeof debug === 'object') {
|
|
return {
|
|
enabled: Boolean(debug.enabled),
|
|
logFile: typeof debug.log_file === 'string' ? debug.log_file : undefined,
|
|
};
|
|
}
|
|
}
|
|
} catch {
|
|
// Ignore parse errors
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Get effective debug config (project overrides global) */
|
|
export function getEffectiveDebugConfig(projectDir?: string): DebugConfig | undefined {
|
|
const globalConfig = loadGlobalConfig();
|
|
let debugConfig = globalConfig.debug;
|
|
|
|
if (projectDir) {
|
|
const projectDebugConfig = loadProjectDebugConfig(projectDir);
|
|
if (projectDebugConfig) {
|
|
debugConfig = projectDebugConfig;
|
|
}
|
|
}
|
|
|
|
return debugConfig;
|
|
}
|