import { mkdir, readFile } from "node:fs/promises"; import { join } from "node:path"; import { isValidTemplateName, DEFAULT_TEMPLATE_NAME } from "../templates/load-template.ts"; import { writeTextFileAtomic } from "../utils/atomic-write.ts"; import { getGlobalExtensionDir } from "./paths.ts"; const CONFIG_FILE_NAME = "ai-git-commit.json"; export interface ActiveTemplateTarget { /** 实际被写入的配置文件路径。 */ path: string; /** 写入的配置层级。 */ scope: "project" | "global"; } interface ConfigChange { path: string; before: Record; after: Record; } /** 判断未知错误是否为指定 Node.js 文件系统错误码。 */ function hasErrorCode(error: unknown, code: string): boolean { return error instanceof Error && (error as NodeJS.ErrnoException).code === code; } /** 读取配置文件的现有内容,文件不存在时返回 undefined。 */ async function readConfigObject(path: string): Promise | undefined> { let content: string; try { content = await readFile(path, "utf8"); } catch (error) { if (hasErrorCode(error, "ENOENT")) { return undefined; } const detail = error instanceof Error ? error.message : String(error); throw new Error(`Failed to read configuration file ${path}: ${detail}`); } let value: unknown; try { value = JSON.parse(content) as unknown; } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`Configuration file ${path} is not valid JSON: ${detail}`); } if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new Error(`The root value in configuration file ${path} must be a JSON object.`); } return value as Record; } /** 将配置对象通过同目录临时文件原子写回。 */ async function writeConfigObject(path: string, config: Record): Promise { await writeTextFileAtomic(path, `${JSON.stringify(config, null, 2)}\n`, { overwrite: true, mode: 0o600, }); } /** 更新全局配置的 template 字段,文件不存在时创建。 */ export async function setGlobalTemplate(name: string): Promise { if (!isValidTemplateName(name)) { throw new Error(`Template name "${name}" is invalid.`); } const dir = getGlobalExtensionDir(); const path = join(dir, CONFIG_FILE_NAME); await mkdir(dir, { recursive: true }); const config = (await readConfigObject(path)) ?? {}; await writeConfigObject(path, { ...config, template: name }); return path; } /** * 激活模板:项目配置文件存在时更新项目层的 template 字段, * 否则写入全局配置(全局文件不存在时自动创建)。 */ export async function setActiveTemplate( name: string, projectDir?: string, ): Promise { if (!isValidTemplateName(name)) { throw new Error(`Template name "${name}" is invalid.`); } if (projectDir !== undefined) { const projectPath = join(projectDir, ".pi", CONFIG_FILE_NAME); const projectConfig = await readConfigObject(projectPath); if (projectConfig !== undefined) { await writeConfigObject(projectPath, { ...projectConfig, template: name }); return { path: projectPath, scope: "project" }; } } return { path: await setGlobalTemplate(name), scope: "global" }; } /** 发生跨配置层写入失败时,回滚此前已经提交的配置变更。 */ async function rollbackConfigChanges(changes: ConfigChange[]): Promise { const failures: string[] = []; for (const change of [...changes].reverse()) { try { await writeConfigObject(change.path, change.before); } catch (error) { const detail = error instanceof Error ? error.message : String(error); failures.push(`${change.path}: ${detail}`); } } if (failures.length > 0) { throw new Error(`Failed to roll back configuration updates:\n${failures.join("\n")}`); } } /** * 将仍引用指定模板的当前项目/全局配置回退到内置 default。 * 所有候选文件先完成解析,再逐层原子更新;中途失败时回滚已写层级。 */ export async function resetTemplateReferences( name: string, projectDir?: string, ): Promise { const globalPath = join(getGlobalExtensionDir(), CONFIG_FILE_NAME); const candidates: string[] = []; if (projectDir !== undefined) { candidates.push(join(projectDir, ".pi", CONFIG_FILE_NAME)); } candidates.push(globalPath); const changes: ConfigChange[] = []; for (const path of candidates) { const config = await readConfigObject(path); if (config !== undefined && config.template === name) { changes.push({ path, before: config, after: { ...config, template: DEFAULT_TEMPLATE_NAME }, }); } } const committed: ConfigChange[] = []; try { for (const change of changes) { await writeConfigObject(change.path, change.after); committed.push(change); } } catch (error) { try { await rollbackConfigChanges(committed); } catch (rollbackError) { const detail = rollbackError instanceof Error ? rollbackError.message : String(rollbackError); throw new Error( `Failed to reset template references and could not fully roll back: ${detail}`, { cause: error, }, ); } throw error; } return changes.map((change) => change.path); }