fs-access.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /**
  2. * The in-host filesystem for shell runs: {@link ShellFileSystem} straight over
  3. * the mounted VFS, plus the path and diagnostic helpers every program shares.
  4. *
  5. * This implementation answers from memory. A command running in its own
  6. * worker uses the message-backed one (`./process/child.ts`), which this one
  7. * serves from the host side.
  8. * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/shell/fs-access
  9. */
  10. import { resolve } from '../module-system/posix-path.ts'
  11. import { requireActiveVfs } from '../storage/active.ts'
  12. import type { VfsError, VfsStats } from '../storage/types.ts'
  13. import type { ShellDirent, ShellFileSystem, ShellStats } from './types.ts'
  14. /**
  15. * Resolve one shell word into an absolute VFS path.
  16. * @param cwd - the shell's working directory.
  17. * @param path - absolute or relative path as the command line spelled it.
  18. * @returns the absolute normalized path.
  19. */
  20. export function resolveIn(cwd: string, path: string): string {
  21. return resolve(cwd, path)
  22. }
  23. /**
  24. * Restate a filesystem failure the way a shell utility reports it, so the model
  25. * reads `cat: /dsh/none: No such file or directory` instead of a Node error
  26. * string.
  27. * @param program - the utility's name, used as the message prefix.
  28. * @param path - the path the utility was working on.
  29. * @param error - the failure the filesystem raised.
  30. * @returns the single-line diagnostic, without a trailing newline.
  31. */
  32. export function describeFailure(program: string, path: string, error: unknown): string {
  33. const code = (error as Partial<VfsError>).code
  34. const reason = code === 'ENOENT'
  35. ? 'No such file or directory'
  36. : code === 'ENOTDIR'
  37. ? 'Not a directory'
  38. : code === 'EISDIR'
  39. ? 'Is a directory'
  40. : code === 'ENOTEMPTY'
  41. ? 'Directory not empty'
  42. : code === 'EEXIST'
  43. ? 'File exists'
  44. : error instanceof Error ? error.message : String(error)
  45. return `${program}: ${path}: ${reason}`
  46. }
  47. /**
  48. * Build a Node-shaped filesystem error, for the conditions this layer detects
  49. * itself and for the worker transport, which can carry a code but not a class.
  50. * @param code - the Node error code (`ENOENT`, `EISDIR`, …).
  51. * @param syscall - the operation that failed.
  52. * @param path - the path it failed on.
  53. * @returns the error to throw.
  54. */
  55. export function filesystemError(code: string, syscall: string, path: string): VfsError {
  56. const reason = code === 'EACCES' ? 'permission denied' : `${syscall} failed`
  57. const error = new Error(`${code}: ${reason}, ${syscall} '${path}'`) as VfsError
  58. error.code = code
  59. error.path = path
  60. error.syscall = syscall
  61. return error
  62. }
  63. /** Project VFS stats onto the facts a program reads. */
  64. function statsOf(stats: VfsStats): ShellStats {
  65. return { directory: stats.isDirectory(), size: stats.size, mtimeMs: stats.mtimeMs }
  66. }
  67. /**
  68. * The filesystem backed by the VFS mounted in this thread.
  69. * @returns the in-host {@link ShellFileSystem}.
  70. */
  71. export function hostFileSystem(): ShellFileSystem {
  72. const vfs = (): ReturnType<typeof requireActiveVfs> => requireActiveVfs()
  73. const stat = (path: string): Promise<ShellStats | undefined> => {
  74. try {
  75. return Promise.resolve(statsOf(vfs().statSync(path) as VfsStats))
  76. } catch {
  77. // Absence is the answer callers branch on; every other failure mode of
  78. // the in-memory backend is also "this path holds nothing readable".
  79. return Promise.resolve(undefined)
  80. }
  81. }
  82. // Several members take no await: the face is asynchronous because a process
  83. // worker's filesystem is, while this backend answers from memory.
  84. return {
  85. stat,
  86. list: async (path: string): Promise<ShellDirent[]> => {
  87. const names = [...vfs().readdirSync(path) as string[]].sort()
  88. const entries: ShellDirent[] = []
  89. for (const name of names) {
  90. entries.push({ name, directory: (await stat(resolve(path, name)))?.directory ?? false })
  91. }
  92. return entries
  93. },
  94. readText: async (path: string): Promise<string> => {
  95. if ((await stat(path))?.directory === true) throw filesystemError('EISDIR', 'read', path)
  96. return vfs().readFileSync(path, 'utf8') as string
  97. },
  98. writeText: (path: string, text: string, append = false): Promise<void> => {
  99. if (append) vfs().appendFileSync(path, text)
  100. else vfs().writeFileSync(path, text)
  101. return Promise.resolve()
  102. },
  103. mkdir: (path: string, recursive: boolean): Promise<void> => {
  104. vfs().mkdirSync(path, { recursive })
  105. return Promise.resolve()
  106. },
  107. remove: (path: string, options: { recursive: boolean; force: boolean }): Promise<void> => {
  108. vfs().rmSync(path, options)
  109. return Promise.resolve()
  110. },
  111. rename: (from: string, to: string): Promise<void> => {
  112. vfs().renameSync(from, to)
  113. return Promise.resolve()
  114. },
  115. }
  116. }