diff --git a/apps/cli/src/commands/install.ts b/apps/cli/src/commands/install.ts index 64eea48..18f4748 100644 --- a/apps/cli/src/commands/install.ts +++ b/apps/cli/src/commands/install.ts @@ -10,6 +10,7 @@ import { AdapterRegistry, AgentDetector, type VibePackage, type TargetPaths } fr import { ConflictDetector } from '../stash/conflict-detector.js'; import { ConflictResolver } from '../stash/conflict-resolver.js'; import { StashManager } from '../stash/stash-manager.js'; +import { isCriticalSystemDirectory } from '../utils/safe-paths.js'; interface GlobalManifest { version: string; @@ -158,6 +159,14 @@ export async function installCommand( source: string, options: { conflict?: string; agent?: string; dryRun?: boolean } = {} ): Promise { + const projectRoot = path.resolve(process.cwd()); + + if (isCriticalSystemDirectory(projectRoot)) { + console.error(chalk.red(`❌ Cannot install in critical system directory: ${projectRoot}`)); + console.error(chalk.yellow('Please run from a safe project directory.')); + throw new Error(`Installation blocked: critical system directory`); + } + const spinner = ora('Installing vibe...').start(); try { @@ -197,7 +206,6 @@ export async function installCommand( spinner.text = 'Detecting agents...'; - const projectRoot = process.cwd(); const adapters = AdapterRegistry.getAllAdapters(); const detector = new AgentDetector(adapters); let detectedAgents = await detector.detectAll(); diff --git a/apps/cli/src/commands/update.ts b/apps/cli/src/commands/update.ts index db560a7..e79e740 100644 --- a/apps/cli/src/commands/update.ts +++ b/apps/cli/src/commands/update.ts @@ -9,6 +9,7 @@ import { getVibesHome, getVibePackageDir } from '../utils/symlink-manager.js'; import { ConflictDetector } from '../stash/conflict-detector.js'; import { ConflictResolver } from '../stash/conflict-resolver.js'; import { StashManager } from '../stash/stash-manager.js'; +import { isCriticalSystemDirectory } from '../utils/safe-paths.js'; interface GlobalManifest { version: string; @@ -63,6 +64,14 @@ export async function updateCommand( dryRun?: boolean; } = {} ): Promise { + const projectRoot = path.resolve(process.cwd()); + + if (isCriticalSystemDirectory(projectRoot)) { + console.error(chalk.red(`❌ Cannot update in critical system directory: ${projectRoot}`)); + console.error(chalk.yellow('Please run from a safe project directory.')); + throw new Error(`Update blocked: critical system directory`); + } + const spinner = ora('Updating vibe...').start(); try { @@ -92,7 +101,6 @@ export async function updateCommand( vibeManifest = result.manifest; } - const projectRoot = process.cwd(); const vibeDir = getVibePackageDir(vibeManifest.name, vibeManifest.version); if (!vibeManifest.symlinks) { diff --git a/apps/cli/src/stash/conflict-detector.ts b/apps/cli/src/stash/conflict-detector.ts index e3ec4b2..82e6720 100644 --- a/apps/cli/src/stash/conflict-detector.ts +++ b/apps/cli/src/stash/conflict-detector.ts @@ -1,5 +1,6 @@ import { existsSync, statSync } from 'node:fs'; import path from 'node:path'; +import crypto from 'node:crypto'; import { HashCalculator } from './hash-calculator.js'; export interface Conflict { @@ -68,7 +69,9 @@ export class ConflictDetector { if (stats.isDirectory()) { const hashes = await this.hashCalculator.calculateDirectory(filePath); const allHashes = Array.from(hashes.values()).join(''); - return this.hashCalculator.calculateFile(Buffer.from(allHashes) as any) || allHashes.substring(0, 64); + const hash = crypto.createHash('sha256'); + hash.update(allHashes); + return hash.digest('hex'); } return this.hashCalculator.calculateFile(filePath); diff --git a/apps/cli/src/utils/__tests__/safe-paths.test.ts b/apps/cli/src/utils/__tests__/safe-paths.test.ts new file mode 100644 index 0000000..f4ebdd7 --- /dev/null +++ b/apps/cli/src/utils/__tests__/safe-paths.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { getCriticalSystemPaths, isCriticalSystemDirectory, isUserHomeDirectory } from '../safe-paths.js'; +import os from 'node:os'; +import path from 'node:path'; + +describe('safe-paths', () => { + const originalPlatform = process.platform; + + afterEach(() => { + Object.defineProperty(process, 'platform', { + value: originalPlatform + }); + }); + + describe('getCriticalSystemPaths', () => { + it('returns Windows paths on win32', () => { + Object.defineProperty(process, 'platform', { + value: 'win32' + }); + + const paths = getCriticalSystemPaths(); + + expect(paths).toContain('C:\\'); + expect(paths.some(p => p.includes('Windows'))).toBe(true); + expect(paths.some(p => p.includes('Program Files'))).toBe(true); + }); + + it('returns macOS paths on darwin', () => { + Object.defineProperty(process, 'platform', { + value: 'darwin' + }); + + const paths = getCriticalSystemPaths(); + + expect(paths).toContain('/'); + expect(paths).toContain('/System'); + expect(paths).toContain('/Library'); + expect(paths).toContain('/Applications'); + }); + + it('returns Linux paths on linux', () => { + Object.defineProperty(process, 'platform', { + value: 'linux' + }); + + const paths = getCriticalSystemPaths(); + + expect(paths).toContain('/'); + expect(paths).toContain('/usr'); + expect(paths).toContain('/etc'); + expect(paths).toContain('/var'); + }); + }); + + describe('isCriticalSystemDirectory', () => { + it('detects root directory as critical', () => { + const isRootCritical = isCriticalSystemDirectory('/'); + expect(isRootCritical).toBe(true); + }); + + it('detects subdirectories of critical paths', () => { + if (process.platform === 'darwin' || process.platform === 'linux') { + expect(isCriticalSystemDirectory('/usr/local')).toBe(true); + expect(isCriticalSystemDirectory('/etc/config')).toBe(true); + } + }); + + it('allows safe project directories', () => { + const homeDir = os.homedir(); + const projectDir = path.join(homeDir, 'projects', 'my-app'); + + expect(isCriticalSystemDirectory(projectDir)).toBe(false); + }); + + it('normalizes paths correctly', () => { + if (process.platform === 'darwin' || process.platform === 'linux') { + expect(isCriticalSystemDirectory('/usr/../usr')).toBe(true); + } + }); + }); + + describe('isUserHomeDirectory', () => { + it('detects user home directory', () => { + const homeDir = os.homedir(); + expect(isUserHomeDirectory(homeDir)).toBe(true); + }); + + it('allows subdirectories of home', () => { + const homeDir = os.homedir(); + const subDir = path.join(homeDir, 'projects'); + + expect(isUserHomeDirectory(subDir)).toBe(false); + }); + }); + + describe('cross-platform compatibility', () => { + it('handles paths with different separators', () => { + const userDir = path.join(os.homedir(), 'projects', 'app'); + + const result = isCriticalSystemDirectory(userDir); + + expect(typeof result).toBe('boolean'); + }); + + it('resolves relative paths', () => { + const relativePath = './some/project'; + const resolvedCheck = isCriticalSystemDirectory(relativePath); + + expect(typeof resolvedCheck).toBe('boolean'); + }); + }); +}); + diff --git a/apps/cli/src/utils/safe-paths.ts b/apps/cli/src/utils/safe-paths.ts new file mode 100644 index 0000000..f021bf0 --- /dev/null +++ b/apps/cli/src/utils/safe-paths.ts @@ -0,0 +1,97 @@ +import path from 'node:path'; +import os from 'node:os'; + +export function getCriticalSystemPaths(): string[] { + const platform = process.platform; + + if (platform === 'win32') { + const systemRoot = process.env.SystemRoot || 'C:\\Windows'; + const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files'; + const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'; + const drives = ['C:\\', 'D:\\', 'E:\\']; + + return [ + ...drives, + systemRoot, + path.join(systemRoot, 'System32'), + programFiles, + programFilesX86, + 'C:\\Windows', + 'C:\\Program Files', + 'C:\\Program Files (x86)' + ]; + } + + if (platform === 'darwin') { + return [ + '/', + '/usr', + '/etc', + '/var', + '/System', + '/Library', + '/bin', + '/sbin', + '/opt', + '/private/etc', + '/private/var', + '/private/tmp', + '/Applications' + ]; + } + + return [ + '/', + '/usr', + '/etc', + '/var', + '/bin', + '/sbin', + '/opt', + '/boot', + '/lib', + '/lib64', + '/sys', + '/proc', + '/dev', + '/root' + ]; +} + +export function isCriticalSystemDirectory(targetPath: string): boolean { + const resolved = path.resolve(targetPath); + const criticalPaths = getCriticalSystemPaths(); + + return criticalPaths.some(critical => { + const resolvedCritical = path.resolve(critical); + + if (resolved === resolvedCritical) { + return true; + } + + const normalizedResolved = resolved + path.sep; + const normalizedCritical = resolvedCritical + path.sep; + + return normalizedResolved.startsWith(normalizedCritical); + }); +} + +export function isUserHomeDirectory(targetPath: string): boolean { + const resolved = path.resolve(targetPath); + const homeDir = os.homedir(); + + return resolved === homeDir; +} + +export function getSafeInstallMessage(targetPath: string): string { + if (isUserHomeDirectory(targetPath)) { + return 'Installing in home directory is not recommended. Please run from a project directory.'; + } + + if (isCriticalSystemDirectory(targetPath)) { + return 'Cannot install in critical system directory. Please run from a safe project directory.'; + } + + return ''; +} +