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