validation.ts 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import chalk from 'chalk'
  2. import { stat } from 'fs/promises'
  3. import { dirname, resolve } from 'path'
  4. import type { ToolPermissionContext } from '../../Tool.js'
  5. import { getErrnoCode } from '../../utils/errors.js'
  6. import { expandPath } from '../../utils/path.js'
  7. import {
  8. allWorkingDirectories,
  9. pathInWorkingPath,
  10. } from '../../utils/permissions/filesystem.js'
  11. export type AddDirectoryResult =
  12. | {
  13. resultType: 'success'
  14. absolutePath: string
  15. }
  16. | {
  17. resultType: 'emptyPath'
  18. }
  19. | {
  20. resultType: 'pathNotFound' | 'notADirectory'
  21. directoryPath: string
  22. absolutePath: string
  23. }
  24. | {
  25. resultType: 'alreadyInWorkingDirectory'
  26. directoryPath: string
  27. workingDir: string
  28. }
  29. export async function validateDirectoryForWorkspace(
  30. directoryPath: string,
  31. permissionContext: ToolPermissionContext,
  32. ): Promise<AddDirectoryResult> {
  33. if (!directoryPath) {
  34. return {
  35. resultType: 'emptyPath',
  36. }
  37. }
  38. // resolve() strips the trailing slash expandPath can leave on absolute
  39. // inputs, so /foo and /foo/ map to the same storage key (CC-33).
  40. const absolutePath = resolve(expandPath(directoryPath))
  41. // Check if path exists and is a directory (single syscall)
  42. try {
  43. const stats = await stat(absolutePath)
  44. if (!stats.isDirectory()) {
  45. return {
  46. resultType: 'notADirectory',
  47. directoryPath,
  48. absolutePath,
  49. }
  50. }
  51. } catch (e: unknown) {
  52. const code = getErrnoCode(e)
  53. // Match prior existsSync() semantics: treat any of these as "not found"
  54. // rather than re-throwing. EACCES/EPERM in particular must not crash
  55. // startup when a settings-configured additional directory is inaccessible.
  56. if (
  57. code === 'ENOENT' ||
  58. code === 'ENOTDIR' ||
  59. code === 'EACCES' ||
  60. code === 'EPERM'
  61. ) {
  62. return {
  63. resultType: 'pathNotFound',
  64. directoryPath,
  65. absolutePath,
  66. }
  67. }
  68. throw e
  69. }
  70. // Get current permission context
  71. const currentWorkingDirs = allWorkingDirectories(permissionContext)
  72. // Check if already within an existing working directory
  73. for (const workingDir of currentWorkingDirs) {
  74. if (pathInWorkingPath(absolutePath, workingDir)) {
  75. return {
  76. resultType: 'alreadyInWorkingDirectory',
  77. directoryPath,
  78. workingDir,
  79. }
  80. }
  81. }
  82. return {
  83. resultType: 'success',
  84. absolutePath,
  85. }
  86. }
  87. export function addDirHelpMessage(result: AddDirectoryResult): string {
  88. switch (result.resultType) {
  89. case 'emptyPath':
  90. return 'Please provide a directory path.'
  91. case 'pathNotFound':
  92. return `Path ${chalk.bold(result.absolutePath)} was not found.`
  93. case 'notADirectory': {
  94. const parentDir = dirname(result.absolutePath)
  95. return `${chalk.bold(result.directoryPath)} is not a directory. Did you mean to add the parent directory ${chalk.bold(parentDir)}?`
  96. }
  97. case 'alreadyInWorkingDirectory':
  98. return `${chalk.bold(result.directoryPath)} is already accessible within the existing working directory ${chalk.bold(result.workingDir)}.`
  99. case 'success':
  100. return `Added ${chalk.bold(result.absolutePath)} as a working directory.`
  101. }
  102. }