UNPKG

@proguardian/cli

Version:

Guardian supervision layer for AI coding assistants

138 lines (118 loc) 4.59 kB
import path from 'path' import chalk from 'chalk' import which from 'which' import { fileURLToPath } from 'url' import { checkPermissions } from '../utils/file-security.js' import { validateOptions, validateSafePath } from '../utils/validation.js' import { handleError, PermissionError } from '../utils/errors.js' import fs from 'fs-extra' import { log, success, error, warn } from '../utils/logger.js' import '../utils/cli-detector.js' // For side effects only const __dirname = path.dirname(fileURLToPath(import.meta.url)) async function installWrapperForCLI(cliName, wrapperName, options) { try { // Find where the CLI is installed let cliPath try { cliPath = await which(cliName) } catch { return false // CLI not found } const cliDir = path.dirname(cliPath) // Check write permissions on the directory if (!(await checkPermissions(cliDir, fs.constants.W_OK))) { throw new PermissionError('write to directory', cliDir) } // Construct backup path - no validation needed for system directories // The CLI directory is a system path that we need to write to const backupPath = path.join(cliDir, `${cliName}-original`) // Backup original binary // Use fs directly for system paths (not subject to path traversal validation) if (!(await fs.pathExists(backupPath))) { log(chalk.gray(`Backing up original ${cliName} to ${path.basename(backupPath)}`)) await fs.copy(cliPath, backupPath) // Make backup executable await fs.chmod(backupPath, '755') } else if (!options.force) { warn(`Backup for ${cliName} already exists. Use --force to overwrite.`) return true // Already installed } // Install our wrapper // Calculate project root and wrapper path without using '..' in validation const projectRoot = path.resolve(__dirname, '..', '..') const wrapperPath = path.join(projectRoot, 'src', 'wrapper', wrapperName) const wrapperSource = validateSafePath(wrapperPath, projectRoot) log(chalk.gray(`Installing wrapper to ${path.basename(cliPath)}`)) // Copy wrapper to system location await fs.copy(wrapperSource, cliPath, { overwrite: true }) await fs.chmod(cliPath, '755') success(`Guardian wrapper installed for ${cliName}!`) log(chalk.gray(` Original ${cliName} backed up to: ${cliName}-original`)) return true } catch (err) { if (err instanceof PermissionError) { error(`Permission denied for ${cliName}: ${err.message}`) log() warn('Try running with sudo:') log(chalk.gray(' sudo proguardian install-wrapper')) log() warn('Or use the alternative approach:') log(chalk.gray(' Add this to your shell profile (~/.bashrc or ~/.zshrc):')) log(chalk.gray(` alias ${cliName}="proguardian-${cliName}"`)) return false } else { throw err } } } export async function installWrapper(options = {}) { try { // Validate command options validateOptions('install-wrapper', options) log(chalk.cyan('Installing Guardian wrapper...\n')) // Check which CLI tools are available let claudeInstalled = false let geminiInstalled = false // Try to install wrapper for Claude try { claudeInstalled = await installWrapperForCLI('claude', 'claude-wrapper.js', options) } catch (err) { if (!(err instanceof PermissionError)) { throw err } } // Try to install wrapper for Gemini try { geminiInstalled = await installWrapperForCLI('gemini', 'gemini-wrapper.js', options) } catch (err) { if (!(err instanceof PermissionError)) { throw err } } // Summary if (!claudeInstalled && !geminiInstalled) { error('No AI CLI tools found to wrap') log() log(chalk.gray('Please install one of the following first:')) log(chalk.gray(' Claude Code: npm install -g @anthropic/claude-code')) log(chalk.gray(' Gemini CLI: npm install -g @google/gemini-cli')) return } // Additional security note log() log(chalk.cyan('Security note:')) log(chalk.gray(' The wrapper enforces Guardian mode when .proguardian exists')) log(chalk.gray(' Run "proguardian check" to verify your setup')) // Show which CLIs were wrapped log() log(chalk.cyan('Wrapped CLIs:')) if (claudeInstalled) { log(chalk.green(' ✓ claude')) } if (geminiInstalled) { log(chalk.green(' ✓ gemini')) } } catch (err) { handleError(err, { exit: true, verbose: options.verbose }) } }