mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: enhance cloud storage path detection in sdk-options
- Introduced macOS-specific cloud storage patterns and home-anchored folder detection to improve accuracy in identifying cloud storage paths. - Updated the isCloudStoragePath function to utilize these new patterns, ensuring better handling of cloud storage locations. - Added comprehensive unit tests to validate detection logic for various cloud storage scenarios, including false positive prevention.
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Options } from '@anthropic-ai/claude-agent-sdk';
|
import type { Options } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { resolveModelString } from '@automaker/model-resolver';
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP, type McpServerConfig } from '@automaker/types';
|
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP, type McpServerConfig } from '@automaker/types';
|
||||||
@@ -63,12 +64,24 @@ export function validateWorkingDirectory(cwd: string): void {
|
|||||||
*
|
*
|
||||||
* @see https://github.com/anthropics/claude-code/issues/XXX (TODO: file upstream issue)
|
* @see https://github.com/anthropics/claude-code/issues/XXX (TODO: file upstream issue)
|
||||||
*/
|
*/
|
||||||
const CLOUD_STORAGE_PATH_PATTERNS = [
|
|
||||||
|
/**
|
||||||
|
* macOS-specific cloud storage patterns that appear under ~/Library/
|
||||||
|
* These are specific enough to use with includes() safely.
|
||||||
|
*/
|
||||||
|
const MACOS_CLOUD_STORAGE_PATTERNS = [
|
||||||
'/Library/CloudStorage/', // Dropbox, Google Drive, OneDrive, Box on macOS
|
'/Library/CloudStorage/', // Dropbox, Google Drive, OneDrive, Box on macOS
|
||||||
'/Library/Mobile Documents/', // iCloud Drive on macOS
|
'/Library/Mobile Documents/', // iCloud Drive on macOS
|
||||||
'/Google Drive/', // Google Drive on some systems
|
] as const;
|
||||||
'/Dropbox/', // Dropbox on Linux/alternative installs
|
|
||||||
'/OneDrive/', // OneDrive on Linux/alternative installs
|
/**
|
||||||
|
* Generic cloud storage folder names that need to be anchored to the home directory
|
||||||
|
* to avoid false positives (e.g., /home/user/my-project-about-dropbox/).
|
||||||
|
*/
|
||||||
|
const HOME_ANCHORED_CLOUD_FOLDERS = [
|
||||||
|
'Google Drive', // Google Drive on some systems
|
||||||
|
'Dropbox', // Dropbox on Linux/alternative installs
|
||||||
|
'OneDrive', // OneDrive on Linux/alternative installs
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,12 +90,34 @@ const CLOUD_STORAGE_PATH_PATTERNS = [
|
|||||||
* Cloud storage providers use virtual filesystem implementations that are
|
* Cloud storage providers use virtual filesystem implementations that are
|
||||||
* incompatible with the Claude CLI sandbox feature, causing process crashes.
|
* incompatible with the Claude CLI sandbox feature, causing process crashes.
|
||||||
*
|
*
|
||||||
|
* Uses two detection strategies:
|
||||||
|
* 1. macOS-specific patterns (under ~/Library/) - checked via includes()
|
||||||
|
* 2. Generic folder names - anchored to home directory to avoid false positives
|
||||||
|
*
|
||||||
* @param cwd - The working directory path to check
|
* @param cwd - The working directory path to check
|
||||||
* @returns true if the path is in a cloud storage location
|
* @returns true if the path is in a cloud storage location
|
||||||
*/
|
*/
|
||||||
export function isCloudStoragePath(cwd: string): boolean {
|
export function isCloudStoragePath(cwd: string): boolean {
|
||||||
const resolvedPath = path.resolve(cwd);
|
const resolvedPath = path.resolve(cwd);
|
||||||
return CLOUD_STORAGE_PATH_PATTERNS.some((pattern) => resolvedPath.includes(pattern));
|
|
||||||
|
// Check macOS-specific patterns (these are specific enough to use includes)
|
||||||
|
if (MACOS_CLOUD_STORAGE_PATTERNS.some((pattern) => resolvedPath.includes(pattern))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check home-anchored patterns to avoid false positives
|
||||||
|
// e.g., /home/user/my-project-about-dropbox/ should NOT match
|
||||||
|
const home = os.homedir();
|
||||||
|
for (const folder of HOME_ANCHORED_CLOUD_FOLDERS) {
|
||||||
|
const cloudPath = path.join(home, folder);
|
||||||
|
// Check if resolved path starts with the cloud storage path followed by a separator
|
||||||
|
// This ensures we match ~/Dropbox/project but not ~/Dropbox-archive or ~/my-dropbox-tool
|
||||||
|
if (resolvedPath === cloudPath || resolvedPath.startsWith(cloudPath + path.sep)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,17 +157,10 @@ export function checkSandboxCompatibility(
|
|||||||
|
|
||||||
// Check for cloud storage incompatibility
|
// Check for cloud storage incompatibility
|
||||||
if (isCloudStoragePath(cwd)) {
|
if (isCloudStoragePath(cwd)) {
|
||||||
const message =
|
|
||||||
`Sandbox mode auto-disabled: Project is in a cloud storage location (${cwd}). ` +
|
|
||||||
`The Claude CLI sandbox feature is incompatible with cloud storage filesystems. ` +
|
|
||||||
`To use sandbox mode, move your project to a local directory.`;
|
|
||||||
|
|
||||||
console.warn(`[SdkOptions] ${message}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
disabledReason: 'cloud_storage',
|
disabledReason: 'cloud_storage',
|
||||||
message,
|
message: `Sandbox mode auto-disabled: Project is in a cloud storage location (${cwd}). The Claude CLI sandbox feature is incompatible with cloud storage filesystems. To use sandbox mode, move your project to a local directory.`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import os from 'os';
|
||||||
|
|
||||||
describe('sdk-options.ts', () => {
|
describe('sdk-options.ts', () => {
|
||||||
let originalEnv: NodeJS.ProcessEnv;
|
let originalEnv: NodeJS.ProcessEnv;
|
||||||
|
let homedirSpy: ReturnType<typeof vi.spyOn>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
originalEnv = { ...process.env };
|
originalEnv = { ...process.env };
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
// Spy on os.homedir and set default return value
|
||||||
|
homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue('/Users/test');
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
process.env = originalEnv;
|
process.env = originalEnv;
|
||||||
|
homedirSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isCloudStoragePath', () => {
|
describe('isCloudStoragePath', () => {
|
||||||
@@ -42,6 +47,25 @@ describe('sdk-options.ts', () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should detect home-anchored Dropbox paths', async () => {
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
expect(isCloudStoragePath('/Users/test/Dropbox')).toBe(true);
|
||||||
|
expect(isCloudStoragePath('/Users/test/Dropbox/project')).toBe(true);
|
||||||
|
expect(isCloudStoragePath('/Users/test/Dropbox/nested/deep/project')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect home-anchored Google Drive paths', async () => {
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
expect(isCloudStoragePath('/Users/test/Google Drive')).toBe(true);
|
||||||
|
expect(isCloudStoragePath('/Users/test/Google Drive/project')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect home-anchored OneDrive paths', async () => {
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
expect(isCloudStoragePath('/Users/test/OneDrive')).toBe(true);
|
||||||
|
expect(isCloudStoragePath('/Users/test/OneDrive/project')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return false for local paths', async () => {
|
it('should return false for local paths', async () => {
|
||||||
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
expect(isCloudStoragePath('/Users/test/projects/myapp')).toBe(false);
|
expect(isCloudStoragePath('/Users/test/projects/myapp')).toBe(false);
|
||||||
@@ -54,6 +78,40 @@ describe('sdk-options.ts', () => {
|
|||||||
expect(isCloudStoragePath('./project')).toBe(false);
|
expect(isCloudStoragePath('./project')).toBe(false);
|
||||||
expect(isCloudStoragePath('../other-project')).toBe(false);
|
expect(isCloudStoragePath('../other-project')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tests for false positive prevention - paths that contain cloud storage names but aren't cloud storage
|
||||||
|
it('should NOT flag paths that merely contain "dropbox" in the name', async () => {
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
// Projects with dropbox-like names
|
||||||
|
expect(isCloudStoragePath('/home/user/my-project-about-dropbox')).toBe(false);
|
||||||
|
expect(isCloudStoragePath('/Users/test/projects/dropbox-clone')).toBe(false);
|
||||||
|
expect(isCloudStoragePath('/Users/test/projects/Dropbox-backup-tool')).toBe(false);
|
||||||
|
// Dropbox folder that's NOT in the home directory
|
||||||
|
expect(isCloudStoragePath('/var/shared/Dropbox/project')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag paths that merely contain "Google Drive" in the name', async () => {
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
expect(isCloudStoragePath('/Users/test/projects/google-drive-api-client')).toBe(false);
|
||||||
|
expect(isCloudStoragePath('/home/user/Google Drive API Tests')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT flag paths that merely contain "OneDrive" in the name', async () => {
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
expect(isCloudStoragePath('/Users/test/projects/onedrive-sync-tool')).toBe(false);
|
||||||
|
expect(isCloudStoragePath('/home/user/OneDrive-migration-scripts')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle different home directories correctly', async () => {
|
||||||
|
// Change the mocked home directory
|
||||||
|
homedirSpy.mockReturnValue('/home/linuxuser');
|
||||||
|
const { isCloudStoragePath } = await import('@/lib/sdk-options.js');
|
||||||
|
|
||||||
|
// Should detect Dropbox under the Linux home directory
|
||||||
|
expect(isCloudStoragePath('/home/linuxuser/Dropbox/project')).toBe(true);
|
||||||
|
// Should NOT detect Dropbox under the old home directory (since home changed)
|
||||||
|
expect(isCloudStoragePath('/Users/test/Dropbox/project')).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('checkSandboxCompatibility', () => {
|
describe('checkSandboxCompatibility', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user