/** * Pure-JS port of the path helpers from `src/pilot/paths.ts`. * * Lets `ui/server/` resolve `~/.pilotdeck` and encode project IDs the * same way the gateway server does, WITHOUT pulling `dist/src/pilot/` * into the express bridge. Keeping the math here means the UI server * can run from source without needing the TypeScript output to exist * on disk first. * * Keep this in sync with `src/pilot/paths.ts` — both must round-trip * identically or `~/.pilotdeck/projects//.cwd` markers written by * the bridge will not be found by `gateway.listProjects()` and vice * versa. */ import { homedir } from 'node:os'; import { resolve } from 'node:path'; import { createHash } from 'node:crypto'; export const DEFAULT_PILOT_HOME = '~/.pilotdeck'; function normalizeHomePath(p) { if (p === '~') return homedir(); if (p.startsWith('~/')) return resolve(homedir(), p.slice(2)); return resolve(p); } /** * Resolve the active PilotDeck home directory. Honors `PILOT_HOME` so * tests / multi-instance setups can isolate state. Defaults to * `~/.pilotdeck`. * * @param {Record} [env] Environment to read. * @returns {string} Absolute path. */ export function resolvePilotHome(env = process.env) { return normalizeHomePath(env.PILOT_HOME ?? DEFAULT_PILOT_HOME); } /** * Encode an absolute project path into the on-disk project ID used under * `~/.pilotdeck/projects//`. * * This is the legacy lossy encoding. New UI-created projects use * `createCollisionResistantProjectId()` only when this id is already claimed * by a different `.cwd` marker. * * @param {string} projectRoot Absolute filesystem path. * @returns {string} Encoded project ID. */ export function createProjectId(projectRoot) { const normalizedRoot = resolve(projectRoot); return createLegacyProjectId(normalizedRoot); } export function createCollisionResistantProjectId(projectRoot) { const normalizedRoot = resolve(projectRoot); const legacyId = createLegacyProjectId(normalizedRoot); const digest = createHash('sha1').update(normalizedRoot).digest('hex').slice(0, 10); return `${legacyId}--${digest}`; } /** * Sanitize a sessionId for safe use as a filename component. * * TUI/CLI sessionKeys embed the absolute project path (e.g. * `tui:project=/Users/foo/work/repo:default`). Without sanitization * the raw `/` characters make `path.resolve()` treat it as multiple * path segments, burying the transcript in nested dirs that * `listProjectSessions` can't find. * * Keep in sync with `src/session/storage/ProjectSessionStorage.ts`. * * @param {string} sessionId Raw session key. * @returns {string} Filename-safe session identifier. */ export function sanitizeSessionIdForPath(sessionId) { const illegal = process.platform === 'win32' ? /[\\/:<>"|?*]+/g : /[\\/]+/g; return sessionId.replace(illegal, '-').replace(/^-+|-+$/g, '') || 'session'; } function createLegacyProjectId(projectRoot) { // Normalize to forward slashes so the same physical path produces the same // project ID on Windows (\) and Unix (/). Also strip a Windows drive-letter // prefix (e.g. "C:") so "C:\Users\foo" slugifies identically to "/Users/foo". const normalized = projectRoot.replace(/\\/g, '/').replace(/^[A-Za-z]:/, ''); return normalized.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'project'; }