/** * Mandu Lockfile 생성 πŸ” * * μ„€μ • νŒŒμΌμ—μ„œ lockfile 생성 */ import { computeConfigHash, normalizeForHash } from "../utils/hasher.js"; import { type ManduLockfile, type LockfileGenerateOptions, LOCKFILE_SCHEMA_VERSION, } from "./types.js"; // ============================================ // Lockfile 생성 // ============================================ /** * μ„€μ •μ—μ„œ Lockfile 생성 * * @example * ```typescript * const config = await loadConfig(); * const lockfile = generateLockfile(config, { * manduVersion: "0.9.46", * includeSnapshot: true, * }); * ``` */ export function generateLockfile( config: Record, options: LockfileGenerateOptions = {}, mcpConfig?: Record | null ): ManduLockfile { const { manduVersion = getManduVersion(), environment = detectEnvironment(), includeSnapshot = false, includeMcpServerHashes = true, } = options; // μ„€μ • ν•΄μ‹œ 계산 const configHash = computeConfigHash(config); // MCP μ„€μ • ν•΄μ‹œ (μžˆλŠ” 경우) const { mcpHashSource, mcpServers } = resolveMcpSources(config, mcpConfig); let mcpConfigHash: string | undefined; let mcpServerHashes: ManduLockfile["mcpServers"]; if (mcpHashSource && Object.keys(mcpHashSource).length > 0) { mcpConfigHash = computeConfigHash(mcpHashSource); if (includeMcpServerHashes && mcpServers) { mcpServerHashes = {}; for (const [name, serverConfig] of Object.entries(mcpServers)) { mcpServerHashes[name] = { hash: computeConfigHash(serverConfig), version: extractServerVersion(serverConfig), }; } } } // Lockfile 생성 const lockfile: ManduLockfile = { schemaVersion: LOCKFILE_SCHEMA_VERSION, manduVersion, configHash, generatedAt: new Date().toISOString(), environment, }; // 선택적 ν•„λ“œ μΆ”κ°€ if (mcpConfigHash) { lockfile.mcpConfigHash = mcpConfigHash; } if (mcpServerHashes) { lockfile.mcpServers = mcpServerHashes; } if (includeSnapshot) { const normalized = normalizeForHash(config); const snapshotConfig = normalized && typeof normalized === "object" ? (normalized as Record) : {}; if (mcpServers) { const normalizedMcp = normalizeForHash(mcpServers); if (normalizedMcp !== undefined) { snapshotConfig.mcpServers = normalizedMcp; } } lockfile.snapshot = { config: snapshotConfig, environment: environment ?? "development", }; } return lockfile; } /** * MCP μ„€μ •μ—μ„œ 별도 Lockfile 데이터 생성 */ export function generateMcpLockData( mcpConfig: Record ): { hash: string; servers: ManduLockfile["mcpServers"] } { const { mcpHashSource, mcpServers } = resolveMcpSources({}, mcpConfig); const hash = mcpHashSource ? computeConfigHash(mcpHashSource) : computeConfigHash(mcpConfig); const servers: ManduLockfile["mcpServers"] = {}; if (mcpServers) { for (const [name, serverConfig] of Object.entries(mcpServers)) { servers[name] = { hash: computeConfigHash(serverConfig), version: extractServerVersion(serverConfig), }; } } return { hash, servers }; } // ============================================ // μœ ν‹Έλ¦¬ν‹° // ============================================ /** * mandu 버전 κ°€μ Έμ˜€κΈ° */ function getManduVersion(): string { // μ‹€μ œ κ΅¬ν˜„μ—μ„œλŠ” package.jsonμ—μ„œ μ½κ±°λ‚˜ λΉŒλ“œ μ‹œ μ£Όμž… try { // @ts-ignore - λΉŒλ“œ μ‹œ μ£Όμž…λ˜λŠ” κ°’ if (typeof __MANDU_VERSION__ !== "undefined") { // @ts-ignore return __MANDU_VERSION__; } } catch { // ignore } // κΈ°λ³Έκ°’ return "0.0.0"; } /** * ν˜„μž¬ ν™˜κ²½ 감지 */ function detectEnvironment(): "development" | "production" | "ci" { // CI ν™˜κ²½ 감지 if ( process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.GITLAB_CI === "true" || process.env.JENKINS_URL ) { return "ci"; } // NODE_ENV 기반 if (process.env.NODE_ENV === "production") { return "production"; } return "development"; } /** * μ„œλ²„ μ„€μ •μ—μ„œ 버전 μΆ”μΆœ */ function extractServerVersion( serverConfig: unknown ): string | undefined { if (typeof serverConfig !== "object" || serverConfig === null) { return undefined; } const config = serverConfig as Record; // version ν•„λ“œ 직접 확인 if (typeof config.version === "string") { return config.version; } // argsμ—μ„œ 버전 νŒ¨ν„΄ μΆ”μΆœ μ‹œλ„ (예: @package/name@1.2.3) if (Array.isArray(config.args)) { for (const arg of config.args) { if (typeof arg === "string") { const match = arg.match(/@[\w-]+\/[\w-]+@([\d.]+)/); if (match) { return match[1]; } } } } return undefined; } // ============================================ // ν•΄μ‹œ μž¬κ³„μ‚° // ============================================ /** * ν˜„μž¬ μ„€μ •μ˜ ν•΄μ‹œλ§Œ λΉ λ₯΄κ²Œ 계산 */ export function computeCurrentHashes( config: Record, mcpConfig?: Record | null ): { configHash: string; mcpConfigHash?: string } { const configHash = computeConfigHash(config); const { mcpHashSource } = resolveMcpSources(config, mcpConfig); const mcpConfigHash = mcpHashSource && Object.keys(mcpHashSource).length > 0 ? computeConfigHash(mcpHashSource) : undefined; return { configHash, mcpConfigHash }; } // ============================================ // MCP μ„€μ • 해석 // ============================================ export function resolveMcpSources( config: Record, mcpConfig?: Record | null ): { mcpHashSource?: Record; mcpServers?: Record; } { if (mcpConfig && typeof mcpConfig === "object") { const mcpServers = (mcpConfig as Record).mcpServers; if (mcpServers && typeof mcpServers === "object" && !Array.isArray(mcpServers)) { return { mcpHashSource: mcpConfig, mcpServers: mcpServers as Record, }; } return { mcpHashSource: mcpConfig, mcpServers: mcpConfig, }; } const configServers = config.mcpServers as Record | undefined; if (configServers && typeof configServers === "object") { return { mcpHashSource: configServers, mcpServers: configServers, }; } return {}; }