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