* 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/<id>/.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<string, string | undefined>} [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/<id>/`.
*
* 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) {
const normalized = projectRoot.replace(/\\/g, '/').replace(/^[A-Za-z]:/, '');
return normalized.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'project';
}