index.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /**
  2. * Shared filesystem path helpers for DeepSeek Harness user data.
  3. *
  4. * @module @deepseek-ai/dsh-paths
  5. */
  6. import { opendir, realpath } from 'node:fs/promises'
  7. import { homedir } from 'node:os'
  8. import { basename, dirname, join, resolve } from 'node:path'
  9. /** Directory name for the default DeepSeek Harness home under the OS home. */
  10. export const DSH_HOME_DIR_NAME = '.dsh'
  11. /** Stable user-facing display form for the default DeepSeek Harness home. */
  12. export const DEFAULT_DSH_HOME_DISPLAY = `~/${DSH_HOME_DIR_NAME}`
  13. /** Environment variable that overrides the default DeepSeek Harness home. */
  14. export const DSH_HOME_ENV = 'DSH_HOME'
  15. /**
  16. * Give a native filesystem watcher one canonical spelling of a path, even
  17. * when its final components do not exist yet. The deepest existing ancestor
  18. * is resolved through {@link realpath}; when a suffix is missing, that
  19. * ancestor is also proved to be an enumerable directory before the suffix is
  20. * restored. This prevents Windows from treating a regular-file ancestor as
  21. * ordinary absence, and prevents short-name aliases from being mixed with
  22. * long paths emitted by the native watcher backend.
  23. * @param path - Watch target or root, resolved against the current directory.
  24. * @returns the target with its existing ancestor canonicalized.
  25. * @throws when ancestor traversal encounters an error other than absence, or
  26. * the existing ancestor of a missing suffix is not an enumerable directory.
  27. */
  28. export async function canonicalizeWatchPath(path: string): Promise<string> {
  29. let current = resolve(path)
  30. const missing: string[] = []
  31. while (true) {
  32. try {
  33. const canonical = await realpath(current)
  34. if (missing.length > 0) {
  35. // A Windows file-as-parent probe reports ENOENT. Opening the resolved
  36. // ancestor preserves the cross-platform directory requirement.
  37. const directory = await opendir(canonical)
  38. await directory.close()
  39. }
  40. return join(canonical, ...missing.reverse())
  41. } catch (error) {
  42. if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error
  43. const parent = dirname(current)
  44. /* v8 ignore next -- a filesystem root exists, so traversal resolves before this guard */
  45. if (parent === current) throw error
  46. missing.push(basename(current))
  47. current = parent
  48. }
  49. }
  50. }
  51. /**
  52. * Resolve the default DeepSeek Harness home using Node's platform path rules.
  53. * @returns the absolute default harness home path.
  54. */
  55. export function defaultDshHome(): string {
  56. return join(homedir(), DSH_HOME_DIR_NAME)
  57. }
  58. /**
  59. * Expand supported tilde prefixes against the operating-system home.
  60. * @param path - configured path that may begin with `~`, `~/`, or `~\`.
  61. * @returns the expanded path, or the original value when no supported prefix is present.
  62. */
  63. export function expandHomePath(path: string): string {
  64. if (path === '~') return homedir()
  65. if (path.startsWith('~/') || path.startsWith('~\\')) return join(homedir(), path.slice(2))
  66. return path
  67. }
  68. /**
  69. * Resolve the single-root DeepSeek Harness home.
  70. *
  71. * Precedence, highest first: an explicit configured path, `$DSH_HOME`, then
  72. * `~/.dsh`. The harness keeps all user data under one root. An empty or
  73. * whitespace-only `$DSH_HOME` is treated as unset, so a blank override never
  74. * resolves the home to the current working directory.
  75. * @param configured - explicit harness-home override, which has highest precedence.
  76. * @param env - environment mapping used to read `DSH_HOME`.
  77. * @returns the normalized absolute harness home path.
  78. */
  79. export function resolveDshHome(configured?: string, env: Record<string, string | undefined> = process.env): string {
  80. const fromEnv = env[DSH_HOME_ENV]
  81. const selected = configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())
  82. return resolve(expandHomePath(selected))
  83. }
  84. /**
  85. * Join path segments onto the resolved DeepSeek Harness home.
  86. * @param segments - path segments appended to the Harness home; an empty list returns the home itself.
  87. * @returns the normalized absolute joined path.
  88. */
  89. export function dshHomePath(...segments: string[]): string {
  90. return join(resolveDshHome(), ...segments)
  91. }
  92. /**
  93. * Describe a resolved harness home symbolically for user-facing display.
  94. *
  95. * It never returns an absolute machine path: the default home is labelled
  96. * `~/.dsh`, and any configured home is labelled `$DSH_HOME`.
  97. * @param resolvedHome - the absolute path returned by {@link resolveDshHome}.
  98. * @returns `~/.dsh` for the default home, otherwise `$DSH_HOME`.
  99. */
  100. export function dshHomeDisplay(resolvedHome: string): string {
  101. return resolvedHome === resolve(defaultDshHome()) ? DEFAULT_DSH_HOME_DISPLAY : `$${DSH_HOME_ENV}`
  102. }