@google/gemini-cli-core 0.1.21 → 0.2.0-preview.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -28
- package/dist/src/code_assist/converter.d.ts +1 -0
- package/dist/src/code_assist/converter.js +1 -0
- package/dist/src/code_assist/converter.js.map +1 -1
- package/dist/src/code_assist/oauth2.d.ts +2 -0
- package/dist/src/code_assist/oauth2.js +44 -22
- package/dist/src/code_assist/oauth2.js.map +1 -1
- package/dist/src/code_assist/oauth2.test.js +99 -8
- package/dist/src/code_assist/oauth2.test.js.map +1 -1
- package/dist/src/code_assist/server.js +1 -1
- package/dist/src/code_assist/server.js.map +1 -1
- package/dist/src/code_assist/setup.js +1 -1
- package/dist/src/code_assist/setup.js.map +1 -1
- package/dist/src/code_assist/setup.test.js +14 -8
- package/dist/src/code_assist/setup.test.js.map +1 -1
- package/dist/src/config/config.d.ts +21 -2
- package/dist/src/config/config.js +36 -10
- package/dist/src/config/config.js.map +1 -1
- package/dist/src/config/config.test.js +22 -0
- package/dist/src/config/config.test.js.map +1 -1
- package/dist/src/core/client.d.ts +4 -11
- package/dist/src/core/client.js +51 -30
- package/dist/src/core/client.js.map +1 -1
- package/dist/src/core/client.test.js +296 -1
- package/dist/src/core/client.test.js.map +1 -1
- package/dist/src/core/contentGenerator.js +19 -9
- package/dist/src/core/contentGenerator.js.map +1 -1
- package/dist/src/core/contentGenerator.test.js +39 -15
- package/dist/src/core/contentGenerator.test.js.map +1 -1
- package/dist/src/core/coreToolScheduler.d.ts +2 -1
- package/dist/src/core/coreToolScheduler.js +26 -4
- package/dist/src/core/coreToolScheduler.js.map +1 -1
- package/dist/src/core/coreToolScheduler.test.js +230 -69
- package/dist/src/core/coreToolScheduler.test.js.map +1 -1
- package/dist/src/core/geminiChat.js +1 -1
- package/dist/src/core/geminiChat.js.map +1 -1
- package/dist/src/core/logger.d.ts +22 -1
- package/dist/src/core/logger.js +103 -17
- package/dist/src/core/logger.js.map +1 -1
- package/dist/src/core/logger.test.js +86 -20
- package/dist/src/core/logger.test.js.map +1 -1
- package/dist/src/core/loggingContentGenerator.d.ts +1 -5
- package/dist/src/core/loggingContentGenerator.js +4 -6
- package/dist/src/core/loggingContentGenerator.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.d.ts +2 -2
- package/dist/src/core/nonInteractiveToolExecutor.js +11 -3
- package/dist/src/core/nonInteractiveToolExecutor.js.map +1 -1
- package/dist/src/core/nonInteractiveToolExecutor.test.js +95 -46
- package/dist/src/core/nonInteractiveToolExecutor.test.js.map +1 -1
- package/dist/src/core/prompts.js +4 -4
- package/dist/src/core/prompts.js.map +1 -1
- package/dist/src/core/subagent.js +5 -5
- package/dist/src/core/subagent.js.map +1 -1
- package/dist/src/core/subagent.test.js +3 -3
- package/dist/src/core/subagent.test.js.map +1 -1
- package/dist/src/generated/git-commit.d.ts +7 -0
- package/dist/src/generated/git-commit.js +10 -0
- package/dist/src/generated/git-commit.js.map +1 -0
- package/dist/src/ide/constants.d.ts +6 -0
- package/dist/src/ide/constants.js +7 -0
- package/dist/src/ide/constants.js.map +1 -0
- package/dist/src/ide/detect-ide.js +8 -8
- package/dist/src/ide/detect-ide.js.map +1 -1
- package/dist/src/ide/ide-client.d.ts +9 -1
- package/dist/src/ide/ide-client.js +92 -22
- package/dist/src/ide/ide-client.js.map +1 -1
- package/dist/src/ide/ide-client.test.d.ts +6 -0
- package/dist/src/ide/ide-client.test.js +43 -0
- package/dist/src/ide/ide-client.test.js.map +1 -0
- package/dist/src/ide/ide-installer.js +22 -8
- package/dist/src/ide/ide-installer.js.map +1 -1
- package/dist/src/ide/ide-installer.test.js +6 -4
- package/dist/src/ide/ide-installer.test.js.map +1 -1
- package/dist/src/ide/ideContext.d.ts +6 -6
- package/dist/src/ide/process-utils.d.ts +19 -0
- package/dist/src/ide/process-utils.js +140 -0
- package/dist/src/ide/process-utils.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/oauth-provider.js +176 -58
- package/dist/src/mcp/oauth-provider.js.map +1 -1
- package/dist/src/mcp/oauth-provider.test.js +132 -62
- package/dist/src/mcp/oauth-provider.test.js.map +1 -1
- package/dist/src/mcp/oauth-utils.d.ts +3 -1
- package/dist/src/mcp/oauth-utils.js +50 -12
- package/dist/src/mcp/oauth-utils.js.map +1 -1
- package/dist/src/mcp/oauth-utils.test.js +17 -2
- package/dist/src/mcp/oauth-utils.test.js.map +1 -1
- package/dist/src/services/chatRecordingService.d.ts +150 -0
- package/dist/src/services/chatRecordingService.js +318 -0
- package/dist/src/services/chatRecordingService.js.map +1 -0
- package/dist/src/services/chatRecordingService.test.d.ts +6 -0
- package/dist/src/services/chatRecordingService.test.js +288 -0
- package/dist/src/services/chatRecordingService.test.js.map +1 -0
- package/dist/src/services/fileSystemService.d.ts +31 -0
- package/dist/src/services/fileSystemService.js +18 -0
- package/dist/src/services/fileSystemService.js.map +1 -0
- package/dist/src/services/fileSystemService.test.d.ts +6 -0
- package/dist/src/services/fileSystemService.test.js +41 -0
- package/dist/src/services/fileSystemService.test.js.map +1 -0
- package/dist/src/services/loopDetectionService.js +5 -5
- package/dist/src/services/loopDetectionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.d.ts +8 -10
- package/dist/src/services/shellExecutionService.js +289 -133
- package/dist/src/services/shellExecutionService.js.map +1 -1
- package/dist/src/services/shellExecutionService.test.js +274 -30
- package/dist/src/services/shellExecutionService.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.d.ts +31 -6
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js +101 -121
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.d.ts +11 -0
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js +97 -36
- package/dist/src/telemetry/clearcut-logger/clearcut-logger.test.js.map +1 -1
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.d.ts +6 -2
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js +12 -11
- package/dist/src/telemetry/clearcut-logger/event-metadata-key.js.map +1 -1
- package/dist/src/telemetry/constants.d.ts +1 -0
- package/dist/src/telemetry/constants.js +1 -0
- package/dist/src/telemetry/constants.js.map +1 -1
- package/dist/src/telemetry/index.d.ts +2 -2
- package/dist/src/telemetry/index.js +2 -2
- package/dist/src/telemetry/index.js.map +1 -1
- package/dist/src/telemetry/integration.test.circular.js +1 -0
- package/dist/src/telemetry/integration.test.circular.js.map +1 -1
- package/dist/src/telemetry/loggers.d.ts +2 -1
- package/dist/src/telemetry/loggers.js +24 -6
- package/dist/src/telemetry/loggers.js.map +1 -1
- package/dist/src/telemetry/loggers.test.js +37 -7
- package/dist/src/telemetry/loggers.test.js.map +1 -1
- package/dist/src/telemetry/metrics.d.ts +5 -1
- package/dist/src/telemetry/metrics.js +23 -9
- package/dist/src/telemetry/metrics.js.map +1 -1
- package/dist/src/telemetry/metrics.test.js +31 -1
- package/dist/src/telemetry/metrics.test.js.map +1 -1
- package/dist/src/telemetry/sdk.js +67 -39
- package/dist/src/telemetry/sdk.js.map +1 -1
- package/dist/src/telemetry/sdk.test.d.ts +6 -0
- package/dist/src/telemetry/sdk.test.js +82 -0
- package/dist/src/telemetry/sdk.test.js.map +1 -0
- package/dist/src/telemetry/types.d.ts +9 -1
- package/dist/src/telemetry/types.js +14 -0
- package/dist/src/telemetry/types.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.d.ts +4 -0
- package/dist/src/telemetry/uiTelemetry.js +14 -1
- package/dist/src/telemetry/uiTelemetry.js.map +1 -1
- package/dist/src/telemetry/uiTelemetry.test.js +45 -9
- package/dist/src/telemetry/uiTelemetry.test.js.map +1 -1
- package/dist/src/test-utils/tools.d.ts +29 -8
- package/dist/src/test-utils/tools.js +79 -15
- package/dist/src/test-utils/tools.js.map +1 -1
- package/dist/src/tools/edit.d.ts +1 -1
- package/dist/src/tools/edit.js +15 -9
- package/dist/src/tools/edit.js.map +1 -1
- package/dist/src/tools/edit.test.js +11 -1
- package/dist/src/tools/edit.test.js.map +1 -1
- package/dist/src/tools/glob.d.ts +1 -1
- package/dist/src/tools/glob.js +8 -8
- package/dist/src/tools/glob.js.map +1 -1
- package/dist/src/tools/glob.test.js +20 -0
- package/dist/src/tools/glob.test.js.map +1 -1
- package/dist/src/tools/grep.d.ts +1 -1
- package/dist/src/tools/grep.js +1 -6
- package/dist/src/tools/grep.js.map +1 -1
- package/dist/src/tools/ls.d.ts +1 -1
- package/dist/src/tools/ls.js +1 -6
- package/dist/src/tools/ls.js.map +1 -1
- package/dist/src/tools/mcp-client-manager.d.ts +38 -0
- package/dist/src/tools/mcp-client-manager.js +74 -0
- package/dist/src/tools/mcp-client-manager.js.map +1 -0
- package/dist/src/tools/mcp-client-manager.test.d.ts +6 -0
- package/dist/src/tools/mcp-client-manager.test.js +39 -0
- package/dist/src/tools/mcp-client-manager.test.js.map +1 -0
- package/dist/src/tools/mcp-client.d.ts +43 -0
- package/dist/src/tools/mcp-client.js +157 -11
- package/dist/src/tools/mcp-client.js.map +1 -1
- package/dist/src/tools/mcp-client.test.js +59 -273
- package/dist/src/tools/mcp-client.test.js.map +1 -1
- package/dist/src/tools/mcp-tool.js +21 -3
- package/dist/src/tools/mcp-tool.js.map +1 -1
- package/dist/src/tools/mcp-tool.test.js +74 -17
- package/dist/src/tools/mcp-tool.test.js.map +1 -1
- package/dist/src/tools/memoryTool.d.ts +1 -1
- package/dist/src/tools/memoryTool.js +1 -6
- package/dist/src/tools/memoryTool.js.map +1 -1
- package/dist/src/tools/read-file.d.ts +1 -1
- package/dist/src/tools/read-file.js +5 -7
- package/dist/src/tools/read-file.js.map +1 -1
- package/dist/src/tools/read-file.test.js +8 -0
- package/dist/src/tools/read-file.test.js.map +1 -1
- package/dist/src/tools/read-many-files.d.ts +0 -1
- package/dist/src/tools/read-many-files.js +16 -11
- package/dist/src/tools/read-many-files.js.map +1 -1
- package/dist/src/tools/read-many-files.test.js +34 -0
- package/dist/src/tools/read-many-files.test.js.map +1 -1
- package/dist/src/tools/shell.d.ts +1 -1
- package/dist/src/tools/shell.js +35 -32
- package/dist/src/tools/shell.js.map +1 -1
- package/dist/src/tools/shell.test.js +23 -19
- package/dist/src/tools/shell.test.js.map +1 -1
- package/dist/src/tools/tool-error.d.ts +1 -0
- package/dist/src/tools/tool-error.js +1 -0
- package/dist/src/tools/tool-error.js.map +1 -1
- package/dist/src/tools/tool-registry.d.ts +8 -3
- package/dist/src/tools/tool-registry.js +58 -34
- package/dist/src/tools/tool-registry.js.map +1 -1
- package/dist/src/tools/tool-registry.test.js +9 -21
- package/dist/src/tools/tool-registry.test.js.map +1 -1
- package/dist/src/tools/tools.d.ts +17 -74
- package/dist/src/tools/tools.js +60 -104
- package/dist/src/tools/tools.js.map +1 -1
- package/dist/src/tools/tools.test.js +91 -2
- package/dist/src/tools/tools.test.js.map +1 -1
- package/dist/src/tools/web-fetch.d.ts +1 -1
- package/dist/src/tools/web-fetch.js +1 -6
- package/dist/src/tools/web-fetch.js.map +1 -1
- package/dist/src/tools/web-search.d.ts +4 -5
- package/dist/src/tools/web-search.js +47 -51
- package/dist/src/tools/web-search.js.map +1 -1
- package/dist/src/tools/web-search.test.d.ts +6 -0
- package/dist/src/tools/web-search.test.js +139 -0
- package/dist/src/tools/web-search.test.js.map +1 -0
- package/dist/src/tools/write-file.d.ts +15 -11
- package/dist/src/tools/write-file.js +133 -145
- package/dist/src/tools/write-file.js.map +1 -1
- package/dist/src/tools/write-file.test.js +82 -126
- package/dist/src/tools/write-file.test.js.map +1 -1
- package/dist/src/utils/browser.js +4 -3
- package/dist/src/utils/browser.js.map +1 -1
- package/dist/src/utils/editCorrector.js +13 -13
- package/dist/src/utils/editCorrector.js.map +1 -1
- package/dist/src/utils/editor.js +1 -1
- package/dist/src/utils/editor.js.map +1 -1
- package/dist/src/utils/editor.test.js +10 -10
- package/dist/src/utils/editor.test.js.map +1 -1
- package/dist/src/utils/environmentContext.js +2 -2
- package/dist/src/utils/environmentContext.js.map +1 -1
- package/dist/src/utils/environmentContext.test.js +3 -2
- package/dist/src/utils/environmentContext.test.js.map +1 -1
- package/dist/src/utils/fileUtils.d.ts +2 -1
- package/dist/src/utils/fileUtils.js +3 -3
- package/dist/src/utils/fileUtils.js.map +1 -1
- package/dist/src/utils/fileUtils.test.js +18 -17
- package/dist/src/utils/fileUtils.test.js.map +1 -1
- package/dist/src/utils/filesearch/crawler.d.ts +15 -0
- package/dist/src/utils/filesearch/crawler.js +50 -0
- package/dist/src/utils/filesearch/crawler.js.map +1 -0
- package/dist/src/utils/filesearch/crawler.test.d.ts +6 -0
- package/dist/src/utils/filesearch/crawler.test.js +468 -0
- package/dist/src/utils/filesearch/crawler.test.js.map +1 -0
- package/dist/src/utils/filesearch/fileSearch.d.ts +9 -53
- package/dist/src/utils/filesearch/fileSearch.js +62 -117
- package/dist/src/utils/filesearch/fileSearch.js.map +1 -1
- package/dist/src/utils/filesearch/fileSearch.test.js +95 -197
- package/dist/src/utils/filesearch/fileSearch.test.js.map +1 -1
- package/dist/src/utils/filesearch/ignore.d.ts +7 -0
- package/dist/src/utils/filesearch/ignore.js +25 -0
- package/dist/src/utils/filesearch/ignore.js.map +1 -1
- package/dist/src/utils/filesearch/ignore.test.js +89 -2
- package/dist/src/utils/filesearch/ignore.test.js.map +1 -1
- package/dist/src/utils/getPty.d.ts +19 -0
- package/dist/src/utils/getPty.js +23 -0
- package/dist/src/utils/getPty.js.map +1 -0
- package/dist/src/utils/memoryDiscovery.js +3 -3
- package/dist/src/utils/memoryDiscovery.js.map +1 -1
- package/dist/src/utils/memoryDiscovery.test.js +3 -2
- package/dist/src/utils/memoryDiscovery.test.js.map +1 -1
- package/dist/src/utils/memoryImportProcessor.js +2 -6
- package/dist/src/utils/memoryImportProcessor.js.map +1 -1
- package/dist/src/utils/paths.d.ts +7 -0
- package/dist/src/utils/paths.js +15 -0
- package/dist/src/utils/paths.js.map +1 -1
- package/dist/src/utils/paths.test.js +74 -2
- package/dist/src/utils/paths.test.js.map +1 -1
- package/dist/src/utils/secure-browser-launcher.js +4 -3
- package/dist/src/utils/secure-browser-launcher.js.map +1 -1
- package/dist/src/utils/shell-utils.d.ts +39 -0
- package/dist/src/utils/shell-utils.js +72 -2
- package/dist/src/utils/shell-utils.js.map +1 -1
- package/dist/src/utils/shell-utils.test.js +132 -4
- package/dist/src/utils/shell-utils.test.js.map +1 -1
- package/dist/src/utils/systemEncoding.js +1 -1
- package/dist/src/utils/systemEncoding.js.map +1 -1
- package/dist/src/utils/systemEncoding.test.js +23 -23
- package/dist/src/utils/systemEncoding.test.js.map +1 -1
- package/dist/src/utils/user_account.js +58 -48
- package/dist/src/utils/user_account.js.map +1 -1
- package/dist/src/utils/user_account.test.js +76 -9
- package/dist/src/utils/user_account.test.js.map +1 -1
- package/dist/src/utils/workspaceContext.d.ts +22 -7
- package/dist/src/utils/workspaceContext.js +81 -55
- package/dist/src/utils/workspaceContext.js.map +1 -1
- package/dist/src/utils/workspaceContext.test.js +221 -137
- package/dist/src/utils/workspaceContext.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -7
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
+
import { isNodeError } from '../utils/errors.js';
|
|
6
7
|
import * as fs from 'fs';
|
|
7
8
|
import * as path from 'path';
|
|
8
9
|
/**
|
|
@@ -11,21 +12,42 @@ import * as path from 'path';
|
|
|
11
12
|
* in a single session.
|
|
12
13
|
*/
|
|
13
14
|
export class WorkspaceContext {
|
|
14
|
-
directories;
|
|
15
|
+
directories = new Set();
|
|
15
16
|
initialDirectories;
|
|
17
|
+
onDirectoriesChangedListeners = new Set();
|
|
16
18
|
/**
|
|
17
19
|
* Creates a new WorkspaceContext with the given initial directory and optional additional directories.
|
|
18
|
-
* @param
|
|
20
|
+
* @param directory The initial working directory (usually cwd)
|
|
19
21
|
* @param additionalDirectories Optional array of additional directories to include
|
|
20
22
|
*/
|
|
21
|
-
constructor(
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
constructor(directory, additionalDirectories = []) {
|
|
24
|
+
this.addDirectory(directory);
|
|
25
|
+
for (const additionalDirectory of additionalDirectories) {
|
|
26
|
+
this.addDirectory(additionalDirectory);
|
|
27
|
+
}
|
|
28
|
+
this.initialDirectories = new Set(this.directories);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Registers a listener that is called when the workspace directories change.
|
|
32
|
+
* @param listener The listener to call.
|
|
33
|
+
* @returns A function to unsubscribe the listener.
|
|
34
|
+
*/
|
|
35
|
+
onDirectoriesChanged(listener) {
|
|
36
|
+
this.onDirectoriesChangedListeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
this.onDirectoriesChangedListeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
notifyDirectoriesChanged() {
|
|
42
|
+
// Iterate over a copy of the set in case a listener unsubscribes itself or others.
|
|
43
|
+
for (const listener of [...this.onDirectoriesChangedListeners]) {
|
|
44
|
+
try {
|
|
45
|
+
listener();
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
// Don't let one listener break others.
|
|
49
|
+
console.error('Error in WorkspaceContext listener:', e);
|
|
50
|
+
}
|
|
29
51
|
}
|
|
30
52
|
}
|
|
31
53
|
/**
|
|
@@ -34,32 +56,14 @@ export class WorkspaceContext {
|
|
|
34
56
|
* @param basePath Optional base path for resolving relative paths (defaults to cwd)
|
|
35
57
|
*/
|
|
36
58
|
addDirectory(directory, basePath = process.cwd()) {
|
|
37
|
-
this.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
* Internal method to add a directory with validation.
|
|
41
|
-
*/
|
|
42
|
-
addDirectoryInternal(directory, basePath = process.cwd()) {
|
|
43
|
-
const absolutePath = path.isAbsolute(directory)
|
|
44
|
-
? directory
|
|
45
|
-
: path.resolve(basePath, directory);
|
|
46
|
-
if (!fs.existsSync(absolutePath)) {
|
|
47
|
-
throw new Error(`Directory does not exist: ${absolutePath}`);
|
|
48
|
-
}
|
|
49
|
-
const stats = fs.statSync(absolutePath);
|
|
50
|
-
if (!stats.isDirectory()) {
|
|
51
|
-
throw new Error(`Path is not a directory: ${absolutePath}`);
|
|
52
|
-
}
|
|
53
|
-
let realPath;
|
|
54
|
-
try {
|
|
55
|
-
realPath = fs.realpathSync(absolutePath);
|
|
56
|
-
}
|
|
57
|
-
catch (_error) {
|
|
58
|
-
throw new Error(`Failed to resolve path: ${absolutePath}`);
|
|
59
|
+
const resolved = this.resolveAndValidateDir(directory, basePath);
|
|
60
|
+
if (this.directories.has(resolved)) {
|
|
61
|
+
return;
|
|
59
62
|
}
|
|
60
|
-
this.directories.add(
|
|
63
|
+
this.directories.add(resolved);
|
|
64
|
+
this.notifyDirectoriesChanged();
|
|
61
65
|
}
|
|
62
|
-
|
|
66
|
+
resolveAndValidateDir(directory, basePath = process.cwd()) {
|
|
63
67
|
const absolutePath = path.isAbsolute(directory)
|
|
64
68
|
? directory
|
|
65
69
|
: path.resolve(basePath, directory);
|
|
@@ -70,14 +74,7 @@ export class WorkspaceContext {
|
|
|
70
74
|
if (!stats.isDirectory()) {
|
|
71
75
|
throw new Error(`Path is not a directory: ${absolutePath}`);
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
realPath = fs.realpathSync(absolutePath);
|
|
76
|
-
}
|
|
77
|
-
catch (_error) {
|
|
78
|
-
throw new Error(`Failed to resolve path: ${absolutePath}`);
|
|
79
|
-
}
|
|
80
|
-
this.initialDirectories.add(realPath);
|
|
77
|
+
return fs.realpathSync(absolutePath);
|
|
81
78
|
}
|
|
82
79
|
/**
|
|
83
80
|
* Gets a copy of all workspace directories.
|
|
@@ -90,9 +87,14 @@ export class WorkspaceContext {
|
|
|
90
87
|
return Array.from(this.initialDirectories);
|
|
91
88
|
}
|
|
92
89
|
setDirectories(directories) {
|
|
93
|
-
|
|
90
|
+
const newDirectories = new Set();
|
|
94
91
|
for (const dir of directories) {
|
|
95
|
-
this.
|
|
92
|
+
newDirectories.add(this.resolveAndValidateDir(dir));
|
|
93
|
+
}
|
|
94
|
+
if (newDirectories.size !== this.directories.size ||
|
|
95
|
+
![...newDirectories].every((d) => this.directories.has(d))) {
|
|
96
|
+
this.directories = newDirectories;
|
|
97
|
+
this.notifyDirectoriesChanged();
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
@@ -102,18 +104,9 @@ export class WorkspaceContext {
|
|
|
102
104
|
*/
|
|
103
105
|
isPathWithinWorkspace(pathToCheck) {
|
|
104
106
|
try {
|
|
105
|
-
const
|
|
106
|
-
let resolvedPath = absolutePath;
|
|
107
|
-
if (fs.existsSync(absolutePath)) {
|
|
108
|
-
try {
|
|
109
|
-
resolvedPath = fs.realpathSync(absolutePath);
|
|
110
|
-
}
|
|
111
|
-
catch (_error) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
107
|
+
const fullyResolvedPath = this.fullyResolvedPath(pathToCheck);
|
|
115
108
|
for (const dir of this.directories) {
|
|
116
|
-
if (this.isPathWithinRoot(
|
|
109
|
+
if (this.isPathWithinRoot(fullyResolvedPath, dir)) {
|
|
117
110
|
return true;
|
|
118
111
|
}
|
|
119
112
|
}
|
|
@@ -123,6 +116,28 @@ export class WorkspaceContext {
|
|
|
123
116
|
return false;
|
|
124
117
|
}
|
|
125
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Fully resolves a path, including symbolic links.
|
|
121
|
+
* If the path does not exist, it returns the fully resolved path as it would be
|
|
122
|
+
* if it did exist.
|
|
123
|
+
*/
|
|
124
|
+
fullyResolvedPath(pathToCheck) {
|
|
125
|
+
try {
|
|
126
|
+
return fs.realpathSync(pathToCheck);
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
if (isNodeError(e) &&
|
|
130
|
+
e.code === 'ENOENT' &&
|
|
131
|
+
e.path &&
|
|
132
|
+
// realpathSync does not set e.path correctly for symlinks to
|
|
133
|
+
// non-existent files.
|
|
134
|
+
!this.isFileSymlink(e.path)) {
|
|
135
|
+
// If it doesn't exist, e.path contains the fully resolved path.
|
|
136
|
+
return e.path;
|
|
137
|
+
}
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
126
141
|
/**
|
|
127
142
|
* Checks if a path is within a given root directory.
|
|
128
143
|
* @param pathToCheck The absolute path to check
|
|
@@ -135,5 +150,16 @@ export class WorkspaceContext {
|
|
|
135
150
|
relative !== '..' &&
|
|
136
151
|
!path.isAbsolute(relative));
|
|
137
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Checks if a file path is a symbolic link that points to a file.
|
|
155
|
+
*/
|
|
156
|
+
isFileSymlink(filePath) {
|
|
157
|
+
try {
|
|
158
|
+
return !fs.readlinkSync(filePath).endsWith('/');
|
|
159
|
+
}
|
|
160
|
+
catch (_error) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
138
164
|
}
|
|
139
165
|
//# sourceMappingURL=workspaceContext.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspaceContext.js","sourceRoot":"","sources":["../../../src/utils/workspaceContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"workspaceContext.js","sourceRoot":"","sources":["../../../src/utils/workspaceContext.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,206 +3,290 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import { WorkspaceContext } from './workspaceContext.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
const mockCwd = path.resolve(path.sep, 'home', 'user', 'project');
|
|
15
|
-
const mockExistingDir = path.resolve(path.sep, 'home', 'user', 'other-project');
|
|
16
|
-
const mockNonExistentDir = path.resolve(path.sep, 'home', 'user', 'does-not-exist');
|
|
17
|
-
const mockSymlinkDir = path.resolve(path.sep, 'home', 'user', 'symlink');
|
|
18
|
-
const mockRealPath = path.resolve(path.sep, 'home', 'user', 'real-directory');
|
|
11
|
+
describe('WorkspaceContext with real filesystem', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
let cwd;
|
|
14
|
+
let otherDir;
|
|
19
15
|
beforeEach(() => {
|
|
20
|
-
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
vi.mocked(fs.statSync).mockImplementation((path) => {
|
|
31
|
-
const pathStr = path.toString();
|
|
32
|
-
if (pathStr === mockNonExistentDir) {
|
|
33
|
-
throw new Error('ENOENT');
|
|
34
|
-
}
|
|
35
|
-
return {
|
|
36
|
-
isDirectory: () => true,
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
// Mock fs.realpathSync
|
|
40
|
-
vi.mocked(fs.realpathSync).mockImplementation((path) => {
|
|
41
|
-
const pathStr = path.toString();
|
|
42
|
-
if (pathStr === mockSymlinkDir) {
|
|
43
|
-
return mockRealPath;
|
|
44
|
-
}
|
|
45
|
-
return pathStr;
|
|
46
|
-
});
|
|
16
|
+
// os.tmpdir() can return a path using a symlink (this is standard on macOS)
|
|
17
|
+
// Use fs.realpathSync to fully resolve the absolute path.
|
|
18
|
+
tempDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'workspace-context-test-')));
|
|
19
|
+
cwd = path.join(tempDir, 'project');
|
|
20
|
+
otherDir = path.join(tempDir, 'other-project');
|
|
21
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
22
|
+
fs.mkdirSync(otherDir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
47
26
|
});
|
|
48
27
|
describe('initialization', () => {
|
|
49
28
|
it('should initialize with a single directory (cwd)', () => {
|
|
50
|
-
workspaceContext = new WorkspaceContext(
|
|
29
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
51
30
|
const directories = workspaceContext.getDirectories();
|
|
52
|
-
expect(directories).
|
|
53
|
-
expect(directories[0]).toBe(mockCwd);
|
|
31
|
+
expect(directories).toEqual([cwd]);
|
|
54
32
|
});
|
|
55
33
|
it('should validate and resolve directories to absolute paths', () => {
|
|
56
|
-
const
|
|
57
|
-
vi.mocked(fs.existsSync).mockImplementation((p) => p === mockCwd || p === absolutePath);
|
|
58
|
-
vi.mocked(fs.realpathSync).mockImplementation((p) => p.toString());
|
|
59
|
-
workspaceContext = new WorkspaceContext(mockCwd, [absolutePath]);
|
|
34
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
60
35
|
const directories = workspaceContext.getDirectories();
|
|
61
|
-
expect(directories).
|
|
36
|
+
expect(directories).toEqual([cwd, otherDir]);
|
|
62
37
|
});
|
|
63
38
|
it('should reject non-existent directories', () => {
|
|
39
|
+
const nonExistentDir = path.join(tempDir, 'does-not-exist');
|
|
64
40
|
expect(() => {
|
|
65
|
-
new WorkspaceContext(
|
|
41
|
+
new WorkspaceContext(cwd, [nonExistentDir]);
|
|
66
42
|
}).toThrow('Directory does not exist');
|
|
67
43
|
});
|
|
68
44
|
it('should handle empty initialization', () => {
|
|
69
|
-
workspaceContext = new WorkspaceContext(
|
|
45
|
+
const workspaceContext = new WorkspaceContext(cwd, []);
|
|
70
46
|
const directories = workspaceContext.getDirectories();
|
|
71
47
|
expect(directories).toHaveLength(1);
|
|
72
|
-
expect(directories[0]).toBe(
|
|
48
|
+
expect(fs.realpathSync(directories[0])).toBe(cwd);
|
|
73
49
|
});
|
|
74
50
|
});
|
|
75
51
|
describe('adding directories', () => {
|
|
76
|
-
beforeEach(() => {
|
|
77
|
-
workspaceContext = new WorkspaceContext(mockCwd);
|
|
78
|
-
});
|
|
79
52
|
it('should add valid directories', () => {
|
|
80
|
-
workspaceContext
|
|
53
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
54
|
+
workspaceContext.addDirectory(otherDir);
|
|
81
55
|
const directories = workspaceContext.getDirectories();
|
|
82
|
-
expect(directories).
|
|
83
|
-
expect(directories).toContain(mockExistingDir);
|
|
56
|
+
expect(directories).toEqual([cwd, otherDir]);
|
|
84
57
|
});
|
|
85
58
|
it('should resolve relative paths to absolute', () => {
|
|
86
|
-
|
|
87
|
-
|
|
59
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
60
|
+
const relativePath = path.relative(cwd, otherDir);
|
|
61
|
+
workspaceContext.addDirectory(relativePath, cwd);
|
|
88
62
|
const directories = workspaceContext.getDirectories();
|
|
89
|
-
expect(directories).
|
|
63
|
+
expect(directories).toEqual([cwd, otherDir]);
|
|
90
64
|
});
|
|
91
65
|
it('should reject non-existent directories', () => {
|
|
66
|
+
const nonExistentDir = path.join(tempDir, 'does-not-exist');
|
|
67
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
92
68
|
expect(() => {
|
|
93
|
-
workspaceContext.addDirectory(
|
|
69
|
+
workspaceContext.addDirectory(nonExistentDir);
|
|
94
70
|
}).toThrow('Directory does not exist');
|
|
95
71
|
});
|
|
96
72
|
it('should prevent duplicate directories', () => {
|
|
97
|
-
workspaceContext
|
|
98
|
-
workspaceContext.addDirectory(
|
|
73
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
74
|
+
workspaceContext.addDirectory(otherDir);
|
|
75
|
+
workspaceContext.addDirectory(otherDir);
|
|
99
76
|
const directories = workspaceContext.getDirectories();
|
|
100
|
-
expect(directories
|
|
77
|
+
expect(directories).toHaveLength(2);
|
|
101
78
|
});
|
|
102
79
|
it('should handle symbolic links correctly', () => {
|
|
103
|
-
|
|
80
|
+
const realDir = path.join(tempDir, 'real');
|
|
81
|
+
fs.mkdirSync(realDir, { recursive: true });
|
|
82
|
+
const symlinkDir = path.join(tempDir, 'symlink-to-real');
|
|
83
|
+
fs.symlinkSync(realDir, symlinkDir, 'dir');
|
|
84
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
85
|
+
workspaceContext.addDirectory(symlinkDir);
|
|
104
86
|
const directories = workspaceContext.getDirectories();
|
|
105
|
-
expect(directories).
|
|
106
|
-
expect(directories).not.toContain(mockSymlinkDir);
|
|
87
|
+
expect(directories).toEqual([cwd, realDir]);
|
|
107
88
|
});
|
|
108
89
|
});
|
|
109
90
|
describe('path validation', () => {
|
|
110
|
-
beforeEach(() => {
|
|
111
|
-
workspaceContext = new WorkspaceContext(mockCwd, [mockExistingDir]);
|
|
112
|
-
});
|
|
113
91
|
it('should accept paths within workspace directories', () => {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
92
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
93
|
+
const validPath1 = path.join(cwd, 'src', 'file.ts');
|
|
94
|
+
const validPath2 = path.join(otherDir, 'lib', 'module.js');
|
|
95
|
+
fs.mkdirSync(path.dirname(validPath1), { recursive: true });
|
|
96
|
+
fs.writeFileSync(validPath1, 'content');
|
|
97
|
+
fs.mkdirSync(path.dirname(validPath2), { recursive: true });
|
|
98
|
+
fs.writeFileSync(validPath2, 'content');
|
|
99
|
+
expect(workspaceContext.isPathWithinWorkspace(validPath1)).toBe(true);
|
|
100
|
+
expect(workspaceContext.isPathWithinWorkspace(validPath2)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
it('should accept non-existent paths within workspace directories', () => {
|
|
103
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
104
|
+
const validPath1 = path.join(cwd, 'src', 'file.ts');
|
|
105
|
+
const validPath2 = path.join(otherDir, 'lib', 'module.js');
|
|
116
106
|
expect(workspaceContext.isPathWithinWorkspace(validPath1)).toBe(true);
|
|
117
107
|
expect(workspaceContext.isPathWithinWorkspace(validPath2)).toBe(true);
|
|
118
108
|
});
|
|
119
109
|
it('should reject paths outside workspace', () => {
|
|
120
|
-
const
|
|
110
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
111
|
+
const invalidPath = path.join(tempDir, 'outside-workspace', 'file.txt');
|
|
121
112
|
expect(workspaceContext.isPathWithinWorkspace(invalidPath)).toBe(false);
|
|
122
113
|
});
|
|
123
|
-
it('should
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
vi.mocked(fs.realpathSync).mockImplementation((p) => {
|
|
128
|
-
if (p === symlinkPath) {
|
|
129
|
-
return realPath;
|
|
130
|
-
}
|
|
131
|
-
return p.toString();
|
|
132
|
-
});
|
|
133
|
-
expect(workspaceContext.isPathWithinWorkspace(symlinkPath)).toBe(true);
|
|
114
|
+
it('should reject non-existent paths outside workspace', () => {
|
|
115
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
116
|
+
const invalidPath = path.join(tempDir, 'outside-workspace', 'file.txt');
|
|
117
|
+
expect(workspaceContext.isPathWithinWorkspace(invalidPath)).toBe(false);
|
|
134
118
|
});
|
|
135
119
|
it('should handle nested directories correctly', () => {
|
|
136
|
-
const
|
|
120
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
121
|
+
const nestedPath = path.join(cwd, 'deeply', 'nested', 'path', 'file.txt');
|
|
137
122
|
expect(workspaceContext.isPathWithinWorkspace(nestedPath)).toBe(true);
|
|
138
123
|
});
|
|
139
124
|
it('should handle edge cases (root, parent references)', () => {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
125
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
126
|
+
const rootPath = path.parse(tempDir).root;
|
|
127
|
+
const parentPath = path.dirname(cwd);
|
|
142
128
|
expect(workspaceContext.isPathWithinWorkspace(rootPath)).toBe(false);
|
|
143
129
|
expect(workspaceContext.isPathWithinWorkspace(parentPath)).toBe(false);
|
|
144
130
|
});
|
|
145
131
|
it('should handle non-existent paths correctly', () => {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
// Should still validate based on path structure
|
|
132
|
+
const workspaceContext = new WorkspaceContext(cwd, [otherDir]);
|
|
133
|
+
const nonExistentPath = path.join(cwd, 'does-not-exist.txt');
|
|
149
134
|
expect(workspaceContext.isPathWithinWorkspace(nonExistentPath)).toBe(true);
|
|
150
135
|
});
|
|
136
|
+
describe('with symbolic link', () => {
|
|
137
|
+
describe('in the workspace', () => {
|
|
138
|
+
let realDir;
|
|
139
|
+
let symlinkDir;
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
realDir = path.join(cwd, 'real-dir');
|
|
142
|
+
fs.mkdirSync(realDir, { recursive: true });
|
|
143
|
+
symlinkDir = path.join(cwd, 'symlink-file');
|
|
144
|
+
fs.symlinkSync(realDir, symlinkDir, 'dir');
|
|
145
|
+
});
|
|
146
|
+
it('should accept dir paths', () => {
|
|
147
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
148
|
+
expect(workspaceContext.isPathWithinWorkspace(symlinkDir)).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it('should accept non-existent paths', () => {
|
|
151
|
+
const filePath = path.join(symlinkDir, 'does-not-exist.txt');
|
|
152
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
153
|
+
expect(workspaceContext.isPathWithinWorkspace(filePath)).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
it('should accept non-existent deep paths', () => {
|
|
156
|
+
const filePath = path.join(symlinkDir, 'deep', 'does-not-exist.txt');
|
|
157
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
158
|
+
expect(workspaceContext.isPathWithinWorkspace(filePath)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('outside the workspace', () => {
|
|
162
|
+
let realDir;
|
|
163
|
+
let symlinkDir;
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
realDir = path.join(tempDir, 'real-dir');
|
|
166
|
+
fs.mkdirSync(realDir, { recursive: true });
|
|
167
|
+
symlinkDir = path.join(cwd, 'symlink-file');
|
|
168
|
+
fs.symlinkSync(realDir, symlinkDir, 'dir');
|
|
169
|
+
});
|
|
170
|
+
it('should reject dir paths', () => {
|
|
171
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
172
|
+
expect(workspaceContext.isPathWithinWorkspace(symlinkDir)).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
it('should reject non-existent paths', () => {
|
|
175
|
+
const filePath = path.join(symlinkDir, 'does-not-exist.txt');
|
|
176
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
177
|
+
expect(workspaceContext.isPathWithinWorkspace(filePath)).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
it('should reject non-existent deep paths', () => {
|
|
180
|
+
const filePath = path.join(symlinkDir, 'deep', 'does-not-exist.txt');
|
|
181
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
182
|
+
expect(workspaceContext.isPathWithinWorkspace(filePath)).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
it('should reject partially non-existent deep paths', () => {
|
|
185
|
+
const deepDir = path.join(symlinkDir, 'deep');
|
|
186
|
+
fs.mkdirSync(deepDir, { recursive: true });
|
|
187
|
+
const filePath = path.join(deepDir, 'does-not-exist.txt');
|
|
188
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
189
|
+
expect(workspaceContext.isPathWithinWorkspace(filePath)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
it('should reject symbolic file links outside the workspace', () => {
|
|
193
|
+
const realFile = path.join(tempDir, 'real-file.txt');
|
|
194
|
+
fs.writeFileSync(realFile, 'content');
|
|
195
|
+
const symlinkFile = path.join(cwd, 'symlink-to-real-file');
|
|
196
|
+
fs.symlinkSync(realFile, symlinkFile, 'file');
|
|
197
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
198
|
+
expect(workspaceContext.isPathWithinWorkspace(symlinkFile)).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
it('should reject non-existent symbolic file links outside the workspace', () => {
|
|
201
|
+
const realFile = path.join(tempDir, 'real-file.txt');
|
|
202
|
+
const symlinkFile = path.join(cwd, 'symlink-to-real-file');
|
|
203
|
+
fs.symlinkSync(realFile, symlinkFile, 'file');
|
|
204
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
205
|
+
expect(workspaceContext.isPathWithinWorkspace(symlinkFile)).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
it('should handle circular symlinks gracefully', () => {
|
|
208
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
209
|
+
const linkA = path.join(cwd, 'link-a');
|
|
210
|
+
const linkB = path.join(cwd, 'link-b');
|
|
211
|
+
// Create a circular dependency: linkA -> linkB -> linkA
|
|
212
|
+
fs.symlinkSync(linkB, linkA, 'dir');
|
|
213
|
+
fs.symlinkSync(linkA, linkB, 'dir');
|
|
214
|
+
// fs.realpathSync should throw ELOOP, and isPathWithinWorkspace should
|
|
215
|
+
// handle it gracefully and return false.
|
|
216
|
+
expect(workspaceContext.isPathWithinWorkspace(linkA)).toBe(false);
|
|
217
|
+
expect(workspaceContext.isPathWithinWorkspace(linkB)).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe('onDirectoriesChanged', () => {
|
|
222
|
+
it('should call listener when adding a directory', () => {
|
|
223
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
224
|
+
const listener = vi.fn();
|
|
225
|
+
workspaceContext.onDirectoriesChanged(listener);
|
|
226
|
+
workspaceContext.addDirectory(otherDir);
|
|
227
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
228
|
+
});
|
|
229
|
+
it('should not call listener when adding a duplicate directory', () => {
|
|
230
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
231
|
+
workspaceContext.addDirectory(otherDir);
|
|
232
|
+
const listener = vi.fn();
|
|
233
|
+
workspaceContext.onDirectoriesChanged(listener);
|
|
234
|
+
workspaceContext.addDirectory(otherDir);
|
|
235
|
+
expect(listener).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
it('should call listener when setting different directories', () => {
|
|
238
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
239
|
+
const listener = vi.fn();
|
|
240
|
+
workspaceContext.onDirectoriesChanged(listener);
|
|
241
|
+
workspaceContext.setDirectories([otherDir]);
|
|
242
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
243
|
+
});
|
|
244
|
+
it('should not call listener when setting same directories', () => {
|
|
245
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
246
|
+
const listener = vi.fn();
|
|
247
|
+
workspaceContext.onDirectoriesChanged(listener);
|
|
248
|
+
workspaceContext.setDirectories([cwd]);
|
|
249
|
+
expect(listener).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
it('should support multiple listeners', () => {
|
|
252
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
253
|
+
const listener1 = vi.fn();
|
|
254
|
+
const listener2 = vi.fn();
|
|
255
|
+
workspaceContext.onDirectoriesChanged(listener1);
|
|
256
|
+
workspaceContext.onDirectoriesChanged(listener2);
|
|
257
|
+
workspaceContext.addDirectory(otherDir);
|
|
258
|
+
expect(listener1).toHaveBeenCalledOnce();
|
|
259
|
+
expect(listener2).toHaveBeenCalledOnce();
|
|
260
|
+
});
|
|
261
|
+
it('should allow unsubscribing a listener', () => {
|
|
262
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
263
|
+
const listener = vi.fn();
|
|
264
|
+
const unsubscribe = workspaceContext.onDirectoriesChanged(listener);
|
|
265
|
+
unsubscribe();
|
|
266
|
+
workspaceContext.addDirectory(otherDir);
|
|
267
|
+
expect(listener).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
it('should not fail if a listener throws an error', () => {
|
|
270
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
271
|
+
const errorListener = () => {
|
|
272
|
+
throw new Error('test error');
|
|
273
|
+
};
|
|
274
|
+
const listener = vi.fn();
|
|
275
|
+
workspaceContext.onDirectoriesChanged(errorListener);
|
|
276
|
+
workspaceContext.onDirectoriesChanged(listener);
|
|
277
|
+
expect(() => {
|
|
278
|
+
workspaceContext.addDirectory(otherDir);
|
|
279
|
+
}).not.toThrow();
|
|
280
|
+
expect(listener).toHaveBeenCalledOnce();
|
|
281
|
+
});
|
|
151
282
|
});
|
|
152
283
|
describe('getDirectories', () => {
|
|
153
284
|
it('should return a copy of directories array', () => {
|
|
154
|
-
workspaceContext = new WorkspaceContext(
|
|
285
|
+
const workspaceContext = new WorkspaceContext(cwd);
|
|
155
286
|
const dirs1 = workspaceContext.getDirectories();
|
|
156
287
|
const dirs2 = workspaceContext.getDirectories();
|
|
157
|
-
expect(dirs1).not.toBe(dirs2);
|
|
158
|
-
expect(dirs1).toEqual(dirs2);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
describe('symbolic link security', () => {
|
|
162
|
-
beforeEach(() => {
|
|
163
|
-
workspaceContext = new WorkspaceContext(mockCwd);
|
|
164
|
-
});
|
|
165
|
-
it('should follow symlinks but validate resolved path', () => {
|
|
166
|
-
const symlinkInsideWorkspace = path.join(mockCwd, 'link-to-subdir');
|
|
167
|
-
const resolvedInsideWorkspace = path.join(mockCwd, 'subdir');
|
|
168
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
169
|
-
vi.mocked(fs.realpathSync).mockImplementation((p) => {
|
|
170
|
-
if (p === symlinkInsideWorkspace) {
|
|
171
|
-
return resolvedInsideWorkspace;
|
|
172
|
-
}
|
|
173
|
-
return p.toString();
|
|
174
|
-
});
|
|
175
|
-
expect(workspaceContext.isPathWithinWorkspace(symlinkInsideWorkspace)).toBe(true);
|
|
176
|
-
});
|
|
177
|
-
it('should prevent sandbox escape via symlinks', () => {
|
|
178
|
-
const symlinkEscape = path.join(mockCwd, 'escape-link');
|
|
179
|
-
const resolvedOutside = path.resolve(mockCwd, '..', 'outside-file');
|
|
180
|
-
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
181
|
-
const pathStr = p.toString();
|
|
182
|
-
return (pathStr === symlinkEscape ||
|
|
183
|
-
pathStr === resolvedOutside ||
|
|
184
|
-
pathStr === mockCwd);
|
|
185
|
-
});
|
|
186
|
-
vi.mocked(fs.realpathSync).mockImplementation((p) => {
|
|
187
|
-
if (p.toString() === symlinkEscape) {
|
|
188
|
-
return resolvedOutside;
|
|
189
|
-
}
|
|
190
|
-
return p.toString();
|
|
191
|
-
});
|
|
192
|
-
vi.mocked(fs.statSync).mockImplementation((p) => ({
|
|
193
|
-
isDirectory: () => p.toString() !== resolvedOutside,
|
|
194
|
-
}));
|
|
195
|
-
workspaceContext = new WorkspaceContext(mockCwd);
|
|
196
|
-
expect(workspaceContext.isPathWithinWorkspace(symlinkEscape)).toBe(false);
|
|
197
|
-
});
|
|
198
|
-
it('should handle circular symlinks', () => {
|
|
199
|
-
const circularLink = path.join(mockCwd, 'circular');
|
|
200
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
201
|
-
vi.mocked(fs.realpathSync).mockImplementation(() => {
|
|
202
|
-
throw new Error('ELOOP: too many symbolic links encountered');
|
|
203
|
-
});
|
|
204
|
-
// Should handle the error gracefully
|
|
205
|
-
expect(workspaceContext.isPathWithinWorkspace(circularLink)).toBe(false);
|
|
288
|
+
expect(dirs1).not.toBe(dirs2);
|
|
289
|
+
expect(dirs1).toEqual(dirs2);
|
|
206
290
|
});
|
|
207
291
|
});
|
|
208
292
|
});
|