UNPKG

@deriv-com/shiftai-cli

Version:

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

308 lines (262 loc) 10.3 kB
const fs = require('fs-extra'); const path = require('path'); const inquirer = require('inquirer'); const chalk = require('chalk'); const { execSync } = require('child_process'); const display = require('../utils/display'); const AIStorage = require('../core/ai-storage'); const { getGitRemoteInfo } = require('../utils/git-utils'); /** * Remove Command - Uninstalls ShiftAI hooks and configuration */ async function removeCommand(options = {}) { try { display.clearAndShowHeader('ShiftAI Removal', 'Removing ShiftAI hooks and configuration'); const cwd = process.cwd(); // Confirm removal unless force flag is used if (!options.force) { const answers = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: 'Are you sure you want to remove ShiftAI from this project?', default: false } ]); if (!answers.confirm) { console.log(`${chalk.blue('ℹ️')} Removal cancelled`); return; } } // Step 1: Remove Git Hooks process.stdout.write(`${chalk.blue('⚙️')} Removing git hooks...`); try { await removeGitHooks(cwd); process.stdout.write(`\r${chalk.green('✅')} Git hooks removed\n`); } catch (error) { process.stdout.write(`\r${chalk.red('❌')} Failed to remove git hooks\n`); throw error; } // Step 2: Remove Configuration Files process.stdout.write(`${chalk.blue('⚙️')} Removing configuration files...`); try { await removeConfigurationFiles(cwd, options.keepConfig); process.stdout.write(`\r${chalk.green('✅')} Configuration files removed\n`); } catch (error) { process.stdout.write(`\r${chalk.red('❌')} Failed to remove configuration files\n`); throw error; } // Step 3: Uninstall ShiftAI package process.stdout.write(`${chalk.blue('⚙️')} Uninstalling shiftai-cli package...`); try { await uninstallShiftAIPackage(cwd); process.stdout.write(`\r${chalk.green('✅')} ShiftAI package uninstalled\n`); } catch (error) { process.stdout.write(`\r${chalk.yellow('⚠️')} Could not uninstall package (may need manual removal)\n`); // Don't throw - this is not critical } // Step 4: Remove Dependencies (optional) if (!options.keepDeps) { process.stdout.write(`${chalk.blue('⚙️')} Removing dependencies...`); try { await removeDependencies(cwd, options.force); process.stdout.write(`\r${chalk.green('✅')} Dependencies removed\n`); } catch (error) { process.stdout.write(`\r${chalk.yellow('⚠️')} Could not remove dependencies\n`); // Don't throw - this is not critical } } // Completion console.log(`${chalk.green('✅')} ShiftAI removed successfully from your project!`); if (options.keepConfig) { console.log(`${chalk.blue('ℹ️')} Configuration files were kept as requested`); } } catch (error) { console.log(display.error('Removal failed', error.message)); process.exit(1); } } /** * Remove git hooks */ async function removeGitHooks(cwd) { try { 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'); // Helper function to clean ShiftAI content from a hook file const cleanHookFile = async (hookPath, hookType) => { if (await fs.pathExists(hookPath)) { const hookContent = await fs.readFile(hookPath, 'utf8'); if (hookContent.includes(`shiftai-cli hook ${hookType}`)) { // Remove only ShiftAI lines from the hook const lines = hookContent.split('\n'); const filteredLines = lines.filter(line => !line.includes(`shiftai-cli hook ${hookType}`) && !line.includes(`# ShiftAI ${hookType.charAt(0).toUpperCase() + hookType.slice(1).replace('-', '-c')} Hook`) && !line.includes('# Note: This runs in background') ); // If only standard husky content remains, keep the file const cleanedContent = filteredLines.join('\n').trim(); if (cleanedContent && !cleanedContent.match(/^#!/)) { // Add shebang if missing const finalContent = '#!/bin/sh\n' + cleanedContent; await fs.writeFile(hookPath, finalContent); } else if (cleanedContent) { await fs.writeFile(hookPath, cleanedContent); } else { // If no meaningful content left, remove the file await fs.remove(hookPath); } } } }; // Clean all hooks await cleanHookFile(preCommitPath, 'pre-commit'); await cleanHookFile(postCommitPath, 'post-commit'); await cleanHookFile(prePushPath, 'pre-push'); // Check if .husky directory is now empty (except for _) if (await fs.pathExists(huskyDir)) { const huskyContents = await fs.readdir(huskyDir); const importantFiles = huskyContents.filter(file => file !== '_' && file !== '.gitignore'); if (importantFiles.length === 0) { // Only remove if empty or only contains husky internals const underscoreDir = path.join(huskyDir, '_'); if (await fs.pathExists(underscoreDir)) { const underscoreContents = await fs.readdir(underscoreDir); if (underscoreContents.every(file => file.startsWith('.') || file === 'husky.sh')) { // Safe to remove entire .husky directory await fs.remove(huskyDir); } } } } } catch (error) { throw error; } } /** * Remove configuration files */ async function removeConfigurationFiles(cwd, keepConfig = false) { try { // Remove local storage config file if (!keepConfig) { const localConfigPath = AIStorage.getConfigPath(); if (await fs.pathExists(localConfigPath)) { await fs.remove(localConfigPath); } } // Remove project-specific AI storage files try { const { organization, repository } = await getGitRemoteInfo(); if (organization && repository) { const aiStorage = new AIStorage({ organization, repository, projectRoot: process.cwd() }); const projectStorageDir = path.join(aiStorage.baseDir, organization, repository); if (await fs.pathExists(projectStorageDir)) { await fs.remove(projectStorageDir); } } } catch (error) { // Ignore git remote errors - not critical for removal } // Remove project files const filesToRemove = ['shiftai-data.json']; // Always remove the generated data file // Note: .env.example no longer created during installation, so no need to remove it for (const file of filesToRemove) { const filePath = path.join(cwd, file); if (await fs.pathExists(filePath)) { await fs.remove(filePath); } } // Clean up .gitignore const gitignorePath = path.join(cwd, '.gitignore'); if (await fs.pathExists(gitignorePath)) { const gitignoreContent = await fs.readFile(gitignorePath, 'utf8'); const lines = gitignoreContent.split('\n'); // Remove ShiftAI related lines const cleanedLines = lines.filter(line => !line.includes('ShiftAI') && line.trim() !== '.env' || lines.some(l => l.includes('.env') && !l.includes('ShiftAI')) ); if (cleanedLines.length !== lines.length) { await fs.writeFile(gitignorePath, cleanedLines.join('\n')); spinner.text = 'Cleaned .gitignore...'; } } } catch (error) { throw error; } } /** * Uninstall ShiftAI package using npm */ async function uninstallShiftAIPackage(cwd) { try { const packageJsonPath = path.join(cwd, 'package.json'); if (!(await fs.pathExists(packageJsonPath))) { return; // No package.json, nothing to uninstall } // Check if shiftai-cli is actually installed const packageJson = await fs.readJson(packageJsonPath); const hasShiftAI = (packageJson.devDependencies && packageJson.devDependencies['shiftai-cli']) || (packageJson.dependencies && packageJson.dependencies['shiftai-cli']); if (hasShiftAI) { execSync('npm uninstall shiftai-cli', { cwd, stdio: 'pipe' }); } // Also remove prepare script if it's just the husky one and no other hooks exist if (packageJson.scripts && packageJson.scripts.prepare === 'husky install') { const huskyDir = path.join(cwd, '.husky'); if (!(await fs.pathExists(huskyDir))) { delete packageJson.scripts.prepare; if (Object.keys(packageJson.scripts).length === 0) { delete packageJson.scripts; } await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } } } catch (error) { throw error; } } /** * Remove dependencies (with user confirmation) */ async function removeDependencies(cwd, force = false) { try { const packageJsonPath = path.join(cwd, 'package.json'); if (!(await fs.pathExists(packageJsonPath))) { return; } const packageJson = await fs.readJson(packageJsonPath); const hasHusky = (packageJson.devDependencies && packageJson.devDependencies.husky) || (packageJson.dependencies && packageJson.dependencies.husky); if (hasHusky) { let removeDeps = force; if (!force) { const answers = await inquirer.prompt([ { type: 'confirm', name: 'removeDeps', message: 'Remove husky dependency? (This may affect other git hooks)', default: false } ]); removeDeps = answers.removeDeps; } if (removeDeps) { try { const { execSync } = require('child_process'); execSync('npm uninstall husky', { cwd, stdio: 'pipe' }); } catch (error) { throw new Error('Failed to remove husky dependency - you may need to run: npm uninstall husky'); } } } } catch (error) { throw error; } } module.exports = removeCommand;