index.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /**
  2. * Browser-safe Workspace path and display helpers.
  3. * @module @deepseek-ai/dsh-util-workspace-path
  4. */
  5. import { sessionFileAddress } from './file-address.ts'
  6. /** Whether a path uses a Windows drive or UNC prefix. */
  7. function isWindowsStylePath(value: string): boolean {
  8. return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith('\\\\')
  9. }
  10. /**
  11. * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`) or Windows drive or UNC.
  12. * @param path - the path to classify.
  13. * @returns `true` for an absolute path; `false` for a Workspace-relative one.
  14. */
  15. export function isAbsoluteWorkspacePath(path: string): boolean {
  16. return path.startsWith('/') || isWindowsStylePath(path)
  17. }
  18. /**
  19. * Resolve a Workspace-relative path into the Host-facing spelling used by path operations.
  20. * @param cwd - Session Workspace root, when known.
  21. * @param path - Absolute or Workspace-relative path.
  22. * @returns an absolute path when a Workspace root is available, otherwise the original path.
  23. */
  24. export function resolveWorkspacePath(cwd: string | undefined, path: string): string {
  25. if (isAbsoluteWorkspacePath(path)) return path
  26. if (cwd === undefined || cwd === '') return path
  27. const separator = isWindowsStylePath(cwd) && cwd.includes('\\') ? '\\' : '/'
  28. const base = cwd.replace(/[/\\]+$/, '')
  29. const relative = path.replace(/^[/\\]+/, '')
  30. return `${base}${separator}${relative}`
  31. }
  32. /**
  33. * Abbreviate a POSIX home directory for display.
  34. * @param path - Absolute or already-short display path.
  35. * @param home - Host account home; absent skips abbreviation.
  36. * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
  37. */
  38. export function abbreviateHomePath(path: string, home?: string): string {
  39. if (home === undefined || home === '') return path
  40. if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path
  41. const root = home.replace(/\/+$/, '')
  42. if (root === '' || root === '/') return path
  43. if (path.replace(/\/+$/, '') === root) return '~'
  44. if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`
  45. return path
  46. }
  47. /**
  48. * Read the final non-empty segment of a Workspace path for display.
  49. * Workspace-label surfaces use this helper instead of deriving another basename.
  50. * @param path - Workspace directory path using POSIX or Windows separators.
  51. * @returns the final segment, or an empty string for a separator-only path.
  52. */
  53. export function workspaceTitleOf(path: string): string {
  54. const trimmed = path.replace(/[/\\]+$/, '')
  55. const separator = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\'))
  56. return trimmed.slice(separator + 1)
  57. }
  58. /**
  59. * Split a path for display: the directories through their last separator, and
  60. * the final segment after it. Both `/` and `\` separate, so a Windows path
  61. * splits where its own segments end; trailing separators are dropped first, so
  62. * a directory path names its own last segment. A path with no separator, or a
  63. * separator-only path, is all name.
  64. * @param path - file or directory path using POSIX or Windows separators.
  65. * @returns the directory prefix (possibly empty) and the final segment.
  66. */
  67. export function pathPartsOf(path: string): { readonly directory: string; readonly name: string } {
  68. const trimmed = path.replace(/[/\\]+$/, '')
  69. if (trimmed === '') return { directory: '', name: path }
  70. const cut = Math.max(trimmed.lastIndexOf('/'), trimmed.lastIndexOf('\\')) + 1
  71. return { directory: trimmed.slice(0, cut), name: trimmed.slice(cut) }
  72. }
  73. export * from './file-address.ts'
  74. /**
  75. * The address for a path as a caller holds it: a relative path, or an absolute
  76. * path inside the Session's workspace, becomes a `session`-scoped address; an
  77. * absolute path outside it, or one whose workspace root is unknown, keeps its
  78. * absolute path in that Session's address.
  79. * @param sessionId - the Session the path is read in.
  80. * @param cwd - that Session's workspace root, when known.
  81. * @param path - absolute or workspace-relative path, in either separator spelling.
  82. * @returns the `dsh-resource://file/…` address.
  83. */
  84. export function fileAddressFor(sessionId: string, cwd: string | undefined, path: string): string {
  85. const normalized = path.replace(/\\/g, '/')
  86. if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized)
  87. const root = cwd === undefined ? '' : cwd.replace(/\\/g, '/').replace(/\/+$/, '')
  88. if (root !== '' && normalized === root) return sessionFileAddress(sessionId, '')
  89. if (root !== '' && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1))
  90. return sessionFileAddress(sessionId, normalized)
  91. }