import { readFileSync, existsSync, readdirSync, statSync, copyFileSync } from 'node:fs'; import { join } from 'node:path'; import type { ConfigService } from './config-service.js'; import type { SoulConfig, UserConfig, ToolsConfig, InstallAction } from '../types.js'; import { soulSchema, userSchema, toolsSchema } from './schemas.js'; import { renderTemplate } from '../template/engine.js'; import { buildSoulTemplateVars, buildUserTemplateVars, buildToolsTemplateVars, } from '../template/builders.js'; import { atomicWrite, backupFile, syncDirectory } from '../platform/file-ops.js'; /** * Parse a SoulConfig from an existing SOUL.md file. */ function parseSoulFromMarkdown(content: string): SoulConfig { const config: SoulConfig = {}; const nameMatch = content.match(/You are \*\*(.+?)\*\*/); if (nameMatch?.[1]) config.agentName = nameMatch[1]; const roleMatch = content.match(/Role identity: (.+)/); if (roleMatch?.[1]) config.roleDescription = roleMatch[1]; if (content.includes('Be direct, concise')) { config.communicationStyle = 'direct'; } else if (content.includes('Be warm and conversational')) { config.communicationStyle = 'friendly'; } else if (content.includes('Use professional, structured')) { config.communicationStyle = 'formal'; } return config; } /** * Parse a UserConfig from an existing USER.md file. */ function parseUserFromMarkdown(content: string): UserConfig { const config: UserConfig = {}; const nameMatch = content.match(/\*\*Name:\*\* (.+)/); if (nameMatch?.[1]) config.userName = nameMatch[1]; const pronounsMatch = content.match(/\*\*Pronouns:\*\* (.+)/); if (pronounsMatch?.[1]) config.pronouns = pronounsMatch[1]; const tzMatch = content.match(/\*\*Timezone:\*\* (.+)/); if (tzMatch?.[1]) config.timezone = tzMatch[1]; return config; } /** * Parse a ToolsConfig from an existing TOOLS.md file. */ function parseToolsFromMarkdown(content: string): ToolsConfig { const config: ToolsConfig = {}; const credsMatch = content.match(/\*\*Location:\*\* (.+)/); if (credsMatch?.[1]) config.credentialsLocation = credsMatch[1]; return config; } export class FileConfigAdapter implements ConfigService { constructor( private mosaicHome: string, private sourceDir: string, ) {} async readSoul(): Promise { const path = join(this.mosaicHome, 'SOUL.md'); if (!existsSync(path)) return {}; return parseSoulFromMarkdown(readFileSync(path, 'utf-8')); } async readUser(): Promise { const path = join(this.mosaicHome, 'USER.md'); if (!existsSync(path)) return {}; return parseUserFromMarkdown(readFileSync(path, 'utf-8')); } async readTools(): Promise { const path = join(this.mosaicHome, 'TOOLS.md'); if (!existsSync(path)) return {}; return parseToolsFromMarkdown(readFileSync(path, 'utf-8')); } async writeSoul(config: SoulConfig): Promise { const validated = soulSchema.parse(config); const templatePath = this.findTemplate('SOUL.md.template'); if (!templatePath) return; const template = readFileSync(templatePath, 'utf-8'); const vars = buildSoulTemplateVars(validated); const output = renderTemplate(template, vars); const outPath = join(this.mosaicHome, 'SOUL.md'); backupFile(outPath); atomicWrite(outPath, output); } async writeUser(config: UserConfig): Promise { const validated = userSchema.parse(config); const templatePath = this.findTemplate('USER.md.template'); if (!templatePath) return; const template = readFileSync(templatePath, 'utf-8'); const vars = buildUserTemplateVars(validated); const output = renderTemplate(template, vars); const outPath = join(this.mosaicHome, 'USER.md'); backupFile(outPath); atomicWrite(outPath, output); } async writeTools(config: ToolsConfig): Promise { const validated = toolsSchema.parse(config); const templatePath = this.findTemplate('TOOLS.md.template'); if (!templatePath) return; const template = readFileSync(templatePath, 'utf-8'); const vars = buildToolsTemplateVars(validated); const output = renderTemplate(template, vars); const outPath = join(this.mosaicHome, 'TOOLS.md'); backupFile(outPath); atomicWrite(outPath, output); } async syncFramework(action: InstallAction): Promise { const preservePaths = action === 'keep' || action === 'reconfigure' ? ['SOUL.md', 'USER.md', 'TOOLS.md', 'memory'] : []; syncDirectory(this.sourceDir, this.mosaicHome, { preserve: preservePaths, excludeGit: true, }); // Copy default root-level .md files (AGENTS.md, STANDARDS.md, etc.) // from framework/defaults/ into mosaicHome root if they don't exist yet. // These are framework contracts — only written on first install, never // overwritten (user may have customized them). const defaultsDir = join(this.sourceDir, 'defaults'); if (existsSync(defaultsDir)) { for (const entry of readdirSync(defaultsDir)) { const dest = join(this.mosaicHome, entry); if (!existsSync(dest)) { const src = join(defaultsDir, entry); if (statSync(src).isFile()) { copyFileSync(src, dest); } } } } } /** * Look for template in source dir first, then mosaic home. */ private findTemplate(name: string): string | null { const candidates = [ join(this.sourceDir, 'templates', name), join(this.mosaicHome, 'templates', name), ]; for (const path of candidates) { if (existsSync(path)) return path; } return null; } }