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