@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
JavaScript
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;