import { resolve, dirname } from "@std/path"; import { existsSync } from "@std/fs"; import { logger } from "./logger.ts"; export interface SlicesConfig { slice?: string; apiUrl?: string; lexiconPath?: string; clientOutputPath?: string; } export class SlicesConfigLoader { private configCache: SlicesConfig | null = null; private configPath: string | null = null; /** * Find and load slices.json config file, starting from current directory * and walking up the directory tree */ async load(startPath = Deno.cwd()): Promise { if (this.configCache && this.configPath) { return this.configCache; } const configPath = this.findConfigFile(startPath); if (!configPath) { logger.debug("No slices.json config file found"); return {}; } this.configPath = configPath; logger.debug(`Loading config from: ${configPath}`); try { const configText = await Deno.readTextFile(configPath); const config = JSON.parse(configText) as SlicesConfig; // Validate config structure this.validateConfig(config); this.configCache = config; return config; } catch (error) { const err = error as Error; logger.warn(`Failed to load config file ${configPath}: ${err.message}`); return {}; } } /** * Get the directory containing the config file */ getConfigDir(): string | null { return this.configPath ? dirname(this.configPath) : null; } /** * Clear the config cache (useful for testing) */ clearCache(): void { this.configCache = null; this.configPath = null; } /** * Find slices.json file by walking up the directory tree */ private findConfigFile(startPath: string): string | null { let currentPath = resolve(startPath); let lastPath = ""; while (currentPath !== lastPath) { const configPath = resolve(currentPath, "slices.json"); if (existsSync(configPath)) { return configPath; } lastPath = currentPath; currentPath = dirname(currentPath); } return null; } /** * Validate the config file structure */ private validateConfig(config: unknown): void { if (typeof config !== "object" || config === null) { throw new Error("Config must be an object"); } const cfg = config as Record; if (cfg.slice !== undefined && typeof cfg.slice !== "string") { throw new Error("Config 'slice' must be a string"); } if (cfg.apiUrl !== undefined && typeof cfg.apiUrl !== "string") { throw new Error("Config 'apiUrl' must be a string"); } if (cfg.lexiconPath !== undefined && typeof cfg.lexiconPath !== "string") { throw new Error("Config 'lexiconPath' must be a string"); } if (cfg.clientOutputPath !== undefined && typeof cfg.clientOutputPath !== "string") { throw new Error("Config 'clientOutputPath' must be a string"); } // Validate slice URI format if provided if (cfg.slice && typeof cfg.slice === "string") { if (!cfg.slice.startsWith("at://")) { throw new Error("Config 'slice' must be a valid AT URI (starting with 'at://')"); } } } } /** * Merge command line arguments with config file values * Command line arguments take precedence over config file */ export function mergeConfig( config: SlicesConfig, args: Record ): SlicesConfig { return { slice: (args.slice as string) || config.slice, apiUrl: (args["api-url"] as string) || config.apiUrl || "https://api.slices.network", lexiconPath: (args.path as string) || config.lexiconPath || "./lexicons", clientOutputPath: (args.output as string) || config.clientOutputPath || "./generated_client.ts", }; }