UNPKG

@cirrusinvicta/ai-commit-toolkit

Version:

AI-powered conventional commit generation with centralized configuration, OpenAI integration, and automated deployment workflows

363 lines (308 loc) 11 kB
const fs = require("fs"); const path = require("path"); const { execSync } = require("child_process"); class AICommitToolkitSetup { constructor(options = {}) { this.options = options; this.projectRoot = process.cwd(); this.templateDir = path.join(__dirname, "..", "templates"); } async install(projectOptions = {}) { console.log("🤖 Setting up AI Commit Toolkit..."); const config = await this.gatherConfiguration(projectOptions); try { await this.checkPrerequisites(); await this.installDependencies(config); await this.setupHusky(config); await this.setupCommitlint(config); await this.setupSemanticRelease(config); await this.copyScripts(); await this.createConfiguration(config); await this.updatePackageJson(config); // Ensure .gitignore exists and is correct const gitignorePath = path.join(this.projectRoot, ".gitignore"); // Read exclusions from ai-prompts.conf if available (after it's been copied) const aiPromptsConfPath = path.join( this.projectRoot, "configs", "ai-prompts.conf" ); let gitignoreExclusions = ["node_modules", ".env"]; if (fs.existsSync(aiPromptsConfPath)) { const conf = fs.readFileSync(aiPromptsConfPath, "utf8"); const match = conf.match(/^GITIGNORE_EXCLUDE="([^"]*)"/m); if (match?.[1]) { gitignoreExclusions = match[1].split(/\s+/).filter(Boolean); } } if (!fs.existsSync(gitignorePath)) { fs.writeFileSync(gitignorePath, gitignoreExclusions.join("\n") + "\n"); console.log( ` ✅ Created .gitignore with exclusions: ${gitignoreExclusions.join( ", " )}` ); } await this.ensureGitignore(gitignoreExclusions); console.log("✅ AI Commit Toolkit setup complete!"); console.log( "💡 Set OPENAI_API_KEY environment variable to enable AI features" ); console.log("🚀 Try: git add . && git commit"); return true; } catch (error) { console.error("❌ Setup failed:", error.message); console.error(error.stack); return false; } } async gatherConfiguration(projectOptions) { // Simple configuration for now - can be enhanced with inquirer later return { type: projectOptions.type || "generic", setupHooks: projectOptions.hooks !== false, setupSemanticRelease: projectOptions.semanticRelease !== false, silent: projectOptions.silent || false, ...projectOptions, }; } async checkPrerequisites() { console.log("🔍 Checking prerequisites..."); // First check if we're in a git repository try { execSync("git rev-parse --git-dir", { stdio: "ignore", cwd: this.projectRoot, }); console.log(" ✅ Git repository detected"); } catch { throw new Error( `❌ Not in a git repository. Please run 'git init' first or navigate to an existing git repository.` ); } const required = ["git", "jq", "curl", "bc", "node", "npm"]; for (const dep of required) { try { execSync(`which ${dep}`, { stdio: "ignore" }); console.log(` ✅ ${dep} found`); } catch { throw new Error( `Missing required dependency: ${dep}. Please install it first.` ); } } } async installDependencies(config) { console.log("📦 Installing dependencies..."); const packagePath = path.join(this.projectRoot, "package.json"); if (!fs.existsSync(packagePath)) { console.log(" ⚠️ No package.json found, creating basic one..."); await this.createBasicPackageJson(); } // Install required dependencies const deps = [ "husky@^9.0.0", "@commitlint/cli@^19.0.0", "@commitlint/config-conventional@^19.0.0", ]; if (config.setupSemanticRelease) { deps.push( "semantic-release@^24.0.0", "@semantic-release/changelog@^6.0.0", "@semantic-release/git@^10.0.0" ); } try { execSync(`npm install --save-dev ${deps.join(" ")}`, { stdio: "inherit", cwd: this.projectRoot, }); console.log(" ✅ Dependencies installed"); } catch (error) { throw new Error(`Failed to install dependencies: ${error.message}`); } } async setupHusky(config) { if (!config.setupHooks) { console.log("⏭️ Skipping Husky setup"); return; } console.log("🪝 Setting up Husky hooks..."); try { // Initialize husky (modern way for v9+) execSync("npx husky init", { stdio: "inherit", cwd: this.projectRoot }); // Copy our enhanced hooks await this.copyTemplate( "hooks/prepare-commit-msg", ".husky/prepare-commit-msg" ); await this.copyTemplate("hooks/commit-msg", ".husky/commit-msg"); // Make executable execSync("chmod +x .husky/prepare-commit-msg .husky/commit-msg", { cwd: this.projectRoot, }); console.log(" ✅ Husky hooks configured"); } catch (error) { throw new Error(`Failed to setup Husky: ${error.message}`); } } async setupCommitlint(config) { console.log("📝 Setting up commitlint..."); try { await this.copyTemplate( "configs/commitlint.config.js", "commitlint.config.js" ); console.log(" ✅ Commitlint configured"); } catch (error) { throw new Error(`Failed to setup commitlint: ${error.message}`); } } async setupSemanticRelease(config) { if (!config.setupSemanticRelease) { console.log("⏭️ Skipping semantic-release setup"); return; } console.log("🏷️ Setting up semantic-release..."); try { const templateName = `configs/.releaserc-${config.type}.json`; const templateExists = fs.existsSync( path.join(this.templateDir, templateName) ); if (templateExists) { await this.copyTemplate(templateName, ".releaserc.json"); } else { await this.copyTemplate("configs/.releaserc.json", ".releaserc.json"); } console.log(" ✅ Semantic-release configured"); } catch (error) { throw new Error(`Failed to setup semantic-release: ${error.message}`); } } async copyScripts() { console.log("📜 Installing AI commit scripts..."); try { // Create scripts directory if (!fs.existsSync(path.join(this.projectRoot, "scripts"))) { fs.mkdirSync(path.join(this.projectRoot, "scripts")); } // Create configs directory for AI prompts configuration if (!fs.existsSync(path.join(this.projectRoot, "configs"))) { fs.mkdirSync(path.join(this.projectRoot, "configs")); } const scripts = [ "ai-commit.sh", "commit-helper-ai.sh", "test-ai-setup.sh", ]; for (const script of scripts) { await this.copyTemplate(`scripts/${script}`, `scripts/${script}`); execSync(`chmod +x scripts/${script}`, { cwd: this.projectRoot }); } // Copy AI prompts configuration file await this.copyTemplate( "configs/ai-prompts.conf", "configs/ai-prompts.conf" ); console.log(" ✅ Scripts installed"); } catch (error) { throw new Error(`Failed to copy scripts: ${error.message}`); } } async createConfiguration(config) { console.log("⚙️ Creating configuration files..."); try { // Create .env if it doesn't exist const envPath = path.join(this.projectRoot, ".env"); if (!fs.existsSync(envPath)) { fs.writeFileSync(envPath, "OPENAI_API_KEY=your-openai-api-key-here\n"); console.log( " ⚠️ Created .env file - remember to add your OpenAI API key" ); } console.log(" ✅ Configuration files created"); } catch (error) { throw new Error(`Failed to create configuration: ${error.message}`); } } async updatePackageJson(config) { console.log("📄 Updating package.json..."); try { const packagePath = path.join(this.projectRoot, "package.json"); const pkg = JSON.parse(fs.readFileSync(packagePath, "utf8")); // Add our scripts pkg.scripts = pkg.scripts || {}; pkg.scripts["ai-commit"] = "ai-commit"; pkg.scripts["ai-commit-helper"] = "ai-commit-helper"; pkg.scripts["test-ai"] = "ai-commit-test"; pkg.scripts["prepare"] = "husky"; // Modern Husky v9+ syntax (not 'husky install') fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2)); console.log(" ✅ package.json updated"); } catch (error) { throw new Error(`Failed to update package.json: ${error.message}`); } } async createBasicPackageJson() { const basicPackage = { name: path.basename(this.projectRoot), version: "1.0.0", description: "", main: "index.js", scripts: { test: 'echo "Error: no test specified" && exit 1', }, keywords: [], author: "", license: "ISC", }; fs.writeFileSync( path.join(this.projectRoot, "package.json"), JSON.stringify(basicPackage, null, 2) ); } async copyTemplate(src, dest) { const srcPath = path.join(this.templateDir, src); const destPath = path.join(this.projectRoot, dest); if (fs.existsSync(srcPath)) { const destDir = path.dirname(destPath); if (!fs.existsSync(destDir)) { fs.mkdirSync(destDir, { recursive: true }); } fs.copyFileSync(srcPath, destPath); } else { console.warn(` ⚠️ Template not found: ${src}`); } } async ensureGitignore(exclusions = ["node_modules", ".env"]) { const gitignorePath = path.join(this.projectRoot, ".gitignore"); let lines = []; if (fs.existsSync(gitignorePath)) { lines = fs.readFileSync(gitignorePath, "utf8").split(/\r?\n/); } let changed = false; exclusions.forEach((entry) => { // Smart duplicate detection - check for both 'node_modules' and 'node_modules/' const normalizedEntry = entry.replace(/\/$/, ""); // Remove trailing slash for comparison const hasExactMatch = lines.some((line) => line.trim() === entry); const hasVariantMatch = lines.some((line) => { const normalizedLine = line.trim().replace(/\/$/, ""); return normalizedLine === normalizedEntry; }); if (!hasExactMatch && !hasVariantMatch) { lines.push(entry); changed = true; } }); if (changed) { // Filter out empty lines and add clean ending const cleanLines = lines.filter((line) => line.trim() !== ""); fs.writeFileSync(gitignorePath, cleanLines.join("\n") + "\n"); console.log( ` ✅ Updated .gitignore to include: ${exclusions.join(", ")}` ); } else { console.log(" ✅ .gitignore already includes all exclusions"); } } } module.exports = AICommitToolkitSetup;