1import fs from 'node:fs';
2import path from 'node:path';
3
4export const IMPECCABLE_DIR = '.impeccable';
5export const LIVE_DIR = 'live';
6export const CRITIQUE_DIR = 'critique';
7
8export function getImpeccableDir(cwd = process.cwd()) {
9 return path.join(cwd, IMPECCABLE_DIR);
10}
11
12export function getDesignSidecarPath(cwd = process.cwd()) {
13 return path.join(getImpeccableDir(cwd), 'design.json');
14}
15
16export 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
26export function resolveDesignSidecarPath(cwd = process.cwd(), contextDir = cwd) {
27 return firstExisting(getDesignSidecarCandidates(cwd, contextDir));
28}
29
30export function getLiveDir(cwd = process.cwd()) {
31 return path.join(getImpeccableDir(cwd), LIVE_DIR);
32}
33
34export function getLiveConfigPath(cwd = process.cwd()) {
35 return path.join(getLiveDir(cwd), 'config.json');
36}
37
38export function getLegacyLiveConfigPath(scriptsDir) {
39 return path.join(scriptsDir, 'config.json');
40}
41
42export 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
56export function getLiveServerPath(cwd = process.cwd()) {
57 return path.join(getLiveDir(cwd), 'server.json');
58}
59
60export function getLegacyLiveServerPath(cwd = process.cwd()) {
61 return path.join(cwd, '.impeccable-live.json');
62}
63
64export function readLiveServerInfo(cwd = process.cwd()) {
65 for (const filePath of [getLiveServerPath(cwd), getLegacyLiveServerPath(cwd)]) {
66 try {
67 return { info: JSON.parse(fs.readFileSync(filePath, 'utf-8')), path: filePath };
68 } catch {
69 /* try next */
70 }
71 }
72 return null;
73}
74
75export function writeLiveServerInfo(cwd = process.cwd(), info) {
76 const filePath = getLiveServerPath(cwd);
77 fs.mkdirSync(path.dirname(filePath), { recursive: true });
78 fs.writeFileSync(filePath, JSON.stringify(info));
79 return filePath;
80}
81
82export function removeLiveServerInfo(cwd = process.cwd()) {
83 for (const filePath of [getLiveServerPath(cwd), getLegacyLiveServerPath(cwd)]) {
84 try { fs.unlinkSync(filePath); } catch {}
85 }
86}
87
88export function getLiveSessionsDir(cwd = process.cwd()) {
89 return path.join(getLiveDir(cwd), 'sessions');
90}
91
92export function getLegacyLiveSessionsDir(cwd = process.cwd()) {
93 return path.join(cwd, '.impeccable-live', 'sessions');
94}
95
96export function getLiveAnnotationsDir(cwd = process.cwd()) {
97 return path.join(getLiveDir(cwd), 'annotations');
98}
99
100export function getCritiqueDir(cwd = process.cwd()) {
101 return path.join(getImpeccableDir(cwd), CRITIQUE_DIR);
102}
103
104export function getLegacyLiveAnnotationsDir(cwd = process.cwd()) {
105 return path.join(cwd, '.impeccable-live', 'annotations');
106}
107
108function firstExisting(paths) {
109 return paths.find((filePath) => fs.existsSync(filePath)) || null;
110}