import { constants } from "node:fs"; import { access, mkdir, readdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import path from "node:path"; import { validateTemplateName } from "./templateName.js"; export const DEFAULT_ORDER_DIR = path.join(homedir(), ".pi", "orders"); export const TEMPLATE_EXISTS_MESSAGE = "模板已存在"; export const TEMPLATE_NOT_FOUND_MESSAGE = "模板不存在"; export const READ_TEMPLATE_FAILED_MESSAGE = "读取模板失败"; export const SAVE_TEMPLATE_FAILED_MESSAGE = "保存模板失败"; export const TEMPLATE_DESCRIPTIONS_FILE = "descriptions.json"; export type TemplateDescriptions = Record; export class TemplateStoreError extends Error { constructor(message: string, public readonly cause?: unknown) { super(message); this.name = "TemplateStoreError"; } } export class TemplateStore { constructor(private readonly orderDir: string = DEFAULT_ORDER_DIR) {} getOrderDir(): string { return this.orderDir; } getTemplatePath(name: string): string { validateTemplateName(name); return path.join(this.orderDir, `${name}.md`); } getDescriptionsPath(): string { return path.join(this.orderDir, TEMPLATE_DESCRIPTIONS_FILE); } async ensureDir(): Promise { try { await mkdir(this.orderDir, { recursive: true }); } catch (error) { throw new TemplateStoreError(SAVE_TEMPLATE_FAILED_MESSAGE, error); } } async exists(name: string): Promise { validateTemplateName(name); try { await access(this.getTemplatePath(name), constants.F_OK); return true; } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { return false; } throw new TemplateStoreError(READ_TEMPLATE_FAILED_MESSAGE, error); } } async createTemplate(name: string): Promise { validateTemplateName(name); await this.ensureDir(); try { await writeFile(this.getTemplatePath(name), "", { encoding: "utf8", flag: "wx" }); } catch (error) { if (isNodeError(error) && error.code === "EEXIST") { throw new TemplateStoreError(TEMPLATE_EXISTS_MESSAGE, error); } throw new TemplateStoreError(SAVE_TEMPLATE_FAILED_MESSAGE, error); } } async readTemplate(name: string): Promise { validateTemplateName(name); try { return await readFile(this.getTemplatePath(name), "utf8"); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { throw new TemplateStoreError(TEMPLATE_NOT_FOUND_MESSAGE, error); } throw new TemplateStoreError(READ_TEMPLATE_FAILED_MESSAGE, error); } } async writeTemplate(name: string, content: string): Promise { validateTemplateName(name); await this.ensureDir(); try { await writeFile(this.getTemplatePath(name), content, "utf8"); } catch (error) { throw new TemplateStoreError(SAVE_TEMPLATE_FAILED_MESSAGE, error); } } async removeTemplate(name: string): Promise { validateTemplateName(name); try { await unlink(this.getTemplatePath(name)); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { throw new TemplateStoreError(TEMPLATE_NOT_FOUND_MESSAGE, error); } throw new TemplateStoreError(SAVE_TEMPLATE_FAILED_MESSAGE, error); } await this.removeDescriptionIfPossible(name); } async requireExists(name: string): Promise { if (!(await this.exists(name))) { throw new TemplateStoreError(TEMPLATE_NOT_FOUND_MESSAGE); } } async listTemplates(): Promise { await this.ensureDir(); try { const entries = await readdir(this.orderDir, { withFileTypes: true }); return entries .filter((entry) => entry.isFile() && entry.name.endsWith(".md")) .map((entry) => entry.name.slice(0, -3)) .sort(); } catch (error) { throw new TemplateStoreError(READ_TEMPLATE_FAILED_MESSAGE, error); } } async readDescriptions(): Promise { try { const raw = await readFile(this.getDescriptionsPath(), "utf8"); const parsed: unknown = JSON.parse(raw); if (!isRecord(parsed)) { return {}; } return Object.fromEntries( Object.entries(parsed).filter((entry): entry is [string, string] => typeof entry[1] === "string"), ); } catch (error) { if (isNodeError(error) && error.code === "ENOENT") { return {}; } throw new TemplateStoreError(READ_TEMPLATE_FAILED_MESSAGE, error); } } async getDescription(name: string): Promise { validateTemplateName(name); const descriptions = await this.readDescriptions(); return descriptions[name] ?? ""; } async setDescription(name: string, description: string): Promise { validateTemplateName(name); await this.ensureDir(); const descriptions = await this.readDescriptions(); const trimmed = description.trim(); if (trimmed) { descriptions[name] = trimmed; } else { delete descriptions[name]; } await this.writeDescriptions(descriptions); } private async removeDescriptionIfPossible(name: string): Promise { try { const descriptions = await this.readDescriptions(); if (descriptions[name] === undefined) { return; } delete descriptions[name]; await this.writeDescriptions(descriptions); } catch { // A stale description is harmless because completions are based on existing .md files. } } private async writeDescriptions(descriptions: TemplateDescriptions): Promise { try { const tempPath = `${this.getDescriptionsPath()}.${process.pid}.${Date.now()}.tmp`; await writeFile(tempPath, `${JSON.stringify(descriptions, null, 2)}\n`, "utf8"); await rename(tempPath, this.getDescriptionsPath()); } catch (error) { throw new TemplateStoreError(SAVE_TEMPLATE_FAILED_MESSAGE, error); } } } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function isNodeError(error: unknown): error is NodeJS.ErrnoException { return error instanceof Error && "code" in error; }