// cross-platform-paths.mjs — Cache/Log/State/Backup dir resolution per OS. // Zero dependencies. macOS uses ~/Library/{Caches,Logs,Application Support}, // Linux uses XDG with fallbacks, Windows uses %LOCALAPPDATA%. import { mkdirSync } from 'node:fs'; import { homedir as osHomedir, platform as osPlatform } from 'node:os'; import { join } from 'node:path'; function resolveOpts(opts = {}) { return { platform: opts.platform ?? osPlatform(), homedir: opts.homedir ?? osHomedir, env: opts.env ?? process.env, }; } function ensureDir(path) { mkdirSync(path, { recursive: true }); return path; } function requireApp(appName) { if (!appName || typeof appName !== 'string') { throw new Error('cross-platform-paths: appName is required and must be a non-empty string'); } } /** * Resolve a per-app cache directory. * @param {string} appName — application identifier, e.g. "ms-ai-architect" * @param {object} [opts] — { platform, homedir, env } for testing * @returns {string} absolute path to the cache directory (created if missing) */ export function getCacheDir(appName, opts) { requireApp(appName); const { platform, homedir, env } = resolveOpts(opts); const home = homedir(); if (platform === 'darwin') { return ensureDir(join(home, 'Library', 'Caches', appName)); } if (platform === 'win32') { const lad = env.LOCALAPPDATA || join(home, 'AppData', 'Local'); return ensureDir(join(lad, appName, 'Cache')); } // linux + everything else const xdg = env.XDG_CACHE_HOME || join(home, '.cache'); return ensureDir(join(xdg, appName)); } /** * Resolve a per-app log directory. * @param {string} appName * @param {object} [opts] * @returns {string} absolute path to the log directory (created if missing) */ export function getLogDir(appName, opts) { requireApp(appName); const { platform, homedir, env } = resolveOpts(opts); const home = homedir(); if (platform === 'darwin') { return ensureDir(join(home, 'Library', 'Logs', appName)); } if (platform === 'win32') { const lad = env.LOCALAPPDATA || join(home, 'AppData', 'Local'); return ensureDir(join(lad, appName, 'Logs')); } const xdg = env.XDG_STATE_HOME || join(home, '.local', 'state'); return ensureDir(join(xdg, appName, 'logs')); } /** * Resolve a per-app state/data directory (persistent app state, not cache). * @param {string} appName * @param {object} [opts] * @returns {string} absolute path (created if missing) */ export function getStateDir(appName, opts) { requireApp(appName); const { platform, homedir, env } = resolveOpts(opts); const home = homedir(); if (platform === 'darwin') { return ensureDir(join(home, 'Library', 'Application Support', appName)); } if (platform === 'win32') { const lad = env.LOCALAPPDATA || join(home, 'AppData', 'Local'); return ensureDir(join(lad, appName, 'State')); } const xdg = env.XDG_STATE_HOME || join(home, '.local', 'state'); return ensureDir(join(xdg, appName)); } /** * Resolve the backup directory under a plugin root. * @param {string} pluginRoot — absolute path to the plugin root * @returns {string} absolute path to /.kb-backup (created if missing) */ export function getBackupDir(pluginRoot) { if (!pluginRoot || typeof pluginRoot !== 'string') { throw new Error('cross-platform-paths: pluginRoot is required and must be a non-empty string'); } return ensureDir(join(pluginRoot, '.kb-backup')); }