From 0d3b10bd00ee247e211f195357de941f49579835 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Thu, 23 Oct 2025 23:44:38 +0000 Subject: [PATCH] feat(mod_commit): Add CLI UI helpers and improve commit workflow with progress, recommendations and summary --- changelog.md | 8 ++ ts/00_commitinfo_data.ts | 2 +- ts/mod_commit/index.ts | 77 ++++++++++---- ts/mod_commit/mod.helpers.ts | 32 +++++- ts/mod_commit/mod.ui.ts | 196 +++++++++++++++++++++++++++++++++++ 5 files changed, 290 insertions(+), 25 deletions(-) create mode 100644 ts/mod_commit/mod.ui.ts diff --git a/changelog.md b/changelog.md index 378ec7f..5228010 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # 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) Stage and commit deno.json when bumping/syncing versions and create/update git tags diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 100bb3e..eddea6c 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { 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.' } diff --git a/ts/mod_commit/index.ts b/ts/mod_commit/index.ts index 740771e..42fa60f 100644 --- a/ts/mod_commit/index.ts +++ b/ts/mod_commit/index.ts @@ -4,6 +4,7 @@ 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'; export const run = async (argvArg: any) => { if (argvArg.format) { @@ -11,7 +12,8 @@ export const run = async (argvArg: any) => { await formatMod.run(); } - logger.log('info', `gathering facts...`); + ui.printHeader('🔍 Analyzing repository changes...'); + const aidoc = new plugins.tsdoc.AiDoc(); await aidoc.start(); @@ -19,16 +21,12 @@ export const run = async (argvArg: any) => { await aidoc.stop(); - logger.log( - 'info', - `--------- - Next recommended commit would be: - =========== - -> ${nextCommitObject.recommendedNextVersion}: - -> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage} - =========== - `, - ); + ui.printRecommendation({ + recommendedNextVersion: nextCommitObject.recommendedNextVersion, + recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel, + recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope, + recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage, + }); const commitInteract = new plugins.smartinteract.SmartInteract(); 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({ executor: 'bash', 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( paths.cwd, commitVersionType, ); 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; changelog = changelog.replaceAll( '{{nextVersion}}', @@ -110,23 +118,54 @@ export const run = async (argvArg: any) => { changelog, 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`); + 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'); - // Detect project type and bump version accordingly + // Step 5: Bumping version + currentStep++; 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 ( answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true') ) { - // Detect current branch instead of hardcoding "master" - const currentBranch = await helpers.detectCurrentBranch(); + 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'); } + + 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 = ( diff --git a/ts/mod_commit/mod.helpers.ts b/ts/mod_commit/mod.helpers.ts index 84c1d9d..0fc8f93 100644 --- a/ts/mod_commit/mod.helpers.ts +++ b/ts/mod_commit/mod.helpers.ts @@ -1,6 +1,7 @@ import * as plugins from './mod.plugins.js'; import * as paths from '../paths.js'; import { logger } from '../gitzone.logging.js'; +import * as ui from './mod.ui.js'; export type ProjectType = 'npm' | 'deno' | 'both' | 'none'; export type VersionType = 'patch' | 'minor' | 'major'; @@ -204,25 +205,40 @@ async function syncVersionToDenoJson(version: string): Promise { * Bumps the project version based on project type * @param projectType The detected project type * @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 */ export async function bumpProjectVersion( projectType: ProjectType, - versionType: VersionType + versionType: VersionType, + currentStep?: number, + totalSteps?: number ): Promise { + 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) { case 'npm': - return await bumpNpmVersion(versionType); + newVersion = await bumpNpmVersion(versionType); + break; case 'deno': - return await bumpDenoVersion(versionType); + newVersion = await bumpDenoVersion(versionType); + break; case 'both': { // Bump npm version first (it handles git tags) - const newVersion = await bumpNpmVersion(versionType); + newVersion = await bumpNpmVersion(versionType); // Then sync to deno.json await syncVersionToDenoJson(newVersion); - return newVersion; + break; } case 'none': @@ -231,4 +247,10 @@ export async function bumpProjectVersion( default: throw new Error(`Unknown project type: ${projectType}`); } + + if (currentStep && totalSteps) { + ui.printStep(currentStep, totalSteps, description, 'done'); + } + + return newVersion; } diff --git a/ts/mod_commit/mod.ui.ts b/ts/mod_commit/mod.ui.ts new file mode 100644 index 0000000..35f92aa --- /dev/null +++ b/ts/mod_commit/mod.ui.ts @@ -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`); +}