UNPKG

@deriv-com/shiftai-cli

Version:

A comprehensive AI code detection and analysis CLI tool for tracking AI-generated code in projects

915 lines (787 loc) • 31.5 kB
const fs = require('fs-extra'); const path = require('path'); const { execSync, spawn } = require('child_process'); const inquirer = require('inquirer'); const chalk = require('chalk'); const simpleGit = require('simple-git'); const display = require('../utils/display'); const AIStorage = require('../core/ai-storage'); const { getGitRemoteInfo } = require('../utils/git-utils'); /** * Install Command - Sets up ShiftAI hooks and configuration * Smart install that auto-detects and cleans existing installations */ async function installCommand(options = {}) { try { display.clearAndShowHeader('ShiftAI Installation', 'Setting up AI code tracking and productivity analytics'); // Auto-detect if we're running in dev mode (shai-dev) const isDevMode = process.argv[1]?.includes('shai-dev') || process.env.npm_command === 'run'; if (isDevMode) { process.env.SHIFTAI_USE_DEV = 'true'; console.log(`${chalk.blue('šŸ”§')} Development mode detected - using local package\n`); } // Step 0: Smart detection and cleanup of existing installations await smartCleanupExisting(); // Step 1: Check Prerequisites process.stdout.write(`${chalk.blue('āš™ļø')} Checking prerequisites...`); const prereqCheck = await checkPrerequisites(); if (!prereqCheck.success) { process.stdout.write(`\r${chalk.red('āŒ')} Prerequisites check failed\n`); console.log(chalk.red(` ${prereqCheck.error}`)); process.exit(1); } process.stdout.write(`\r${chalk.green('āœ…')} Prerequisites check passed\n`); // Step 2: Validate Project Structure process.stdout.write(`${chalk.blue('āš™ļø')} Validating project structure...`); const projectCheck = await validateProjectStructure(); if (!projectCheck.success) { process.stdout.write(`\r${chalk.red('āŒ')} Project validation failed\n`); console.log(chalk.red(` ${projectCheck.error}`)); process.exit(1); } process.stdout.write(`\r${chalk.green('āœ…')} Project structure validated\n`); // Step 3: Install Dependencies if (!options.skipDeps) { process.stdout.write(`${chalk.blue('āš™ļø')} Installing dependencies...`); const depsResult = await installDependencies(); if (!depsResult.success) { process.stdout.write(`\r${chalk.yellow('āš ļø')} Dependencies installed with issues\n`); console.log(chalk.yellow(` ${depsResult.error}`)); } else { process.stdout.write(`\r${chalk.green('āœ…')} Dependencies installed successfully\n`); } } else { console.log(chalk.gray('ā­ļø Skipping dependency installation (--skip-deps)')); } // Step 4: Configure Local Storage process.stdout.write(`${chalk.blue('āš™ļø')} Configuring local storage...`); const config = await configureLocal(options); if (!config.success) { process.stdout.write(`\r${chalk.red('āŒ')} Configuration failed\n`); console.log(chalk.red(` ${config.error}`)); process.exit(1); } process.stdout.write(`\r${chalk.green('āœ…')} Local storage configured\n`); // Step 5: Setup Git Hooks process.stdout.write(`${chalk.blue('āš™ļø')} Setting up git hooks...`); const hooksResult = await setupGitHooks(options.force); if (!hooksResult.success) { process.stdout.write(`\r${chalk.red('āŒ')} Git hooks setup failed\n`); console.log(chalk.red(` ${hooksResult.error}`)); process.exit(1); } process.stdout.write(`\r${chalk.green('āœ…')} Git hooks configured\n`); // Step 6: Setup GitHub Workflow (disabled) process.stdout.write(`${chalk.blue('āš™ļø')} Checking GitHub workflow setup...`); const workflowResult = await setupGitHubWorkflow(); if (!workflowResult.success) { process.stdout.write(`\r${chalk.yellow('āš ļø')} GitHub workflow setup had issues\n`); console.log(chalk.yellow(` ${workflowResult.error}`)); } else { process.stdout.write(`\r${chalk.gray('ā­ļø')} AI dashboard workflow creation skipped\n`); } // Step 7: Test Configuration process.stdout.write(`${chalk.blue('āš™ļø')} Testing configuration...`); const testResult = await testConfiguration(config.orgName, config.storageDir); if (!testResult.success) { process.stdout.write(`\r${chalk.yellow('āš ļø')} Configuration test had issues\n`); console.log(chalk.yellow(` ${testResult.error}`)); } else { process.stdout.write(`\r${chalk.green('āœ…')} Configuration test passed\n`); } // Step 8: Complete Installation process.stdout.write(`${chalk.blue('āš™ļø')} Finalizing installation...`); await writeConfig(config); process.stdout.write(`\r${chalk.green('āœ…')} Installation finalized\n`); // Show final success message console.log(chalk.green.bold('šŸŽ‰ ShiftAI installation completed successfully!')); console.log(chalk.gray(' Your project is now configured for AI code tracking and productivity analytics.')); // Show next steps const nextSteps = [ 'Add AI code markers to your code using the supported formats', 'Commit your changes - the pre-commit hook will automatically extract AI code', 'View analytics with: shai analytics', 'Check status with: shai status' ]; console.log(chalk.bold.blue('\nšŸš€ Next Steps:')); nextSteps.forEach((step, i) => { console.log(chalk.gray(` ${i + 1}. ${step}`)); }); } catch (error) { console.log(display.error('Installation failed', error.message)); process.exit(1); } } /** * Check prerequisites for ShiftAI installation */ async function checkPrerequisites() { try { const checks = []; // Check Node.js try { const nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim(); const major = parseInt(nodeVersion.slice(1).split('.')[0]); if (major >= 14) { checks.push({ label: `Node.js ${nodeVersion}`, status: 'success' }); } else { checks.push({ label: `Node.js ${nodeVersion} (requires >=14)`, status: 'error' }); } } catch (error) { checks.push({ label: 'Node.js', status: 'error', value: 'Not found' }); } // Check npm try { const npmVersion = execSync('npm --version', { encoding: 'utf8' }).trim(); checks.push({ label: `npm ${npmVersion}`, status: 'success' }); } catch (error) { checks.push({ label: 'npm', status: 'error', value: 'Not found' }); } // Check Git try { const gitVersion = execSync('git --version', { encoding: 'utf8' }).trim(); checks.push({ label: gitVersion, status: 'success' }); } catch (error) { checks.push({ label: 'Git', status: 'error', value: 'Not found' }); } // Check Git repository try { execSync('git rev-parse --git-dir', { encoding: 'utf8', stdio: 'pipe' }); checks.push({ label: 'Git repository', status: 'success' }); } catch (error) { checks.push({ label: 'Git repository', status: 'error', value: 'Not a git repository' }); } const hasErrors = checks.some(check => check.status === 'error'); return { success: !hasErrors, error: hasErrors ? 'Some prerequisites are missing. Please install the required software.' : null, checks }; } catch (error) { return { success: false, error: error.message }; } } /** * Validate project structure */ async function validateProjectStructure() { try { const checks = []; const cwd = process.cwd(); // Check for package.json const packageJsonPath = path.join(cwd, 'package.json'); if (await fs.pathExists(packageJsonPath)) { checks.push({ label: 'package.json', status: 'success' }); try { const packageJson = await fs.readJson(packageJsonPath); checks.push({ label: `Project: ${packageJson.name || 'unnamed'}`, status: 'success' }); } catch (error) { checks.push({ label: 'package.json parsing', status: 'warning', value: 'Invalid JSON' }); } } else { checks.push({ label: 'package.json', status: 'warning', value: 'Not found' }); } // Check for existing ShiftAI configuration const configExists = await fs.pathExists(AIStorage.getConfigPath()); if (configExists) { checks.push({ label: 'ShiftAI config', status: 'warning', value: 'Already exists' }); } else { checks.push({ label: 'ShiftAI config', status: 'success', value: 'Ready to create' }); } // Check for .git directory const gitExists = await fs.pathExists(path.join(cwd, '.git')); if (gitExists) { checks.push({ label: 'Git repository', status: 'success' }); } else { checks.push({ label: 'Git repository', status: 'error', value: 'Not found' }); } // Check for existing hooks const hooksDir = path.join(cwd, '.git', 'hooks'); const huskyDir = path.join(cwd, '.husky'); if (await fs.pathExists(huskyDir)) { checks.push({ label: 'Husky hooks', status: 'success', value: 'Found' }); } else if (await fs.pathExists(hooksDir)) { checks.push({ label: 'Git hooks directory', status: 'success' }); } else { checks.push({ label: 'Hooks setup', status: 'success', value: 'Ready to configure' }); } const hasErrors = checks.some(check => check.status === 'error'); return { success: !hasErrors, error: hasErrors ? 'Project structure validation failed' : null, checks }; } catch (error) { return { success: false, error: error.message }; } } /** * Install required dependencies */ async function installDependencies() { try { const cwd = process.cwd(); const packageJsonPath = path.join(cwd, 'package.json'); if (!(await fs.pathExists(packageJsonPath))) { return { success: false, error: 'No package.json found. Please run npm init first.' }; } // Check if husky is already installed const packageJson = await fs.readJson(packageJsonPath); const hasHusky = (packageJson.devDependencies && packageJson.devDependencies.husky) || (packageJson.dependencies && packageJson.dependencies.husky); if (!hasHusky) { try { execSync('npm install --save-dev husky', { cwd, stdio: 'pipe' }); execSync('npx husky install', { cwd, stdio: 'pipe' }); } catch (error) { return { success: false, error: `Husky installation failed: ${error.message}` }; } } return { success: true, installed: ['husky'] }; } catch (error) { return { success: false, error: error.message }; } } /** * Configure local storage */ async function configureLocal(options = {}) { try { let orgName = options.org; let repoName = options.repo; // Try auto-detection first if (!orgName || !repoName) { try { const gitInfo = await getGitRemoteInfo(); if (gitInfo.organization && gitInfo.repository) { orgName = orgName || gitInfo.organization; repoName = repoName || gitInfo.repository; } } catch (error) { // Auto-detection failed, will prompt if needed } } // If still not available, prompt user if (!orgName) { const answers = await inquirer.prompt([ { type: 'input', name: 'orgName', message: 'Organization/Project name:', default: 'my-organization', validate: (input) => { if (!input || input.length < 2) { return 'Please enter a valid organization name'; } return true; } } ]); orgName = answers.orgName; } // GitHub token will be added manually to .env file // Initialize AI storage const storage = new AIStorage({ organization: orgName, repository: repoName, projectRoot: process.cwd() }); const initResult = await storage.initialize(); if (!initResult.success) { return { success: false, error: initResult.error }; } return { success: true, orgName, repoName, storageDir: initResult.shiftaiDir }; } catch (error) { return { success: false, error: error.message }; } } /** * Setup Git hooks */ async function setupGitHooks(force = false) { try { const cwd = process.cwd(); const huskyDir = path.join(cwd, '.husky'); const preCommitPath = path.join(huskyDir, 'pre-commit'); const postCommitPath = path.join(huskyDir, 'post-commit'); const prePushPath = path.join(huskyDir, 'pre-push'); // Ensure .husky directory exists await fs.ensureDir(huskyDir); // Set up pre-commit hook for AI marker detection and stripping const preCommitHookContent = ` # ShiftAI Pre-commit Hook - Detect and strip AI markers npx @deriv-com/shiftai-cli hook pre-commit`; // Set up post-commit hook for analysis table display const postCommitHookContent = ` # ShiftAI Post-commit Hook - Display AI analysis table npx @deriv-com/shiftai-cli hook post-commit`; // Set up pre-push hook for intercepted push with PR management const prePushHookContent = ` # ShiftAI Pre-push Hook - Intercept push for AI analysis and PR management # Git pre-push hook passes: $1=remote_name, $2=remote_url # šŸ—‘ļø EARLY EXIT: Check for delete operations at shell level if ps aux | grep -v grep | grep -q "git.*push.*--delete"; then exit 0 fi # Check command line arguments in current process tree if pgrep -f "git.*push.*--delete" > /dev/null 2>&1; then exit 0 fi export GIT_PUSH_REMOTE_NAME="$1" export GIT_PUSH_REMOTE_URL="$2" npx @deriv-com/shiftai-cli hook pre-push exit $?`; // Set up pre-commit hook if (await fs.pathExists(preCommitPath)) { const existingContent = await fs.readFile(preCommitPath, 'utf8'); if (!existingContent.includes('@deriv-com/shiftai-cli hook pre-commit')) { // Remove any existing exit statements and trailing whitespace const cleanedContent = existingContent .replace(/^\s*#\s*Preserve exit code.*$/gm, '') .replace(/^\s*exit\s+\$\?.*$/gm, '') .replace(/^\s*exit\s+\d+.*$/gm, '') .trimEnd(); // Add ShiftAI content and final exit statement const updatedContent = cleanedContent + '\n' + preCommitHookContent + '\n\n# Preserve exit code\nexit $?'; await fs.writeFile(preCommitPath, updatedContent); } } else { const newHookContent = `#!/bin/sh . "$(dirname "$0")/_/husky.sh" ${preCommitHookContent} # Preserve exit code exit $?`; await fs.writeFile(preCommitPath, newHookContent); } // Set up post-commit hook if (await fs.pathExists(postCommitPath)) { const existingContent = await fs.readFile(postCommitPath, 'utf8'); if (!existingContent.includes('@deriv-com/shiftai-cli hook post-commit')) { // Remove any existing exit statements and trailing whitespace const cleanedContent = existingContent .replace(/^\s*#\s*Preserve exit code.*$/gm, '') .replace(/^\s*exit\s+\$\?.*$/gm, '') .replace(/^\s*exit\s+\d+.*$/gm, '') .trimEnd(); // Add ShiftAI content and final exit statement (exit 0 for post-commit to not block) const updatedContent = cleanedContent + '\n' + postCommitHookContent + '\n\n# Preserve exit code (don\'t block if analysis fails)\nexit 0'; await fs.writeFile(postCommitPath, updatedContent); } } else { const newHookContent = `#!/bin/sh . "$(dirname "$0")/_/husky.sh" ${postCommitHookContent} # Preserve exit code (don't block if analysis fails) exit 0`; await fs.writeFile(postCommitPath, newHookContent); } // Set up pre-push hook if (await fs.pathExists(prePushPath)) { const existingContent = await fs.readFile(prePushPath, 'utf8'); if (!existingContent.includes('@deriv-com/shiftai-cli hook pre-push')) { // Add ShiftAI hook to existing file const updatedContent = existingContent.trimEnd() + '\n' + prePushHookContent; await fs.writeFile(prePushPath, updatedContent); } else if (!existingContent.includes('Check for delete operations at shell level')) { // Update existing ShiftAI hook with delete detection logic const lines = existingContent.split('\n'); const shebangAndHusky = lines.slice(0, 3).join('\n'); // Keep #!/bin/sh, husky.sh, and blank line const newHookContent = shebangAndHusky + '\n' + prePushHookContent; await fs.writeFile(prePushPath, newHookContent); } } else { const newHookContent = `#!/bin/sh . "$(dirname "$0")/_/husky.sh" ${prePushHookContent}`; await fs.writeFile(prePushPath, newHookContent); } // Make hooks executable await fs.chmod(preCommitPath, 0o755); await fs.chmod(postCommitPath, 0o755); await fs.chmod(prePushPath, 0o755); // Install shiftai-cli as devDependency using npm await installShiftAIPackage(cwd); return { success: true, hookPath: `${preCommitPath}, ${postCommitPath}, ${prePushPath}`, message: 'Pre-commit, post-commit, and pre-push hooks configured' }; } catch (error) { return { success: false, error: error.message }; } } /** * Test the configuration */ async function testConfiguration(orgName, storageDir) { try { // Test AI detection const AIDetector = require('../core/ai-detector'); const detector = new AIDetector(); // Create a temporary test file const testCode = `// [AI] function testFunction() { return "This is AI generated code"; } // [/AI]`; const result = detector.extractAICode(testCode, 'javascript'); if (!result.hasAICode || result.blocks.length === 0) { throw new Error('AI detection test failed'); } // Test local storage if configured if (orgName && storageDir) { const storage = new LocalStorage({ organization: orgName, projectRoot: process.cwd() }); const storageInfo = await storage.getStorageInfo(); if (!storageInfo.exists) { throw new Error(`Local storage not accessible: ${storageDir}`); } } return { success: true, aiDetection: true, localStorage: !!(orgName && storageDir) }; } catch (error) { return { success: false, error: error.message }; } } /** * Write configuration files */ async function writeConfig(config) { try { const cwd = process.cwd(); // Write config to local storage const configPath = AIStorage.getConfigPath(); await fs.ensureDir(path.dirname(configPath)); const configContent = `module.exports = { // Organization settings organization: '${config.orgName}', // Local storage settings storage: { enabled: true, skipOnError: true, autoStageJson: true }, // AI detection settings detection: { supportedExtensions: ['js', 'jsx', 'ts', 'tsx', 'dart', 'java', 'c', 'cpp', 'h', 'cs', 'php', 'go', 'rs', 'swift', 'kt', 'scala', 'py', 'pl', 'pm', 'r', 'rb', 'sh', 'yml', 'yaml', 'toml', 'gitignore', 'dockerignore', 'env', 'css', 'scss', 'sass', 'less', 'html', 'htm', 'xml', 'md'], strictMode: false, reportUnclosed: true }, // Hook settings hooks: { preCommit: true, prePush: true, postCommit: false } };`; await fs.writeFile(configPath, configContent); // Note: .env.example creation removed - users can create their own .env file if needed // Add .env to .gitignore if it doesn't exist const gitignorePath = path.join(cwd, '.gitignore'); if (await fs.pathExists(gitignorePath)) { const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); if (!gitignoreContent.includes('.env')) { await fs.appendFile(gitignorePath, '\n# ShiftAI environment variables\n.env\n'); } } else { await fs.writeFile(gitignorePath, '# ShiftAI environment variables\n.env\n'); } } catch (error) { throw error; } } /** * Install shiftai-cli as devDependency using npm */ async function installShiftAIPackage(cwd) { try { // Use production by default, local package only when explicitly in development const useDevelopment = process.env.SHIFTAI_USE_DEV === 'true'; // Read package.json to get actual package name and version for .tgz file const packageJsonPath = path.join(__dirname, '../../package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); const tarballName = `${packageJson.name.replace('@', '').replace('/', '-')}-${packageJson.version}.tgz`; const localPackagePath = path.join(__dirname, '../../', tarballName); if (useDevelopment && await fs.pathExists(localPackagePath)) { // Use local package for development execSync(`npm install --save-dev ${localPackagePath}`, { cwd, stdio: 'pipe' }); } else { // Use npm registry (production default) execSync('npm install --save-dev @deriv-com/shiftai-cli', { cwd, stdio: 'pipe' }); } // Add prepare script if it doesn't exist await addPrepareScript(cwd); } catch (error) { throw new Error(`npm install failed: ${error.message}`); } } /** * Add prepare script to package.json */ async function addPrepareScript(cwd) { const packageJsonPath = path.join(cwd, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJson(packageJsonPath); if (!packageJson.scripts) { packageJson.scripts = {}; } if (!packageJson.scripts.prepare) { packageJson.scripts.prepare = 'husky install'; await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } } } /** * Setup GitHub workflow for AI dashboard (disabled) */ async function setupGitHubWorkflow() { // AI dashboard workflow creation has been disabled return { success: true, message: 'AI dashboard workflow creation skipped' }; } /** * Surgically update package.json without changing formatting (deprecated - use installShiftAIPackage instead) */ async function updatePackageJsonSurgically(packageJsonPath) { try { if (!(await fs.pathExists(packageJsonPath))) { console.log('package.json not found, skipping devDependency update'); return; } // Read as text to preserve formatting const originalContent = await fs.readFile(packageJsonPath, 'utf8'); const packageJson = JSON.parse(originalContent); // Add shiftai-cli as devDependency (same version as currently installed globally) if (!packageJson.devDependencies) { packageJson.devDependencies = {}; } const cliVersion = require('../../package.json').version; if (packageJson.devDependencies['@deriv-com/shiftai-cli']) { console.log(`@deriv-com/shiftai-cli already exists in devDependencies: ${packageJson.devDependencies['@deriv-com/shiftai-cli']}`); } else { packageJson.devDependencies['@deriv-com/shiftai-cli'] = `^${cliVersion}`; console.log(`Adding @deriv-com/shiftai-cli@^${cliVersion} to devDependencies (matching globally installed version)`); } // Add prepare script if it doesn't exist if (!packageJson.scripts) { packageJson.scripts = {}; } if (!packageJson.scripts.prepare) { packageJson.scripts.prepare = 'husky install'; console.log('Adding prepare script: husky install'); } let updatedContent = originalContent; // Handle scripts.prepare addition if (!originalContent.includes('"prepare"')) { const scriptsMatch = updatedContent.match(/("scripts"\s*:\s*{[^}]*)(})/); if (scriptsMatch) { const beforeClosing = scriptsMatch[1]; const hasExistingScripts = beforeClosing.includes(':'); const comma = hasExistingScripts ? ',' : ''; const newScripts = `${beforeClosing}${comma}\n "prepare": "husky install"\n }`; updatedContent = updatedContent.replace(scriptsMatch[0], newScripts); } else { // Add scripts object if it doesn't exist const packageMatch = updatedContent.match(/({[^}]*)(}[^}]*$)/s); if (packageMatch) { const beforeClosing = packageMatch[1]; const hasExistingFields = beforeClosing.includes(':'); const comma = hasExistingFields ? ',' : ''; const newContent = `${beforeClosing}${comma}\n "scripts": {\n "prepare": "husky install"\n }\n}`; updatedContent = updatedContent.replace(packageMatch[0], newContent); } } } // Handle devDependencies.@deriv-com/shiftai-cli addition if (!originalContent.includes('"@deriv-com/shiftai-cli"')) { const devDepsMatch = updatedContent.match(/("devDependencies"\s*:\s*{[^}]*)(})/); if (devDepsMatch) { const beforeClosing = devDepsMatch[1]; const hasExistingDeps = beforeClosing.includes(':'); const comma = hasExistingDeps ? ',' : ''; const newDevDeps = `${beforeClosing}${comma}\n "@deriv-com/shiftai-cli": "^${cliVersion}"\n }`; updatedContent = updatedContent.replace(devDepsMatch[0], newDevDeps); } else { // Add devDependencies object if it doesn't exist const packageMatch = updatedContent.match(/({[^}]*)(}[^}]*$)/s); if (packageMatch) { const beforeClosing = packageMatch[1]; const hasExistingFields = beforeClosing.includes(':'); const comma = hasExistingFields ? ',' : ''; const newContent = `${beforeClosing}${comma}\n "devDependencies": {\n "@deriv-com/shiftai-cli": "^${cliVersion}"\n }\n}`; updatedContent = updatedContent.replace(packageMatch[0], newContent); } } } // Write back only if content changed if (updatedContent !== originalContent) { await fs.writeFile(packageJsonPath, updatedContent); console.log('āœ… package.json updated surgically (preserving formatting)'); } else { console.log('šŸ“‹ package.json already up to date'); } } catch (error) { console.log(`āš ļø Surgical update failed: ${error.message}`); console.log('šŸ“ Falling back to standard JSON write...'); // Fallback to standard approach try { const packageJson = await fs.readJson(packageJsonPath); if (!packageJson.scripts) packageJson.scripts = {}; if (!packageJson.scripts.prepare) packageJson.scripts.prepare = 'husky install'; if (!packageJson.devDependencies) packageJson.devDependencies = {}; if (!packageJson.devDependencies['@deriv-com/shiftai-cli']) { const cliVersion = require('../../package.json').version; packageJson.devDependencies['@deriv-com/shiftai-cli'] = `^${cliVersion}`; } await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); console.log('āœ… package.json updated using fallback method'); } catch (fallbackError) { console.log(`āŒ Fallback also failed: ${fallbackError.message}`); throw fallbackError; } } } /** * Smart cleanup of existing ShiftAI installations * Auto-detects and removes old versions before fresh install */ async function smartCleanupExisting() { const cwd = process.cwd(); try { // Check if ShiftAI is already installed (old or new package) const packageJsonPath = path.join(cwd, 'package.json'); if (!(await fs.pathExists(packageJsonPath))) { return; // No package.json, nothing to clean } const packageJson = await fs.readJson(packageJsonPath); const hasOldShiftAI = (packageJson.devDependencies && packageJson.devDependencies['shiftai-cli']) || (packageJson.dependencies && packageJson.dependencies['shiftai-cli']); const hasNewShiftAI = (packageJson.devDependencies && packageJson.devDependencies['@deriv-com/shiftai-cli']) || (packageJson.dependencies && packageJson.dependencies['@deriv-com/shiftai-cli']); if (hasOldShiftAI || hasNewShiftAI) { console.log(`${chalk.blue('šŸ”')} Existing ShiftAI installation detected - cleaning up...\n`); // Clean Git Hooks await cleanGitHooks(cwd); // Remove old packages if (hasOldShiftAI) { try { execSync('npm uninstall shiftai-cli', { cwd, stdio: 'pipe' }); console.log(`${chalk.green('āœ…')} Removed old shiftai-cli package`); } catch (error) { // Continue if uninstall fails } } if (hasNewShiftAI) { try { execSync('npm uninstall @deriv-com/shiftai-cli', { cwd, stdio: 'pipe' }); console.log(`${chalk.green('āœ…')} Removed existing @deriv-com/shiftai-cli package`); } catch (error) { // Continue if uninstall fails } } console.log(`${chalk.green('āœ…')} Cleanup completed - ready for fresh installation\n`); } } catch (error) { // Don't fail the entire installation if cleanup fails console.log(`${chalk.yellow('āš ļø')} Cleanup had issues, continuing with installation\n`); } } /** * Clean Git hooks - remove ShiftAI-related lines */ async function cleanGitHooks(cwd) { try { const preCommitPath = path.join(cwd, '.husky', 'pre-commit'); if (await fs.pathExists(preCommitPath)) { const content = await fs.readFile(preCommitPath, 'utf8'); // Remove ShiftAI-related lines and cleanup exit statements const cleanedContent = content .replace(/^\s*#\s*ShiftAI.*$/gm, '') .replace(/^\s*npx.*shiftai-cli.*$/gm, '') .replace(/^\s*npx.*@deriv-com\/shiftai-cli.*$/gm, '') .replace(/^\s*#\s*Preserve exit code.*$/gm, '') .replace(/^\s*exit\s+\$\?.*$/gm, '') .replace(/^\s*exit\s+\d+.*$/gm, '') .replace(/\n\s*\n\s*\n/g, '\n\n') // Remove multiple blank lines .trimEnd(); const lines = cleanedContent.split('\n'); const filteredLines = lines.filter(line => line.trim() !== ''); // Keep only essential content const meaningfulLines = filteredLines.filter(line => { const trimmed = line.trim(); return trimmed && !trimmed.startsWith('#') && !trimmed.includes('husky.sh') && !trimmed.startsWith('.'); }); if (meaningfulLines.length === 0) { // Keep only the husky setup with proper exit const huskyLines = filteredLines.filter(line => { const trimmed = line.trim(); return trimmed.includes('husky.sh') || trimmed.startsWith('.') || !trimmed; }); const finalContent = huskyLines.join('\n') + '\n\n# Preserve exit code\nexit $?'; await fs.writeFile(preCommitPath, finalContent); } else { // Add proper exit statement at the end const finalContent = filteredLines.join('\n') + '\n\n# Preserve exit code\nexit $?'; await fs.writeFile(preCommitPath, finalContent); } console.log(`${chalk.green('āœ…')} Cleaned git hooks`); } } catch (error) { // Continue if hook cleaning fails } } module.exports = installCommand;