summaryrefslogtreecommitdiff
path: root/packages/core/src/utils/workspaceContext.ts
blob: 16d1b4c9760416cedd22004ae0534b091a92393c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import * as fs from 'fs';
import * as path from 'path';

/**
 * WorkspaceContext manages multiple workspace directories and validates paths
 * against them. This allows the CLI to operate on files from multiple directories
 * in a single session.
 */
export class WorkspaceContext {
  private directories: Set<string>;

  /**
   * Creates a new WorkspaceContext with the given initial directory and optional additional directories.
   * @param initialDirectory The initial working directory (usually cwd)
   * @param additionalDirectories Optional array of additional directories to include
   */
  constructor(initialDirectory: string, additionalDirectories: string[] = []) {
    this.directories = new Set<string>();

    this.addDirectoryInternal(initialDirectory);

    for (const dir of additionalDirectories) {
      this.addDirectoryInternal(dir);
    }
  }

  /**
   * Adds a directory to the workspace.
   * @param directory The directory path to add (can be relative or absolute)
   * @param basePath Optional base path for resolving relative paths (defaults to cwd)
   */
  addDirectory(directory: string, basePath: string = process.cwd()): void {
    this.addDirectoryInternal(directory, basePath);
  }

  /**
   * Internal method to add a directory with validation.
   */
  private addDirectoryInternal(
    directory: string,
    basePath: string = process.cwd(),
  ): void {
    const absolutePath = path.isAbsolute(directory)
      ? directory
      : path.resolve(basePath, directory);

    if (!fs.existsSync(absolutePath)) {
      throw new Error(`Directory does not exist: ${absolutePath}`);
    }

    const stats = fs.statSync(absolutePath);
    if (!stats.isDirectory()) {
      throw new Error(`Path is not a directory: ${absolutePath}`);
    }

    let realPath: string;
    try {
      realPath = fs.realpathSync(absolutePath);
    } catch (_error) {
      throw new Error(`Failed to resolve path: ${absolutePath}`);
    }

    this.directories.add(realPath);
  }

  /**
   * Gets a copy of all workspace directories.
   * @returns Array of absolute directory paths
   */
  getDirectories(): readonly string[] {
    return Array.from(this.directories);
  }

  /**
   * Checks if a given path is within any of the workspace directories.
   * @param pathToCheck The path to validate
   * @returns True if the path is within the workspace, false otherwise
   */
  isPathWithinWorkspace(pathToCheck: string): boolean {
    try {
      const absolutePath = path.resolve(pathToCheck);

      let resolvedPath = absolutePath;
      if (fs.existsSync(absolutePath)) {
        try {
          resolvedPath = fs.realpathSync(absolutePath);
        } catch (_error) {
          return false;
        }
      }

      for (const dir of this.directories) {
        if (this.isPathWithinRoot(resolvedPath, dir)) {
          return true;
        }
      }

      return false;
    } catch (_error) {
      return false;
    }
  }

  /**
   * Checks if a path is within a given root directory.
   * @param pathToCheck The absolute path to check
   * @param rootDirectory The absolute root directory
   * @returns True if the path is within the root directory, false otherwise
   */
  private isPathWithinRoot(
    pathToCheck: string,
    rootDirectory: string,
  ): boolean {
    const relative = path.relative(rootDirectory, pathToCheck);
    return (
      !relative.startsWith(`..${path.sep}`) &&
      relative !== '..' &&
      !path.isAbsolute(relative)
    );
  }
}