pilotPaths.js 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. /**
  2. * Pure-JS port of the path helpers from `src/pilot/paths.ts`.
  3. *
  4. * Lets `ui/server/` resolve `~/.pilotdeck` and encode project IDs the
  5. * same way the gateway server does, WITHOUT pulling `dist/src/pilot/`
  6. * into the express bridge. Keeping the math here means the UI server
  7. * can run from source without needing the TypeScript output to exist
  8. * on disk first.
  9. *
  10. * Keep this in sync with `src/pilot/paths.ts` — both must round-trip
  11. * identically or `~/.pilotdeck/projects/<id>/.cwd` markers written by
  12. * the bridge will not be found by `gateway.listProjects()` and vice
  13. * versa.
  14. */
  15. import { homedir } from 'node:os';
  16. import { resolve } from 'node:path';
  17. import { createHash } from 'node:crypto';
  18. export const DEFAULT_PILOT_HOME = '~/.pilotdeck';
  19. function normalizeHomePath(p) {
  20. if (p === '~') return homedir();
  21. if (p.startsWith('~/')) return resolve(homedir(), p.slice(2));
  22. return resolve(p);
  23. }
  24. /**
  25. * Resolve the active PilotDeck home directory. Honors `PILOT_HOME` so
  26. * tests / multi-instance setups can isolate state. Defaults to
  27. * `~/.pilotdeck`.
  28. *
  29. * @param {Record<string, string | undefined>} [env] Environment to read.
  30. * @returns {string} Absolute path.
  31. */
  32. export function resolvePilotHome(env = process.env) {
  33. return normalizeHomePath(env.PILOT_HOME ?? DEFAULT_PILOT_HOME);
  34. }
  35. /**
  36. * Encode an absolute project path into the on-disk project ID used under
  37. * `~/.pilotdeck/projects/<id>/`.
  38. *
  39. * This is the legacy lossy encoding. New UI-created projects use
  40. * `createCollisionResistantProjectId()` only when this id is already claimed
  41. * by a different `.cwd` marker.
  42. *
  43. * @param {string} projectRoot Absolute filesystem path.
  44. * @returns {string} Encoded project ID.
  45. */
  46. export function createProjectId(projectRoot) {
  47. const normalizedRoot = resolve(projectRoot);
  48. return createLegacyProjectId(normalizedRoot);
  49. }
  50. export function createCollisionResistantProjectId(projectRoot) {
  51. const normalizedRoot = resolve(projectRoot);
  52. const legacyId = createLegacyProjectId(normalizedRoot);
  53. const digest = createHash('sha1').update(normalizedRoot).digest('hex').slice(0, 10);
  54. return `${legacyId}--${digest}`;
  55. }
  56. /**
  57. * Sanitize a sessionId for safe use as a filename component.
  58. *
  59. * TUI/CLI sessionKeys embed the absolute project path (e.g.
  60. * `tui:project=/Users/foo/work/repo:default`). Without sanitization
  61. * the raw `/` characters make `path.resolve()` treat it as multiple
  62. * path segments, burying the transcript in nested dirs that
  63. * `listProjectSessions` can't find.
  64. *
  65. * Keep in sync with `src/session/storage/ProjectSessionStorage.ts`.
  66. *
  67. * @param {string} sessionId Raw session key.
  68. * @returns {string} Filename-safe session identifier.
  69. */
  70. export function sanitizeSessionIdForPath(sessionId) {
  71. const illegal = process.platform === 'win32' ? /[\\/:<>"|?*]+/g : /[\\/]+/g;
  72. return sessionId.replace(illegal, '-').replace(/^-+|-+$/g, '') || 'session';
  73. }
  74. function createLegacyProjectId(projectRoot) {
  75. // Normalize to forward slashes so the same physical path produces the same
  76. // project ID on Windows (\) and Unix (/). Also strip a Windows drive-letter
  77. // prefix (e.g. "C:") so "C:\Users\foo" slugifies identically to "/Users/foo".
  78. const normalized = projectRoot.replace(/\\/g, '/').replace(/^[A-Za-z]:/, '');
  79. return normalized.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'project';
  80. }