feat(mod_commit): Add CLI UI helpers and improve commit workflow with progress, recommendations and summary
This commit is contained in:
		| @@ -1,5 +1,13 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## 2025-10-23 - 1.19.0 - feat(mod_commit) | ||||||
|  | Add CLI UI helpers and improve commit workflow with progress, recommendations and summary | ||||||
|  |  | ||||||
|  | - Introduce ts/mod_commit/mod.ui.ts: reusable CLI UI helpers (pretty headers, sections, AI recommendation box, step printer, commit summary and helpers for consistent messaging). | ||||||
|  | - Refactor ts/mod_commit/index.ts: use new UI functions to display AI recommendations, show step-by-step progress for baking commit info, generating changelog, staging, committing, bumping version and optional push; include commit SHA in final summary. | ||||||
|  | - Enhance ts/mod_commit/mod.helpers.ts: bumpProjectVersion now accepts currentStep/totalSteps to report progress and returns a consistent newVersion after handling npm/deno/both cases. | ||||||
|  | - Add .claude/settings.local.json: local permissions configuration for development tooling. | ||||||
|  |  | ||||||
| ## 2025-10-23 - 1.18.9 - fix(mod_commit) | ## 2025-10-23 - 1.18.9 - fix(mod_commit) | ||||||
| Stage and commit deno.json when bumping/syncing versions and create/update git tags | Stage and commit deno.json when bumping/syncing versions and create/update git tags | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,6 @@ | |||||||
|  */ |  */ | ||||||
| export const commitinfo = { | export const commitinfo = { | ||||||
|   name: '@git.zone/cli', |   name: '@git.zone/cli', | ||||||
|   version: '1.18.9', |   version: '1.19.0', | ||||||
|   description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' |   description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ import * as plugins from './mod.plugins.js'; | |||||||
| import * as paths from '../paths.js'; | import * as paths from '../paths.js'; | ||||||
| import { logger } from '../gitzone.logging.js'; | import { logger } from '../gitzone.logging.js'; | ||||||
| import * as helpers from './mod.helpers.js'; | import * as helpers from './mod.helpers.js'; | ||||||
|  | import * as ui from './mod.ui.js'; | ||||||
|  |  | ||||||
| export const run = async (argvArg: any) => { | export const run = async (argvArg: any) => { | ||||||
|   if (argvArg.format) { |   if (argvArg.format) { | ||||||
| @@ -11,7 +12,8 @@ export const run = async (argvArg: any) => { | |||||||
|     await formatMod.run(); |     await formatMod.run(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   logger.log('info', `gathering facts...`); |   ui.printHeader('🔍 Analyzing repository changes...'); | ||||||
|  |  | ||||||
|   const aidoc = new plugins.tsdoc.AiDoc(); |   const aidoc = new plugins.tsdoc.AiDoc(); | ||||||
|   await aidoc.start(); |   await aidoc.start(); | ||||||
|  |  | ||||||
| @@ -19,16 +21,12 @@ export const run = async (argvArg: any) => { | |||||||
|  |  | ||||||
|   await aidoc.stop(); |   await aidoc.stop(); | ||||||
|  |  | ||||||
|   logger.log( |   ui.printRecommendation({ | ||||||
|     'info', |     recommendedNextVersion: nextCommitObject.recommendedNextVersion, | ||||||
|     `--------- |     recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel, | ||||||
|     Next recommended commit would be: |     recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope, | ||||||
|     =========== |     recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage, | ||||||
|     -> ${nextCommitObject.recommendedNextVersion}: |   }); | ||||||
|     -> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage} |  | ||||||
|     =========== |  | ||||||
|   `, |  | ||||||
|   ); |  | ||||||
|   const commitInteract = new plugins.smartinteract.SmartInteract(); |   const commitInteract = new plugins.smartinteract.SmartInteract(); | ||||||
|   commitInteract.addQuestions([ |   commitInteract.addQuestions([ | ||||||
|     { |     { | ||||||
| @@ -70,20 +68,30 @@ export const run = async (argvArg: any) => { | |||||||
|     } |     } | ||||||
|   })(); |   })(); | ||||||
|  |  | ||||||
|   logger.log('info', `OK! Creating commit with message '${commitString}'`); |   ui.printHeader('✨ Creating Semantic Commit'); | ||||||
|  |   ui.printCommitMessage(commitString); | ||||||
|   const smartshellInstance = new plugins.smartshell.Smartshell({ |   const smartshellInstance = new plugins.smartshell.Smartshell({ | ||||||
|     executor: 'bash', |     executor: 'bash', | ||||||
|     sourceFilePaths: [], |     sourceFilePaths: [], | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   logger.log('info', `Baking commitinfo into code ...`); |   // Determine total steps (6 if pushing, 5 if not) | ||||||
|  |   const totalSteps = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true') ? 6 : 5; | ||||||
|  |   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( |   const commitInfo = new plugins.commitinfo.CommitInfo( | ||||||
|     paths.cwd, |     paths.cwd, | ||||||
|     commitVersionType, |     commitVersionType, | ||||||
|   ); |   ); | ||||||
|   await commitInfo.writeIntoPotentialDirs(); |   await commitInfo.writeIntoPotentialDirs(); | ||||||
|  |   ui.printStep(currentStep, totalSteps, '🔧 Baking commit info into code', 'done'); | ||||||
|  |  | ||||||
|   logger.log('info', `Writing changelog.md ...`); |   // Step 2: Writing changelog | ||||||
|  |   currentStep++; | ||||||
|  |   ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'in-progress'); | ||||||
|   let changelog = nextCommitObject.changelog; |   let changelog = nextCommitObject.changelog; | ||||||
|   changelog = changelog.replaceAll( |   changelog = changelog.replaceAll( | ||||||
|     '{{nextVersion}}', |     '{{nextVersion}}', | ||||||
| @@ -110,23 +118,54 @@ export const run = async (argvArg: any) => { | |||||||
|     changelog, |     changelog, | ||||||
|     plugins.path.join(paths.cwd, `changelog.md`), |     plugins.path.join(paths.cwd, `changelog.md`), | ||||||
|   ); |   ); | ||||||
|  |   ui.printStep(currentStep, totalSteps, '📄 Generating changelog.md', 'done'); | ||||||
|  |  | ||||||
|   logger.log('info', `Staging files for commit:`); |   // Step 3: Staging files | ||||||
|  |   currentStep++; | ||||||
|  |   ui.printStep(currentStep, totalSteps, '📦 Staging files', 'in-progress'); | ||||||
|   await smartshellInstance.exec(`git add -A`); |   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}"`); |   await smartshellInstance.exec(`git commit -m "${commitString}"`); | ||||||
|  |   ui.printStep(currentStep, totalSteps, '💾 Creating git commit', 'done'); | ||||||
|  |  | ||||||
|   // Detect project type and bump version accordingly |   // Step 5: Bumping version | ||||||
|  |   currentStep++; | ||||||
|   const projectType = await helpers.detectProjectType(); |   const projectType = await helpers.detectProjectType(); | ||||||
|   await helpers.bumpProjectVersion(projectType, commitVersionType); |   const newVersion = await helpers.bumpProjectVersion(projectType, commitVersionType, currentStep, totalSteps); | ||||||
|  |  | ||||||
|  |   // Step 6: Push to remote (optional) | ||||||
|  |   const currentBranch = await helpers.detectCurrentBranch(); | ||||||
|   if ( |   if ( | ||||||
|     answerBucket.getAnswerFor('pushToOrigin') && |     answerBucket.getAnswerFor('pushToOrigin') && | ||||||
|     !(process.env.CI === 'true') |     !(process.env.CI === 'true') | ||||||
|   ) { |   ) { | ||||||
|     // Detect current branch instead of hardcoding "master" |     currentStep++; | ||||||
|     const currentBranch = await helpers.detectCurrentBranch(); |     ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'in-progress'); | ||||||
|     await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`); |     await smartshellInstance.exec(`git push origin ${currentBranch} --follow-tags`); | ||||||
|  |     ui.printStep(currentStep, totalSteps, `🚀 Pushing to origin/${currentBranch}`, 'done'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   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: answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'), | ||||||
|  |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const createCommitStringFromAnswerBucket = ( | const createCommitStringFromAnswerBucket = ( | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as plugins from './mod.plugins.js'; | import * as plugins from './mod.plugins.js'; | ||||||
| import * as paths from '../paths.js'; | import * as paths from '../paths.js'; | ||||||
| import { logger } from '../gitzone.logging.js'; | import { logger } from '../gitzone.logging.js'; | ||||||
|  | import * as ui from './mod.ui.js'; | ||||||
|  |  | ||||||
| export type ProjectType = 'npm' | 'deno' | 'both' | 'none'; | export type ProjectType = 'npm' | 'deno' | 'both' | 'none'; | ||||||
| export type VersionType = 'patch' | 'minor' | 'major'; | export type VersionType = 'patch' | 'minor' | 'major'; | ||||||
| @@ -204,25 +205,40 @@ async function syncVersionToDenoJson(version: string): Promise<void> { | |||||||
|  * Bumps the project version based on project type |  * Bumps the project version based on project type | ||||||
|  * @param projectType The detected project type |  * @param projectType The detected project type | ||||||
|  * @param versionType The type of version bump |  * @param versionType The type of version bump | ||||||
|  |  * @param currentStep The current step number for progress display | ||||||
|  |  * @param totalSteps The total number of steps for progress display | ||||||
|  * @returns The new version string |  * @returns The new version string | ||||||
|  */ |  */ | ||||||
| export async function bumpProjectVersion( | export async function bumpProjectVersion( | ||||||
|   projectType: ProjectType, |   projectType: ProjectType, | ||||||
|   versionType: VersionType |   versionType: VersionType, | ||||||
|  |   currentStep?: number, | ||||||
|  |   totalSteps?: number | ||||||
| ): Promise<string> { | ): Promise<string> { | ||||||
|  |   const projectEmoji = projectType === 'npm' ? '📦' : projectType === 'deno' ? '🦕' : '🔀'; | ||||||
|  |   const description = `🏷️  Bumping version (${projectEmoji} ${projectType})`; | ||||||
|  |  | ||||||
|  |   if (currentStep && totalSteps) { | ||||||
|  |     ui.printStep(currentStep, totalSteps, description, 'in-progress'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let newVersion: string; | ||||||
|  |  | ||||||
|   switch (projectType) { |   switch (projectType) { | ||||||
|     case 'npm': |     case 'npm': | ||||||
|       return await bumpNpmVersion(versionType); |       newVersion = await bumpNpmVersion(versionType); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|     case 'deno': |     case 'deno': | ||||||
|       return await bumpDenoVersion(versionType); |       newVersion = await bumpDenoVersion(versionType); | ||||||
|  |       break; | ||||||
|  |  | ||||||
|     case 'both': { |     case 'both': { | ||||||
|       // Bump npm version first (it handles git tags) |       // Bump npm version first (it handles git tags) | ||||||
|       const newVersion = await bumpNpmVersion(versionType); |       newVersion = await bumpNpmVersion(versionType); | ||||||
|       // Then sync to deno.json |       // Then sync to deno.json | ||||||
|       await syncVersionToDenoJson(newVersion); |       await syncVersionToDenoJson(newVersion); | ||||||
|       return newVersion; |       break; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     case 'none': |     case 'none': | ||||||
| @@ -231,4 +247,10 @@ export async function bumpProjectVersion( | |||||||
|     default: |     default: | ||||||
|       throw new Error(`Unknown project type: ${projectType}`); |       throw new Error(`Unknown project type: ${projectType}`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   if (currentStep && totalSteps) { | ||||||
|  |     ui.printStep(currentStep, totalSteps, description, 'done'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return newVersion; | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										196
									
								
								ts/mod_commit/mod.ui.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								ts/mod_commit/mod.ui.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | |||||||
|  | import { logger } from '../gitzone.logging.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * UI helper module for beautiful CLI output | ||||||
|  |  */ | ||||||
|  |  | ||||||
|  | interface ICommitSummary { | ||||||
|  |   projectType: string; | ||||||
|  |   branch: string; | ||||||
|  |   commitType: string; | ||||||
|  |   commitScope: string; | ||||||
|  |   commitMessage: string; | ||||||
|  |   newVersion: string; | ||||||
|  |   commitSha?: string; | ||||||
|  |   pushed: boolean; | ||||||
|  |   repoUrl?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | interface IRecommendation { | ||||||
|  |   recommendedNextVersion: string; | ||||||
|  |   recommendedNextVersionLevel: string; | ||||||
|  |   recommendedNextVersionScope: string; | ||||||
|  |   recommendedNextVersionMessage: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print a header with a box around it | ||||||
|  |  */ | ||||||
|  | export function printHeader(title: string): void { | ||||||
|  |   const width = 57; | ||||||
|  |   const padding = Math.max(0, width - title.length - 2); | ||||||
|  |   const leftPad = Math.floor(padding / 2); | ||||||
|  |   const rightPad = padding - leftPad; | ||||||
|  |  | ||||||
|  |   console.log(''); | ||||||
|  |   console.log('╭─' + '─'.repeat(width) + '─╮'); | ||||||
|  |   console.log('│  ' + title + ' '.repeat(rightPad + leftPad) + '  │'); | ||||||
|  |   console.log('╰─' + '─'.repeat(width) + '─╯'); | ||||||
|  |   console.log(''); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print a section with a border | ||||||
|  |  */ | ||||||
|  | export function printSection(title: string, lines: string[]): void { | ||||||
|  |   const width = 59; | ||||||
|  |  | ||||||
|  |   console.log('┌─ ' + title + ' ' + '─'.repeat(Math.max(0, width - title.length - 3)) + '┐'); | ||||||
|  |   console.log('│' + ' '.repeat(width) + '│'); | ||||||
|  |  | ||||||
|  |   for (const line of lines) { | ||||||
|  |     const padding = width - line.length; | ||||||
|  |     console.log('│  ' + line + ' '.repeat(Math.max(0, padding - 2)) + '│'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   console.log('│' + ' '.repeat(width) + '│'); | ||||||
|  |   console.log('└─' + '─'.repeat(width) + '─┘'); | ||||||
|  |   console.log(''); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print AI recommendations in a nice box | ||||||
|  |  */ | ||||||
|  | export function printRecommendation(recommendation: IRecommendation): void { | ||||||
|  |   const lines = [ | ||||||
|  |     `Suggested Version:  v${recommendation.recommendedNextVersion}`, | ||||||
|  |     `Suggested Type:     ${recommendation.recommendedNextVersionLevel}`, | ||||||
|  |     `Suggested Scope:    ${recommendation.recommendedNextVersionScope}`, | ||||||
|  |     `Suggested Message:  ${recommendation.recommendedNextVersionMessage}`, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   printSection('📊 AI Recommendations', lines); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print a progress step | ||||||
|  |  */ | ||||||
|  | export function printStep( | ||||||
|  |   current: number, | ||||||
|  |   total: number, | ||||||
|  |   description: string, | ||||||
|  |   status: 'in-progress' | 'done' | 'error' | ||||||
|  | ): void { | ||||||
|  |   const statusIcon = status === 'done' ? '✓' : status === 'error' ? '✗' : '⏳'; | ||||||
|  |   const dots = '.'.repeat(Math.max(0, 40 - description.length)); | ||||||
|  |  | ||||||
|  |   console.log(`  [${current}/${total}] ${description}${dots} ${statusIcon}`); | ||||||
|  |  | ||||||
|  |   // Clear the line on next update if in progress | ||||||
|  |   if (status === 'in-progress') { | ||||||
|  |     process.stdout.write('\x1b[1A'); // Move cursor up one line | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get emoji for project type | ||||||
|  |  */ | ||||||
|  | function getProjectTypeEmoji(projectType: string): string { | ||||||
|  |   switch (projectType) { | ||||||
|  |     case 'npm': | ||||||
|  |       return '📦 npm'; | ||||||
|  |     case 'deno': | ||||||
|  |       return '🦕 Deno'; | ||||||
|  |     case 'both': | ||||||
|  |       return '🔀 npm + Deno'; | ||||||
|  |     default: | ||||||
|  |       return '❓ Unknown'; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get emoji for commit type | ||||||
|  |  */ | ||||||
|  | function getCommitTypeEmoji(commitType: string): string { | ||||||
|  |   switch (commitType) { | ||||||
|  |     case 'fix': | ||||||
|  |       return '🔧 fix'; | ||||||
|  |     case 'feat': | ||||||
|  |       return '✨ feat'; | ||||||
|  |     case 'BREAKING CHANGE': | ||||||
|  |       return '💥 BREAKING CHANGE'; | ||||||
|  |     default: | ||||||
|  |       return commitType; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print final commit summary | ||||||
|  |  */ | ||||||
|  | export function printSummary(summary: ICommitSummary): void { | ||||||
|  |   const lines = [ | ||||||
|  |     `Project Type:   ${getProjectTypeEmoji(summary.projectType)}`, | ||||||
|  |     `Branch:         🌿 ${summary.branch}`, | ||||||
|  |     `Commit Type:    ${getCommitTypeEmoji(summary.commitType)}`, | ||||||
|  |     `Scope:          📍 ${summary.commitScope}`, | ||||||
|  |     `New Version:    🏷️  v${summary.newVersion}`, | ||||||
|  |   ]; | ||||||
|  |  | ||||||
|  |   if (summary.commitSha) { | ||||||
|  |     lines.push(`Commit SHA:     📌 ${summary.commitSha}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (summary.pushed) { | ||||||
|  |     lines.push(`Remote:         ✓ Pushed successfully`); | ||||||
|  |   } else { | ||||||
|  |     lines.push(`Remote:         ⊘ Not pushed (local only)`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (summary.repoUrl && summary.commitSha) { | ||||||
|  |     lines.push(''); | ||||||
|  |     lines.push(`View at: ${summary.repoUrl}/commit/${summary.commitSha}`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   printSection('✅ Commit Summary', lines); | ||||||
|  |  | ||||||
|  |   if (summary.pushed) { | ||||||
|  |     console.log('🎉 All done! Your changes are committed and pushed.\n'); | ||||||
|  |   } else { | ||||||
|  |     console.log('✓ Commit created successfully.\n'); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print an info message with consistent formatting | ||||||
|  |  */ | ||||||
|  | export function printInfo(message: string): void { | ||||||
|  |   console.log(`  ℹ️  ${message}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print a success message | ||||||
|  |  */ | ||||||
|  | export function printSuccess(message: string): void { | ||||||
|  |   console.log(`  ✓ ${message}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print a warning message | ||||||
|  |  */ | ||||||
|  | export function printWarning(message: string): void { | ||||||
|  |   logger.log('warn', `⚠️  ${message}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print an error message | ||||||
|  |  */ | ||||||
|  | export function printError(message: string): void { | ||||||
|  |   logger.log('error', `✗ ${message}`); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Print commit message being created | ||||||
|  |  */ | ||||||
|  | export function printCommitMessage(commitString: string): void { | ||||||
|  |   console.log(`\n  📝 Commit: ${commitString}\n`); | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user