import { mkdir, readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' /** * Project-scoped settings, living in `.helder/` in the opened project's root. * - config.default.json full built-in defaults, REGENERATED on every launch * (live documentation; the app never reads user edits here) * - config.json sparse — only user-overridden values * - theme.css custom CSS over the built-in dark theme; the CODE FONT * and FONT SIZE live here (as CSS vars), not in the JSON * Effective value = config.json over config.default.json, merged key by key. */ export type DiffMode = 'original' | 'updated' | 'diff' export interface HelderConfig { ai: { command: string; autoLaunch: boolean } editor: { autoSave: boolean; tabSize: number } git: { confirmDiscard: boolean; confirmStage: boolean; confirmUnstage: boolean; defaultDiffMode: DiffMode } files: { exclude: string[]; followGitignore: boolean } terminal: { shell: string | null } session: { restoreOnLaunch: boolean } } export const DEFAULTS: HelderConfig = { ai: { command: 'claude', autoLaunch: true }, editor: { autoSave: false, tabSize: 4 }, git: { confirmDiscard: true, confirmStage: false, confirmUnstage: false, defaultDiffMode: 'diff' }, files: { exclude: [], followGitignore: true }, terminal: { shell: null }, session: { restoreOnLaunch: true }, } const THEME_TEMPLATE = `/* Helder theme — custom CSS applied OVER the built-in dark theme. * This file is created once and never overwritten; edit it freely. * The code font and font size live here (not in config.json). Uncomment and * tweak any variable below; you can also override any --token from the built-in * theme (see the design tokens in the app's styles). */ :root { /* Code surfaces (editor + terminals) */ /* --code-font: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; */ /* --code-size: 13px; */ /* editor font size */ /* --term-size: 12.5px; */ /* terminal font size */ /* Example accent override: */ /* --accent: #4d8dff; */ } ` /** Recently-opened files, newest first. Local machine state — git-ignored. */ const RECENT_FILE = 'recent.json' const MAX_RECENT = 100 const GITIGNORE_BODY = `# Helder — local, machine-specific state (do not commit)\n${RECENT_FILE}\n` let current: HelderConfig = DEFAULTS let themeCss = '' /** Create `.helder/.gitignore` (ignoring recent.json) only when it's missing. */ async function ensureGitignore(dir: string): Promise { const path = join(dir, '.gitignore') try { await readFile(path, 'utf8') } catch { await writeFile(path, GITIGNORE_BODY) } } export async function getRecent(root: string): Promise { try { const arr = JSON.parse(await readFile(join(root, '.helder', RECENT_FILE), 'utf8')) return Array.isArray(arr) ? arr.filter((p): p is string => typeof p === 'string').slice(0, MAX_RECENT) : [] } catch { return [] } } export async function setRecent(root: string, list: string[]): Promise { try { const dir = join(root, '.helder') await mkdir(dir, { recursive: true }) await ensureGitignore(dir) await writeFile(join(dir, RECENT_FILE), JSON.stringify(list.slice(0, MAX_RECENT), null, 2) + '\n') } catch { /* read-only / inaccessible root — recents just won't persist */ } } function isPlainObject(v: unknown): v is Record { return !!v && typeof v === 'object' && !Array.isArray(v) } function deepMerge(base: T, over: unknown): T { if (!isPlainObject(base) || !isPlainObject(over)) return base const out: Record = { ...base } for (const key of Object.keys(over)) { const b = (base as Record)[key] const o = over[key] if (isPlainObject(b) && isPlainObject(o)) out[key] = deepMerge(b, o) else if (o !== undefined) out[key] = o } return out as T } /** (Re)resolve config + theme for a project root, regenerating the defaults file. */ export async function resolveConfig(root: string): Promise { const dir = join(root, '.helder') try { await mkdir(dir, { recursive: true }) // Always regenerate the defaults file — it documents every setting. await writeFile(join(dir, 'config.default.json'), JSON.stringify(DEFAULTS, null, 2) + '\n') let override: unknown = {} try { override = JSON.parse(await readFile(join(dir, 'config.json'), 'utf8')) } catch { /* none / invalid */ } current = deepMerge(DEFAULTS, override) try { themeCss = await readFile(join(dir, 'theme.css'), 'utf8') } catch { themeCss = THEME_TEMPLATE await writeFile(join(dir, 'theme.css'), THEME_TEMPLATE) } await ensureGitignore(dir) } catch { // Read-only / inaccessible root: fall back to built-in defaults. current = DEFAULTS themeCss = '' } } export function getConfig(): HelderConfig { return current } export function getThemeCss(): string { return themeCss }