/** * Mandu Lockfile I/O ๐Ÿ“ * * Lockfile ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ฐ ๊ณต๊ฐœ API */ import { mkdir } from "node:fs/promises"; import path from "node:path"; import { type ManduLockfile, type LockfileError, LOCKFILE_PATH, LOCKFILE_DIR, LOCKFILE_SCHEMA_VERSION, } from "./types.js"; // ============================================ // ์ฝ๊ธฐ // ============================================ /** * Lockfile ์ฝ๊ธฐ * * @param projectRoot ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ * @returns Lockfile ๋˜๋Š” null (์—†๋Š” ๊ฒฝ์šฐ) * @throws ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์‹œ * * @example * ```typescript * const lockfile = await readLockfile(process.cwd()); * if (lockfile) { * console.log(`Config hash: ${lockfile.configHash}`); * } * ``` */ export async function readLockfile( projectRoot: string ): Promise { const lockfilePath = path.join(projectRoot, LOCKFILE_PATH); try { const file = Bun.file(lockfilePath); const exists = await file.exists(); if (!exists) { return null; } const content = await file.text(); const data = JSON.parse(content) as ManduLockfile; // ์Šคํ‚ค๋งˆ ๋ฒ„์ „ ์ฒดํฌ if (data.schemaVersion !== LOCKFILE_SCHEMA_VERSION) { console.warn( `[Mandu] Lockfile schema version mismatch: expected ${LOCKFILE_SCHEMA_VERSION}, got ${data.schemaVersion}` ); } return data; } catch (error) { if (error instanceof SyntaxError) { throw new Error( `Failed to parse lockfile at ${lockfilePath}: ${error.message}`, { cause: error } ); } throw error; } } /** * MCP ์„ค์ • ์ฝ๊ธฐ (.mcp.json) */ export async function readMcpConfig( projectRoot: string ): Promise | null> { const mcpPath = path.join(projectRoot, ".mcp.json"); try { const file = Bun.file(mcpPath); const exists = await file.exists(); if (!exists) return null; const content = await file.text(); const data = JSON.parse(content) as Record; return data ?? null; } catch (error) { if (error instanceof SyntaxError) { throw new Error(`Failed to parse .mcp.json at ${mcpPath}: ${error.message}`, { cause: error }); } throw error; } } /** * Lockfile ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ */ export async function lockfileExists(projectRoot: string): Promise { const lockfilePath = path.join(projectRoot, LOCKFILE_PATH); const file = Bun.file(lockfilePath); return file.exists(); } // ============================================ // ์“ฐ๊ธฐ // ============================================ /** * Lockfile ์“ฐ๊ธฐ * * @param projectRoot ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ * @param lockfile Lockfile ๋ฐ์ดํ„ฐ * * @example * ```typescript * const lockfile = generateLockfile(config); * await writeLockfile(process.cwd(), lockfile); * ``` */ export async function writeLockfile( projectRoot: string, lockfile: ManduLockfile ): Promise { const lockfileDir = path.join(projectRoot, LOCKFILE_DIR); const lockfilePath = path.join(projectRoot, LOCKFILE_PATH); // ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ await mkdir(lockfileDir, { recursive: true }); // JSON ํฌ๋งทํŒ… (๊ฐ€๋…์„ฑ) const content = JSON.stringify(lockfile, null, 2); // ์“ฐ๊ธฐ await Bun.write(lockfilePath, content); } /** * Lockfile ์‚ญ์ œ */ export async function deleteLockfile(projectRoot: string): Promise { const lockfilePath = path.join(projectRoot, LOCKFILE_PATH); try { const file = Bun.file(lockfilePath); const exists = await file.exists(); if (!exists) { return false; } const { unlink } = await import("node:fs/promises"); await unlink(lockfilePath); return true; } catch { return false; } } // ============================================ // ์œ ํ‹ธ๋ฆฌํ‹ฐ // ============================================ /** * Lockfile ๊ฒฝ๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ */ export function getLockfilePath(projectRoot: string): string { return path.join(projectRoot, LOCKFILE_PATH); } /** * Lockfile ์˜ค๋ฅ˜ ์ƒ์„ฑ ํ—ฌํผ */ export function createLockfileError( code: LockfileError["code"], message: string, details?: Record ): LockfileError { return { code, message, details }; } // ============================================ // Re-exports // ============================================ export * from "./types.js"; export * from "./generate.js"; export * from "./validate.js";