| 1 | import fs from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | |
| 4 | export const IMPECCABLE_DIR = '.impeccable'; |
| 5 | export const LIVE_DIR = 'live'; |
| 6 | export const CRITIQUE_DIR = 'critique'; |
| 7 | |
| 8 | export function getImpeccableDir(cwd = process.cwd()) { |
| 9 | return path.join(cwd, IMPECCABLE_DIR); |
| 10 | } |
| 11 | |
| 12 | export function getDesignSidecarPath(cwd = process.cwd()) { |
| 13 | return path.join(getImpeccableDir(cwd), 'design.json'); |
| 14 | } |
| 15 | |
| 16 | export function getDesignSidecarCandidates(cwd = process.cwd(), contextDir = cwd) { |
| 17 | const candidates = [ |
| 18 | getDesignSidecarPath(cwd), |
| 19 | path.join(cwd, 'DESIGN.json'), |
| 20 | ]; |
| 21 | const contextLegacy = path.join(contextDir, 'DESIGN.json'); |
| 22 | if (!candidates.includes(contextLegacy)) candidates.push(contextLegacy); |
| 23 | return candidates; |
| 24 | } |
| 25 | |
| 26 | export function resolveDesignSidecarPath(cwd = process.cwd(), contextDir = cwd) { |
| 27 | return firstExisting(getDesignSidecarCandidates(cwd, contextDir)); |
| 28 | } |
| 29 | |
| 30 | export function getLiveDir(cwd = process.cwd()) { |
| 31 | return path.join(getImpeccableDir(cwd), LIVE_DIR); |
| 32 | } |
| 33 | |
| 34 | export function getLiveConfigPath(cwd = process.cwd()) { |
| 35 | return path.join(getLiveDir(cwd), 'config.json'); |
| 36 | } |
| 37 | |
| 38 | export function getLegacyLiveConfigPath(scriptsDir) { |
| 39 | return path.join(scriptsDir, 'config.json'); |
| 40 | } |
| 41 | |
| 42 | export function resolveLiveConfigPath({ cwd = process.cwd(), scriptsDir, env = process.env } = {}) { |
| 43 | if (env.IMPECCABLE_LIVE_CONFIG && env.IMPECCABLE_LIVE_CONFIG.trim()) { |
| 44 | const configured = env.IMPECCABLE_LIVE_CONFIG.trim(); |
| 45 | return path.isAbsolute(configured) ? configured : path.resolve(cwd, configured); |
| 46 | } |
| 47 | const primary = getLiveConfigPath(cwd); |
| 48 | if (fs.existsSync(primary)) return primary; |
| 49 | if (scriptsDir) { |
| 50 | const legacy = getLegacyLiveConfigPath(scriptsDir); |
| 51 | if (fs.existsSync(legacy)) return legacy; |
| 52 | } |
| 53 | return primary; |
| 54 | } |
| 55 | |
| 56 | export function getLiveServerPath(cwd = process.cwd()) { |
| 57 | return path.join(getLiveDir(cwd), 'server.json'); |
| 58 | } |
| 59 | |
| 60 | export function getLegacyLiveServerPath(cwd = process.cwd()) { |
| 61 | return path.join(cwd, '.impeccable-live.json'); |
| 62 | } |
| 63 | |
| 64 | export function readLiveServerInfo(cwd = process.cwd()) { |
| 65 | for (const filePath of [getLiveServerPath(cwd), getLegacyLiveServerPath(cwd)]) { |
| 66 | try { |
| 67 | const info = JSON.parse(fs.readFileSync(filePath, 'utf-8')); |
| 68 | if (info && typeof info.pid === 'number' && !isLiveServerPidReachable(info.pid)) { |
| 69 | try { fs.unlinkSync(filePath); } catch {} |
| 70 | continue; |
| 71 | } |
| 72 | return { info, path: filePath }; |
| 73 | } catch { |
| 74 | /* try next */ |
| 75 | } |
| 76 | } |
| 77 | return null; |
| 78 | } |
| 79 | |
| 80 | export function isLiveServerPidReachable(pid) { |
| 81 | try { |
| 82 | process.kill(pid, 0); |
| 83 | return true; |
| 84 | } catch (err) { |
| 85 | // ESRCH means "no such process". EPERM means the process exists but this |
| 86 | // user cannot signal it, so the live server info is still valid. |
| 87 | return err?.code !== 'ESRCH'; |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | export function writeLiveServerInfo(cwd = process.cwd(), info) { |
| 92 | const filePath = getLiveServerPath(cwd); |
| 93 | fs.mkdirSync(path.dirname(filePath), { recursive: true }); |
| 94 | fs.writeFileSync(filePath, JSON.stringify(info)); |
| 95 | return filePath; |
| 96 | } |
| 97 | |
| 98 | export function removeLiveServerInfo(cwd = process.cwd()) { |
| 99 | for (const filePath of [getLiveServerPath(cwd), getLegacyLiveServerPath(cwd)]) { |
| 100 | try { fs.unlinkSync(filePath); } catch {} |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | export function getLiveSessionsDir(cwd = process.cwd()) { |
| 105 | return path.join(getLiveDir(cwd), 'sessions'); |
| 106 | } |
| 107 | |
| 108 | export function getLegacyLiveSessionsDir(cwd = process.cwd()) { |
| 109 | return path.join(cwd, '.impeccable-live', 'sessions'); |
| 110 | } |
| 111 | |
| 112 | export function getLiveAnnotationsDir(cwd = process.cwd()) { |
| 113 | return path.join(getLiveDir(cwd), 'annotations'); |
| 114 | } |
| 115 | |
| 116 | export function getCritiqueDir(cwd = process.cwd()) { |
| 117 | return path.join(getImpeccableDir(cwd), CRITIQUE_DIR); |
| 118 | } |
| 119 | |
| 120 | export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) { |
| 121 | return path.join(cwd, '.impeccable-live', 'annotations'); |
| 122 | } |
| 123 | |
| 124 | function firstExisting(paths) { |
| 125 | return paths.find((filePath) => fs.existsSync(filePath)) || null; |
| 126 | } |
| 127 |