ai-coding-assistants-setup
Version:
Setup tool for integrating AI coding assistants into development workflows
263 lines (227 loc) • 7.54 kB
text/typescript
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();