// this file contains code to create commits in a consistent way import * as plugins from './mod.plugins.js'; import * as paths from '../paths.js'; import { logger } from '../gitzone.logging.js'; import * as helpers from './mod.helpers.js'; import * as ui from './mod.ui.js'; import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js'; export const run = async (argvArg: any) => { // Check if release flag is set and validate registries early const wantsRelease = !!(argvArg.r || argvArg.release); let releaseConfig: ReleaseConfig | null = null; if (wantsRelease) { releaseConfig = await ReleaseConfig.fromCwd(); if (!releaseConfig.hasRegistries()) { logger.log('error', 'No release registries configured.'); console.log(''); console.log(' Run `gitzone config add ` to add registries.'); console.log(''); process.exit(1); } } if (argvArg.format) { const formatMod = await import('../mod_format/index.js'); await formatMod.run(); } ui.printHeader('🔍 Analyzing repository changes...'); const aidoc = new plugins.tsdoc.AiDoc(); await aidoc.start(); const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd); await aidoc.stop(); ui.printRecommendation({ recommendedNextVersion: nextCommitObject.recommendedNextVersion, recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel, recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope, recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage, }); let answerBucket: plugins.smartinteract.AnswerBucket; // Check if -y/--yes flag is set AND version is not a breaking change // Breaking changes (major version bumps) always require manual confirmation const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === 'BREAKING CHANGE'; const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange; if (canAutoAccept) { // Auto-mode: create AnswerBucket programmatically logger.log('info', '✓ Auto-accepting AI recommendations (--yes flag)'); answerBucket = new plugins.smartinteract.AnswerBucket(); answerBucket.addAnswer({ name: 'commitType', value: nextCommitObject.recommendedNextVersionLevel, }); answerBucket.addAnswer({ name: 'commitScope', value: nextCommitObject.recommendedNextVersionScope, }); answerBucket.addAnswer({ name: 'commitDescription', value: nextCommitObject.recommendedNextVersionMessage, }); answerBucket.addAnswer({ name: 'pushToOrigin', value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided }); answerBucket.addAnswer({ name: 'createRelease', value: wantsRelease, }); } else { // Warn if --yes was provided but we're requiring confirmation due to breaking change if (isBreakingChange && (argvArg.y || argvArg.yes)) { logger.log('warn', '⚠️ BREAKING CHANGE detected - manual confirmation required'); } // Interactive mode: prompt user for input const commitInteract = new plugins.smartinteract.SmartInteract(); commitInteract.addQuestions([ { type: 'list', name: `commitType`, message: `Choose TYPE of the commit:`, choices: [`fix`, `feat`, `BREAKING CHANGE`], default: nextCommitObject.recommendedNextVersionLevel, }, { type: 'input', name: `commitScope`, message: `What is the SCOPE of the commit:`, default: nextCommitObject.recommendedNextVersionScope, }, { type: `input`, name: `commitDescription`, message: `What is the DESCRIPTION of the commit?`, default: nextCommitObject.recommendedNextVersionMessage, }, { type: 'confirm', name: `pushToOrigin`, message: `Do you want to push this version now?`, default: true, }, { type: 'confirm', name: `createRelease`, message: `Do you want to publish to npm registries?`, default: wantsRelease, }, ]); answerBucket = await commitInteract.runQueue(); } const commitString = createCommitStringFromAnswerBucket(answerBucket); const commitVersionType = (() => { switch (answerBucket.getAnswerFor('commitType')) { case 'fix': return 'patch'; case 'feat': return 'minor'; case 'BREAKING CHANGE': return 'major'; } })(); ui.printHeader('✨ Creating Semantic Commit'); ui.printCommitMessage(commitString); const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', sourceFilePaths: [], }); // Load release config if user wants to release (interactively selected) if (answerBucket.getAnswerFor('createRelease') && !releaseConfig) { releaseConfig = await ReleaseConfig.fromCwd(); if (!releaseConfig.hasRegistries()) { logger.log('error', 'No release registries configured.'); console.log(''); console.log(' Run `gitzone config add ` to add registries.'); console.log(''); process.exit(1); } } // Determine total steps based on options const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'); const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries(); let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version if (willPush) totalSteps++; if (willRelease) totalSteps++; let currentStep = 0; // Step 1: Baking commitinfo currentStep++; ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'in-progress'); const commitInfo = new plugins.commitinfo.CommitInfo( paths.cwd, commitVersionType, ); await commitInfo.writeIntoPotentialDirs(); ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'done'); // Step 2: Writing changelog currentStep++; ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'in-progress'); let changelog = nextCommitObject.changelog; changelog = changelog.replaceAll( '{{nextVersion}}', (await commitInfo.getNextPlannedVersion()).versionString, ); changelog = changelog.replaceAll( '{{nextVersionScope}}', `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`, ); changelog = changelog.replaceAll( '{{nextVersionMessage}}', nextCommitObject.recommendedNextVersionMessage, ); if (nextCommitObject.recommendedNextVersionDetails?.length > 0) { changelog = changelog.replaceAll( '{{nextVersionDetails}}', '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '), ); } else { changelog = changelog.replaceAll('\n{{nextVersionDetails}}', ''); } await plugins.smartfs .file(plugins.path.join(paths.cwd, `changelog.md`)) .encoding('utf8') .write(changelog); ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done'); // Step 3: Staging files currentStep++; ui.printStep(currentStep, totalSteps, '📦 Staging files', 'in-progress'); await smartshellInstance.exec(`git add -A`); ui.printStep(currentStep, totalSteps, '📦 Staging files', 'done'); // Step 4: Creating commit currentStep++; ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'in-progress'); await smartshellInstance.exec(`git commit -m "${commitString}"`); ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'done'); // Step 5: Bumping version currentStep++; const projectType = await helpers.detectProjectType(); const newVersion = await helpers.bumpProjectVersion(projectType, commitVersionType, currentStep, totalSteps); // Step 6: Push to remote (optional) const currentBranch = await helpers.detectCurrentBranch(); if (willPush) { currentStep++; ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress'); await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`); ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done'); } // Step 7: Publish to npm registries (optional) let releasedRegistries: string[] = []; if (willRelease && releaseConfig) { currentStep++; const registries = releaseConfig.getRegistries(); ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'in-progress'); const accessLevel = releaseConfig.getAccessLevel(); for (const registry of registries) { try { await smartshellInstance.exec(`npm publish --registry=${registry} --access=${accessLevel}`); releasedRegistries.push(registry); } catch (error) { logger.log('error', `Failed to publish to ${registry}: ${error}`); } } if (releasedRegistries.length === registries.length) { ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'done'); } else { ui.printStep(currentStep, totalSteps, `📦 Publishing to ${registries.length} registr${registries.length === 1 ? 'y' : 'ies'}`, 'error'); } } console.log(''); // Add spacing before summary // Get commit SHA for summary const commitShaResult = await smartshellInstance.exec('git rev-parse --short HEAD'); const commitSha = commitShaResult.stdout.trim(); // Print final summary ui.printSummary({ projectType, branch: currentBranch, commitType: answerBucket.getAnswerFor('commitType'), commitScope: answerBucket.getAnswerFor('commitScope'), commitMessage: answerBucket.getAnswerFor('commitDescription'), newVersion: newVersion, commitSha: commitSha, pushed: willPush, released: releasedRegistries.length > 0, releasedRegistries: releasedRegistries.length > 0 ? releasedRegistries : undefined, }); }; const createCommitStringFromAnswerBucket = ( answerBucket: plugins.smartinteract.AnswerBucket, ) => { const commitType = answerBucket.getAnswerFor('commitType'); const commitScope = answerBucket.getAnswerFor('commitScope'); const commitDescription = answerBucket.getAnswerFor('commitDescription'); return `${commitType}(${commitScope}): ${commitDescription}`; };