UNPKG

ai-coding-assistants-setup

Version:

Setup tool for integrating AI coding assistants into development workflows

263 lines (227 loc) 7.54 kB
#!/usr/bin/env node import { Command } from 'commander'; import fs from 'fs-extra'; import path from 'path'; import { Feedback } from './utils/feedback.js'; import { TemplateScanner } from './utils/template-scanner.js'; import { updatePackageScripts, getExistingScripts, } from './utils/package-scripts.js'; // Initialize the command line parser const program = new Command(); // Set up program metadata program .name('ai-coding-assistants-setup') .description( 'Setup tool for integrating AI coding assistants into development workflows' ) .version('2.0.0'); // Default command handler program .option('-n, --non-interactive', 'Run in non-interactive mode') .option('-f, --force', 'Force overwrite of existing files') .option('-v, --verbose', 'Enable verbose output') .option('-d, --dry-run', 'Preview what would be done without making changes') .action(async options => { const interactive = !options.nonInteractive; const verbose = options.verbose; const dryRun = options.dryRun; const force = options.force; try { await setupAiCodingAssistants(interactive, verbose, dryRun, force); } catch (error) { Feedback.error( `Setup failed: ${error instanceof Error ? error.message : String(error)}` ); process.exit(1); } }); /** * Main setup function - simplified to only copy files and update package.json */ async function setupAiCodingAssistants( interactive: boolean = true, verbose: boolean = false, dryRun: boolean = false, force: boolean = false ): Promise<void> { // Welcome message Feedback.section('AI Coding Assistants Setup'); if (dryRun) { Feedback.warning('DRY RUN MODE - No files will be modified'); } Feedback.info( 'Setting up AI coding assistants configuration in your project...' ); // Get the current working directory const cwd = process.cwd(); if (verbose) { Feedback.info(`Working directory: ${cwd}`); } // Get the package directory (where our templates are) // This file will be at dist/index.js, so we need to go up one level from dist const distDir = path.dirname(new URL(import.meta.url).pathname); const packageDir = path.dirname(distDir); const templateDir = path.join(packageDir, 'src', 'templates'); if (verbose) { Feedback.info(`Template directory: ${templateDir}`); } // Check if templates directory exists if (!fs.existsSync(templateDir)) { throw new Error(`Template directory not found: ${templateDir}`); } // Get files to copy Feedback.info('Scanning for template files...'); const filesToCopy = await getFilesToCopy(templateDir, cwd, verbose); if (filesToCopy.size === 0) { Feedback.warning('No files to copy. Setup complete.'); return; } // In dry-run mode, just show what would be done if (dryRun) { Feedback.section('Files that would be copied:'); let fileCount = 0; for (const [_source, target] of filesToCopy) { const relativePath = path.relative(cwd, target); const exists = await fs.pathExists(target); if (exists) { Feedback.info(` [UPDATE] ${relativePath}`); } else { Feedback.info(` [CREATE] ${relativePath}`); } fileCount++; } Feedback.info(`\nTotal files: ${fileCount}`); // Show what scripts would be added Feedback.info('\nPackage.json scripts that would be added:'); const existingScripts = await getExistingScripts(cwd); const requiredScripts = [ 'lint', 'typecheck', 'format:check', 'build', 'test', 'test:unit', 'test:integration', 'test:e2e', ]; const missingScripts = requiredScripts.filter( script => !existingScripts || !existingScripts[script] ); if (missingScripts.length > 0) { Feedback.info(` ${missingScripts.join(', ')}`); } else { Feedback.info(' All required scripts already exist'); } Feedback.success('\nDry run completed - no files were modified'); return; } // Copy files Feedback.section('Copying configuration files...'); let copiedCount = 0; let skippedCount = 0; for (const [source, target] of filesToCopy) { const relativePath = path.relative(cwd, target); const exists = await fs.pathExists(target); if (exists && !force) { // Check if files are identical const sourceContent = await fs.readFile(source, 'utf-8'); const targetContent = await fs.readFile(target, 'utf-8'); if (sourceContent === targetContent) { // Files are identical, skip silently if (verbose) { Feedback.info(` [SKIP] ${relativePath} (identical)`); } skippedCount++; continue; } if (interactive) { const { default: inquirer } = await import('inquirer'); const { overwrite } = await inquirer.prompt([ { type: 'confirm', name: 'overwrite', message: `File ${relativePath} already exists. Overwrite?`, default: false, }, ]); if (!overwrite) { if (verbose) { Feedback.info(` [SKIP] ${relativePath}`); } skippedCount++; continue; } } else { // In non-interactive mode, skip existing files if (verbose) { Feedback.info(` [SKIP] ${relativePath} (already exists)`); } skippedCount++; continue; } } // Copy the file try { await fs.ensureDir(path.dirname(target)); await fs.copyFile(source, target); Feedback.success(` [COPIED] ${relativePath}`); copiedCount++; } catch (error) { Feedback.error(` [ERROR] Failed to copy ${relativePath}: ${error}`); } } // Update package.json scripts Feedback.section('Updating package.json scripts...'); await updatePackageScripts(cwd); // Final summary Feedback.section('Setup Summary'); Feedback.info(`Files copied: ${copiedCount}`); if (skippedCount > 0) { Feedback.info(`Files skipped: ${skippedCount}`); } Feedback.success('\nSetup completed successfully!'); // Show next steps Feedback.section('Next Steps'); Feedback.info('1. Review the copied configuration files'); Feedback.info('2. Install any needed dependencies:'); Feedback.info( ' npm install --save-dev eslint prettier husky @commitlint/cli @commitlint/config-conventional' ); Feedback.info( '3. Update the placeholder npm scripts in package.json with your actual commands' ); Feedback.info( '4. Configure your API keys in .env and .claude/settings.local.json' ); } /** * Get a map of files to copy from template directory to target directory */ async function getFilesToCopy( templateDir: string, targetDir: string, verbose: boolean = false ): Promise<Map<string, string>> { try { const scanner = new TemplateScanner(templateDir, targetDir, verbose); const templates = await scanner.scan(); if (templates.length === 0) { return new Map<string, string>(); } // Get the map of source to target paths const filesToCopy = scanner.getTemplateMap(); if (verbose) { Feedback.info(`Found ${filesToCopy.size} files to copy`); } return filesToCopy; } catch (error) { Feedback.error( `Error scanning template files: ${error instanceof Error ? error.message : String(error)}` ); return new Map<string, string>(); } } // Parse command line arguments and run the program program.parse();