From df0949e5605165b41f34f19846c302cc0d08d89d Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 17:44:18 -0600 Subject: [PATCH 01/77] feat: complete Step 3 - configuration validation with rich error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Enhanced ValidationError interface with rich context fields (fieldDisplay, userMessage, issue, etc.) • Fixed ConfigError import in loader.ts (separated type import from value import) • Updated all validator error creation with comprehensive context and user-friendly messages • Implemented character analysis for invalid ID formats (identifies uppercase, dashes, numbers, etc.) • Rewrote error formatting in loader.ts with structured, scannable output • Removed 'lab init' suggestion from validation errors (not appropriate for existing configs) • Added specific issue identification and actionable guidance for each error type • Tested validation with single and multiple errors - all working correctly --- src/lib/config/loader.ts | 50 +++++++++---- src/lib/config/types.ts | 18 +++-- src/lib/config/validator.ts | 135 +++++++++++++++++++++++++++++++----- 3 files changed, 168 insertions(+), 35 deletions(-) diff --git a/src/lib/config/loader.ts b/src/lib/config/loader.ts index ede2bf2..503bfd8 100644 --- a/src/lib/config/loader.ts +++ b/src/lib/config/loader.ts @@ -15,9 +15,9 @@ import type { RawConfig, ConfigLoadResult, ProjectRoot, - CachedConfig, - ConfigError + CachedConfig } from './types.js'; +import { ConfigError } from './types.js'; import { mergeWithDefaults, createFallbackConfig } from './defaults.js'; import { ConfigValidator } from './validator.js'; @@ -248,19 +248,43 @@ export class ConfigLoader { const validationResult = validator.validate(rawConfig); if (!validationResult.valid) { - // Transform validation errors into user-friendly ConfigError - const errorMessages = validationResult.errors.map(error => - `${error.field}: ${error.message}` - ).join('\n'); + // Format errors with rich context for user-friendly output + const formattedErrors = validationResult.errors.map((error, index) => { + const count = index + 1; + const location = error.fieldDisplay || error.field; + + let errorBlock = `\n${count}. ${location}:\n`; + errorBlock += ` ${error.userMessage || error.message}\n`; + + if (error.value !== undefined) { + errorBlock += ` Found: ${JSON.stringify(error.value)}\n`; + } + + if (error.issue) { + errorBlock += ` Issue: ${error.issue}\n`; + } + + if (error.expectedFormat) { + errorBlock += ` Rule: ${error.expectedFormat}\n`; + } + + if (error.examples && error.examples.length > 0) { + errorBlock += ` Examples: ${error.examples.join(', ')}\n`; + } + + return errorBlock; + }).join('\n'); - throw new (Error as any)( // TODO: Use proper ConfigError import - `Invalid configuration in ${configPath}`, - `Configuration validation failed:\n${errorMessages}`, + const count = validationResult.errors.length; + const plural = count === 1 ? 'error' : 'errors'; + const filename = path.basename(configPath); + + throw new ConfigError( + `Configuration Error: ${filename}`, + `Found ${count} validation ${plural}:${formattedErrors}`, [ - 'Check the configuration file syntax and required fields', - 'Ensure all commit types have valid "id" and "description" fields', - 'Verify that type IDs contain only lowercase letters (a-z)', - 'Run \'lab init\' to generate a valid configuration file' + `Edit ${filename} to fix the issues listed above`, + 'See documentation for valid field formats: https://github.com/labcatr/labcommitr#config' ], configPath ); diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts index 661ee12..dc91be2 100644 --- a/src/lib/config/types.ts +++ b/src/lib/config/types.ts @@ -144,15 +144,25 @@ export interface ValidationResult { /** * Individual validation error - * Provides specific information about what failed validation + * Provides specific information about what failed validation with rich context */ export interface ValidationError { - /** The configuration field that failed validation */ + /** Technical field path (e.g., "types[0].id") */ field: string; - /** Human-readable error message */ + /** User-friendly field description (e.g., "Commit type #1 → ID field") */ + fieldDisplay: string; + /** Technical error message for developers */ message: string; - /** The actual value that failed validation */ + /** User-friendly error explanation */ + userMessage: string; + /** The actual problematic value */ value?: unknown; + /** What format/type was expected */ + expectedFormat?: string; + /** Array of valid example values */ + examples?: string[]; + /** Specific issue identified (e.g., "Contains dash (-)") */ + issue?: string; } /** diff --git a/src/lib/config/validator.ts b/src/lib/config/validator.ts index dd54808..1118d7d 100644 --- a/src/lib/config/validator.ts +++ b/src/lib/config/validator.ts @@ -27,8 +27,12 @@ export class ConfigValidator { if (!this.isValidConfigStructure(config)) { errors.push({ field: 'root', + fieldDisplay: 'Configuration root', message: 'Configuration must be an object', - value: config + userMessage: 'Configuration file must contain an object with key-value pairs', + value: config, + expectedFormat: 'YAML object with fields like "version", "types", etc.', + issue: 'Found non-object value at root level' }); return { valid: false, errors }; } @@ -59,8 +63,13 @@ export class ConfigValidator { if (!config.types) { errors.push({ field: 'types', + fieldDisplay: 'Commit types array', message: 'Required field "types" is missing', - value: undefined + userMessage: 'Configuration must include a "types" array', + value: undefined, + expectedFormat: 'array with at least one commit type object', + examples: ['feat', 'fix', 'docs', 'refactor', 'test'], + issue: 'Missing required field' }); return errors; } @@ -69,8 +78,12 @@ export class ConfigValidator { if (!Array.isArray(config.types)) { errors.push({ field: 'types', + fieldDisplay: 'Commit types', message: 'Field "types" must be an array', - value: config.types + userMessage: 'The "types" field must be a list of commit type objects', + value: config.types, + expectedFormat: 'array with at least one commit type object', + issue: 'Found non-array value' }); return errors; } @@ -79,8 +92,13 @@ export class ConfigValidator { if (config.types.length === 0) { errors.push({ field: 'types', + fieldDisplay: 'Commit types array', message: 'Field "types" must contain at least one commit type', - value: config.types + userMessage: 'Configuration must define at least one commit type', + value: config.types, + expectedFormat: 'array with at least one commit type object', + examples: ['feat (features)', 'fix (bug fixes)', 'docs (documentation)', 'refactor (code restructuring)', 'test (testing)'], + issue: 'Empty types array' }); return errors; } @@ -102,13 +120,18 @@ export class ConfigValidator { private validateCommitType(type: unknown, index: number): ValidationError[] { const errors: ValidationError[] = []; const fieldPrefix = `types[${index}]`; + const displayPrefix = `Commit type #${index + 1}`; // Check if type is an object if (!type || typeof type !== 'object' || Array.isArray(type)) { errors.push({ field: fieldPrefix, + fieldDisplay: displayPrefix, message: 'Each commit type must be an object', - value: type + userMessage: 'Each commit type must be an object with id and description fields', + value: type, + expectedFormat: 'object with "id" and "description" fields', + issue: 'Found non-object value' }); return errors; } @@ -119,26 +142,67 @@ export class ConfigValidator { if (!commitType.id) { errors.push({ field: `${fieldPrefix}.id`, + fieldDisplay: `${displayPrefix} → ID field`, message: 'Required field "id" is missing', - value: commitType.id + userMessage: 'Every commit type must have an ID', + value: commitType.id, + expectedFormat: 'lowercase letters only (a-z)', + examples: ['feat', 'fix', 'docs', 'refactor', 'test'], + issue: 'Missing required field' }); } else if (typeof commitType.id !== 'string') { errors.push({ field: `${fieldPrefix}.id`, + fieldDisplay: `${displayPrefix} → ID field`, message: 'Field "id" must be a string', - value: commitType.id + userMessage: 'Commit type ID must be text', + value: commitType.id, + expectedFormat: 'lowercase letters only (a-z)', + examples: ['feat', 'fix', 'docs', 'refactor', 'test'], + issue: 'Found non-string value' }); } else if (commitType.id.trim() === '') { errors.push({ field: `${fieldPrefix}.id`, + fieldDisplay: `${displayPrefix} → ID field`, message: 'Field "id" cannot be empty', - value: commitType.id + userMessage: 'Commit type ID cannot be empty', + value: commitType.id, + expectedFormat: 'lowercase letters only (a-z)', + examples: ['feat', 'fix', 'docs', 'refactor', 'test'], + issue: 'Empty string' }); } else if (!/^[a-z]+$/.test(commitType.id)) { + // Identify specific problematic characters + const invalidChars = commitType.id + .split('') + .filter(char => !/[a-z]/.test(char)) + .filter((char, idx, arr) => arr.indexOf(char) === idx) // unique + .map(char => { + if (char === char.toUpperCase() && char !== char.toLowerCase()) { + return `${char} (uppercase)`; + } else if (char === '-') { + return `- (dash)`; + } else if (char === '_') { + return `_ (underscore)`; + } else if (/\d/.test(char)) { + return `${char} (number)`; + } else if (char === ' ') { + return '(space)'; + } else { + return `${char} (special character)`; + } + }); + errors.push({ field: `${fieldPrefix}.id`, + fieldDisplay: `${displayPrefix} → ID field`, message: 'Field "id" must contain only lowercase letters (a-z)', - value: commitType.id + userMessage: 'Commit type IDs must be lowercase letters only', + value: commitType.id, + expectedFormat: 'lowercase letters only (a-z)', + examples: ['feat', 'fix', 'docs', 'refactor', 'test'], + issue: `Contains invalid characters: ${invalidChars.join(', ')}` }); } @@ -146,20 +210,32 @@ export class ConfigValidator { if (!commitType.description) { errors.push({ field: `${fieldPrefix}.description`, + fieldDisplay: `${displayPrefix} → description field`, message: 'Required field "description" is missing', - value: commitType.description + userMessage: 'Every commit type must have a description', + value: commitType.description, + examples: ['"A new feature"', '"Bug fix for users"', '"Documentation changes"'], + issue: 'Missing required field' }); } else if (typeof commitType.description !== 'string') { errors.push({ field: `${fieldPrefix}.description`, + fieldDisplay: `${displayPrefix} → description field`, message: 'Field "description" must be a string', - value: commitType.description + userMessage: 'Commit type description must be text', + value: commitType.description, + examples: ['"A new feature"', '"Bug fix for users"', '"Documentation changes"'], + issue: 'Found non-string value' }); } else if (commitType.description.trim() === '') { errors.push({ field: `${fieldPrefix}.description`, + fieldDisplay: `${displayPrefix} → description field`, message: 'Field "description" cannot be empty', - value: commitType.description + userMessage: 'Commit type description cannot be empty', + value: commitType.description, + examples: ['"A new feature"', '"Bug fix for users"', '"Documentation changes"'], + issue: 'Empty string' }); } @@ -168,8 +244,11 @@ export class ConfigValidator { if (typeof commitType.emoji !== 'string') { errors.push({ field: `${fieldPrefix}.emoji`, + fieldDisplay: `${displayPrefix} → emoji field`, message: 'Field "emoji" must be a string', - value: commitType.emoji + userMessage: 'Emoji field must be text if provided', + value: commitType.emoji, + issue: 'Found non-string value' }); } // Note: Emoji format validation will be added in Phase 3 @@ -190,8 +269,12 @@ export class ConfigValidator { if (config.version !== undefined && typeof config.version !== 'string') { errors.push({ field: 'version', + fieldDisplay: 'Schema version', message: 'Field "version" must be a string', - value: config.version + userMessage: 'The version field must be text', + value: config.version, + expectedFormat: 'version string (e.g., "1.0")', + issue: 'Found non-string value' }); } @@ -199,8 +282,12 @@ export class ConfigValidator { if (config.config !== undefined && (typeof config.config !== 'object' || Array.isArray(config.config))) { errors.push({ field: 'config', + fieldDisplay: 'Config section', message: 'Field "config" must be an object', - value: config.config + userMessage: 'The config section must be an object with key-value pairs', + value: config.config, + expectedFormat: 'object with configuration settings', + issue: 'Found non-object value' }); } @@ -208,8 +295,12 @@ export class ConfigValidator { if (config.format !== undefined && (typeof config.format !== 'object' || Array.isArray(config.format))) { errors.push({ field: 'format', + fieldDisplay: 'Format section', message: 'Field "format" must be an object', - value: config.format + userMessage: 'The format section must be an object with formatting rules', + value: config.format, + expectedFormat: 'object with format settings', + issue: 'Found non-object value' }); } @@ -217,8 +308,12 @@ export class ConfigValidator { if (config.validation !== undefined && (typeof config.validation !== 'object' || Array.isArray(config.validation))) { errors.push({ field: 'validation', + fieldDisplay: 'Validation section', message: 'Field "validation" must be an object', - value: config.validation + userMessage: 'The validation section must be an object with validation rules', + value: config.validation, + expectedFormat: 'object with validation settings', + issue: 'Found non-object value' }); } @@ -226,8 +321,12 @@ export class ConfigValidator { if (config.advanced !== undefined && (typeof config.advanced !== 'object' || Array.isArray(config.advanced))) { errors.push({ field: 'advanced', + fieldDisplay: 'Advanced section', message: 'Field "advanced" must be an object', - value: config.advanced + userMessage: 'The advanced section must be an object with advanced settings', + value: config.advanced, + expectedFormat: 'object with advanced configuration', + issue: 'Found non-object value' }); } From 70ec71371f5522e4cb110355daf6c28ca6f08bb2 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:02:45 -0600 Subject: [PATCH 02/77] build: add commander.js for CLI framework MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added commander@14.0.2 for CLI argument parsing • Updated package-lock with new dependencies • Provides type-safe CLI framework for command handling --- package.json | 4 +++- pnpm-lock.yaml | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b8bf9b2..a39fc4e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ }, "type": "module", "bin": { - "labcommitr": "./dist/index.js" + "labcommitr": "./dist/index.js", + "lab": "./dist/index.js" }, "keywords": [ "git", @@ -31,6 +32,7 @@ "@changesets/cli": "^2.29.7", "@types/node": "^24.3.3", "boxen": "^8.0.1", + "commander": "^14.0.2", "consola": "^3.4.2", "cosmiconfig": "^9.0.0", "js-yaml": "^4.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9bed2a..e802795 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: boxen: specifier: ^8.0.1 version: 8.0.1 + commander: + specifier: ^14.0.2 + version: 14.0.2 consola: specifier: ^3.4.2 version: 3.4.2 @@ -229,6 +232,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -859,6 +866,8 @@ snapshots: cli-boxes@3.0.0: {} + commander@14.0.2: {} + consola@3.4.2: {} cosmiconfig@9.0.0(typescript@5.9.2): From 73fd92a166f0647fb20878905ac1e854bf8ae190 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:06:29 -0600 Subject: [PATCH 03/77] feat: implement CLI utility modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Created version.ts for package version retrieval • Created error-handler.ts for centralized error handling • Utilities support ESM with import.meta.url • ConfigError integration with formatted output • Debug mode support via DEBUG env variable --- src/cli/utils/error-handler.ts | 64 ++++++++++++++++++++++++++++++++++ src/cli/utils/version.ts | 35 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/cli/utils/error-handler.ts create mode 100644 src/cli/utils/version.ts diff --git a/src/cli/utils/error-handler.ts b/src/cli/utils/error-handler.ts new file mode 100644 index 0000000..62c043e --- /dev/null +++ b/src/cli/utils/error-handler.ts @@ -0,0 +1,64 @@ +/** + * CLI Error Handler + * + * Centralized error handling for the CLI application. + * Transforms different error types into user-friendly output. + * + * Error Types Handled: + * - ConfigError: Configuration validation and loading errors + * - Commander errors: Invalid options, missing arguments + * - System errors: File permissions, network issues + * - Unknown errors: Unexpected exceptions + */ + +import { Logger } from '../../lib/logger.js'; +import { ConfigError } from '../../lib/config/types.js'; + +/** + * Handle CLI errors with appropriate formatting + * Determines error type and displays user-friendly message + * + * @param error - Error object to handle + */ +export function handleCliError(error: unknown): void { + if (error instanceof ConfigError) { + // Configuration errors (already formatted in Step 3) + Logger.error(error.message); + + if (error.details) { + console.error(error.details); + } + + if (error.solutions && error.solutions.length > 0) { + console.error('\n💡 Solutions:'); + error.solutions.forEach((solution, index) => { + console.error(` ${index + 1}. ${solution}`); + }); + } + + return; + } + + if (error instanceof Error) { + // Standard errors + if (error.name === 'CommanderError') { + // Commander.js validation errors (handled by Commander itself) + return; + } + + // Generic error + Logger.error(`Error: ${error.message}`); + + if (process.env.DEBUG) { + console.error('\nStack trace:'); + console.error(error.stack); + } + + return; + } + + // Unknown error type + Logger.error('An unexpected error occurred'); + console.error(error); +} + diff --git a/src/cli/utils/version.ts b/src/cli/utils/version.ts new file mode 100644 index 0000000..b465e04 --- /dev/null +++ b/src/cli/utils/version.ts @@ -0,0 +1,35 @@ +/** + * Version utilities + * + * Utilities for retrieving and displaying version information. + * Provides consistent version formatting across the CLI. + */ + +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +/** + * Get package version from package.json + * @returns Version string (e.g., "0.0.1") + */ +export function getVersion(): string { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const packageJsonPath = join(__dirname, '../../../package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + return packageJson.version; +} + +/** + * Get full version info (name + version) + * @returns Full version string (e.g., "@labcatr/labcommitr v0.0.1") + */ +export function getFullVersion(): string { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const packageJsonPath = join(__dirname, '../../../package.json'); + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + return `${packageJson.name} v${packageJson.version}`; +} + From 627f56b352b3aabf15916351580e95eb0be5004f Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:06:37 -0600 Subject: [PATCH 04/77] feat: implement config show command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Created config command with show subcommand • Displays loaded configuration with source info • Shows emoji mode detection status • Integrates with Step 3 validation error handling • Provides debugging utility for users --- src/cli/commands/config.ts | 75 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/cli/commands/config.ts diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts new file mode 100644 index 0000000..9ca6975 --- /dev/null +++ b/src/cli/commands/config.ts @@ -0,0 +1,75 @@ +/** + * Config command implementation + * + * Provides utilities for working with labcommitr configuration: + * - show: Display currently loaded configuration + * - validate: Check configuration validity without loading + * + * This command is primarily for debugging and troubleshooting. + */ + +import { Command } from 'commander'; +import { loadConfig } from '../../lib/config/index.js'; +import { Logger } from '../../lib/logger.js'; +import { ConfigError } from '../../lib/config/types.js'; + +/** + * Config command + */ +export const configCommand = new Command('config') + .description('Manage labcommitr configuration') + .addCommand( + new Command('show') + .description('Display the current configuration') + .option('-p, --path ', 'Start search from specific directory') + .action(showConfigAction), + ); + +/** + * Show config action handler + * Loads and displays the current configuration with source information + */ +async function showConfigAction(options: { path?: string }): Promise { + try { + Logger.info('Loading configuration...\n'); + + const result = await loadConfig(options.path); + + // Display config source + Logger.success(`Configuration loaded from: ${result.source}`); + + if (result.source === 'defaults') { + Logger.warn('Using built-in defaults (no config file found)'); + } + + if (result.path) { + Logger.info(`Config file path: ${result.path}`); + } + + Logger.info(`Emoji mode: ${result.emojiModeActive ? 'enabled' : 'disabled (terminal fallback)'}`); + + // Display configuration (formatted JSON) + console.log('\nConfiguration:'); + console.log(JSON.stringify(result.config, null, 2)); + } catch (error) { + if (error instanceof ConfigError) { + // ConfigError already has formatted output + Logger.error(error.message); + if (error.details) { + console.error(error.details); + } + if (error.solutions && error.solutions.length > 0) { + console.error('\nSolutions:'); + error.solutions.forEach((solution, index) => { + console.error(` ${index + 1}. ${solution}`); + }); + } + } else { + // Unexpected error + Logger.error('Failed to load configuration'); + console.error(error); + } + process.exit(1); + } +} + From d7460d4b708e77568b905303c107481b226537be Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:07:16 -0600 Subject: [PATCH 05/77] feat: add init command placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Created init command structure with options • Added --force and --preset flags • Placeholder for Step 5 implementation • Documents planned interactive features • Non-destructive update support ready --- src/cli/commands/init.ts | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/cli/commands/init.ts diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts new file mode 100644 index 0000000..a174bc2 --- /dev/null +++ b/src/cli/commands/init.ts @@ -0,0 +1,55 @@ +/** + * Init command implementation + * + * Interactive initialization command that guides users through creating + * a .labcommitr.config.yaml file with preset options. + * + * Step 5 Implementation: Astro-like interactive experience + * - Preset selection (Conventional Commits, Gitmoji, Angular, Custom) + * - Configuration options (emoji support, scope rules, etc.) + * - Config file generation with user choices + * + * Current Status: Placeholder for Step 4 + */ + +import { Command } from 'commander'; +import { Logger } from '../../lib/logger.js'; + +/** + * Init command + */ +export const initCommand = new Command('init') + .description('Initialize labcommitr configuration in your project') + .option('-f, --force', 'Overwrite existing configuration') + .option( + '--preset ', + 'Use a specific preset (conventional, gitmoji, angular)', + ) + .action(initAction); + +/** + * Init action handler (placeholder) + * TODO (Step 5): Implement interactive initialization flow + */ +async function initAction(options: { + force?: boolean; + preset?: string; +}): Promise { + Logger.info('Init command - Coming in Step 5!'); + Logger.info('\nPlanned features:'); + console.log( + ' • Interactive preset selection (Conventional Commits, Gitmoji, etc.)', + ); + console.log(' • Customizable commit types and formats'); + console.log(' • Automatic .labcommitr.config.yaml generation'); + console.log(' • Non-destructive updates (--force to override)'); + + if (options.preset) { + Logger.info(`\nYou specified preset: ${options.preset}`); + } + + if (options.force) { + Logger.warn('Force mode will overwrite existing config when implemented'); + } +} + From 3db60c70b65361a38a4d649374a217cd68a6eab8 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:07:22 -0600 Subject: [PATCH 06/77] feat: add commit command placeholder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Created commit command structure with options • Added type, scope, message flags • Added commit alias 'c' for quick access • Placeholder for Step 6 implementation • Documents planned interactive workflow --- src/cli/commands/commit.ts | 66 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/cli/commands/commit.ts diff --git a/src/cli/commands/commit.ts b/src/cli/commands/commit.ts new file mode 100644 index 0000000..6f04980 --- /dev/null +++ b/src/cli/commands/commit.ts @@ -0,0 +1,66 @@ +/** + * Commit command implementation + * + * Interactive commit creation with standardized formatting based on + * project configuration. + * + * Step 6 Implementation: Interactive commit workflow + * - Type selection from configured commit types + * - Optional scope input + * - Subject and description prompts + * - Git commit execution with formatted message + * - Dynamic emoji support based on terminal capabilities + * + * Current Status: Placeholder for Step 4 + */ + +import { Command } from 'commander'; +import { Logger } from '../../lib/logger.js'; + +/** + * Commit command + */ +export const commitCommand = new Command('commit') + .description('Create a standardized commit (interactive)') + .alias('c') + .option('-t, --type ', 'Commit type (feat, fix, etc.)') + .option('-s, --scope ', 'Commit scope') + .option('-m, --message ', 'Commit subject') + .option('--no-verify', 'Bypass git hooks') + .action(commitAction); + +/** + * Commit action handler (placeholder) + * TODO (Step 6): Implement interactive commit workflow + */ +async function commitAction(options: { + type?: string; + scope?: string; + message?: string; + verify?: boolean; +}): Promise { + Logger.info('Commit command - Coming in Step 6!'); + Logger.info('\nPlanned features:'); + console.log(' • Interactive type selection from your config'); + console.log(' • Optional scope and description prompts'); + console.log(' • Automatic emoji detection and fallback'); + console.log(' • Git commit execution with formatted message'); + console.log(' • Validation against configured rules'); + + if (options.type) { + Logger.info(`\nYou specified type: ${options.type}`); + } + + if (options.scope) { + Logger.info(`You specified scope: ${options.scope}`); + } + + if (options.message) { + Logger.info(`You specified message: ${options.message}`); + } + + if (options.verify === false) { + Logger.warn('Git hooks will be bypassed when implemented'); + } +} + From 69d6d9388d84906e71ac1bb40eac2173db8b9954 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:07:29 -0600 Subject: [PATCH 07/77] refactor: centralize command exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Created central export point for all commands • Simplifies imports in program.ts • Provides overview of available commands • Supports clean module organization --- src/cli/commands/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/cli/commands/index.ts diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts new file mode 100644 index 0000000..4219134 --- /dev/null +++ b/src/cli/commands/index.ts @@ -0,0 +1,11 @@ +/** + * Command exports + * + * Central export point for all CLI commands. + * Makes imports cleaner and provides single location for command overview. + */ + +export { configCommand } from './config.js'; +export { initCommand } from './init.js'; +export { commitCommand } from './commit.js'; + From 422083f4970403056f732164c24a837b02e79ca3 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:07:37 -0600 Subject: [PATCH 08/77] feat: implement Commander.js program setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Created main program instance with metadata • Added help text customization with examples • Configured version display from package.json • Set up command registration structure • Added error suggestions for unknown commands • Supports both labcommitr and lab aliases --- src/cli/program.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/cli/program.ts diff --git a/src/cli/program.ts b/src/cli/program.ts new file mode 100644 index 0000000..8b54461 --- /dev/null +++ b/src/cli/program.ts @@ -0,0 +1,65 @@ +/** + * Commander.js program configuration + * + * This module sets up the CLI program structure including: + * - Program metadata (name, version, description) + * - Global options (--verbose, --silent, etc.) + * - Command registration + * - Help text customization + */ + +import { Command } from 'commander'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +// Commands +import { configCommand } from './commands/config.js'; +import { initCommand } from './commands/init.js'; +import { commitCommand } from './commands/commit.js'; + +// Get package.json for version info +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageJsonPath = join(__dirname, '../../package.json'); +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + +/** + * Main CLI program instance + */ +export const program = new Command(); + +// Program metadata +program + .name('labcommitr') + .description( + 'A CLI tool for standardized git commits with customizable workflows', + ) + .version(packageJson.version, '-v, --version', 'Display version number') + .helpOption('-h, --help', 'Display help information'); + +// Global options (future: --verbose, --no-emoji, etc.) +// program.option('--verbose', 'Enable verbose logging'); + +// Register commands +program.addCommand(configCommand); +program.addCommand(initCommand); +program.addCommand(commitCommand); + +// Customize help text +program.addHelpText( + 'after', + ` +Examples: + $ labcommitr init Initialize config in current project + $ lab commit Create a standardized commit (interactive) + $ lab config show Display current configuration + +Documentation: + https://github.com/labcatr/labcommitr#readme +`, +); + +// Error on unknown commands +program.showSuggestionAfterError(true); + From f20b4e91286708b5e22e7483ce6dcecca52fd169 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 18:07:44 -0600 Subject: [PATCH 09/77] feat: implement CLI entry point with Commander.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Rewrote index.ts as CLI orchestrator • Added shebang for executable npm package • Integrated program setup and error handling • Minimal orchestration logic only • Supports async command implementations • Global error handling for all commands --- src/index.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 019c0f4..6187916 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,33 @@ -console.log("Hello World!"); +#!/usr/bin/env node + +/** + * Labcommitr CLI Entry Point + * + * This file serves as the main entry point for the labcommitr CLI tool. + * It initializes the Commander.js program and delegates command handling + * to modular command implementations. + * + * Architecture: + * - Minimal orchestration logic (setup and error handling only) + * - Command implementations delegated to src/cli/commands/ + * - Global error handling for uncaught exceptions + */ + +import { program } from './cli/program.js'; +import { handleCliError } from './cli/utils/error-handler.js'; + +/** + * Main CLI execution + * Parses process arguments and executes the appropriate command + */ +async function main(): Promise { + try { + await program.parseAsync(process.argv); + } catch (error) { + handleCliError(error); + process.exit(1); + } +} + +// Execute CLI +main(); From d580cc4851b9c7ec98c968e2d2a6e06d77a27913 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 20:11:29 -0600 Subject: [PATCH 10/77] chore: enhance .gitignore with comprehensive exclusions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added multiple TypeScript build output directories (build/, out/) • Added coverage and testing directories • Added npm/yarn/pnpm debug logs and cache directories • Added OS-specific files (.DS_Store, Thumbs.db) • Added IDE/editor directories (.vscode/, .idea/, etc.) • Added temporary files and cache directories • Prevents accidental commits of generated artifacts --- .gitignore | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 957676a..bfed485 100644 --- a/.gitignore +++ b/.gitignore @@ -24,14 +24,52 @@ jspm_packages/ # Snowpack dependency directory (https://snowpack.dev/) web_modules/ -# TypeScript cache +# TypeScript build artifacts *.tsbuildinfo +dist/ +build/ +out/ + +# Coverage reports +coverage/ +.nyc_output/ +*.lcov + +# Testing +.test-temp/ +test-results/ + +# npm/yarn/pnpm +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.npm +.yarn + +# OS files +.DS_Store +Thumbs.db + +# IDE/Editor +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.classpath +.settings/ + +# Temporary files +*.tmp +*.temp +.cache/ ### Other # Remove rust files for now rust-src/ -dist/ ### Development # Development progress tracking (internal use only) From d4acc05669878fcf18f1c32c1a6b4def0ec9b5bb Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 20:11:58 -0600 Subject: [PATCH 11/77] chore: remove dist/ from git tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Removed build artifacts from version control • dist/ directory properly ignored via .gitignore • Local dist/ files preserved for development • Follows Node.js/TypeScript best practices --- dist/index.d.ts | 1 - dist/index.js | 3 --- dist/index.js.map | 1 - 3 files changed, 5 deletions(-) delete mode 100644 dist/index.d.ts delete mode 100644 dist/index.js delete mode 100644 dist/index.js.map diff --git a/dist/index.d.ts b/dist/index.d.ts deleted file mode 100644 index cb0ff5c..0000000 --- a/dist/index.d.ts +++ /dev/null @@ -1 +0,0 @@ -export {}; diff --git a/dist/index.js b/dist/index.js deleted file mode 100644 index 3040bf5..0000000 --- a/dist/index.js +++ /dev/null @@ -1,3 +0,0 @@ -console.log("Hello World!"); -export {}; -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map deleted file mode 100644 index 895157f..0000000 --- a/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC"} \ No newline at end of file From 8837714487a7c5fda0a7550f3947a8a4f1e96c6b Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 20:22:19 -0600 Subject: [PATCH 12/77] chore: add changeset for CLI framework implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Added minor version bump for CLI framework • Documents new user-facing commands (--help, --version, config show) • Notes dual alias support (labcommitr and lab) • Describes foundation for future init and commit commands --- .changeset/cli-framework-implementation.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/cli-framework-implementation.md diff --git a/.changeset/cli-framework-implementation.md b/.changeset/cli-framework-implementation.md new file mode 100644 index 0000000..0ca7174 --- /dev/null +++ b/.changeset/cli-framework-implementation.md @@ -0,0 +1,14 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: add working CLI framework with basic commands + +- Tool now provides functional command-line interface with help system +- Both `labcommitr` and `lab` command aliases are now available +- Added `--version` flag to display current tool version +- Added `config show` command to display and debug configuration +- Interactive help system guides users through available commands +- Clear error messages when invalid commands are used +- Foundation ready for init and commit commands in upcoming releases + From 783d7623c7d1040989ca2fab7bbd9b2a4d9d2f23 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:57:52 -0600 Subject: [PATCH 13/77] build: add dependencies for interactive CLI - Added @clack/prompts for modern interactive prompts - Added picocolors for terminal color styling - Required for Step 5 init command implementation --- package.json | 2 + pnpm-lock.yaml | 1317 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 898 insertions(+), 421 deletions(-) diff --git a/package.json b/package.json index a39fc4e..2e9cb14 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "license": "ISC", "dependencies": { "@changesets/cli": "^2.29.7", + "@clack/prompts": "^0.11.0", "@types/node": "^24.3.3", "boxen": "^8.0.1", "commander": "^14.0.2", @@ -37,6 +38,7 @@ "cosmiconfig": "^9.0.0", "js-yaml": "^4.1.0", "magicast": "^0.3.5", + "picocolors": "^1.1.1", "prettier": "^3.6.2", "typescript": "^5.9.2", "ufo": "^1.6.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e802795..605c165 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,17 +1,19 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: - .: dependencies: - '@changesets/cli': + "@changesets/cli": specifier: ^2.29.7 version: 2.29.7(@types/node@24.3.3) - '@types/node': + "@clack/prompts": + specifier: ^0.11.0 + version: 0.11.0 + "@types/node": specifier: ^24.3.3 version: 24.3.3 boxen: @@ -32,6 +34,9 @@ importers: magicast: specifier: ^0.3.5 version: 0.3.5 + picocolors: + specifier: ^1.1.1 + version: 1.1.1 prettier: specifier: ^3.6.2 version: 3.6.2 @@ -42,600 +47,1057 @@ importers: specifier: ^1.6.1 version: 1.6.1 devDependencies: - '@types/js-yaml': + "@types/js-yaml": specifier: ^4.0.9 version: 4.0.9 packages: - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} - engines: {node: '>=6.0.0'} + "@babel/code-frame@7.27.1": + resolution: + { + integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-string-parser@7.25.9": + resolution: + { + integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-identifier@7.25.9": + resolution: + { + integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==, + } + engines: { node: ">=6.9.0" } + + "@babel/helper-validator-identifier@7.27.1": + resolution: + { + integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==, + } + engines: { node: ">=6.9.0" } + + "@babel/parser@7.27.0": + resolution: + { + integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==, + } + engines: { node: ">=6.0.0" } hasBin: true - '@babel/runtime@7.27.0': - resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} - engines: {node: '>=6.9.0'} - - '@changesets/apply-release-plan@7.0.13': - resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} - - '@changesets/assemble-release-plan@6.0.9': - resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} - - '@changesets/changelog-git@0.2.1': - resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - - '@changesets/cli@2.29.7': - resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} + "@babel/runtime@7.27.0": + resolution: + { + integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==, + } + engines: { node: ">=6.9.0" } + + "@babel/types@7.27.0": + resolution: + { + integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==, + } + engines: { node: ">=6.9.0" } + + "@changesets/apply-release-plan@7.0.13": + resolution: + { + integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==, + } + + "@changesets/assemble-release-plan@6.0.9": + resolution: + { + integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==, + } + + "@changesets/changelog-git@0.2.1": + resolution: + { + integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==, + } + + "@changesets/cli@2.29.7": + resolution: + { + integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==, + } hasBin: true - '@changesets/config@3.1.1': - resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} - - '@changesets/errors@0.2.0': - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} - - '@changesets/get-dependents-graph@2.1.3': - resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - - '@changesets/get-release-plan@4.0.13': - resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} - - '@changesets/get-version-range-type@0.4.0': - resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} - - '@changesets/git@3.0.4': - resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} - - '@changesets/logger@0.1.1': - resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} - - '@changesets/parse@0.4.1': - resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} - - '@changesets/pre@2.0.2': - resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} - - '@changesets/read@0.6.5': - resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} - - '@changesets/should-skip-package@0.1.2': - resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} - - '@changesets/types@4.1.0': - resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} - - '@changesets/types@6.1.0': - resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} - - '@changesets/write@0.4.0': - resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - - '@inquirer/external-editor@1.0.1': - resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==} - engines: {node: '>=18'} + "@changesets/config@3.1.1": + resolution: + { + integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==, + } + + "@changesets/errors@0.2.0": + resolution: + { + integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==, + } + + "@changesets/get-dependents-graph@2.1.3": + resolution: + { + integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==, + } + + "@changesets/get-release-plan@4.0.13": + resolution: + { + integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==, + } + + "@changesets/get-version-range-type@0.4.0": + resolution: + { + integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==, + } + + "@changesets/git@3.0.4": + resolution: + { + integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==, + } + + "@changesets/logger@0.1.1": + resolution: + { + integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==, + } + + "@changesets/parse@0.4.1": + resolution: + { + integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==, + } + + "@changesets/pre@2.0.2": + resolution: + { + integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==, + } + + "@changesets/read@0.6.5": + resolution: + { + integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==, + } + + "@changesets/should-skip-package@0.1.2": + resolution: + { + integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==, + } + + "@changesets/types@4.1.0": + resolution: + { + integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==, + } + + "@changesets/types@6.1.0": + resolution: + { + integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==, + } + + "@changesets/write@0.4.0": + resolution: + { + integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==, + } + + "@clack/core@0.5.0": + resolution: + { + integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==, + } + + "@clack/prompts@0.11.0": + resolution: + { + integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==, + } + + "@inquirer/external-editor@1.0.1": + resolution: + { + integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==, + } + engines: { node: ">=18" } peerDependencies: - '@types/node': '>=18' + "@types/node": ">=18" peerDependenciesMeta: - '@types/node': + "@types/node": optional: true - '@manypkg/find-root@1.1.0': - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} - - '@manypkg/get-packages@1.1.3': - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@types/js-yaml@4.0.9': - resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} - - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - - '@types/node@24.3.3': - resolution: {integrity: sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==} + "@manypkg/find-root@1.1.0": + resolution: + { + integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==, + } + + "@manypkg/get-packages@1.1.3": + resolution: + { + integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==, + } + + "@nodelib/fs.scandir@2.1.5": + resolution: + { + integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, + } + engines: { node: ">= 8" } + + "@nodelib/fs.stat@2.0.5": + resolution: + { + integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, + } + engines: { node: ">= 8" } + + "@nodelib/fs.walk@1.2.8": + resolution: + { + integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, + } + engines: { node: ">= 8" } + + "@types/js-yaml@4.0.9": + resolution: + { + integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==, + } + + "@types/node@12.20.55": + resolution: + { + integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==, + } + + "@types/node@24.3.3": + resolution: + { + integrity: sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==, + } ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + resolution: + { + integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==, + } ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, + } + engines: { node: ">=6" } ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, + } + engines: { node: ">=8" } ansi-regex@6.1.0: - resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==, + } + engines: { node: ">=12" } ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==, + } + engines: { node: ">=12" } argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + resolution: + { + integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, + } argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + resolution: + { + integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, + } array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, + } + engines: { node: ">=8" } better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==, + } + engines: { node: ">=4" } boxen@8.0.1: - resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==, + } + engines: { node: ">=18" } braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, + } + engines: { node: ">=8" } callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: ">=6" } camelcase@8.0.0: - resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==, + } + engines: { node: ">=16" } chalk@5.4.1: - resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + resolution: + { + integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==, + } + engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } chardet@2.1.0: - resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + resolution: + { + integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==, + } ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, + } + engines: { node: ">=8" } cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==, + } + engines: { node: ">=10" } commander@14.0.2: - resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} - engines: {node: '>=20'} + resolution: + { + integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==, + } + engines: { node: ">=20" } consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} + resolution: + { + integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==, + } + engines: { node: ^14.18.0 || >=16.10.0 } cosmiconfig@9.0.0: - resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==, + } + engines: { node: ">=14" } peerDependencies: - typescript: '>=4.9.5' + typescript: ">=4.9.5" peerDependenciesMeta: typescript: optional: true cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, + } + engines: { node: ">= 8" } detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==, + } + engines: { node: ">=8" } dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, + } + engines: { node: ">=8" } emoji-regex@10.5.0: - resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + resolution: + { + integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==, + } emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + resolution: + { + integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, + } enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==, + } + engines: { node: ">=8.6" } env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==, + } + engines: { node: ">=6" } error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + resolution: + { + integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==, + } esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, + } + engines: { node: ">=4" } hasBin: true extendable-error@0.1.7: - resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + resolution: + { + integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==, + } fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} + resolution: + { + integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, + } + engines: { node: ">=8.6.0" } fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + resolution: + { + integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==, + } fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, + } + engines: { node: ">=8" } find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, + } + engines: { node: ">=8" } fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} + resolution: + { + integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==, + } + engines: { node: ">=6 <7 || >=8" } fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} + resolution: + { + integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==, + } + engines: { node: ">=6 <7 || >=8" } get-east-asian-width@1.4.0: - resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==, + } + engines: { node: ">=18" } glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, + } + engines: { node: ">= 6" } globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, + } + engines: { node: ">=10" } graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, + } human-id@4.1.1: - resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} + resolution: + { + integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==, + } hasBin: true iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, + } + engines: { node: ">=0.10.0" } ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} + resolution: + { + integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, + } + engines: { node: ">= 4" } import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: ">=6" } is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + resolution: + { + integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, + } is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, + } + engines: { node: ">=0.10.0" } is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==, + } + engines: { node: ">=8" } is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, + } + engines: { node: ">=0.10.0" } is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} + resolution: + { + integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, + } + engines: { node: ">=0.12.0" } is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==, + } + engines: { node: ">=4" } is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==, + } + engines: { node: ">=0.10.0" } isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + resolution: + { + integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, + } js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + resolution: + { + integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, + } hasBin: true js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + resolution: + { + integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, + } hasBin: true json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + resolution: + { + integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, + } jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + resolution: + { + integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, + } lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + resolution: + { + integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, + } locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, + } + engines: { node: ">=8" } lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + resolution: + { + integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==, + } magicast@0.3.5: - resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + resolution: + { + integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==, + } merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, + } + engines: { node: ">= 8" } micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, + } + engines: { node: ">=8.6" } mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, + } + engines: { node: ">=4" } outdent@0.5.0: - resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + resolution: + { + integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==, + } p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==, + } + engines: { node: ">=8" } p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, + } + engines: { node: ">=6" } p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, + } + engines: { node: ">=8" } p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==, + } + engines: { node: ">=6" } p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, + } + engines: { node: ">=6" } package-manager-detector@0.2.11: - resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + resolution: + { + integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==, + } parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: ">=6" } parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, + } + engines: { node: ">=8" } path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, + } + engines: { node: ">=8" } path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, + } + engines: { node: ">=8" } path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, + } + engines: { node: ">=8" } picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + resolution: + { + integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, + } + engines: { node: ">=8.6" } pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, + } + engines: { node: ">=6" } prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, + } + engines: { node: ">=10.13.0" } hasBin: true prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==, + } + engines: { node: ">=14" } hasBin: true quansync@0.2.10: - resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + resolution: + { + integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==, + } queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + resolution: + { + integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, + } read-yaml-file@1.1.0: - resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==, + } + engines: { node: ">=6" } regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + resolution: + { + integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, + } resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: ">=4" } resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, + } + engines: { node: ">=8" } reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + resolution: + { + integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, + } + engines: { iojs: ">=1.0.0", node: ">=0.10.0" } run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + resolution: + { + integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, + } safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, + } semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==, + } + engines: { node: ">=10" } hasBin: true shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, + } + engines: { node: ">=8" } shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, + } + engines: { node: ">=8" } signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} + resolution: + { + integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, + } + engines: { node: ">=14" } + + sisteransi@1.0.5: + resolution: + { + integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, + } slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, + } + engines: { node: ">=8" } source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: ">=0.10.0" } spawndamnit@3.0.1: - resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + resolution: + { + integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==, + } sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + resolution: + { + integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, + } string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, + } + engines: { node: ">=8" } string-width@7.2.0: - resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, + } + engines: { node: ">=18" } strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, + } + engines: { node: ">=8" } strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==, + } + engines: { node: ">=12" } strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, + } + engines: { node: ">=4" } term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} + resolution: + { + integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==, + } + engines: { node: ">=8" } to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + resolution: + { + integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, + } + engines: { node: ">=8.0" } type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} + resolution: + { + integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, + } + engines: { node: ">=16" } typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} - engines: {node: '>=14.17'} + resolution: + { + integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==, + } + engines: { node: ">=14.17" } hasBin: true ufo@1.6.1: - resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + resolution: + { + integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==, + } undici-types@7.10.0: - resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + resolution: + { + integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==, + } universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} + resolution: + { + integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, + } + engines: { node: ">= 4.0.0" } which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} + resolution: + { + integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, + } + engines: { node: ">= 8" } hasBin: true widest-line@5.0.0: - resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==, + } + engines: { node: ">=18" } wrap-ansi@9.0.2: - resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==, + } + engines: { node: ">=18" } snapshots: - - '@babel/code-frame@7.27.1': + "@babel/code-frame@7.27.1": dependencies: - '@babel/helper-validator-identifier': 7.27.1 + "@babel/helper-validator-identifier": 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/helper-string-parser@7.25.9': {} + "@babel/helper-string-parser@7.25.9": {} - '@babel/helper-validator-identifier@7.25.9': {} + "@babel/helper-validator-identifier@7.25.9": {} - '@babel/helper-validator-identifier@7.27.1': {} + "@babel/helper-validator-identifier@7.27.1": {} - '@babel/parser@7.27.0': + "@babel/parser@7.27.0": dependencies: - '@babel/types': 7.27.0 + "@babel/types": 7.27.0 - '@babel/runtime@7.27.0': + "@babel/runtime@7.27.0": dependencies: regenerator-runtime: 0.14.1 - '@babel/types@7.27.0': + "@babel/types@7.27.0": dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 + "@babel/helper-string-parser": 7.25.9 + "@babel/helper-validator-identifier": 7.25.9 - '@changesets/apply-release-plan@7.0.13': + "@changesets/apply-release-plan@7.0.13": dependencies: - '@changesets/config': 3.1.1 - '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.4 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/config": 3.1.1 + "@changesets/get-version-range-type": 0.4.0 + "@changesets/git": 3.0.4 + "@changesets/should-skip-package": 0.1.2 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 detect-indent: 6.1.0 fs-extra: 7.0.1 lodash.startcase: 4.4.0 @@ -644,37 +1106,37 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.1 - '@changesets/assemble-release-plan@6.0.9': + "@changesets/assemble-release-plan@6.0.9": dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/errors": 0.2.0 + "@changesets/get-dependents-graph": 2.1.3 + "@changesets/should-skip-package": 0.1.2 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 semver: 7.7.1 - '@changesets/changelog-git@0.2.1': - dependencies: - '@changesets/types': 6.1.0 - - '@changesets/cli@2.29.7(@types/node@24.3.3)': - dependencies: - '@changesets/apply-release-plan': 7.0.13 - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.1 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.13 - '@changesets/git': 3.0.4 - '@changesets/logger': 0.1.1 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.5 - '@changesets/should-skip-package': 0.1.2 - '@changesets/types': 6.1.0 - '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.1(@types/node@24.3.3) - '@manypkg/get-packages': 1.1.3 + "@changesets/changelog-git@0.2.1": + dependencies: + "@changesets/types": 6.1.0 + + "@changesets/cli@2.29.7(@types/node@24.3.3)": + dependencies: + "@changesets/apply-release-plan": 7.0.13 + "@changesets/assemble-release-plan": 6.0.9 + "@changesets/changelog-git": 0.2.1 + "@changesets/config": 3.1.1 + "@changesets/errors": 0.2.0 + "@changesets/get-dependents-graph": 2.1.3 + "@changesets/get-release-plan": 4.0.13 + "@changesets/git": 3.0.4 + "@changesets/logger": 0.1.1 + "@changesets/pre": 2.0.2 + "@changesets/read": 0.6.5 + "@changesets/should-skip-package": 0.1.2 + "@changesets/types": 6.1.0 + "@changesets/write": 0.4.0 + "@inquirer/external-editor": 1.0.1(@types/node@24.3.3) + "@manypkg/get-packages": 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 enquirer: 2.4.1 @@ -688,130 +1150,141 @@ snapshots: spawndamnit: 3.0.1 term-size: 2.2.1 transitivePeerDependencies: - - '@types/node' + - "@types/node" - '@changesets/config@3.1.1': + "@changesets/config@3.1.1": dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.3 - '@changesets/logger': 0.1.1 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/errors": 0.2.0 + "@changesets/get-dependents-graph": 2.1.3 + "@changesets/logger": 0.1.1 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 fs-extra: 7.0.1 micromatch: 4.0.8 - '@changesets/errors@0.2.0': + "@changesets/errors@0.2.0": dependencies: extendable-error: 0.1.7 - '@changesets/get-dependents-graph@2.1.3': + "@changesets/get-dependents-graph@2.1.3": dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 picocolors: 1.1.1 semver: 7.7.1 - '@changesets/get-release-plan@4.0.13': + "@changesets/get-release-plan@4.0.13": dependencies: - '@changesets/assemble-release-plan': 6.0.9 - '@changesets/config': 3.1.1 - '@changesets/pre': 2.0.2 - '@changesets/read': 0.6.5 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/assemble-release-plan": 6.0.9 + "@changesets/config": 3.1.1 + "@changesets/pre": 2.0.2 + "@changesets/read": 0.6.5 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 - '@changesets/get-version-range-type@0.4.0': {} + "@changesets/get-version-range-type@0.4.0": {} - '@changesets/git@3.0.4': + "@changesets/git@3.0.4": dependencies: - '@changesets/errors': 0.2.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/errors": 0.2.0 + "@manypkg/get-packages": 1.1.3 is-subdir: 1.2.0 micromatch: 4.0.8 spawndamnit: 3.0.1 - '@changesets/logger@0.1.1': + "@changesets/logger@0.1.1": dependencies: picocolors: 1.1.1 - '@changesets/parse@0.4.1': + "@changesets/parse@0.4.1": dependencies: - '@changesets/types': 6.1.0 + "@changesets/types": 6.1.0 js-yaml: 3.14.1 - '@changesets/pre@2.0.2': + "@changesets/pre@2.0.2": dependencies: - '@changesets/errors': 0.2.0 - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/errors": 0.2.0 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 fs-extra: 7.0.1 - '@changesets/read@0.6.5': + "@changesets/read@0.6.5": dependencies: - '@changesets/git': 3.0.4 - '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.1 - '@changesets/types': 6.1.0 + "@changesets/git": 3.0.4 + "@changesets/logger": 0.1.1 + "@changesets/parse": 0.4.1 + "@changesets/types": 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 picocolors: 1.1.1 - '@changesets/should-skip-package@0.1.2': + "@changesets/should-skip-package@0.1.2": dependencies: - '@changesets/types': 6.1.0 - '@manypkg/get-packages': 1.1.3 + "@changesets/types": 6.1.0 + "@manypkg/get-packages": 1.1.3 - '@changesets/types@4.1.0': {} + "@changesets/types@4.1.0": {} - '@changesets/types@6.1.0': {} + "@changesets/types@6.1.0": {} - '@changesets/write@0.4.0': + "@changesets/write@0.4.0": dependencies: - '@changesets/types': 6.1.0 + "@changesets/types": 6.1.0 fs-extra: 7.0.1 human-id: 4.1.1 prettier: 2.8.8 - '@inquirer/external-editor@1.0.1(@types/node@24.3.3)': + "@clack/core@0.5.0": + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + "@clack/prompts@0.11.0": + dependencies: + "@clack/core": 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + + "@inquirer/external-editor@1.0.1(@types/node@24.3.3)": dependencies: chardet: 2.1.0 iconv-lite: 0.6.3 optionalDependencies: - '@types/node': 24.3.3 + "@types/node": 24.3.3 - '@manypkg/find-root@1.1.0': + "@manypkg/find-root@1.1.0": dependencies: - '@babel/runtime': 7.27.0 - '@types/node': 12.20.55 + "@babel/runtime": 7.27.0 + "@types/node": 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 - '@manypkg/get-packages@1.1.3': + "@manypkg/get-packages@1.1.3": dependencies: - '@babel/runtime': 7.27.0 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 + "@babel/runtime": 7.27.0 + "@changesets/types": 4.1.0 + "@manypkg/find-root": 1.1.0 fs-extra: 8.1.0 globby: 11.1.0 read-yaml-file: 1.1.0 - '@nodelib/fs.scandir@2.1.5': + "@nodelib/fs.scandir@2.1.5": dependencies: - '@nodelib/fs.stat': 2.0.5 + "@nodelib/fs.stat": 2.0.5 run-parallel: 1.2.0 - '@nodelib/fs.stat@2.0.5': {} + "@nodelib/fs.stat@2.0.5": {} - '@nodelib/fs.walk@1.2.8': + "@nodelib/fs.walk@1.2.8": dependencies: - '@nodelib/fs.scandir': 2.1.5 + "@nodelib/fs.scandir": 2.1.5 fastq: 1.19.1 - '@types/js-yaml@4.0.9': {} + "@types/js-yaml@4.0.9": {} - '@types/node@12.20.55': {} + "@types/node@12.20.55": {} - '@types/node@24.3.3': + "@types/node@24.3.3": dependencies: undici-types: 7.10.0 @@ -912,8 +1385,8 @@ snapshots: fast-glob@3.3.3: dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 + "@nodelib/fs.stat": 2.0.5 + "@nodelib/fs.walk": 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 @@ -1020,8 +1493,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 + "@babel/parser": 7.27.0 + "@babel/types": 7.27.0 source-map-js: 1.2.1 merge2@1.4.1: {} @@ -1061,7 +1534,7 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.27.1 + "@babel/code-frame": 7.27.1 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -1117,6 +1590,8 @@ snapshots: signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + slash@3.0.0: {} source-map-js@1.2.1: {} From 6a41237b14f2ee19e40733f34cf4dd026967b557 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:58:09 -0600 Subject: [PATCH 14/77] feat: implement preset system for configuration templates - Created preset interface and registry structure - Implemented four presets: Conventional, Gitmoji, Angular, and Minimal - Added buildConfig function to generate complete configurations - Each preset includes commit types with descriptions and emojis - Presets provide sensible defaults for emoji and scope modes --- src/lib/presets/angular.ts | 74 +++++++++++++++++++++ src/lib/presets/conventional.ts | 59 +++++++++++++++++ src/lib/presets/gitmoji.ts | 74 +++++++++++++++++++++ src/lib/presets/index.ts | 114 ++++++++++++++++++++++++++++++++ src/lib/presets/minimal.ts | 44 ++++++++++++ 5 files changed, 365 insertions(+) create mode 100644 src/lib/presets/angular.ts create mode 100644 src/lib/presets/conventional.ts create mode 100644 src/lib/presets/gitmoji.ts create mode 100644 src/lib/presets/index.ts create mode 100644 src/lib/presets/minimal.ts diff --git a/src/lib/presets/angular.ts b/src/lib/presets/angular.ts new file mode 100644 index 0000000..32c7fd0 --- /dev/null +++ b/src/lib/presets/angular.ts @@ -0,0 +1,74 @@ +/** + * Angular Preset + * + * Strict commit message convention used by the Angular project + * and many enterprise teams. Enforces consistent commit formatting + * with comprehensive type coverage. + * + * Format: type(scope): subject + * Example: feat(compiler): add support for standalone components + */ + +import type { Preset } from "./index.js"; + +export const angularPreset: Preset = { + id: "angular", + name: "Angular Convention", + description: "Strict format used by Angular and enterprise teams", + defaults: { + emoji_enabled: false, + scope_mode: "optional", + }, + types: [ + { + id: "feat", + description: "A new feature", + emoji: "✨", + }, + { + id: "fix", + description: "A bug fix", + emoji: "🐛", + }, + { + id: "docs", + description: "Documentation only changes", + emoji: "📚", + }, + { + id: "style", + description: "Changes that do not affect code meaning", + emoji: "💄", + }, + { + id: "refactor", + description: "Code change that neither fixes a bug nor adds a feature", + emoji: "♻️", + }, + { + id: "perf", + description: "Code change that improves performance", + emoji: "⚡", + }, + { + id: "test", + description: "Adding missing tests or correcting existing tests", + emoji: "🧪", + }, + { + id: "build", + description: "Changes that affect the build system or dependencies", + emoji: "🏗️", + }, + { + id: "ci", + description: "Changes to CI configuration files and scripts", + emoji: "💚", + }, + { + id: "chore", + description: "Other changes that don't modify src or test files", + emoji: "🔧", + }, + ], +}; diff --git a/src/lib/presets/conventional.ts b/src/lib/presets/conventional.ts new file mode 100644 index 0000000..7360c85 --- /dev/null +++ b/src/lib/presets/conventional.ts @@ -0,0 +1,59 @@ +/** + * Conventional Commits Preset + * + * Industry-standard commit message convention used widely in + * open-source projects. Provides clear semantic commit types + * with optional scopes for better organization. + * + * Format: type(scope): subject + * Example: feat(api): add user authentication endpoint + */ + +import type { Preset } from "./index.js"; + +export const conventionalPreset: Preset = { + id: "conventional", + name: "Conventional Commits", + description: "Industry-standard format used by most open-source projects", + defaults: { + emoji_enabled: false, + scope_mode: "optional", + }, + types: [ + { + id: "feat", + description: "A new feature for the user", + emoji: "✨", + }, + { + id: "fix", + description: "A bug fix for the user", + emoji: "🐛", + }, + { + id: "docs", + description: "Documentation changes", + emoji: "📚", + }, + { + id: "style", + description: "Code style changes (formatting, semicolons, etc.)", + emoji: "💄", + }, + { + id: "refactor", + description: "Code refactoring without changing functionality", + emoji: "♻️", + }, + { + id: "test", + description: "Adding or updating tests", + emoji: "🧪", + }, + { + id: "chore", + description: "Maintenance tasks, build changes, etc.", + emoji: "🔧", + }, + ], +}; diff --git a/src/lib/presets/gitmoji.ts b/src/lib/presets/gitmoji.ts new file mode 100644 index 0000000..a897b29 --- /dev/null +++ b/src/lib/presets/gitmoji.ts @@ -0,0 +1,74 @@ +/** + * Gitmoji Preset + * + * Visual commit format using emojis to represent commit types. + * Popular in creative and frontend development communities for + * improved scannability in commit logs. + * + * Format: emoji type(scope): subject + * Example: ✨ feat(ui): add dark mode toggle + */ + +import type { Preset } from "./index.js"; + +export const gitmojiPreset: Preset = { + id: "gitmoji", + name: "Gitmoji Style", + description: "Visual commits with emojis for better scannability", + defaults: { + emoji_enabled: true, + scope_mode: "optional", + }, + types: [ + { + id: "feat", + description: "Introduce new features", + emoji: "✨", + }, + { + id: "fix", + description: "Fix a bug", + emoji: "🐛", + }, + { + id: "docs", + description: "Add or update documentation", + emoji: "📚", + }, + { + id: "style", + description: "Improve structure or format of code", + emoji: "🎨", + }, + { + id: "refactor", + description: "Refactor code", + emoji: "♻️", + }, + { + id: "perf", + description: "Improve performance", + emoji: "⚡", + }, + { + id: "test", + description: "Add or update tests", + emoji: "✅", + }, + { + id: "build", + description: "Add or update build scripts", + emoji: "👷", + }, + { + id: "ci", + description: "Add or update CI configuration", + emoji: "💚", + }, + { + id: "chore", + description: "Miscellaneous chores", + emoji: "🔧", + }, + ], +}; diff --git a/src/lib/presets/index.ts b/src/lib/presets/index.ts new file mode 100644 index 0000000..56df7ef --- /dev/null +++ b/src/lib/presets/index.ts @@ -0,0 +1,114 @@ +/** + * Preset System + * + * Provides pre-configured commit convention templates that can be + * used to quickly initialize project configuration. Each preset + * includes commit types, format settings, and sensible defaults. + * + * Presets available: + * - Conventional: Industry-standard semantic commit format + * - Gitmoji: Visual emoji-based commit types + * - Angular: Strict format used in Angular projects + * - Minimal: Basic setup for custom configuration + */ + +import type { LabcommitrConfig } from "../config/types.js"; + +/** + * Preset definition interface + * Defines structure and defaults for a commit convention + */ +export interface Preset { + id: string; + name: string; + description: string; + defaults: { + emoji_enabled: boolean; + scope_mode: "optional" | "selective" | "always" | "never"; + }; + types: Array<{ + id: string; + description: string; + emoji: string; + }>; +} + +// Import individual preset definitions +import { conventionalPreset } from "./conventional.js"; +import { gitmojiPreset } from "./gitmoji.js"; +import { angularPreset } from "./angular.js"; +import { minimalPreset } from "./minimal.js"; + +/** + * Preset registry + * Maps preset IDs to their configurations + */ +export const PRESETS: Record = { + conventional: conventionalPreset, + gitmoji: gitmojiPreset, + angular: angularPreset, + minimal: minimalPreset, +}; + +/** + * Get preset configuration by ID + * Throws error if preset not found + */ +export function getPreset(id: string): Preset { + const preset = PRESETS[id]; + if (!preset) { + throw new Error(`Unknown preset: ${id}`); + } + return preset; +} + +/** + * Build complete configuration from preset and user choices + * Merges preset defaults with user customizations + */ +export function buildConfig( + presetId: string, + customizations: { + emoji?: boolean; + scope?: "optional" | "selective" | "always" | "never"; + scopeRequiredFor?: string[]; + }, +): LabcommitrConfig { + const preset = getPreset(presetId); + + // Determine which types require scopes + let requireScopeFor: string[] = []; + const scopeMode = customizations.scope ?? preset.defaults.scope_mode; + + if (scopeMode === "always") { + requireScopeFor = preset.types.map((t) => t.id); + } else if (scopeMode === "selective" && customizations.scopeRequiredFor) { + requireScopeFor = customizations.scopeRequiredFor; + } + + return { + version: "1.0", + config: { + emoji_enabled: customizations.emoji ?? preset.defaults.emoji_enabled, + force_emoji_detection: null, + }, + format: { + template: "{type}({scope}): {subject}", + subject_max_length: 50, + }, + types: preset.types, + validation: { + require_scope_for: requireScopeFor, + allowed_scopes: [], + subject_min_length: 3, + prohibited_words: [], + }, + advanced: { + aliases: {}, + git: { + auto_stage: false, + sign_commits: false, + }, + }, + }; +} diff --git a/src/lib/presets/minimal.ts b/src/lib/presets/minimal.ts new file mode 100644 index 0000000..cd9b088 --- /dev/null +++ b/src/lib/presets/minimal.ts @@ -0,0 +1,44 @@ +/** + * Minimal Preset + * + * Basic starting configuration with essential commit types. + * Designed for teams who want to build custom conventions + * from a simple foundation. + * + * Format: type(scope): subject + * Example: feat: implement new feature + */ + +import type { Preset } from "./index.js"; + +export const minimalPreset: Preset = { + id: "minimal", + name: "Minimal Setup", + description: "Start with basics, customize everything yourself later", + defaults: { + emoji_enabled: false, + scope_mode: "optional", + }, + types: [ + { + id: "feat", + description: "New feature", + emoji: "✨", + }, + { + id: "fix", + description: "Bug fix", + emoji: "🐛", + }, + { + id: "docs", + description: "Documentation", + emoji: "📚", + }, + { + id: "chore", + description: "Maintenance", + emoji: "🔧", + }, + ], +}; From a0e2eaf59abedd27cfa225bb5e50a93da84bdf10 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:58:23 -0600 Subject: [PATCH 15/77] feat: implement Clef animated mascot - Created Clef character with full-body ASCII art frames - Implemented ANSI-based terminal animations using cursor control - Added walk animations for smooth horizontal movement - Implemented intro, processing, and outro sequences - Terminal capability detection for graceful degradation - Screen clearing between sections for clean output - Non-blocking animations that enhance UX without distraction --- src/cli/commands/init/clef.ts | 250 ++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 src/cli/commands/init/clef.ts diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts new file mode 100644 index 0000000..ba64415 --- /dev/null +++ b/src/cli/commands/init/clef.ts @@ -0,0 +1,250 @@ +/** + * Clef the Cat - Animated CLI Mascot + * + * Provides animated character that appears at key moments during + * the initialization flow. Uses ANSI escape codes for terminal + * control and animation effects. + * + * Appearance pattern: + * - Intro: Walks in, introduces tool, walks off + * - Processing: Appears during config generation + * - Outro: Celebrates completion and exits + * + * Gracefully degrades to static display in non-TTY environments. + */ + +import { setTimeout as sleep } from "timers/promises"; +import pc from "picocolors"; + +interface AnimationCapabilities { + supportsAnimation: boolean; + supportsColor: boolean; + terminalWidth: number; + terminalHeight: number; +} + +/** + * Clef mascot controller + * Manages all animation sequences and terminal interactions + */ +class Clef { + private caps: AnimationCapabilities; + private currentX: number = 0; + + // ASCII art frames for different states + private readonly frames = { + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_) `, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|) `, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_) `, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_) `, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_) `, + }; + + constructor() { + this.caps = this.detectCapabilities(); + } + + /** + * Detect terminal animation and color support + * Returns capability object for conditional rendering + */ + private detectCapabilities(): AnimationCapabilities { + return { + supportsAnimation: + process.stdout.isTTY && + process.env.TERM !== "dumb" && + !process.env.CI && + process.env.NO_COLOR !== "1", + supportsColor: process.stdout.isTTY && process.env.NO_COLOR !== "1", + terminalWidth: process.stdout.columns || 80, + terminalHeight: process.stdout.rows || 24, + }; + } + + /** + * Clear entire screen including scrollback buffer + */ + private clearScreen(): void { + if (this.caps.supportsAnimation) { + process.stdout.write("\x1B[2J"); // Clear screen + process.stdout.write("\x1B[H"); // Move cursor to home + process.stdout.write("\x1B[3J"); // Clear scrollback + } + } + + /** + * Hide terminal cursor during animations + */ + private hideCursor(): void { + if (this.caps.supportsAnimation) { + process.stdout.write("\x1B[?25l"); + } + } + + /** + * Show terminal cursor after animations + */ + private showCursor(): void { + if (this.caps.supportsAnimation) { + process.stdout.write("\x1B[?25h"); + } + } + + /** + * Render ASCII art frame at specific horizontal position + * Uses absolute cursor positioning for each line + */ + private renderFrame(frame: string, x: number): void { + const lines = frame.split("\n"); + lines.forEach((line, idx) => { + // Move cursor to position (row, column) + process.stdout.write(`\x1B[${idx + 5};${x}H`); + process.stdout.write(line); + }); + } + + /** + * Animate character walking from start to end position + * Creates smooth horizontal movement using frame interpolation + */ + private async walk( + startX: number, + endX: number, + duration: number, + ): Promise { + if (!this.caps.supportsAnimation) return; + + const frames = [this.frames.walk1, this.frames.walk2]; + const steps = 15; + const delay = duration / steps; + + this.hideCursor(); + + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const currentX = Math.floor(startX + (endX - startX) * progress); + + this.clearScreen(); + this.renderFrame(frames[i % 2], currentX); + + await sleep(delay); + } + + this.showCursor(); + this.currentX = endX; + } + + /** + * Introduction sequence + * Character walks in from left, displays greeting, then exits + * Duration: approximately 3 seconds + */ + async intro(): Promise { + if (!this.caps.supportsAnimation) { + // Static fallback for non-TTY environments + console.log(this.frames.standing); + console.log("Hey there! My name is Clef!"); + console.log("Let me help you get started...meoww!\n"); + await sleep(2000); + return; + } + + // Walk in from left side + await this.walk(0, 10, 1000); + + // Display introduction message + this.clearScreen(); + console.log(this.frames.standing); + console.log(pc.cyan(" Hey there! My name is Clef!")); + console.log(pc.cyan(" Let me help you get started...meoww!\n")); + + await sleep(2000); + + // Walk off to right side + await this.walk(10, this.caps.terminalWidth, 800); + + // Complete clear for next section + this.clearScreen(); + } + + /** + * Processing sequence + * Shows character typing while async task executes + * Duration: depends on task execution time + */ + async processing(message: string, task: () => Promise): Promise { + if (!this.caps.supportsAnimation) { + // Static fallback + console.log(this.frames.typing); + console.log(message); + await task(); + return; + } + + // Walk in from left + await this.walk(0, 10, 800); + + // Show typing animation + this.clearScreen(); + console.log(this.frames.typing); + console.log(pc.dim(` ${message}`)); + + // Execute actual task + await task(); + + await sleep(500); + + // Walk off to right + await this.walk(10, this.caps.terminalWidth, 800); + + // Complete clear + this.clearScreen(); + } + + /** + * Outro sequence + * Character celebrates completion and waves goodbye + * Duration: approximately 4 seconds + */ + async outro(): Promise { + if (!this.caps.supportsAnimation) { + // Static fallback + console.log(this.frames.waving); + console.log("Happy committing!\n"); + await sleep(2000); + return; + } + + // Walk in quickly + await this.walk(0, 10, 600); + + // Show celebration + this.clearScreen(); + console.log(this.frames.celebrate); + await sleep(1000); + + // Wave goodbye + this.clearScreen(); + console.log(this.frames.waving); + console.log(pc.cyan(" Happy committing!\n")); + await sleep(2000); + + // Walk off + await this.walk(10, this.caps.terminalWidth, 800); + + // Final complete clear + this.clearScreen(); + } + + /** + * Stop any running animation + * Ensures cursor is visible on interrupt + */ + stop(): void { + this.showCursor(); + } +} + +// Singleton instance for use across init command +export const clef = new Clef(); From 6b2e9d6d61a507a9f3846799c4ca8bad604a8fd7 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:58:38 -0600 Subject: [PATCH 16/77] feat: implement clean interactive prompts - Created compact color-coded label system for visual hierarchy - Implemented preset selection prompt with four options - Added emoji support preference prompt - Implemented scope configuration with multiple modes - Added selective scope type selection for custom rules - Display functions for summary and next steps - Clean minimal design without boxes or excessive whitespace --- src/cli/commands/init/prompts.ts | 194 +++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 src/cli/commands/init/prompts.ts diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts new file mode 100644 index 0000000..94626f9 --- /dev/null +++ b/src/cli/commands/init/prompts.ts @@ -0,0 +1,194 @@ +/** + * Interactive Prompts + * + * Provides clean, minimal prompts for user configuration choices. + * Uses compact color-coded labels on the left side for visual + * hierarchy without boxes or excessive whitespace. + * + * Label pattern: [colored label] [2 spaces] [content] + */ + +import { select, multiselect, isCancel } from "@clack/prompts"; +import pc from "picocolors"; + +/** + * Create compact color-coded label + * Labels are 7 characters wide (6 chars + padding) for alignment + */ +function label( + text: string, + color: "magenta" | "cyan" | "blue" | "yellow" | "green", +): string { + const colorFn = { + magenta: pc.bgMagenta, + cyan: pc.bgCyan, + blue: pc.bgBlue, + yellow: pc.bgYellow, + green: pc.bgGreen, + }[color]; + + return colorFn(pc.black(` ${text.padEnd(6)} `)); +} + +/** + * Handle prompt cancellation + * Exits process gracefully when user cancels + */ +function handleCancel(value: unknown): void { + if (isCancel(value)) { + console.log("\nSetup cancelled."); + process.exit(0); + } +} + +/** + * Prompt for commit style preset selection + */ +export async function promptPreset(): Promise { + const preset = await select({ + message: `${label("preset", "magenta")} Which commit style fits your project?`, + options: [ + { + value: "conventional", + label: "Conventional Commits", + hint: "Industry-standard format (recommended)", + }, + { + value: "gitmoji", + label: "Gitmoji Style", + hint: "Visual commits with emojis", + }, + { + value: "angular", + label: "Angular Convention", + hint: "Strict enterprise format", + }, + { + value: "minimal", + label: "Minimal Setup", + hint: "Start basic, customize later", + }, + ], + }); + + handleCancel(preset); + return preset as string; +} + +/** + * Prompt for emoji support preference + */ +export async function promptEmoji(): Promise { + const emoji = await select({ + message: `${label("emoji", "cyan")} Enable emoji support?`, + options: [ + { + value: false, + label: "No", + hint: "Text-only (recommended)", + }, + { + value: true, + label: "Yes", + hint: "Visual emojis in commits", + }, + ], + }); + + handleCancel(emoji); + return emoji as boolean; +} + +/** + * Prompt for scope configuration mode + */ +export async function promptScope(): Promise< + "optional" | "selective" | "always" | "never" +> { + const scope = await select({ + message: `${label("scope", "blue")} How should scopes work?`, + options: [ + { + value: "optional", + label: "Optional", + hint: "Flexible (recommended)", + }, + { + value: "selective", + label: "Required for specific types", + hint: "Customizable rules", + }, + { + value: "always", + label: "Always required", + hint: "Strict enforcement", + }, + { + value: "never", + label: "Never use scopes", + hint: "Simplest format", + }, + ], + }); + + handleCancel(scope); + return scope as "optional" | "selective" | "always" | "never"; +} + +/** + * Prompt for selective scope type selection + * Only shown when user selects "selective" scope mode + */ +export async function promptScopeTypes( + types: Array<{ id: string; description: string }>, +): Promise { + const selected = await multiselect({ + message: `${label("types", "blue")} Which types require a scope?`, + options: types.map((type) => ({ + value: type.id, + label: type.id, + hint: type.description, + })), + required: false, + }); + + handleCancel(selected); + return selected as string[]; +} + +/** + * Display configuration summary + * Shows user choices before config generation + */ +export function displaySummary(config: { + preset: string; + emoji: boolean; + scope: string; +}): void { + console.log("\n✓ Configuration ready!\n"); + console.log(` ■ Preset: ${config.preset}`); + console.log(` ■ Emoji: ${config.emoji ? "Enabled" : "Disabled"}`); + console.log(` ■ Scope: ${config.scope}`); + console.log(` ■ Types: feat, fix, docs, style, refactor, test, chore\n`); +} + +/** + * Display configuration file write result + */ +export function displayConfigResult(filename: string): void { + console.log(`${label("config", "green")} Writing ${pc.cyan(filename)}`); + console.log(" Done\n"); +} + +/** + * Display next steps after successful setup + */ +export function displayNextSteps(): void { + console.log("✓ Ready to commit!\n"); + console.log(`${label("next", "cyan")} Get started with these commands:\n`); + console.log(" lab config show View your configuration"); + console.log(" lab commit Create your first commit\n"); + console.log( + " Customize anytime by editing .labcommitr.config.yaml\n", + ); +} From c908b1f6bdafad7fea2fc75026e7e70f688f3ed4 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:58:51 -0600 Subject: [PATCH 17/77] feat: implement configuration file generator - YAML generation with proper formatting and indentation - Pre-write validation to ensure valid output - Header comments with documentation link - Integration with ConfigValidator for quality assurance - Clean file writing to project root directory --- src/cli/commands/init/config-generator.ts | 65 +++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/cli/commands/init/config-generator.ts diff --git a/src/cli/commands/init/config-generator.ts b/src/cli/commands/init/config-generator.ts new file mode 100644 index 0000000..c3fb083 --- /dev/null +++ b/src/cli/commands/init/config-generator.ts @@ -0,0 +1,65 @@ +/** + * Configuration File Generator + * + * Generates YAML configuration files from user choices and preset + * definitions. Includes validation before writing to ensure all + * generated configs are valid. + */ + +import { writeFile } from "fs/promises"; +import { dump } from "js-yaml"; +import path from "path"; +import type { LabcommitrConfig } from "../../../lib/config/types.js"; +import { ConfigValidator } from "../../../lib/config/validator.js"; + +/** + * Add header comment to YAML output + * Provides context and documentation link for users + */ +function addHeader(yaml: string): string { + return `# Labcommitr Configuration +# Generated by: lab init +# Edit this file to customize your commit workflow +# Documentation: https://github.com/labcatr/labcommitr#config + +${yaml}`; +} + +/** + * Generate and write configuration file + * Validates config before writing to prevent invalid output + * + * @param config - Complete configuration object + * @param projectRoot - Path to project root directory + * @returns Path to written config file + */ +export async function generateConfigFile( + config: LabcommitrConfig, + projectRoot: string, +): Promise { + // Validate config before writing + const validator = new ConfigValidator(); + const validation = validator.validate(config); + + if (!validation.valid) { + throw new Error( + `Generated config failed validation: ${validation.errors[0].message}`, + ); + } + + // Convert to YAML with formatting options + const yaml = dump(config, { + indent: 2, + lineWidth: 80, + noRefs: true, + }); + + // Add header comments + const content = addHeader(yaml); + + // Write to file + const configPath = path.join(projectRoot, ".labcommitr.config.yaml"); + await writeFile(configPath, content, "utf-8"); + + return configPath; +} From 17f5d5d16a317f327a64de36f6fe04ebaa3c4330 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:59:13 -0600 Subject: [PATCH 18/77] feat: implement complete init command flow - Orchestrated full initialization experience with Clef animations - Project root detection with git repository priority - Existing configuration detection with force override option - Integration of prompts, preset selection, and config generation - Processing animation during config file creation - Clean screen transitions between sections - Comprehensive error handling with cursor cleanup - Removed old placeholder init command - Updated command exports and program registration --- src/cli/commands/index.ts | 7 +- src/cli/commands/init.ts | 55 ----------- src/cli/commands/init/index.ts | 165 +++++++++++++++++++++++++++++++++ src/cli/program.ts | 29 +++--- 4 files changed, 182 insertions(+), 74 deletions(-) delete mode 100644 src/cli/commands/init.ts create mode 100644 src/cli/commands/init/index.ts diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index 4219134..6930219 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -5,7 +5,6 @@ * Makes imports cleaner and provides single location for command overview. */ -export { configCommand } from './config.js'; -export { initCommand } from './init.js'; -export { commitCommand } from './commit.js'; - +export { configCommand } from "./config.js"; +export { initCommand } from "./init/index.js"; +export { commitCommand } from "./commit.js"; diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts deleted file mode 100644 index a174bc2..0000000 --- a/src/cli/commands/init.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Init command implementation - * - * Interactive initialization command that guides users through creating - * a .labcommitr.config.yaml file with preset options. - * - * Step 5 Implementation: Astro-like interactive experience - * - Preset selection (Conventional Commits, Gitmoji, Angular, Custom) - * - Configuration options (emoji support, scope rules, etc.) - * - Config file generation with user choices - * - * Current Status: Placeholder for Step 4 - */ - -import { Command } from 'commander'; -import { Logger } from '../../lib/logger.js'; - -/** - * Init command - */ -export const initCommand = new Command('init') - .description('Initialize labcommitr configuration in your project') - .option('-f, --force', 'Overwrite existing configuration') - .option( - '--preset ', - 'Use a specific preset (conventional, gitmoji, angular)', - ) - .action(initAction); - -/** - * Init action handler (placeholder) - * TODO (Step 5): Implement interactive initialization flow - */ -async function initAction(options: { - force?: boolean; - preset?: string; -}): Promise { - Logger.info('Init command - Coming in Step 5!'); - Logger.info('\nPlanned features:'); - console.log( - ' • Interactive preset selection (Conventional Commits, Gitmoji, etc.)', - ); - console.log(' • Customizable commit types and formats'); - console.log(' • Automatic .labcommitr.config.yaml generation'); - console.log(' • Non-destructive updates (--force to override)'); - - if (options.preset) { - Logger.info(`\nYou specified preset: ${options.preset}`); - } - - if (options.force) { - Logger.warn('Force mode will overwrite existing config when implemented'); - } -} - diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts new file mode 100644 index 0000000..ff1883f --- /dev/null +++ b/src/cli/commands/init/index.ts @@ -0,0 +1,165 @@ +/** + * Init Command + * + * Interactive initialization command that guides users through creating + * a project-specific configuration file. Provides preset selection, + * customization options, and animated mascot for enhanced UX. + * + * Flow: + * 1. Intro animation (Clef introduces tool) + * 2. User prompts (preset, emoji, scope choices) + * 3. Processing animation (config generation) + * 4. Outro animation (Clef celebrates) + */ + +import { Command } from "commander"; +import { existsSync } from "fs"; +import path from "path"; +import { clef } from "./clef.js"; +import { + promptPreset, + promptEmoji, + promptScope, + promptScopeTypes, + displaySummary, + displayConfigResult, + displayNextSteps, +} from "./prompts.js"; +import { buildConfig, getPreset } from "../../../lib/presets/index.js"; +import { generateConfigFile } from "./config-generator.js"; +import { Logger } from "../../../lib/logger.js"; + +/** + * Detect project root directory + * Prioritizes git repository root, falls back to current directory + */ +async function detectProjectRoot(): Promise { + const { execSync } = await import("child_process"); + + try { + // Try to find git root + const gitRoot = execSync("git rev-parse --show-toplevel", { + encoding: "utf-8", + stdio: ["pipe", "pipe", "ignore"], + }).trim(); + + return gitRoot; + } catch { + // Not in a git repository + return null; + } +} + +/** + * Check if configuration file already exists + */ +function configExists(projectRoot: string): boolean { + const configPath = path.join(projectRoot, ".labcommitr.config.yaml"); + return existsSync(configPath); +} + +/** + * Init command definition + */ +export const initCommand = new Command("init") + .description("Initialize labcommitr configuration in your project") + .option("-f, --force", "Overwrite existing configuration") + .option( + "--preset ", + "Use a specific preset (conventional, gitmoji, angular, minimal)", + ) + .action(initAction); + +/** + * Init action handler + * Orchestrates the complete initialization flow + */ +async function initAction(options: { + force?: boolean; + preset?: string; +}): Promise { + try { + // Intro: Clef introduces herself + await clef.intro(); + // Screen is now completely clear + + // Detect project root + const projectRoot = await detectProjectRoot(); + if (!projectRoot) { + Logger.error("Not a git repository. Initialize git first: git init"); + process.exit(1); + } + + // Check for existing config + if (configExists(projectRoot) && !options.force) { + Logger.error("Configuration already exists. Use --force to overwrite."); + Logger.info(`File: ${path.join(projectRoot, ".labcommitr.config.yaml")}`); + process.exit(1); + } + + // Prompts: Clean labels, no cat + const presetId = options.preset || (await promptPreset()); + const preset = getPreset(presetId); + + const emojiEnabled = await promptEmoji(); + const scopeMode = await promptScope(); + + // If selective scope mode, ask which types require scopes + let scopeRequiredFor: string[] = []; + if (scopeMode === "selective") { + scopeRequiredFor = await promptScopeTypes(preset.types); + } + + // Display summary + displaySummary({ + preset: preset.name, + emoji: emojiEnabled, + scope: scopeMode, + }); + + // Small pause before processing + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Build config from choices + const config = buildConfig(presetId, { + emoji: emojiEnabled, + scope: scopeMode, + scopeRequiredFor, + }); + + // Processing: Clef reappears + await clef.processing("Creating your configuration...meoww!", async () => { + await generateConfigFile(config, projectRoot); + // Simulate some processing time for animation + await new Promise((resolve) => setTimeout(resolve, 1000)); + }); + // Screen is now completely clear + + // Display result + displayConfigResult(".labcommitr.config.yaml"); + + // Small pause + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Display next steps + displayNextSteps(); + + // Small pause before finale + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // Outro: Clef celebrates and exits + await clef.outro(); + // Screen is now completely clear - back to terminal + } catch (error) { + // Ensure cursor is visible on error + clef.stop(); + + if (error instanceof Error) { + Logger.error(error.message); + } else { + Logger.error("Failed to initialize configuration"); + } + + process.exit(1); + } +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 8b54461..23ae0ee 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -8,21 +8,21 @@ * - Help text customization */ -import { Command } from 'commander'; -import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; +import { Command } from "commander"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; // Commands -import { configCommand } from './commands/config.js'; -import { initCommand } from './commands/init.js'; -import { commitCommand } from './commands/commit.js'; +import { configCommand } from "./commands/config.js"; +import { initCommand } from "./commands/init/index.js"; +import { commitCommand } from "./commands/commit.js"; // Get package.json for version info const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -const packageJsonPath = join(__dirname, '../../package.json'); -const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); +const packageJsonPath = join(__dirname, "../../package.json"); +const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); /** * Main CLI program instance @@ -31,12 +31,12 @@ export const program = new Command(); // Program metadata program - .name('labcommitr') + .name("labcommitr") .description( - 'A CLI tool for standardized git commits with customizable workflows', + "A CLI tool for standardized git commits with customizable workflows", ) - .version(packageJson.version, '-v, --version', 'Display version number') - .helpOption('-h, --help', 'Display help information'); + .version(packageJson.version, "-v, --version", "Display version number") + .helpOption("-h, --help", "Display help information"); // Global options (future: --verbose, --no-emoji, etc.) // program.option('--verbose', 'Enable verbose logging'); @@ -48,7 +48,7 @@ program.addCommand(commitCommand); // Customize help text program.addHelpText( - 'after', + "after", ` Examples: $ labcommitr init Initialize config in current project @@ -62,4 +62,3 @@ Documentation: // Error on unknown commands program.showSuggestionAfterError(true); - From 677a4adff440fa031e8aa47e434af6754eb902db Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:59:28 -0600 Subject: [PATCH 19/77] docs: add changeset for init command feature - Documented new interactive init command with Clef mascot - Listed key features: presets, animations, clean UI - Marked as minor version bump for new user-facing feature --- .changeset/init-command-with-clef.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .changeset/init-command-with-clef.md diff --git a/.changeset/init-command-with-clef.md b/.changeset/init-command-with-clef.md new file mode 100644 index 0000000..ba6635a --- /dev/null +++ b/.changeset/init-command-with-clef.md @@ -0,0 +1,15 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: add interactive init command with Clef mascot + +- Introduced Clef, an animated cat mascot for enhanced user experience +- Implemented clean, minimal CLI prompts following modern design patterns +- Added four configuration presets: Conventional Commits, Gitmoji, Angular, and Minimal +- Created interactive setup flow with preset selection, emoji support, and scope configuration +- Integrated animated character that appears at key moments: intro, processing, and outro +- Automatic YAML configuration file generation with validation +- Non-intrusive design with clean labels and compact spacing +- Graceful degradation for terminals without animation support + From ffcbd210ec8486609e61e9c6a5f458e04ef2da8e Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 21:59:41 -0600 Subject: [PATCH 20/77] style: format code with prettier - Applied consistent formatting across all files - Updated changeset formatting - Code style consistency maintained --- .changeset/cli-framework-implementation.md | 1 - .changeset/config-loading-system.md | 2 +- src/cli/commands/commit.ts | 35 +- src/cli/commands/config.ts | 35 +- src/cli/utils/error-handler.ts | 13 +- src/cli/utils/version.ts | 15 +- src/index.ts | 4 +- src/lib/config/defaults.ts | 93 ++-- src/lib/config/index.ts | 20 +- src/lib/config/loader.ts | 468 +++++++++++---------- src/lib/config/types.ts | 32 +- src/lib/config/validator.ts | 290 +++++++------ 12 files changed, 535 insertions(+), 473 deletions(-) diff --git a/.changeset/cli-framework-implementation.md b/.changeset/cli-framework-implementation.md index 0ca7174..51a796c 100644 --- a/.changeset/cli-framework-implementation.md +++ b/.changeset/cli-framework-implementation.md @@ -11,4 +11,3 @@ feat: add working CLI framework with basic commands - Interactive help system guides users through available commands - Clear error messages when invalid commands are used - Foundation ready for init and commit commands in upcoming releases - diff --git a/.changeset/config-loading-system.md b/.changeset/config-loading-system.md index 4609f7b..9216ff1 100644 --- a/.changeset/config-loading-system.md +++ b/.changeset/config-loading-system.md @@ -8,4 +8,4 @@ feat: add intelligent configuration file discovery - Prioritizes git repositories and supports monorepo structures - Provides clear error messages when configuration files have issues - Improved performance with smart caching for faster subsequent runs -- Works reliably across different project structures and environments \ No newline at end of file +- Works reliably across different project structures and environments diff --git a/src/cli/commands/commit.ts b/src/cli/commands/commit.ts index 6f04980..ca10ef4 100644 --- a/src/cli/commands/commit.ts +++ b/src/cli/commands/commit.ts @@ -14,19 +14,19 @@ * Current Status: Placeholder for Step 4 */ -import { Command } from 'commander'; -import { Logger } from '../../lib/logger.js'; +import { Command } from "commander"; +import { Logger } from "../../lib/logger.js"; /** * Commit command */ -export const commitCommand = new Command('commit') - .description('Create a standardized commit (interactive)') - .alias('c') - .option('-t, --type ', 'Commit type (feat, fix, etc.)') - .option('-s, --scope ', 'Commit scope') - .option('-m, --message ', 'Commit subject') - .option('--no-verify', 'Bypass git hooks') +export const commitCommand = new Command("commit") + .description("Create a standardized commit (interactive)") + .alias("c") + .option("-t, --type ", "Commit type (feat, fix, etc.)") + .option("-s, --scope ", "Commit scope") + .option("-m, --message ", "Commit subject") + .option("--no-verify", "Bypass git hooks") .action(commitAction); /** @@ -39,13 +39,13 @@ async function commitAction(options: { message?: string; verify?: boolean; }): Promise { - Logger.info('Commit command - Coming in Step 6!'); - Logger.info('\nPlanned features:'); - console.log(' • Interactive type selection from your config'); - console.log(' • Optional scope and description prompts'); - console.log(' • Automatic emoji detection and fallback'); - console.log(' • Git commit execution with formatted message'); - console.log(' • Validation against configured rules'); + Logger.info("Commit command - Coming in Step 6!"); + Logger.info("\nPlanned features:"); + console.log(" • Interactive type selection from your config"); + console.log(" • Optional scope and description prompts"); + console.log(" • Automatic emoji detection and fallback"); + console.log(" • Git commit execution with formatted message"); + console.log(" • Validation against configured rules"); if (options.type) { Logger.info(`\nYou specified type: ${options.type}`); @@ -60,7 +60,6 @@ async function commitAction(options: { } if (options.verify === false) { - Logger.warn('Git hooks will be bypassed when implemented'); + Logger.warn("Git hooks will be bypassed when implemented"); } } - diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts index 9ca6975..ac04374 100644 --- a/src/cli/commands/config.ts +++ b/src/cli/commands/config.ts @@ -8,20 +8,20 @@ * This command is primarily for debugging and troubleshooting. */ -import { Command } from 'commander'; -import { loadConfig } from '../../lib/config/index.js'; -import { Logger } from '../../lib/logger.js'; -import { ConfigError } from '../../lib/config/types.js'; +import { Command } from "commander"; +import { loadConfig } from "../../lib/config/index.js"; +import { Logger } from "../../lib/logger.js"; +import { ConfigError } from "../../lib/config/types.js"; /** * Config command */ -export const configCommand = new Command('config') - .description('Manage labcommitr configuration') +export const configCommand = new Command("config") + .description("Manage labcommitr configuration") .addCommand( - new Command('show') - .description('Display the current configuration') - .option('-p, --path ', 'Start search from specific directory') + new Command("show") + .description("Display the current configuration") + .option("-p, --path ", "Start search from specific directory") .action(showConfigAction), ); @@ -31,25 +31,27 @@ export const configCommand = new Command('config') */ async function showConfigAction(options: { path?: string }): Promise { try { - Logger.info('Loading configuration...\n'); + Logger.info("Loading configuration...\n"); const result = await loadConfig(options.path); // Display config source Logger.success(`Configuration loaded from: ${result.source}`); - if (result.source === 'defaults') { - Logger.warn('Using built-in defaults (no config file found)'); + if (result.source === "defaults") { + Logger.warn("Using built-in defaults (no config file found)"); } if (result.path) { Logger.info(`Config file path: ${result.path}`); } - Logger.info(`Emoji mode: ${result.emojiModeActive ? 'enabled' : 'disabled (terminal fallback)'}`); + Logger.info( + `Emoji mode: ${result.emojiModeActive ? "enabled" : "disabled (terminal fallback)"}`, + ); // Display configuration (formatted JSON) - console.log('\nConfiguration:'); + console.log("\nConfiguration:"); console.log(JSON.stringify(result.config, null, 2)); } catch (error) { if (error instanceof ConfigError) { @@ -59,17 +61,16 @@ async function showConfigAction(options: { path?: string }): Promise { console.error(error.details); } if (error.solutions && error.solutions.length > 0) { - console.error('\nSolutions:'); + console.error("\nSolutions:"); error.solutions.forEach((solution, index) => { console.error(` ${index + 1}. ${solution}`); }); } } else { // Unexpected error - Logger.error('Failed to load configuration'); + Logger.error("Failed to load configuration"); console.error(error); } process.exit(1); } } - diff --git a/src/cli/utils/error-handler.ts b/src/cli/utils/error-handler.ts index 62c043e..3624b13 100644 --- a/src/cli/utils/error-handler.ts +++ b/src/cli/utils/error-handler.ts @@ -11,8 +11,8 @@ * - Unknown errors: Unexpected exceptions */ -import { Logger } from '../../lib/logger.js'; -import { ConfigError } from '../../lib/config/types.js'; +import { Logger } from "../../lib/logger.js"; +import { ConfigError } from "../../lib/config/types.js"; /** * Handle CLI errors with appropriate formatting @@ -30,7 +30,7 @@ export function handleCliError(error: unknown): void { } if (error.solutions && error.solutions.length > 0) { - console.error('\n💡 Solutions:'); + console.error("\n💡 Solutions:"); error.solutions.forEach((solution, index) => { console.error(` ${index + 1}. ${solution}`); }); @@ -41,7 +41,7 @@ export function handleCliError(error: unknown): void { if (error instanceof Error) { // Standard errors - if (error.name === 'CommanderError') { + if (error.name === "CommanderError") { // Commander.js validation errors (handled by Commander itself) return; } @@ -50,7 +50,7 @@ export function handleCliError(error: unknown): void { Logger.error(`Error: ${error.message}`); if (process.env.DEBUG) { - console.error('\nStack trace:'); + console.error("\nStack trace:"); console.error(error.stack); } @@ -58,7 +58,6 @@ export function handleCliError(error: unknown): void { } // Unknown error type - Logger.error('An unexpected error occurred'); + Logger.error("An unexpected error occurred"); console.error(error); } - diff --git a/src/cli/utils/version.ts b/src/cli/utils/version.ts index b465e04..2966cb5 100644 --- a/src/cli/utils/version.ts +++ b/src/cli/utils/version.ts @@ -5,9 +5,9 @@ * Provides consistent version formatting across the CLI. */ -import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; /** * Get package version from package.json @@ -16,8 +16,8 @@ import { dirname, join } from 'path'; export function getVersion(): string { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); - const packageJsonPath = join(__dirname, '../../../package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const packageJsonPath = join(__dirname, "../../../package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); return packageJson.version; } @@ -28,8 +28,7 @@ export function getVersion(): string { export function getFullVersion(): string { const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); - const packageJsonPath = join(__dirname, '../../../package.json'); - const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + const packageJsonPath = join(__dirname, "../../../package.json"); + const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); return `${packageJson.name} v${packageJson.version}`; } - diff --git a/src/index.ts b/src/index.ts index 6187916..579048b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,8 +13,8 @@ * - Global error handling for uncaught exceptions */ -import { program } from './cli/program.js'; -import { handleCliError } from './cli/utils/error-handler.js'; +import { program } from "./cli/program.js"; +import { handleCliError } from "./cli/utils/error-handler.js"; /** * Main CLI execution diff --git a/src/lib/config/defaults.ts b/src/lib/config/defaults.ts index edd2bd6..ebecc58 100644 --- a/src/lib/config/defaults.ts +++ b/src/lib/config/defaults.ts @@ -1,26 +1,26 @@ /** * Default configuration values for labcommitr - * + * * This module provides sensible default values for all optional configuration fields, * ensuring the tool works out-of-the-box without requiring any configuration. * These defaults are merged with user-provided configuration to create the final config. */ -import type { LabcommitrConfig, RawConfig } from './types.js'; +import type { LabcommitrConfig, RawConfig } from "./types.js"; /** * Complete default configuration object - * + * * This serves as the baseline configuration when no user config is provided. * All fields are populated with sensible defaults that provide a good starting point. - * + * * Note: The 'types' array is intentionally empty here as it must be provided by the user * or through preset initialization. This ensures users consciously choose their commit types. */ export const DEFAULT_CONFIG: LabcommitrConfig = { /** Default schema version for new configurations */ - version: '1.0', - + version: "1.0", + /** Basic configuration with emoji enabled by default */ config: { // Enable emoji mode with automatic terminal detection @@ -28,18 +28,18 @@ export const DEFAULT_CONFIG: LabcommitrConfig = { // Let the system auto-detect emoji support (null = auto-detect) force_emoji_detection: null, }, - + /** Standard commit message format following conventional commits */ format: { // Template supports both emoji and text modes through variable substitution - template: '{emoji}{type}({scope}): {subject}', + template: "{emoji}{type}({scope}): {subject}", // Standard 50-character limit for commit subjects (git best practice) subject_max_length: 50, }, - + /** Empty types array - must be provided by user or preset */ types: [], - + /** Minimal validation rules - not overly restrictive by default */ validation: { // No types require scope by default (user can enable per project) @@ -51,7 +51,7 @@ export const DEFAULT_CONFIG: LabcommitrConfig = { // No prohibited words by default (user can customize) prohibited_words: [], }, - + /** Conservative advanced settings - minimal automation by default */ advanced: { // No aliases by default (user can add custom shortcuts) @@ -68,99 +68,102 @@ export const DEFAULT_CONFIG: LabcommitrConfig = { /** * Standard commit types for reference and preset initialization - * + * * These represent a curated set of commit types following conventional commits * and common industry practices. They are used in presets but not automatically * merged with user configuration - user must explicitly choose their types. */ export const DEFAULT_COMMIT_TYPES = [ { - id: 'feat', - description: 'A new feature for the user', - emoji: '✨', + id: "feat", + description: "A new feature for the user", + emoji: "✨", }, { - id: 'fix', - description: 'A bug fix for the user', - emoji: '🐛', + id: "fix", + description: "A bug fix for the user", + emoji: "🐛", }, { - id: 'docs', - description: 'Documentation changes', - emoji: '📚', + id: "docs", + description: "Documentation changes", + emoji: "📚", }, { - id: 'style', - description: 'Code style changes (formatting, missing semicolons, etc.)', - emoji: '💄', + id: "style", + description: "Code style changes (formatting, missing semicolons, etc.)", + emoji: "💄", }, { - id: 'refactor', - description: 'Code refactoring without changing functionality', - emoji: '♻️', + id: "refactor", + description: "Code refactoring without changing functionality", + emoji: "♻️", }, { - id: 'test', - description: 'Adding or updating tests', - emoji: '🧪', + id: "test", + description: "Adding or updating tests", + emoji: "🧪", }, { - id: 'chore', - description: 'Maintenance tasks, build changes, etc.', - emoji: '🔧', + id: "chore", + description: "Maintenance tasks, build changes, etc.", + emoji: "🔧", }, ]; /** * Merges user-provided raw configuration with default values - * + * * This function implements the "defaults filling" logic where user-provided * values take precedence over defaults, but missing fields are filled in * with sensible defaults. - * + * * @param rawConfig - User-provided configuration (potentially incomplete) * @returns Complete configuration with all fields populated */ export function mergeWithDefaults(rawConfig: RawConfig): LabcommitrConfig { // Create a deep copy of defaults to avoid mutation const merged: LabcommitrConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); - + // Apply user-provided values, preserving the types array merged.version = rawConfig.version ?? merged.version; merged.types = rawConfig.types; // Required field, always from user - + // Merge nested objects while preserving user preferences if (rawConfig.config) { merged.config = { ...merged.config, ...rawConfig.config }; } - + if (rawConfig.format) { merged.format = { ...merged.format, ...rawConfig.format }; } - + if (rawConfig.validation) { merged.validation = { ...merged.validation, ...rawConfig.validation }; } - + if (rawConfig.advanced) { merged.advanced = { ...merged.advanced, ...rawConfig.advanced }; - + // Handle nested git configuration if (rawConfig.advanced.git) { - merged.advanced.git = { ...merged.advanced.git, ...rawConfig.advanced.git }; + merged.advanced.git = { + ...merged.advanced.git, + ...rawConfig.advanced.git, + }; } } - + return merged; } /** * Creates a complete default configuration when no user config exists - * + * * This is used as a fallback when no configuration file can be found * and the user chooses not to initialize one. Provides a minimal but * functional configuration using the standard commit types. - * + * * @returns Complete default configuration ready for use */ export function createFallbackConfig(): LabcommitrConfig { diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 0468f94..8cf9a4b 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -1,35 +1,37 @@ /** * Configuration system exports for labcommitr - * + * * This module provides the public API for the configuration loading system. * It exports the main classes, interfaces, and utility functions needed * by other parts of the application. */ // Re-export all types and interfaces -export * from './types.js'; +export * from "./types.js"; // Re-export configuration defaults and utilities -export * from './defaults.js'; +export * from "./defaults.js"; // Re-export main configuration loader -export * from './loader.js'; +export * from "./loader.js"; // Re-export configuration validator -export * from './validator.js'; +export * from "./validator.js"; /** * Convenience function to create and use a ConfigLoader instance - * + * * This provides a simple API for one-off configuration loading without * needing to manage ConfigLoader instances manually. Most consumers * should use this function rather than instantiating ConfigLoader directly. - * + * * @param startPath - Directory to start searching from (defaults to process.cwd()) * @returns Promise resolving to complete configuration with metadata */ -export async function loadConfig(startPath?: string): Promise { - const { ConfigLoader } = await import('./loader.js'); +export async function loadConfig( + startPath?: string, +): Promise { + const { ConfigLoader } = await import("./loader.js"); const loader = new ConfigLoader(); return loader.load(startPath); } diff --git a/src/lib/config/loader.ts b/src/lib/config/loader.ts index 503bfd8..bd9cac4 100644 --- a/src/lib/config/loader.ts +++ b/src/lib/config/loader.ts @@ -1,25 +1,25 @@ /** * Configuration loading system for labcommitr - * + * * This module handles the discovery, parsing, and processing of configuration files. * It implements the async-first architecture with git-prioritized project root detection, * smart caching, and comprehensive error handling. */ -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import * as yaml from 'js-yaml'; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import * as yaml from "js-yaml"; -import type { - LabcommitrConfig, - RawConfig, - ConfigLoadResult, +import type { + LabcommitrConfig, + RawConfig, + ConfigLoadResult, ProjectRoot, - CachedConfig -} from './types.js'; -import { ConfigError } from './types.js'; -import { mergeWithDefaults, createFallbackConfig } from './defaults.js'; -import { ConfigValidator } from './validator.js'; + CachedConfig, +} from "./types.js"; +import { ConfigError } from "./types.js"; +import { mergeWithDefaults, createFallbackConfig } from "./defaults.js"; +import { ConfigValidator } from "./validator.js"; /** * Configuration file names to search for (in priority order) @@ -27,13 +27,13 @@ import { ConfigValidator } from './validator.js'; * Fallback: .labcommitr.config.yml */ const CONFIG_FILENAMES = [ - '.labcommitr.config.yaml', - '.labcommitr.config.yml' + ".labcommitr.config.yaml", + ".labcommitr.config.yml", ] as const; /** * Main configuration loader class - * + * * Handles the complete configuration loading pipeline: * 1. Project root detection (git-prioritized) * 2. Configuration file discovery @@ -45,48 +45,47 @@ const CONFIG_FILENAMES = [ export class ConfigLoader { /** Cache for loaded configurations to improve performance */ private configCache = new Map(); - + /** Cache for project root paths to avoid repeated filesystem traversal */ private projectRootCache = new Map(); /** * Main entry point for configuration loading - * + * * This method orchestrates the entire configuration loading process, * from project root detection through final configuration assembly. - * + * * @param startPath - Directory to start searching from (defaults to process.cwd()) * @returns Promise resolving to complete configuration with metadata */ public async load(startPath?: string): Promise { const searchStartPath = startPath ?? process.cwd(); - + try { // Step 1: Detect project root with git prioritization const projectRoot = await this.findProjectRoot(searchStartPath); - + // Step 2: Look for configuration file within project boundaries const configPath = await this.findConfigFile(projectRoot.path); - + if (!configPath) { // No config found - return fallback configuration return this.createFallbackResult(projectRoot); } - + // Step 3: Check if we have this config cached const cached = this.getCachedConfig(configPath); - if (cached && await this.isCacheValid(cached, configPath)) { + if (cached && (await this.isCacheValid(cached, configPath))) { return cached.data; } - + // Step 4: Load and process the configuration file const result = await this.loadConfigFile(configPath, projectRoot); - + // Step 5: Cache the result for future use this.cacheConfig(configPath, result); - + return result; - } catch (error) { // Transform any errors into user-friendly ConfigError instances throw this.transformError(error, searchStartPath); @@ -95,13 +94,13 @@ export class ConfigLoader { /** * Finds the project root directory using git-prioritized detection - * + * * Search strategy: * 1. Traverse upward from start directory * 2. Priority 1: Look for .git directory (git repository root) * 3. Priority 2: Look for package.json (Node.js project root) * 4. Fallback: Stop at filesystem root - * + * * @param startPath - Directory to begin search from * @returns Promise resolving to project root information */ @@ -111,42 +110,42 @@ export class ConfigLoader { if (cachedRoot) { return cachedRoot; } - + let currentDir = path.resolve(startPath); let projectRoot: ProjectRoot | null = null; - + // Traverse upward until we find a project marker or hit filesystem root while (currentDir !== path.dirname(currentDir)) { // Priority 1: Check for .git directory (git repository root) - if (await this.directoryExists(path.join(currentDir, '.git'))) { - projectRoot = await this.createProjectRoot(currentDir, 'git'); + if (await this.directoryExists(path.join(currentDir, ".git"))) { + projectRoot = await this.createProjectRoot(currentDir, "git"); break; } - + // Priority 2: Check for package.json (Node.js project root) - if (await this.fileExists(path.join(currentDir, 'package.json'))) { - projectRoot = await this.createProjectRoot(currentDir, 'package.json'); + if (await this.fileExists(path.join(currentDir, "package.json"))) { + projectRoot = await this.createProjectRoot(currentDir, "package.json"); break; } - + // Move up one directory level currentDir = path.dirname(currentDir); } - + // Fallback: Use filesystem root if no project markers found if (!projectRoot) { - projectRoot = await this.createProjectRoot(currentDir, 'filesystem-root'); + projectRoot = await this.createProjectRoot(currentDir, "filesystem-root"); } - + // Cache the result for future lookups this.projectRootCache.set(startPath, projectRoot); - + return projectRoot; } /** * Searches for configuration file within project boundaries - * + * * @param projectRoot - Project root directory to search in * @returns Promise resolving to config file path or null if not found */ @@ -154,38 +153,41 @@ export class ConfigLoader { // Search for each possible config filename in order of preference for (const filename of CONFIG_FILENAMES) { const configPath = path.join(projectRoot, filename); - + if (await this.fileExists(configPath)) { return configPath; } } - + return null; } /** * Creates a ProjectRoot object with monorepo detection - * + * * @param rootPath - The detected project root path * @param markerType - What type of marker identified this as the root * @returns Promise resolving to complete ProjectRoot information */ - private async createProjectRoot(rootPath: string, markerType: ProjectRoot['markerType']): Promise { + private async createProjectRoot( + rootPath: string, + markerType: ProjectRoot["markerType"], + ): Promise { // For now, implement basic monorepo detection by counting package.json files // More sophisticated detection can be added later const subprojects = await this.findSubprojects(rootPath); - + return { path: rootPath, markerType, isMonorepo: subprojects.length > 1, // Multiple package.json = likely monorepo - subprojects + subprojects, }; } /** * Finds subprojects within the project root (basic monorepo support) - * + * * @param rootPath - Project root directory to search * @returns Promise resolving to array of subproject paths */ @@ -195,16 +197,20 @@ export class ConfigLoader { try { const entries = await fs.readdir(rootPath, { withFileTypes: true }); const subprojects: string[] = []; - + for (const entry of entries) { - if (entry.isDirectory() && !entry.name.startsWith('.')) { - const packageJsonPath = path.join(rootPath, entry.name, 'package.json'); + if (entry.isDirectory() && !entry.name.startsWith(".")) { + const packageJsonPath = path.join( + rootPath, + entry.name, + "package.json", + ); if (await this.fileExists(packageJsonPath)) { subprojects.push(path.join(rootPath, entry.name)); } } } - + return subprojects; } catch { // If we can't read the directory, return empty array @@ -214,97 +220,102 @@ export class ConfigLoader { /** * Creates a fallback configuration result when no config file is found - * + * * @param projectRoot - Project root information * @returns ConfigLoadResult with default configuration */ private createFallbackResult(projectRoot: ProjectRoot): ConfigLoadResult { const fallbackConfig = createFallbackConfig(); - + return { config: fallbackConfig, - source: 'defaults', + source: "defaults", loadedAt: Date.now(), - emojiModeActive: this.detectEmojiSupport() // TODO: Implement emoji detection + emojiModeActive: this.detectEmojiSupport(), // TODO: Implement emoji detection }; } /** * Loads and processes a configuration file - * + * * @param configPath - Path to the configuration file * @param projectRoot - Project root information * @returns Promise resolving to processed configuration */ - private async loadConfigFile(configPath: string, projectRoot: ProjectRoot): Promise { + private async loadConfigFile( + configPath: string, + projectRoot: ProjectRoot, + ): Promise { // Validate file permissions before attempting to read await this.validateFilePermissions(configPath); - + // Parse the YAML file const rawConfig = await this.parseYamlFile(configPath); - + // Validate the parsed configuration const validator = new ConfigValidator(); const validationResult = validator.validate(rawConfig); - + if (!validationResult.valid) { // Format errors with rich context for user-friendly output - const formattedErrors = validationResult.errors.map((error, index) => { - const count = index + 1; - const location = error.fieldDisplay || error.field; - - let errorBlock = `\n${count}. ${location}:\n`; - errorBlock += ` ${error.userMessage || error.message}\n`; - - if (error.value !== undefined) { - errorBlock += ` Found: ${JSON.stringify(error.value)}\n`; - } - - if (error.issue) { - errorBlock += ` Issue: ${error.issue}\n`; - } - - if (error.expectedFormat) { - errorBlock += ` Rule: ${error.expectedFormat}\n`; - } - - if (error.examples && error.examples.length > 0) { - errorBlock += ` Examples: ${error.examples.join(', ')}\n`; - } - - return errorBlock; - }).join('\n'); - + const formattedErrors = validationResult.errors + .map((error, index) => { + const count = index + 1; + const location = error.fieldDisplay || error.field; + + let errorBlock = `\n${count}. ${location}:\n`; + errorBlock += ` ${error.userMessage || error.message}\n`; + + if (error.value !== undefined) { + errorBlock += ` Found: ${JSON.stringify(error.value)}\n`; + } + + if (error.issue) { + errorBlock += ` Issue: ${error.issue}\n`; + } + + if (error.expectedFormat) { + errorBlock += ` Rule: ${error.expectedFormat}\n`; + } + + if (error.examples && error.examples.length > 0) { + errorBlock += ` Examples: ${error.examples.join(", ")}\n`; + } + + return errorBlock; + }) + .join("\n"); + const count = validationResult.errors.length; - const plural = count === 1 ? 'error' : 'errors'; + const plural = count === 1 ? "error" : "errors"; const filename = path.basename(configPath); - + throw new ConfigError( `Configuration Error: ${filename}`, `Found ${count} validation ${plural}:${formattedErrors}`, [ `Edit ${filename} to fix the issues listed above`, - 'See documentation for valid field formats: https://github.com/labcatr/labcommitr#config' + "See documentation for valid field formats: https://github.com/labcatr/labcommitr#config", ], - configPath + configPath, ); } - + // Merge with defaults to create complete configuration const processedConfig = mergeWithDefaults(rawConfig); - + return { config: processedConfig, - source: 'project', + source: "project", path: configPath, loadedAt: Date.now(), - emojiModeActive: this.detectEmojiSupport() // TODO: Implement emoji detection + emojiModeActive: this.detectEmojiSupport(), // TODO: Implement emoji detection }; } /** * Utility: Check if a file exists - * + * * @param filePath - Path to check * @returns Promise resolving to whether file exists */ @@ -319,7 +330,7 @@ export class ConfigLoader { /** * Utility: Check if a directory exists - * + * * @param dirPath - Directory path to check * @returns Promise resolving to whether directory exists */ @@ -334,10 +345,10 @@ export class ConfigLoader { /** * Detects whether the current terminal supports emoji display - * + * * TODO: Implement proper emoji detection logic * For now, returns true as a placeholder - * + * * @returns Whether emojis should be displayed */ private detectEmojiSupport(): boolean { @@ -347,10 +358,10 @@ export class ConfigLoader { /** * Retrieves cached configuration if available - * + * * This method checks the in-memory cache for previously loaded configurations * to avoid redundant file system operations and parsing. - * + * * @param configPath - Path to configuration file * @returns Cached configuration or undefined if not cached */ @@ -360,24 +371,27 @@ export class ConfigLoader { /** * Validates whether cached configuration is still valid - * + * * Cache validity is determined by comparing file modification time * with the cache timestamp. If the file has been modified since * caching, the cache is considered invalid. - * + * * @param cached - Cached configuration entry * @param configPath - Path to configuration file * @returns Promise resolving to whether cache is valid */ - private async isCacheValid(cached: CachedConfig, configPath: string): Promise { + private async isCacheValid( + cached: CachedConfig, + configPath: string, + ): Promise { try { // Get file modification time const stats = await fs.stat(configPath); const fileModifiedTime = stats.mtime.getTime(); - + // Cache is valid if file hasn't been modified since caching // Allow small time difference (1 second) to account for filesystem precision - return fileModifiedTime <= (cached.timestamp + 1000); + return fileModifiedTime <= cached.timestamp + 1000; } catch { // If we can't stat the file, assume cache is invalid // This handles cases where file was deleted or permissions changed @@ -387,11 +401,11 @@ export class ConfigLoader { /** * Caches a configuration result for performance optimization - * + * * Stores the configuration result in memory with metadata for * cache invalidation. Future loads of the same file will use * the cached result if the file hasn't been modified. - * + * * @param configPath - Path to configuration file * @param result - Configuration result to cache */ @@ -399,14 +413,15 @@ export class ConfigLoader { const cacheEntry: CachedConfig = { data: result, timestamp: Date.now(), - watchedPaths: [configPath] // For future file watching enhancement + watchedPaths: [configPath], // For future file watching enhancement }; - + this.configCache.set(configPath, cacheEntry); - + // Optional: Implement cache size limit to prevent memory leaks // For now, keep it simple - can add LRU eviction later if needed - if (this.configCache.size > 50) { // Arbitrary limit + if (this.configCache.size > 50) { + // Arbitrary limit // Remove oldest entries (simple FIFO eviction) const entries = Array.from(this.configCache.entries()); const oldestKey = entries[0][0]; @@ -416,10 +431,10 @@ export class ConfigLoader { /** * Validates that a file exists and is readable - * + * * This method performs pre-read validation to provide clear error messages * when files cannot be accessed due to permission issues or missing files. - * + * * @param filePath - Path to file to validate * @throws ConfigError if file cannot be read */ @@ -427,60 +442,60 @@ export class ConfigLoader { try { await fs.access(filePath, fs.constants.R_OK); } catch (error: any) { - if (error.code === 'ENOENT') { + if (error.code === "ENOENT") { // File not found - this is handled upstream, but provide clear error if called directly throw new (Error as any)( // TODO: Use proper ConfigError import `Configuration file not found: ${filePath}`, - 'The file does not exist', - ['Run \'lab init\' to create a configuration file'], - filePath + "The file does not exist", + ["Run 'lab init' to create a configuration file"], + filePath, ); - } else if (error.code === 'EACCES') { + } else if (error.code === "EACCES") { // Permission denied - provide actionable solutions throw new (Error as any)( // TODO: Use proper ConfigError import `Cannot read configuration file: ${filePath}`, - 'Permission denied - insufficient file permissions', + "Permission denied - insufficient file permissions", [ `Check file permissions: ls -la ${path.basename(filePath)}`, `Fix permissions: chmod 644 ${path.basename(filePath)}`, - 'Verify file ownership with your system administrator' + "Verify file ownership with your system administrator", ], - filePath + filePath, ); - } else if (error.code === 'ENOTDIR') { + } else if (error.code === "ENOTDIR") { // Path component is not a directory throw new (Error as any)( // TODO: Use proper ConfigError import `Invalid path to configuration file: ${filePath}`, - 'A component in the path is not a directory', + "A component in the path is not a directory", [ - 'Verify the file path is correct', - 'Check that all parent directories exist' + "Verify the file path is correct", + "Check that all parent directories exist", ], - filePath + filePath, ); } - + // Re-throw unexpected errors with additional context throw new (Error as any)( // TODO: Use proper ConfigError import `Failed to access configuration file: ${filePath}`, `System error: ${error.message}`, [ - 'Check file and directory permissions', - 'Verify the file path is correct', - 'Contact system administrator if the problem persists' + "Check file and directory permissions", + "Verify the file path is correct", + "Contact system administrator if the problem persists", ], - filePath + filePath, ); } } /** * Parses a YAML configuration file with comprehensive error handling - * + * * This method reads and parses YAML files using js-yaml's safe loader * to prevent code execution. It provides detailed error messages for * common YAML syntax issues and validation problems. - * + * * @param filePath - Path to YAML file to parse * @returns Promise resolving to parsed configuration object * @throws ConfigError for YAML syntax or structure errors @@ -488,66 +503,65 @@ export class ConfigLoader { private async parseYamlFile(filePath: string): Promise { try { // Read file content as UTF-8 text - const fileContent = await fs.readFile(filePath, 'utf8'); - + const fileContent = await fs.readFile(filePath, "utf8"); + // Check for empty file (common user error) if (!fileContent.trim()) { throw new (Error as any)( // TODO: Use proper ConfigError import `Configuration file is empty: ${filePath}`, - 'The file contains no content or only whitespace', + "The file contains no content or only whitespace", [ - 'Add configuration content to the file', - 'Run \'lab init\' to generate a valid configuration file', - 'Copy from another project or use documentation examples' + "Add configuration content to the file", + "Run 'lab init' to generate a valid configuration file", + "Copy from another project or use documentation examples", ], - filePath + filePath, ); } - + // Parse YAML with safe loader (prevents code execution) // Use DEFAULT_SCHEMA for full YAML 1.2 compatibility - const parsed = yaml.load(fileContent, { + const parsed = yaml.load(fileContent, { schema: yaml.DEFAULT_SCHEMA, - filename: filePath // Helps with error reporting + filename: filePath, // Helps with error reporting }); - + // Validate that result is an object (not null, string, array, etc.) - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - const actualType = Array.isArray(parsed) ? 'array' : typeof parsed; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + const actualType = Array.isArray(parsed) ? "array" : typeof parsed; throw new (Error as any)( // TODO: Use proper ConfigError import `Invalid configuration structure in ${filePath}`, `Configuration must be a YAML object, but got ${actualType}`, [ - 'Ensure the file contains a valid YAML object (key-value pairs)', - 'Check that the file starts with object properties, not a list or scalar', - 'Run \'lab init\' to generate a valid configuration file' + "Ensure the file contains a valid YAML object (key-value pairs)", + "Check that the file starts with object properties, not a list or scalar", + "Run 'lab init' to generate a valid configuration file", ], - filePath + filePath, ); } - + // Basic structure validation - ensure required 'types' field exists const config = parsed as any; if (!Array.isArray(config.types)) { throw new (Error as any)( // TODO: Use proper ConfigError import `Missing required 'types' field in ${filePath}`, - 'Configuration must include a \'types\' array defining commit types', + "Configuration must include a 'types' array defining commit types", [ - 'Add a \'types\' field with an array of commit type objects', - 'Each type should have \'id\', \'description\', and optionally \'emoji\'', - 'Run \'lab init\' to generate a valid configuration file' + "Add a 'types' field with an array of commit type objects", + "Each type should have 'id', 'description', and optionally 'emoji'", + "Run 'lab init' to generate a valid configuration file", ], - filePath + filePath, ); } - + return config as RawConfig; - } catch (error: any) { // Transform YAML parsing errors into user-friendly messages if (error instanceof yaml.YAMLException) { const { message, mark } = error; - + // Extract line and column information if available if (mark) { const lineInfo = `line ${mark.line + 1}, column ${mark.column + 1}`; @@ -556,11 +570,11 @@ export class ConfigLoader { `Parsing error: ${message}`, [ `Check the syntax around line ${mark.line + 1}`, - 'Common issues: incorrect indentation, missing colons, unquoted special characters', - 'Use a YAML validator (e.g., yamllint) to identify syntax issues', - 'Run \'lab init\' to generate a fresh config file' + "Common issues: incorrect indentation, missing colons, unquoted special characters", + "Use a YAML validator (e.g., yamllint) to identify syntax issues", + "Run 'lab init' to generate a fresh config file", ], - filePath + filePath, ); } else { // YAML error without specific location @@ -568,140 +582,142 @@ export class ConfigLoader { `Invalid YAML syntax in ${filePath}`, `Parsing error: ${message}`, [ - 'Check YAML syntax throughout the file', - 'Common issues: incorrect indentation, missing colons, unquoted special characters', - 'Use a YAML validator to identify issues', - 'Run \'lab init\' to generate a fresh config file' + "Check YAML syntax throughout the file", + "Common issues: incorrect indentation, missing colons, unquoted special characters", + "Use a YAML validator to identify issues", + "Run 'lab init' to generate a fresh config file", ], - filePath + filePath, ); } } - + // Re-throw if it's already a ConfigError (from our validation above) - if (error.name === 'ConfigError') { + if (error.name === "ConfigError") { throw error; } - + // Handle file system errors that might occur during reading - if (error.code === 'EISDIR') { + if (error.code === "EISDIR") { throw new (Error as any)( // TODO: Use proper ConfigError import `Cannot read configuration: ${filePath} is a directory`, - 'Expected a file but found a directory', + "Expected a file but found a directory", [ - 'Ensure the path points to a file, not a directory', - 'Check for naming conflicts with directories' + "Ensure the path points to a file, not a directory", + "Check for naming conflicts with directories", ], - filePath + filePath, ); } - + // Generic error fallback with context throw new (Error as any)( // TODO: Use proper ConfigError import `Failed to parse configuration file: ${filePath}`, `Unexpected error: ${error.message}`, [ - 'Verify the file is a valid YAML file', - 'Check file encoding (should be UTF-8)', - 'Run \'lab init\' to generate a fresh config file' + "Verify the file is a valid YAML file", + "Check file encoding (should be UTF-8)", + "Run 'lab init' to generate a fresh config file", ], - filePath + filePath, ); } } /** * Transforms various error types into user-friendly ConfigError instances - * + * * This method serves as the central error transformation point, ensuring * all errors thrown by the configuration system provide actionable guidance * to users rather than technical implementation details. - * + * * @param error - The original error that occurred * @param context - Additional context about where the error occurred * @returns ConfigError with user-friendly messaging and solutions */ private transformError(error: any, context: string): Error { // If it's already a ConfigError, pass it through unchanged - if (error.name === 'ConfigError') { + if (error.name === "ConfigError") { return error; } - + // Handle common file system errors with specific guidance - if (error.code === 'ENOENT') { + if (error.code === "ENOENT") { return new (Error as any)( // TODO: Use proper ConfigError import `No configuration found starting from ${context}`, - 'Could not locate a labcommitr configuration file in the project', + "Could not locate a labcommitr configuration file in the project", [ - 'Run \'lab init\' to create a configuration file', - 'Ensure you\'re in a git repository or Node.js project', - 'Check that you have read permissions for the directory tree' - ] + "Run 'lab init' to create a configuration file", + "Ensure you're in a git repository or Node.js project", + "Check that you have read permissions for the directory tree", + ], ); } - - if (error.code === 'EACCES') { + + if (error.code === "EACCES") { return new (Error as any)( // TODO: Use proper ConfigError import `Permission denied while searching for configuration`, `Cannot access directory or file: ${error.path || context}`, [ - 'Check directory permissions in the project tree', - 'Ensure you have read access to the project directory', - 'Contact your system administrator if in a shared environment' - ] + "Check directory permissions in the project tree", + "Ensure you have read access to the project directory", + "Contact your system administrator if in a shared environment", + ], ); } - - if (error.code === 'ENOTDIR') { + + if (error.code === "ENOTDIR") { return new (Error as any)( // TODO: Use proper ConfigError import `Invalid directory structure encountered`, `Expected directory but found file: ${error.path || context}`, [ - 'Verify the project directory structure is correct', - 'Check for files where directories are expected' - ] + "Verify the project directory structure is correct", + "Check for files where directories are expected", + ], ); } - + // Handle YAML-related errors (these should typically be caught upstream) if (error instanceof yaml.YAMLException) { return new (Error as any)( // TODO: Use proper ConfigError import `Configuration file contains invalid YAML syntax`, `YAML parsing error: ${error.message}`, [ - 'Check YAML syntax in your configuration file', - 'Use a YAML validator to identify issues', - 'Run \'lab init\' to generate a fresh config file' - ] + "Check YAML syntax in your configuration file", + "Use a YAML validator to identify issues", + "Run 'lab init' to generate a fresh config file", + ], ); } - + // Handle timeout errors (e.g., from slow file systems) - if (error.code === 'ETIMEDOUT') { + if (error.code === "ETIMEDOUT") { return new (Error as any)( // TODO: Use proper ConfigError import `Timeout while accessing configuration files`, - 'File system operation took too long to complete', + "File system operation took too long to complete", [ - 'Check if the file system is responsive', - 'Try again in a few moments', - 'Consider checking disk space and system load' - ] + "Check if the file system is responsive", + "Try again in a few moments", + "Consider checking disk space and system load", + ], ); } - + // Generic error fallback with as much context as possible - const errorMessage = error.message || 'Unknown error occurred'; - const errorContext = error.stack ? `\n\nTechnical details:\n${error.stack}` : ''; - + const errorMessage = error.message || "Unknown error occurred"; + const errorContext = error.stack + ? `\n\nTechnical details:\n${error.stack}` + : ""; + return new (Error as any)( // TODO: Use proper ConfigError import `Configuration loading failed`, `${errorMessage}${errorContext}`, [ - 'Check file permissions and syntax', - 'Verify you\'re in a valid project directory', - 'Run \'lab init\' to reset configuration', - 'Report this issue if the problem persists with details about your setup' - ] + "Check file permissions and syntax", + "Verify you're in a valid project directory", + "Run 'lab init' to reset configuration", + "Report this issue if the problem persists with details about your setup", + ], ); } } diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts index dc91be2..fa419df 100644 --- a/src/lib/config/types.ts +++ b/src/lib/config/types.ts @@ -1,6 +1,6 @@ /** * TypeScript interfaces for labcommitr configuration system - * + * * This file defines the core types used throughout the config loading system, * ensuring type safety and clear contracts between components. */ @@ -75,15 +75,15 @@ export interface RawConfig { /** Schema version for future compatibility */ version?: string; /** Basic configuration settings */ - config?: Partial; + config?: Partial; /** Commit message formatting rules */ - format?: Partial; + format?: Partial; /** Array of available commit types - REQUIRED FIELD */ types: CommitType[]; /** Validation rules for commit messages */ - validation?: Partial; + validation?: Partial; /** Advanced configuration options */ - advanced?: Partial; + advanced?: Partial; } /** @@ -94,7 +94,7 @@ export interface ConfigLoadResult { /** The fully processed configuration */ config: LabcommitrConfig; /** Source of the configuration */ - source: 'project' | 'global' | 'defaults'; + source: "project" | "global" | "defaults"; /** Absolute path to config file (if loaded from file) */ path?: string; /** Timestamp when config was loaded */ @@ -111,7 +111,7 @@ export interface ProjectRoot { /** Absolute path to the project root directory */ path: string; /** Type of marker that identified this as project root */ - markerType: 'git' | 'package.json' | 'filesystem-root'; + markerType: "git" | "package.json" | "filesystem-root"; /** Whether this appears to be a monorepo structure */ isMonorepo: boolean; /** Paths to detected subprojects (if any) */ @@ -172,7 +172,7 @@ export interface ValidationError { export class ConfigError extends Error { /** * Creates a new configuration error with user-friendly messaging - * + * * @param message - Primary error message (what went wrong) * @param details - Technical details about the error * @param solutions - Array of actionable solutions for the user @@ -182,33 +182,33 @@ export class ConfigError extends Error { message: string, public readonly details: string, public readonly solutions: string[], - public readonly filePath?: string + public readonly filePath?: string, ) { super(message); - this.name = 'ConfigError'; - + this.name = "ConfigError"; + // Ensure proper prototype chain for instanceof checks Object.setPrototypeOf(this, ConfigError.prototype); } - + /** * Formats the error for display to users * Includes the message, details, and actionable solutions */ public formatForUser(): string { let output = `❌ ${this.message}\n`; - + if (this.details) { output += `\nDetails: ${this.details}\n`; } - + if (this.solutions.length > 0) { output += `\nSolutions:\n`; - this.solutions.forEach(solution => { + this.solutions.forEach((solution) => { output += ` ${solution}\n`; }); } - + return output; } } diff --git a/src/lib/config/validator.ts b/src/lib/config/validator.ts index 1118d7d..8f68ad1 100644 --- a/src/lib/config/validator.ts +++ b/src/lib/config/validator.ts @@ -1,13 +1,18 @@ /** * Configuration validation system for labcommitr - * + * * Implements incremental validation following the CONFIG_SCHEMA.md specification. * Phase 1: Basic schema validation (required fields, types, structure) - * Phase 2: Business logic validation (uniqueness, cross-references) + * Phase 2: Business logic validation (uniqueness, cross-references) * Phase 3: Advanced validation (templates, industry standards) */ -import type { RawConfig, ValidationResult, ValidationError, CommitType } from './types.js'; +import type { + RawConfig, + ValidationResult, + ValidationError, + CommitType, +} from "./types.js"; /** * Configuration validator class @@ -22,32 +27,33 @@ export class ConfigValidator { */ validate(config: unknown): ValidationResult { const errors: ValidationError[] = []; - + // Phase 1: Basic structure validation if (!this.isValidConfigStructure(config)) { errors.push({ - field: 'root', - fieldDisplay: 'Configuration root', - message: 'Configuration must be an object', - userMessage: 'Configuration file must contain an object with key-value pairs', + field: "root", + fieldDisplay: "Configuration root", + message: "Configuration must be an object", + userMessage: + "Configuration file must contain an object with key-value pairs", value: config, expectedFormat: 'YAML object with fields like "version", "types", etc.', - issue: 'Found non-object value at root level' + issue: "Found non-object value at root level", }); return { valid: false, errors }; } - + const typedConfig = config as RawConfig; - + // Validate required types array errors.push(...this.validateTypes(typedConfig)); - + // Validate optional sections (only basic structure for Phase 1) errors.push(...this.validateOptionalSections(typedConfig)); - + return { valid: errors.length === 0, - errors + errors, }; } @@ -58,56 +64,62 @@ export class ConfigValidator { */ private validateTypes(config: RawConfig): ValidationError[] { const errors: ValidationError[] = []; - + // Check if types field exists if (!config.types) { errors.push({ - field: 'types', - fieldDisplay: 'Commit types array', + field: "types", + fieldDisplay: "Commit types array", message: 'Required field "types" is missing', userMessage: 'Configuration must include a "types" array', value: undefined, - expectedFormat: 'array with at least one commit type object', - examples: ['feat', 'fix', 'docs', 'refactor', 'test'], - issue: 'Missing required field' + expectedFormat: "array with at least one commit type object", + examples: ["feat", "fix", "docs", "refactor", "test"], + issue: "Missing required field", }); return errors; } - + // Check if types is an array if (!Array.isArray(config.types)) { errors.push({ - field: 'types', - fieldDisplay: 'Commit types', + field: "types", + fieldDisplay: "Commit types", message: 'Field "types" must be an array', userMessage: 'The "types" field must be a list of commit type objects', value: config.types, - expectedFormat: 'array with at least one commit type object', - issue: 'Found non-array value' + expectedFormat: "array with at least one commit type object", + issue: "Found non-array value", }); return errors; } - + // Check if types array is non-empty if (config.types.length === 0) { errors.push({ - field: 'types', - fieldDisplay: 'Commit types array', + field: "types", + fieldDisplay: "Commit types array", message: 'Field "types" must contain at least one commit type', - userMessage: 'Configuration must define at least one commit type', + userMessage: "Configuration must define at least one commit type", value: config.types, - expectedFormat: 'array with at least one commit type object', - examples: ['feat (features)', 'fix (bug fixes)', 'docs (documentation)', 'refactor (code restructuring)', 'test (testing)'], - issue: 'Empty types array' + expectedFormat: "array with at least one commit type object", + examples: [ + "feat (features)", + "fix (bug fixes)", + "docs (documentation)", + "refactor (code restructuring)", + "test (testing)", + ], + issue: "Empty types array", }); return errors; } - + // Validate each commit type config.types.forEach((type, index) => { errors.push(...this.validateCommitType(type, index)); }); - + return errors; } @@ -121,139 +133,152 @@ export class ConfigValidator { const errors: ValidationError[] = []; const fieldPrefix = `types[${index}]`; const displayPrefix = `Commit type #${index + 1}`; - + // Check if type is an object - if (!type || typeof type !== 'object' || Array.isArray(type)) { + if (!type || typeof type !== "object" || Array.isArray(type)) { errors.push({ field: fieldPrefix, fieldDisplay: displayPrefix, - message: 'Each commit type must be an object', - userMessage: 'Each commit type must be an object with id and description fields', + message: "Each commit type must be an object", + userMessage: + "Each commit type must be an object with id and description fields", value: type, expectedFormat: 'object with "id" and "description" fields', - issue: 'Found non-object value' + issue: "Found non-object value", }); return errors; } - + const commitType = type as Partial; - + // Validate required 'id' field if (!commitType.id) { errors.push({ field: `${fieldPrefix}.id`, fieldDisplay: `${displayPrefix} → ID field`, message: 'Required field "id" is missing', - userMessage: 'Every commit type must have an ID', + userMessage: "Every commit type must have an ID", value: commitType.id, - expectedFormat: 'lowercase letters only (a-z)', - examples: ['feat', 'fix', 'docs', 'refactor', 'test'], - issue: 'Missing required field' + expectedFormat: "lowercase letters only (a-z)", + examples: ["feat", "fix", "docs", "refactor", "test"], + issue: "Missing required field", }); - } else if (typeof commitType.id !== 'string') { + } else if (typeof commitType.id !== "string") { errors.push({ field: `${fieldPrefix}.id`, fieldDisplay: `${displayPrefix} → ID field`, message: 'Field "id" must be a string', - userMessage: 'Commit type ID must be text', + userMessage: "Commit type ID must be text", value: commitType.id, - expectedFormat: 'lowercase letters only (a-z)', - examples: ['feat', 'fix', 'docs', 'refactor', 'test'], - issue: 'Found non-string value' + expectedFormat: "lowercase letters only (a-z)", + examples: ["feat", "fix", "docs", "refactor", "test"], + issue: "Found non-string value", }); - } else if (commitType.id.trim() === '') { + } else if (commitType.id.trim() === "") { errors.push({ field: `${fieldPrefix}.id`, fieldDisplay: `${displayPrefix} → ID field`, message: 'Field "id" cannot be empty', - userMessage: 'Commit type ID cannot be empty', + userMessage: "Commit type ID cannot be empty", value: commitType.id, - expectedFormat: 'lowercase letters only (a-z)', - examples: ['feat', 'fix', 'docs', 'refactor', 'test'], - issue: 'Empty string' + expectedFormat: "lowercase letters only (a-z)", + examples: ["feat", "fix", "docs", "refactor", "test"], + issue: "Empty string", }); } else if (!/^[a-z]+$/.test(commitType.id)) { // Identify specific problematic characters const invalidChars = commitType.id - .split('') - .filter(char => !/[a-z]/.test(char)) + .split("") + .filter((char) => !/[a-z]/.test(char)) .filter((char, idx, arr) => arr.indexOf(char) === idx) // unique - .map(char => { + .map((char) => { if (char === char.toUpperCase() && char !== char.toLowerCase()) { return `${char} (uppercase)`; - } else if (char === '-') { + } else if (char === "-") { return `- (dash)`; - } else if (char === '_') { + } else if (char === "_") { return `_ (underscore)`; } else if (/\d/.test(char)) { return `${char} (number)`; - } else if (char === ' ') { - return '(space)'; + } else if (char === " ") { + return "(space)"; } else { return `${char} (special character)`; } }); - + errors.push({ field: `${fieldPrefix}.id`, fieldDisplay: `${displayPrefix} → ID field`, message: 'Field "id" must contain only lowercase letters (a-z)', - userMessage: 'Commit type IDs must be lowercase letters only', + userMessage: "Commit type IDs must be lowercase letters only", value: commitType.id, - expectedFormat: 'lowercase letters only (a-z)', - examples: ['feat', 'fix', 'docs', 'refactor', 'test'], - issue: `Contains invalid characters: ${invalidChars.join(', ')}` + expectedFormat: "lowercase letters only (a-z)", + examples: ["feat", "fix", "docs", "refactor", "test"], + issue: `Contains invalid characters: ${invalidChars.join(", ")}`, }); } - + // Validate required 'description' field if (!commitType.description) { errors.push({ field: `${fieldPrefix}.description`, fieldDisplay: `${displayPrefix} → description field`, message: 'Required field "description" is missing', - userMessage: 'Every commit type must have a description', + userMessage: "Every commit type must have a description", value: commitType.description, - examples: ['"A new feature"', '"Bug fix for users"', '"Documentation changes"'], - issue: 'Missing required field' + examples: [ + '"A new feature"', + '"Bug fix for users"', + '"Documentation changes"', + ], + issue: "Missing required field", }); - } else if (typeof commitType.description !== 'string') { + } else if (typeof commitType.description !== "string") { errors.push({ field: `${fieldPrefix}.description`, fieldDisplay: `${displayPrefix} → description field`, message: 'Field "description" must be a string', - userMessage: 'Commit type description must be text', + userMessage: "Commit type description must be text", value: commitType.description, - examples: ['"A new feature"', '"Bug fix for users"', '"Documentation changes"'], - issue: 'Found non-string value' + examples: [ + '"A new feature"', + '"Bug fix for users"', + '"Documentation changes"', + ], + issue: "Found non-string value", }); - } else if (commitType.description.trim() === '') { + } else if (commitType.description.trim() === "") { errors.push({ field: `${fieldPrefix}.description`, fieldDisplay: `${displayPrefix} → description field`, message: 'Field "description" cannot be empty', - userMessage: 'Commit type description cannot be empty', + userMessage: "Commit type description cannot be empty", value: commitType.description, - examples: ['"A new feature"', '"Bug fix for users"', '"Documentation changes"'], - issue: 'Empty string' + examples: [ + '"A new feature"', + '"Bug fix for users"', + '"Documentation changes"', + ], + issue: "Empty string", }); } - + // Validate optional 'emoji' field if (commitType.emoji !== undefined) { - if (typeof commitType.emoji !== 'string') { + if (typeof commitType.emoji !== "string") { errors.push({ field: `${fieldPrefix}.emoji`, fieldDisplay: `${displayPrefix} → emoji field`, message: 'Field "emoji" must be a string', - userMessage: 'Emoji field must be text if provided', + userMessage: "Emoji field must be text if provided", value: commitType.emoji, - issue: 'Found non-string value' + issue: "Found non-string value", }); } // Note: Emoji format validation will be added in Phase 3 } - + return errors; } @@ -264,72 +289,89 @@ export class ConfigValidator { */ private validateOptionalSections(config: RawConfig): ValidationError[] { const errors: ValidationError[] = []; - + // Validate version field if present - if (config.version !== undefined && typeof config.version !== 'string') { + if (config.version !== undefined && typeof config.version !== "string") { errors.push({ - field: 'version', - fieldDisplay: 'Schema version', + field: "version", + fieldDisplay: "Schema version", message: 'Field "version" must be a string', - userMessage: 'The version field must be text', + userMessage: "The version field must be text", value: config.version, expectedFormat: 'version string (e.g., "1.0")', - issue: 'Found non-string value' + issue: "Found non-string value", }); } - + // Validate config section if present - if (config.config !== undefined && (typeof config.config !== 'object' || Array.isArray(config.config))) { + if ( + config.config !== undefined && + (typeof config.config !== "object" || Array.isArray(config.config)) + ) { errors.push({ - field: 'config', - fieldDisplay: 'Config section', + field: "config", + fieldDisplay: "Config section", message: 'Field "config" must be an object', - userMessage: 'The config section must be an object with key-value pairs', + userMessage: + "The config section must be an object with key-value pairs", value: config.config, - expectedFormat: 'object with configuration settings', - issue: 'Found non-object value' + expectedFormat: "object with configuration settings", + issue: "Found non-object value", }); } - + // Validate format section if present - if (config.format !== undefined && (typeof config.format !== 'object' || Array.isArray(config.format))) { + if ( + config.format !== undefined && + (typeof config.format !== "object" || Array.isArray(config.format)) + ) { errors.push({ - field: 'format', - fieldDisplay: 'Format section', + field: "format", + fieldDisplay: "Format section", message: 'Field "format" must be an object', - userMessage: 'The format section must be an object with formatting rules', + userMessage: + "The format section must be an object with formatting rules", value: config.format, - expectedFormat: 'object with format settings', - issue: 'Found non-object value' + expectedFormat: "object with format settings", + issue: "Found non-object value", }); } - + // Validate validation section if present - if (config.validation !== undefined && (typeof config.validation !== 'object' || Array.isArray(config.validation))) { + if ( + config.validation !== undefined && + (typeof config.validation !== "object" || + Array.isArray(config.validation)) + ) { errors.push({ - field: 'validation', - fieldDisplay: 'Validation section', + field: "validation", + fieldDisplay: "Validation section", message: 'Field "validation" must be an object', - userMessage: 'The validation section must be an object with validation rules', + userMessage: + "The validation section must be an object with validation rules", value: config.validation, - expectedFormat: 'object with validation settings', - issue: 'Found non-object value' + expectedFormat: "object with validation settings", + issue: "Found non-object value", }); } - + // Validate advanced section if present - if (config.advanced !== undefined && (typeof config.advanced !== 'object' || Array.isArray(config.advanced))) { + if ( + config.advanced !== undefined && + (typeof config.advanced !== "object" || Array.isArray(config.advanced)) + ) { errors.push({ - field: 'advanced', - fieldDisplay: 'Advanced section', + field: "advanced", + fieldDisplay: "Advanced section", message: 'Field "advanced" must be an object', - userMessage: 'The advanced section must be an object with advanced settings', + userMessage: + "The advanced section must be an object with advanced settings", value: config.advanced, - expectedFormat: 'object with advanced configuration', - issue: 'Found non-object value' + expectedFormat: "object with advanced configuration", + issue: "Found non-object value", }); } - + return errors; } @@ -339,9 +381,11 @@ export class ConfigValidator { * @returns Whether input is a valid object structure */ private isValidConfigStructure(config: unknown): config is RawConfig { - return config !== null && - config !== undefined && - typeof config === 'object' && - !Array.isArray(config); + return ( + config !== null && + config !== undefined && + typeof config === "object" && + !Array.isArray(config) + ); } } From 7d89e2ed6d59db2b72e3e2101985b3e654ae492c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 22:26:40 -0600 Subject: [PATCH 21/77] fix: improve Clef intro animation with Houston-style behavior - Implement stationary cat with animated legs instead of horizontal movement - Add typewriter effect with character-by-character positioning - Position text beside cat with proper cursor management for concurrent animations - Replace walk-off exit with Houston-style bottom-to-top fade out - Adjust spacing to match @clack/prompts UI (catX=1, textX=20) - Fix text positioning bug by repositioning cursor before each character - Ensure clean side-by-side layout with symmetric spacing --- src/cli/commands/init/clef.ts | 117 ++++++++++++++++++++++++++++++---- 1 file changed, 104 insertions(+), 13 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index ba64415..aaa8966 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -104,9 +104,72 @@ class Clef { }); } + /** + * Type text character by character at specific position + * Creates typewriter effect for introducing Clef + * Repositions cursor for each character to handle concurrent animations + */ + private async typeText( + text: string, + x: number, + y: number, + delay: number = 40, + ): Promise { + // Type each character with explicit positioning + // This ensures text appears correctly even while cat legs animate + for (let i = 0; i < text.length; i++) { + // Reposition cursor for each character (handles concurrent animations) + process.stdout.write(`\x1B[${y};${x + i}H`); + process.stdout.write(pc.cyan(text[i])); + await sleep(delay); + } + } + + /** + * Clear a specific line from startX to end of line + */ + private clearLine(y: number, startX: number): void { + process.stdout.write(`\x1B[${y};${startX}H`); + process.stdout.write("\x1B[K"); // Clear from cursor to end of line + } + + /** + * Animate legs in place without horizontal movement + * Continues until shouldContinue callback returns false + */ + private async animateLegs( + x: number, + shouldContinue: () => boolean, + ): Promise { + const frames = [this.frames.walk1, this.frames.walk2]; + let frameIndex = 0; + + while (shouldContinue()) { + this.renderFrame(frames[frameIndex % 2], x); + frameIndex++; + await sleep(200); // Leg animation speed + } + } + + /** + * Fade out cat Houston-style + * Erases cat from bottom to top for smooth disappearance + */ + private async fadeOut(x: number): Promise { + const catLines = this.frames.standing.split("\n"); + + // Erase from bottom to top + for (let i = catLines.length - 1; i >= 0; i--) { + process.stdout.write(`\x1B[${5 + i};${x}H`); + process.stdout.write(" ".repeat(20)); // Clear line with spaces + await sleep(80); + } + } + /** * Animate character walking from start to end position * Creates smooth horizontal movement using frame interpolation + * Used for processing and outro sequences */ private async walk( startX: number, @@ -137,8 +200,9 @@ class Clef { /** * Introduction sequence - * Character walks in from left, displays greeting, then exits - * Duration: approximately 3 seconds + * Cat appears stationary with animated legs, text types beside it + * Houston-style: text types out, clears, new text types, then fades + * Duration: approximately 5 seconds */ async intro(): Promise { if (!this.caps.supportsAnimation) { @@ -150,22 +214,49 @@ class Clef { return; } - // Walk in from left side - await this.walk(0, 10, 1000); - - // Display introduction message + this.hideCursor(); this.clearScreen(); - console.log(this.frames.standing); - console.log(pc.cyan(" Hey there! My name is Clef!")); - console.log(pc.cyan(" Let me help you get started...meoww!\n")); - await sleep(2000); + const catX = 1; // Match @clack/prompts left margin + const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) + const textX = catX + catWidth + 1; // 1 space padding for symmetry + const textY = 7; // Vertically centered with cat (cat starts at line 5, 4 lines tall) - // Walk off to right side - await this.walk(10, this.caps.terminalWidth, 800); + // Messages to type + const messages = [ + "Hey there! My name is Clef!", + "Let me help you get started...meoww!", + ]; - // Complete clear for next section + // Start leg animation in background (non-blocking) + let isAnimating = true; + const animationPromise = this.animateLegs(catX, () => isAnimating); + + // Type first message + await this.typeText(messages[0], textX, textY); + await sleep(1000); + + // Clear first message + this.clearLine(textY, textX); + await sleep(300); + + // Type second message + await this.typeText(messages[1], textX, textY); + await sleep(1200); + + // Stop leg animation + isAnimating = false; + await animationPromise; + + // Fade out cat Houston-style + await this.fadeOut(catX); + + // Small pause before clearing + await sleep(200); + + // Clear screen for prompts this.clearScreen(); + this.showCursor(); } /** From 0249d6ff642b1f141587b3d2be1cd4e4b16684b1 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 22:27:10 -0600 Subject: [PATCH 22/77] style: remove trailing newline from changeset --- .changeset/init-command-with-clef.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/init-command-with-clef.md b/.changeset/init-command-with-clef.md index ba6635a..589c4fd 100644 --- a/.changeset/init-command-with-clef.md +++ b/.changeset/init-command-with-clef.md @@ -12,4 +12,3 @@ feat: add interactive init command with Clef mascot - Automatic YAML configuration file generation with validation - Non-intrusive design with clean labels and compact spacing - Graceful degradation for terminals without animation support - From 18f6f5a93e60df340df6eb022d68fbf7393acb0f Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 26 Oct 2025 23:32:43 -0600 Subject: [PATCH 23/77] feat: implement bright color palette and Astro-style flow - Add custom ANSI 256 color system with bright, energetic palette - Bright label backgrounds (magenta, cyan, blue, yellow, green) - Normal white text for intro/outro messages (clean, readable) - Replace summary section with Astro-style processing checklist - Add dynamic title that changes from 'initializing' to 'initialized' in green - Implement stationary outro cat with side-by-side message display - Remove redundant summary displays for cleaner flow - Keep prompts visible throughout entire initialization - Add proper spacing between sections - Fix cursor positioning for dynamic title updates --- src/cli/commands/init/clef.ts | 50 ++++++------ src/cli/commands/init/colors.ts | 131 +++++++++++++++++++++++++++++++ src/cli/commands/init/index.ts | 88 ++++++++++++--------- src/cli/commands/init/prompts.ts | 86 ++++++++++++++------ 4 files changed, 272 insertions(+), 83 deletions(-) create mode 100644 src/cli/commands/init/colors.ts diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index aaa8966..841bf5b 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -14,7 +14,7 @@ */ import { setTimeout as sleep } from "timers/promises"; -import pc from "picocolors"; +import { textColors, success, attention } from "./colors.js"; interface AnimationCapabilities { supportsAnimation: boolean; @@ -120,7 +120,8 @@ class Clef { for (let i = 0; i < text.length; i++) { // Reposition cursor for each character (handles concurrent animations) process.stdout.write(`\x1B[${y};${x + i}H`); - process.stdout.write(pc.cyan(text[i])); + // Use normal white for clean, readable intro text + process.stdout.write(textColors.white(text[i])); await sleep(delay); } } @@ -279,7 +280,7 @@ class Clef { // Show typing animation this.clearScreen(); console.log(this.frames.typing); - console.log(pc.dim(` ${message}`)); + console.log(` ${attention(message)}`); // Execute actual task await task(); @@ -295,37 +296,38 @@ class Clef { /** * Outro sequence - * Character celebrates completion and waves goodbye - * Duration: approximately 4 seconds + * Cat and text display side by side using normal console output + * Astro Houston-style: stays on screen as final message (no clear, no walk off) + * Duration: approximately 2 seconds */ async outro(): Promise { if (!this.caps.supportsAnimation) { // Static fallback console.log(this.frames.waving); - console.log("Happy committing!\n"); - await sleep(2000); - return; + console.log("You're all set! Happy committing!"); + return; // No clear - message stays visible } - // Walk in quickly - await this.walk(0, 10, 600); - - // Show celebration - this.clearScreen(); - console.log(this.frames.celebrate); - await sleep(1000); + // Split cat into lines for side-by-side display + const catLines = this.frames.waving.split("\n"); + const message = ` ${textColors.white("You're all set! Happy committing!")}`; + + // Display cat and message side by side (line by line) + for (let i = 0; i < catLines.length; i++) { + if (i === 2) { + // Show message on the middle line of the cat (vertically centered) + console.log(catLines[i] + message); + } else { + console.log(catLines[i]); + } + } - // Wave goodbye - this.clearScreen(); - console.log(this.frames.waving); - console.log(pc.cyan(" Happy committing!\n")); - await sleep(2000); + console.log(); // Extra line at end - // Walk off - await this.walk(10, this.caps.terminalWidth, 800); + // Small pause to let user see the message + await sleep(1500); - // Final complete clear - this.clearScreen(); + // Done - cat and message remain visible (no clear, no cursor hide) } /** diff --git a/src/cli/commands/init/colors.ts b/src/cli/commands/init/colors.ts new file mode 100644 index 0000000..afe694e --- /dev/null +++ b/src/cli/commands/init/colors.ts @@ -0,0 +1,131 @@ +/** + * Custom Color Palette + * + * Provides bright, energetic colors using ANSI 256-color codes + * for a modern, high-contrast CLI experience. + * + * Design Philosophy: + * - Vibrant but readable + * - Positive energy with warm, inviting tones + * - High contrast for easy scanning + * - Consistent visual hierarchy + */ + +/** + * Bright background colors for step labels + * Uses ANSI 256-color palette for vibrant, saturated colors + * Text is black (30m) for maximum contrast + */ +export const labelColors = { + /** + * Bright Magenta - Vibrant, energetic, attention-grabbing + * Perfect for the first step (preset selection) + */ + bgBrightMagenta: (text: string) => `\x1b[48;5;201m\x1b[30m${text}\x1b[0m`, + + /** + * Bright Cyan - Fresh, modern, tech-forward + * Great for emoji and next steps + */ + bgBrightCyan: (text: string) => `\x1b[48;5;51m\x1b[30m${text}\x1b[0m`, + + /** + * Bright Blue - Clear, professional, confident + * Ideal for scope and types configuration + */ + bgBrightBlue: (text: string) => `\x1b[48;5;39m\x1b[30m${text}\x1b[0m`, + + /** + * Bright Yellow - Attention-grabbing, action-oriented + * Perfect for call-to-action (next steps) + */ + bgBrightYellow: (text: string) => `\x1b[48;5;226m\x1b[30m${text}\x1b[0m`, + + /** + * Bright Green - Success, completion, positive + * Excellent for config result and success messages + */ + bgBrightGreen: (text: string) => `\x1b[48;5;46m\x1b[30m${text}\x1b[0m`, +}; + +/** + * Bright foreground colors for text + * High visibility, energetic tones + */ +export const textColors = { + /** + * Bright Cyan - Clear, friendly, welcoming + * Perfect for intro text and informational content + */ + brightCyan: (text: string) => `\x1b[38;5;51m${text}\x1b[0m`, + + /** + * Bright Green - Celebratory, positive, success + * Ideal for completion messages and success indicators + */ + brightGreen: (text: string) => `\x1b[38;5;46m${text}\x1b[0m`, + + /** + * Bright Yellow - Active, processing, attention + * Great for processing messages and highlights + */ + brightYellow: (text: string) => `\x1b[38;5;226m${text}\x1b[0m`, + + /** + * Bright Magenta - Emphasis, important, highlight + * Perfect for filenames and key information + */ + brightMagenta: (text: string) => `\x1b[38;5;201m${text}\x1b[0m`, + + /** + * Bright White - Maximum visibility, bold statements + * Excellent for headings and important text + */ + brightWhite: (text: string) => `\x1b[38;5;231m${text}\x1b[0m`, + + /** + * Normal White - Clean, neutral, readable + * Perfect for intro/outro messages (normal weight, not bold) + */ + white: (text: string) => `\x1b[37m${text}\x1b[0m`, +}; + +/** + * Text modifiers + */ +export const modifiers = { + /** + * Bold text for emphasis + */ + bold: (text: string) => `\x1b[1m${text}\x1b[22m`, + + /** + * Combine bold with color + */ + boldColor: (text: string, colorFn: (s: string) => string) => + `\x1b[1m${colorFn(text)}\x1b[22m\x1b[0m`, +}; + +/** + * Convenience function: Bold + Bright Green (success messages) + */ +export const success = (text: string) => + modifiers.boldColor(text, textColors.brightGreen); + +/** + * Convenience function: Bold + Bright Cyan (friendly messages) + */ +export const info = (text: string) => + modifiers.boldColor(text, textColors.brightCyan); + +/** + * Convenience function: Bold + Bright Yellow (attention/action) + */ +export const attention = (text: string) => + modifiers.boldColor(text, textColors.brightYellow); + +/** + * Convenience function: Bold + Bright Magenta (highlight/important) + */ +export const highlight = (text: string) => + modifiers.boldColor(text, textColors.brightMagenta); diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts index ff1883f..35f0ea9 100644 --- a/src/cli/commands/init/index.ts +++ b/src/cli/commands/init/index.ts @@ -5,11 +5,12 @@ * a project-specific configuration file. Provides preset selection, * customization options, and animated mascot for enhanced UX. * - * Flow: - * 1. Intro animation (Clef introduces tool) + * Flow (Astro-style): + * 1. Intro animation (Clef introduces tool, then clears) * 2. User prompts (preset, emoji, scope choices) - * 3. Processing animation (config generation) - * 4. Outro animation (Clef celebrates) + * 3. Summary display (show choices, then clear) + * 4. Processing checklist (compact steps, stays visible) + * 5. Outro animation (Clef appears below, stays on screen) */ import { Command } from "commander"; @@ -21,9 +22,7 @@ import { promptEmoji, promptScope, promptScopeTypes, - displaySummary, - displayConfigResult, - displayNextSteps, + displayProcessingSteps, } from "./prompts.js"; import { buildConfig, getPreset } from "../../../lib/presets/index.js"; import { generateConfigFile } from "./config-generator.js"; @@ -98,6 +97,7 @@ async function initAction(options: { } // Prompts: Clean labels, no cat + // Note: @clack/prompts clears each prompt after selection (their default behavior) const presetId = options.preset || (await promptPreset()); const preset = getPreset(presetId); @@ -110,15 +110,11 @@ async function initAction(options: { scopeRequiredFor = await promptScopeTypes(preset.types); } - // Display summary - displaySummary({ - preset: preset.name, - emoji: emojiEnabled, - scope: scopeMode, - }); - // Small pause before processing - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 800)); + + // Add spacing before processing section + console.log(); // Build config from choices const config = buildConfig(presetId, { @@ -127,29 +123,47 @@ async function initAction(options: { scopeRequiredFor, }); - // Processing: Clef reappears - await clef.processing("Creating your configuration...meoww!", async () => { - await generateConfigFile(config, projectRoot); - // Simulate some processing time for animation - await new Promise((resolve) => setTimeout(resolve, 1000)); - }); - // Screen is now completely clear - - // Display result - displayConfigResult(".labcommitr.config.yaml"); - - // Small pause - await new Promise((resolve) => setTimeout(resolve, 500)); - - // Display next steps - displayNextSteps(); - - // Small pause before finale - await new Promise((resolve) => setTimeout(resolve, 1000)); - - // Outro: Clef celebrates and exits + // Show title "Labcommitr initializing..." + console.log("Labcommitr initializing...\n"); + + // Show compact processing steps (Astro pattern: checklist stays visible) + await displayProcessingSteps([ + { + message: "Writing .labcommitr.config.yaml", + task: async () => { + await generateConfigFile(config, projectRoot); + await new Promise((resolve) => setTimeout(resolve, 800)); + }, + }, + { + message: "Validating configuration", + task: async () => { + await new Promise((resolve) => setTimeout(resolve, 600)); + }, + }, + { + message: "Setup complete", + task: async () => { + await new Promise((resolve) => setTimeout(resolve, 400)); + }, + }, + ]); + + // Change title to "Labcommitr initialized!" in green + // Move up to overwrite the "initializing..." title + // Current position is after the blank line following "Setup complete" + // Need to go up: 1 blank line + 3 steps (each with ✔) + 1 blank line after title + 1 title line = 6 lines + process.stdout.write("\x1B[6A"); // Move up 6 lines to title + process.stdout.write("\r"); // Move to start of line + process.stdout.write("\x1B[K"); // Clear the line + console.log("\x1B[32mLabcommitr initialized!\x1B[0m"); // Green text + process.stdout.write("\x1B[5B"); // Move back down 5 lines (title + blank + 3 steps) + + // Processing list stays visible (no clear) + + // Outro: Clef appears below processing list (Astro pattern) await clef.outro(); - // Screen is now completely clear - back to terminal + // Cat and message stay on screen - done! } catch (error) { // Ensure cursor is visible on error clef.stop(); diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 94626f9..138f21e 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -9,25 +9,33 @@ */ import { select, multiselect, isCancel } from "@clack/prompts"; -import pc from "picocolors"; +import { + labelColors, + textColors, + success, + info, + attention, + highlight, +} from "./colors.js"; /** * Create compact color-coded label * Labels are 7 characters wide (6 chars + padding) for alignment + * Uses bright ANSI 256 colors for high visibility */ function label( text: string, color: "magenta" | "cyan" | "blue" | "yellow" | "green", ): string { const colorFn = { - magenta: pc.bgMagenta, - cyan: pc.bgCyan, - blue: pc.bgBlue, - yellow: pc.bgYellow, - green: pc.bgGreen, + magenta: labelColors.bgBrightMagenta, + cyan: labelColors.bgBrightCyan, + blue: labelColors.bgBrightBlue, + yellow: labelColors.bgBrightYellow, + green: labelColors.bgBrightGreen, }[color]; - return colorFn(pc.black(` ${text.padEnd(6)} `)); + return colorFn(` ${text.padEnd(6)} `); } /** @@ -157,38 +165,72 @@ export async function promptScopeTypes( } /** - * Display configuration summary - * Shows user choices before config generation + * Display completed prompts in compact form (Astro pattern) + * Shows what the user selected after @clack/prompts clears itself + * This simulates keeping prompts visible on screen */ -export function displaySummary(config: { +export function displayCompletedPrompts(config: { preset: string; emoji: boolean; scope: string; }): void { - console.log("\n✓ Configuration ready!\n"); - console.log(` ■ Preset: ${config.preset}`); - console.log(` ■ Emoji: ${config.emoji ? "Enabled" : "Disabled"}`); - console.log(` ■ Scope: ${config.scope}`); - console.log(` ■ Types: feat, fix, docs, style, refactor, test, chore\n`); + console.log( + `${label("preset", "magenta")} ${textColors.brightCyan(config.preset)}`, + ); + console.log( + `${label("emoji", "cyan")} ${textColors.brightCyan(config.emoji ? "Yes" : "No")}`, + ); + console.log( + `${label("scope", "blue")} ${textColors.brightCyan(config.scope)}`, + ); + console.log(); // Extra line +} + +/** + * Display processing steps as compact checklist (Astro-style) + * Shows what's happening during config generation + * Each step executes its task and displays success when complete + */ +export async function displayProcessingSteps( + steps: Array<{ message: string; task: () => Promise }>, +): Promise { + for (const step of steps) { + // Show pending state with spinning indicator + process.stdout.write(` ${textColors.brightCyan("◐")} ${step.message}...`); + + // Execute task + await step.task(); + + // Clear line and show success checkmark + process.stdout.write("\r"); // Return to start of line + console.log(` ${success("✔")} ${step.message}`); + } + console.log(); // Extra newline after all steps } /** * Display configuration file write result */ export function displayConfigResult(filename: string): void { - console.log(`${label("config", "green")} Writing ${pc.cyan(filename)}`); - console.log(" Done\n"); + console.log(`${label("config", "green")} Writing ${highlight(filename)}`); + console.log(` ${success("Done")}\n`); } /** * Display next steps after successful setup */ export function displayNextSteps(): void { - console.log("✓ Ready to commit!\n"); - console.log(`${label("next", "cyan")} Get started with these commands:\n`); - console.log(" lab config show View your configuration"); - console.log(" lab commit Create your first commit\n"); + console.log(`${success("✓ Ready to commit!")}\n`); + console.log( + `${label("next", "yellow")} ${attention("Get started with these commands:")}\n`, + ); + console.log( + ` ${textColors.brightCyan("lab config show")} View your configuration`, + ); + console.log( + ` ${textColors.brightCyan("lab commit")} Create your first commit\n`, + ); console.log( - " Customize anytime by editing .labcommitr.config.yaml\n", + ` ${textColors.brightYellow("Customize anytime by editing .labcommitr.config.yaml")}\n`, ); } From cf1097fc0df664801be937f5aa0995c77c4b600b Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:28:26 -0600 Subject: [PATCH 24/77] fix: reduce intro animation spacing to match prompt labels - Move cat rendering from line 5 to line 1 (remove blank lines) - Cat now starts at same vertical position as prompt labels - Eliminates excessive whitespace in intro sequence --- src/cli/commands/init/clef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 841bf5b..503d23e 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -99,7 +99,7 @@ class Clef { const lines = frame.split("\n"); lines.forEach((line, idx) => { // Move cursor to position (row, column) - process.stdout.write(`\x1B[${idx + 5};${x}H`); + process.stdout.write(`\x1B[${idx + 1};${x}H`); process.stdout.write(line); }); } From c1221e9c4d0d978306a573399992481cff761301 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:31:31 -0600 Subject: [PATCH 25/77] fix: add 3-line padding and align intro text with cat - Add 3 lines of padding above cat (starts at line 4) - Left-aligned cat at column 1 (no indentation) - Increase spacing between cat and text to 3 spaces - Vertically center text with cat (line 5) - Update fadeOut to use new line positions --- src/cli/commands/init/clef.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 503d23e..e7ed493 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -94,12 +94,14 @@ class Clef { /** * Render ASCII art frame at specific horizontal position * Uses absolute cursor positioning for each line + * With 3 lines of padding above the cat */ private renderFrame(frame: string, x: number): void { const lines = frame.split("\n"); lines.forEach((line, idx) => { // Move cursor to position (row, column) - process.stdout.write(`\x1B[${idx + 1};${x}H`); + // Start at line 4 (3 lines of padding + cat starts at idx 0) + process.stdout.write(`\x1B[${idx + 4};${x}H`); process.stdout.write(line); }); } @@ -160,8 +162,9 @@ class Clef { const catLines = this.frames.standing.split("\n"); // Erase from bottom to top + // Cat starts at line 4 (3 lines of padding), so fade from line 7 to line 4 for (let i = catLines.length - 1; i >= 0; i--) { - process.stdout.write(`\x1B[${5 + i};${x}H`); + process.stdout.write(`\x1B[${4 + i};${x}H`); process.stdout.write(" ".repeat(20)); // Clear line with spaces await sleep(80); } @@ -218,10 +221,10 @@ class Clef { this.hideCursor(); this.clearScreen(); - const catX = 1; // Match @clack/prompts left margin + const catX = 1; // Left edge of terminal (no indentation) const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) - const textX = catX + catWidth + 1; // 1 space padding for symmetry - const textY = 7; // Vertically centered with cat (cat starts at line 5, 4 lines tall) + const textX = catX + catWidth + 3; // 3 spaces padding for symmetry + const textY = 5; // Vertically centered with cat (cat is 4 lines tall, centered at ~line 5) // Messages to type const messages = [ From 26a05428e5f5eeb93f19a0922ed1e29052ea0a27 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:33:08 -0600 Subject: [PATCH 26/77] fix: reduce top padding from 3 to 2 lines and side spacing from 3 to 1 space - Cat starts at line 3 (was line 4) with 2-line top padding - Reduced spacing between cat and text from 3 to 1 space - Updated fadeOut to use new position (lines 3-6) - Creates more compact, cleaner layout --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index e7ed493..58142ff 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -94,14 +94,14 @@ class Clef { /** * Render ASCII art frame at specific horizontal position * Uses absolute cursor positioning for each line - * With 3 lines of padding above the cat + * With 2 lines of padding above the cat */ private renderFrame(frame: string, x: number): void { const lines = frame.split("\n"); lines.forEach((line, idx) => { // Move cursor to position (row, column) - // Start at line 4 (3 lines of padding + cat starts at idx 0) - process.stdout.write(`\x1B[${idx + 4};${x}H`); + // Start at line 3 (2 lines of padding + cat starts at idx 0) + process.stdout.write(`\x1B[${idx + 3};${x}H`); process.stdout.write(line); }); } @@ -162,9 +162,9 @@ class Clef { const catLines = this.frames.standing.split("\n"); // Erase from bottom to top - // Cat starts at line 4 (3 lines of padding), so fade from line 7 to line 4 + // Cat starts at line 3 (2 lines of padding), so fade from line 6 to line 3 for (let i = catLines.length - 1; i >= 0; i--) { - process.stdout.write(`\x1B[${4 + i};${x}H`); + process.stdout.write(`\x1B[${3 + i};${x}H`); process.stdout.write(" ".repeat(20)); // Clear line with spaces await sleep(80); } @@ -223,7 +223,7 @@ class Clef { const catX = 1; // Left edge of terminal (no indentation) const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) - const textX = catX + catWidth + 3; // 3 spaces padding for symmetry + const textX = catX + catWidth + 1; // 1 space padding on either side const textY = 5; // Vertically centered with cat (cat is 4 lines tall, centered at ~line 5) // Messages to type From 6a47a10a9312a1bc29415a7b922c83a4f0959de0 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:34:21 -0600 Subject: [PATCH 27/77] fix: remove all padding above cat to eliminate excessive whitespace - Cat now starts at line 1 with zero padding - Text centered at line 3 (was line 5) to align with cat - Updated fadeOut to use line positions 1-4 (was 3-6) - Eliminates all blank lines above the animation --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 58142ff..1ccfcbb 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -94,14 +94,14 @@ class Clef { /** * Render ASCII art frame at specific horizontal position * Uses absolute cursor positioning for each line - * With 2 lines of padding above the cat + * No padding above the cat - starts at line 1 */ private renderFrame(frame: string, x: number): void { const lines = frame.split("\n"); lines.forEach((line, idx) => { // Move cursor to position (row, column) - // Start at line 3 (2 lines of padding + cat starts at idx 0) - process.stdout.write(`\x1B[${idx + 3};${x}H`); + // Start at line 1 (no padding) + process.stdout.write(`\x1B[${idx + 1};${x}H`); process.stdout.write(line); }); } @@ -162,9 +162,9 @@ class Clef { const catLines = this.frames.standing.split("\n"); // Erase from bottom to top - // Cat starts at line 3 (2 lines of padding), so fade from line 6 to line 3 + // Cat starts at line 1 (no padding), so fade from line 4 to line 1 for (let i = catLines.length - 1; i >= 0; i--) { - process.stdout.write(`\x1B[${3 + i};${x}H`); + process.stdout.write(`\x1B[${1 + i};${x}H`); process.stdout.write(" ".repeat(20)); // Clear line with spaces await sleep(80); } @@ -224,7 +224,7 @@ class Clef { const catX = 1; // Left edge of terminal (no indentation) const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) const textX = catX + catWidth + 1; // 1 space padding on either side - const textY = 5; // Vertically centered with cat (cat is 4 lines tall, centered at ~line 5) + const textY = 3; // Vertically centered with cat (cat is 4 lines tall, centered at line 3) // Messages to type const messages = [ From 91084b2d437026ee0d7d9b9ed15c811c2bfd11ca Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:36:17 -0600 Subject: [PATCH 28/77] fix: align text with cat's face instead of middle - Changed textY from line 3 to line 2 (face/eyes level) - Text now visually aligns with cat's expression - Creates more natural, eye-level reading position --- src/cli/commands/init/clef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 1ccfcbb..e96d489 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -224,7 +224,7 @@ class Clef { const catX = 1; // Left edge of terminal (no indentation) const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) const textX = catX + catWidth + 1; // 1 space padding on either side - const textY = 3; // Vertically centered with cat (cat is 4 lines tall, centered at line 3) + const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) // Messages to type const messages = [ From e1c67b7d6ff2f7e961e20cd9b0955bdb633601ac Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:37:29 -0600 Subject: [PATCH 29/77] fix: adjust catX to account for ASCII art leading spaces - Changed catX from 1 to 8 (7 leading spaces + 1) - Cat now aligns with the intended left edge position - Left margin now matches the visual cat position --- src/cli/commands/init/clef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index e96d489..7332c9e 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -221,7 +221,7 @@ class Clef { this.hideCursor(); this.clearScreen(); - const catX = 1; // Left edge of terminal (no indentation) + const catX = 8; // Account for 7 leading spaces in ASCII art + 1 for terminal edge const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) const textX = catX + catWidth + 1; // 1 space padding on either side const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) From 3787283481a9e935d6173bf7d0a105b0ffcbc51c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:39:28 -0600 Subject: [PATCH 30/77] fix: remove leading spaces from ASCII art to eliminate extra spacing - Removed all leading and trailing spaces from cat ASCII art frames - Changed catX from 8 to 1 (left edge positioning) - Updated catWidth from 18 to 5 (actual visible width) - Cat now renders flush with left edge of terminal --- src/cli/commands/init/clef.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 7332c9e..e9aa1d1 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -31,14 +31,14 @@ class Clef { private caps: AnimationCapabilities; private currentX: number = 0; - // ASCII art frames for different states + // ASCII art frames for different states (no leading/trailing spaces) private readonly frames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_) `, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|) `, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_) `, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_) `, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_) `, + standing: `/\\_/\\\n( ^.^ )\n/| |\n(_| |_)`, + walk1: `/\\_/\\\n( ^.^ )\n/| |\\\n(_| _|)`, + walk2: `/\\_/\\\n( ^.^ )\n/| |\\\n(|_ |_)`, + typing: `/\\_/\\\n( -.- )\n/|⌨ |\n(_|_|_)`, + celebrate: `/\\_/\\\n( ^ω^ )\n | | \n/ \\`, + waving: `/\\_/\\\n( ^.^ )~\n/| |\n(_| |_)`, }; constructor() { @@ -221,8 +221,8 @@ class Clef { this.hideCursor(); this.clearScreen(); - const catX = 8; // Account for 7 leading spaces in ASCII art + 1 for terminal edge - const catWidth = 18; // Actual visible width of cat ASCII art (rightmost char) + const catX = 1; // Left edge (no leading spaces in ASCII art) + const catWidth = 5; // Actual visible width of cat ASCII art (max width is 5 chars) const textX = catX + catWidth + 1; // 1 space padding on either side const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) From 5ca5ba3c812665c2c395396cd40db85b352f6b76 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:42:39 -0600 Subject: [PATCH 31/77] fix: add uniform padding to ASCII frames and correct catWidth - Added trailing spaces to all frame lines for uniform width - Updated catWidth from 5 to 7 (actual max width of cat face) - Frames now align properly without affecting visual appearance - Text positioning now correct with 7-char width --- src/cli/commands/init/clef.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index e9aa1d1..65ab4b2 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -31,14 +31,14 @@ class Clef { private caps: AnimationCapabilities; private currentX: number = 0; - // ASCII art frames for different states (no leading/trailing spaces) + // ASCII art frames for different states (uniform width, no leading/trailing spaces) private readonly frames = { - standing: `/\\_/\\\n( ^.^ )\n/| |\n(_| |_)`, - walk1: `/\\_/\\\n( ^.^ )\n/| |\\\n(_| _|)`, - walk2: `/\\_/\\\n( ^.^ )\n/| |\\\n(|_ |_)`, - typing: `/\\_/\\\n( -.- )\n/|⌨ |\n(_|_|_)`, - celebrate: `/\\_/\\\n( ^ω^ )\n | | \n/ \\`, - waving: `/\\_/\\\n( ^.^ )~\n/| |\n(_| |_)`, + standing: `/\\_/\\ \n( ^.^ ) \n/| | \n(_| |_)`, + walk1: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(_| _|)`, + walk2: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(|_ |_)`, + typing: `/\\_/\\ \n( -.- ) \n/|⌨ | \n(_|_|_)`, + celebrate: `/\\_/\\ \n( ^ω^ ) \n | | \n/ \\`, + waving: `/\\_/\\ \n( ^.^ )~ \n/| | \n(_| |_)`, }; constructor() { @@ -222,7 +222,7 @@ class Clef { this.clearScreen(); const catX = 1; // Left edge (no leading spaces in ASCII art) - const catWidth = 5; // Actual visible width of cat ASCII art (max width is 5 chars) + const catWidth = 7; // Actual visible width of cat ASCII art (max width is 7 chars for the face) const textX = catX + catWidth + 1; // 1 space padding on either side const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) From befd289ccacceb81240e601c188e73d0e72bd41c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:47:20 -0600 Subject: [PATCH 32/77] fix: pad all ASCII frame lines to exactly 8 characters for proper alignment - Added trailing spaces to ensure every line is exactly 8 characters - Resolves ear, hand, and body misalignment issues - All frames now maintain consistent column positioning --- src/cli/commands/init/clef.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 65ab4b2..48ec550 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -31,14 +31,14 @@ class Clef { private caps: AnimationCapabilities; private currentX: number = 0; - // ASCII art frames for different states (uniform width, no leading/trailing spaces) + // ASCII art frames for different states (uniform 8-char width) private readonly frames = { - standing: `/\\_/\\ \n( ^.^ ) \n/| | \n(_| |_)`, - walk1: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(_| _|)`, - walk2: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(|_ |_)`, - typing: `/\\_/\\ \n( -.- ) \n/|⌨ | \n(_|_|_)`, - celebrate: `/\\_/\\ \n( ^ω^ ) \n | | \n/ \\`, - waving: `/\\_/\\ \n( ^.^ )~ \n/| | \n(_| |_)`, + standing: `/\\_/\\ \n( ^.^ ) \n/| | \n(_| |_)`, + walk1: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(_| _|) `, + walk2: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(|_ |_) `, + typing: `/\\_/\\ \n( -.- ) \n/|⌨ | \n(_|_|_) `, + celebrate: `/\\_/\\ \n( ^ω^ ) \n | | \n/ \\ `, + waving: `/\\_/\\ \n( ^.^ )~ \n/| | \n(_| |_)`, }; constructor() { From 720e7e03f46788cfd4532c73dfe3e65df8e582a7 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:48:45 -0600 Subject: [PATCH 33/77] fix: add extra space to ears line for proper visual alignment - Changed ears line from 7 to 8 characters by adding one more trailing space - Ears now align properly with face, body, and feet lines - Maintains consistent 8-char width across all frame lines --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 48ec550..5e24c98 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // ASCII art frames for different states (uniform 8-char width) private readonly frames = { - standing: `/\\_/\\ \n( ^.^ ) \n/| | \n(_| |_)`, - walk1: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(_| _|) `, - walk2: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(|_ |_) `, - typing: `/\\_/\\ \n( -.- ) \n/|⌨ | \n(_|_|_) `, - celebrate: `/\\_/\\ \n( ^ω^ ) \n | | \n/ \\ `, - waving: `/\\_/\\ \n( ^.^ )~ \n/| | \n(_| |_)`, + standing: `/\\_/\\ \n( ^.^ ) \n/| | \n(_| |_)`, + walk1: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(_| _|) `, + walk2: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(|_ |_) `, + typing: `/\\_/\\ \n( -.- ) \n/|⌨ | \n(_|_|_) `, + celebrate: `/\\_/\\ \n( ^ω^ ) \n | | \n/ \\ `, + waving: `/\\_/\\ \n( ^.^ )~ \n/| | \n(_| |_)`, }; constructor() { From 03cc0ac55387ae00805a646e5adc084e8124e510 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:50:23 -0600 Subject: [PATCH 34/77] fix: add leading space to all ASCII frame lines for proper alignment - Added leading space to all lines in all frames - Ears now align with face, body, and feet - Consistent visual column positioning across all lines --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 5e24c98..8b4a4de 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // ASCII art frames for different states (uniform 8-char width) private readonly frames = { - standing: `/\\_/\\ \n( ^.^ ) \n/| | \n(_| |_)`, - walk1: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(_| _|) `, - walk2: `/\\_/\\ \n( ^.^ ) \n/| |\\ \n(|_ |_) `, - typing: `/\\_/\\ \n( -.- ) \n/|⌨ | \n(_|_|_) `, - celebrate: `/\\_/\\ \n( ^ω^ ) \n | | \n/ \\ `, - waving: `/\\_/\\ \n( ^.^ )~ \n/| | \n(_| |_)`, + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, }; constructor() { From 23cd9bc1e46134024cb3c5b2c2f05326e346fee1 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:51:26 -0600 Subject: [PATCH 35/77] fix: adjust catX to 0 to account for leading spaces in ASCII frames - Changed catX from 1 to 0 (all frames have leading space) - Preserves the leading spaces in all frame lines - Cat now positions correctly at the left edge --- src/cli/commands/init/clef.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 8b4a4de..7b6205e 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -221,7 +221,7 @@ class Clef { this.hideCursor(); this.clearScreen(); - const catX = 1; // Left edge (no leading spaces in ASCII art) + const catX = 0; // Start at column 0 (accounts for leading space in ASCII art) const catWidth = 7; // Actual visible width of cat ASCII art (max width is 7 chars for the face) const textX = catX + catWidth + 1; // 1 space padding on either side const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) From 872f876454587c08f67ccfff7e0e7ad89a0d9f20 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:57:46 -0600 Subject: [PATCH 36/77] feat: implement robust frame normalization for terminal compatibility - Added normalizeFrames() method that automatically calculates max width and height - All frames now uniform dimensions (prevents misalignment issues) - Dynamic frameWidth calculation replaces hardcoded catWidth - Separates rawFrames (unprocessed) from frames (normalized) - Works across different terminal sizes, fonts, and configurations - Maintains leading spaces in ASCII art for proper visual alignment - Graceful degradation for non-TTY environments already implemented --- src/cli/commands/init/clef.ts | 52 ++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 7b6205e..7076980 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -31,8 +31,8 @@ class Clef { private caps: AnimationCapabilities; private currentX: number = 0; - // ASCII art frames for different states (uniform 8-char width) - private readonly frames = { + // Raw ASCII art frames (unprocessed) + private readonly rawFrames = { standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, @@ -41,8 +41,53 @@ class Clef { waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, }; + // Normalized frames (uniform dimensions) + private frames!: typeof this.rawFrames; + + // Frame dimensions after normalization + private frameWidth = 0; + private frameHeight = 0; + constructor() { this.caps = this.detectCapabilities(); + this.normalizeFrames(); // Initializes this.frames + } + + /** + * Normalize all frames to uniform width and height + * Ensures consistent alignment across all animation frames + * Critical for terminal compatibility with different fonts/dimensions + */ + private normalizeFrames(): void { + // Find maximum width across all frames + const allLines = Object.values(this.rawFrames).flatMap((frame: string) => + frame.split("\n"), + ); + this.frameWidth = Math.max(...allLines.map((line: string) => line.length)); + + // Find maximum height across all frames + this.frameHeight = Math.max( + ...Object.values(this.rawFrames).map( + (frame: string) => frame.split("\n").length, + ), + ); + + // Normalize each frame to maximum dimensions + const keyedFrames: Partial = {}; + (Object.keys(this.rawFrames) as Array).forEach( + (key) => { + const lines = this.rawFrames[key].split("\n"); + const normalized = lines.map((line: string) => + line.padEnd(this.frameWidth, " "), + ); + // Pad height if necessary + while (normalized.length < this.frameHeight) { + normalized.push(" ".repeat(this.frameWidth)); + } + keyedFrames[key] = normalized.join("\n"); + }, + ); + this.frames = keyedFrames as typeof this.rawFrames; } /** @@ -222,8 +267,7 @@ class Clef { this.clearScreen(); const catX = 0; // Start at column 0 (accounts for leading space in ASCII art) - const catWidth = 7; // Actual visible width of cat ASCII art (max width is 7 chars for the face) - const textX = catX + catWidth + 1; // 1 space padding on either side + const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) // Messages to type From 66c4516d3defb802a7449d9497e1aa60eee1d920 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 18:59:39 -0600 Subject: [PATCH 37/77] fix: adjust ears line spacing to align with face line - Added extra leading space to all ears lines (2 spaces instead of 1) - Ears line now: ' /\_/\ ' (9 chars before padding) - Aligns ears visually with face and body lines - Maintains visual symmetry after normalization to 9 chars --- src/cli/commands/init/clef.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 7076980..1800688 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // Raw ASCII art frames (unprocessed) private readonly rawFrames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, }; // Normalized frames (uniform dimensions) @@ -51,6 +51,19 @@ class Clef { constructor() { this.caps = this.detectCapabilities(); this.normalizeFrames(); // Initializes this.frames + + // Debug: Log normalized frame dimensions + if (process.env.LABCOMMITR_DEBUG) { + console.log( + `Normalized frame dimensions: ${this.frameWidth} x ${this.frameHeight}`, + ); + console.log( + "Standing frame lines:", + this.frames.standing + .split("\n") + .map((l, i) => `${i}: [${l}] (len=${l.length})`), + ); + } } /** From 71372187a37db4fee9b107a7991e8fb331cd5a8d Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:02:07 -0600 Subject: [PATCH 38/77] fix: align feet line with face and body by adding extra leading space - Added extra leading space to all feet lines (2 spaces instead of 1) - Feet line now: ' (_| |_)' (aligns with face and body) - Maintains visual symmetry across all lines of the cat - Consistent left edge alignment after normalization --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 1800688..e2f15d4 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // Raw ASCII art frames (unprocessed) private readonly rawFrames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, }; // Normalized frames (uniform dimensions) From 5ddf5081073f130e57cdc621b18163dc5752a88b Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:03:36 -0600 Subject: [PATCH 39/77] fix: remove extra character from feet lines to match body width - Changed (_| |_) to (_| |_) to match /| | width (8 chars) - Changed (|_ |_) to (|_ |_) for walk2 animation (8 chars) - Feet now align with body line after padding to 9 chars - Maintains consistent left edge alignment --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index e2f15d4..f0ed018 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // Raw ASCII art frames (unprocessed) private readonly rawFrames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| |_)`, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, }; // Normalized frames (uniform dimensions) From fe5746a75eb79df8189347675f905c24c8c59975 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:04:31 -0600 Subject: [PATCH 40/77] Revert "fix: remove extra character from feet lines to match body width" This reverts commit 5ddf5081073f130e57cdc621b18163dc5752a88b. --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index f0ed018..e2f15d4 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // Raw ASCII art frames (unprocessed) private readonly rawFrames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| |_)`, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, }; // Normalized frames (uniform dimensions) From 7ee1244e684028a911afbe4dc438f224da255d6c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:09:35 -0600 Subject: [PATCH 41/77] fix: align feet line by adding space between legs - Changed (|_ |_) to (|_ |_) (3 spaces between legs instead of 2) - Feet line now aligns properly with body line above it - Right edge aligns correctly after normalization to uniform width - Eliminates remaining misalignment issues --- src/cli/commands/init/clef.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index e2f15d4..6c3fa0d 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -33,12 +33,12 @@ class Clef { // Raw ASCII art frames (unprocessed) private readonly rawFrames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n (_| |_)`, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (_| _|)`, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n (|_ |_)`, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n (_|_|_)`, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n / \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n (_| |_)`, + standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n(|_ |_)`, + walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n(_| _|)`, + walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n(|_ |_)`, + typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n(_|__|_)`, + celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n/ \\ `, + waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n(|_ |_)`, }; // Normalized frames (uniform dimensions) From c81519623579034e32a916532acedd174d5a55aa Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:12:00 -0600 Subject: [PATCH 42/77] feat: add 1-line top padding and 1-column left padding to cat animation - Changed renderFrame to start at line 2 (idx + 2) instead of line 1 - Changed catX from 0 to 1 (adds 1 column of left padding) - Updated textY from 2 to 3 to align with face after top padding - Updated fadeOut to fade lines 2-5 (was 1-4) to match new positions - Cat now has breathing room on top and left sides --- src/cli/commands/init/clef.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 6c3fa0d..fc2b5ae 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -152,14 +152,14 @@ class Clef { /** * Render ASCII art frame at specific horizontal position * Uses absolute cursor positioning for each line - * No padding above the cat - starts at line 1 + * Adds 1 line of padding above the cat (starts at line 2) */ private renderFrame(frame: string, x: number): void { const lines = frame.split("\n"); lines.forEach((line, idx) => { // Move cursor to position (row, column) - // Start at line 1 (no padding) - process.stdout.write(`\x1B[${idx + 1};${x}H`); + // Start at line 2 (1 line padding above) + process.stdout.write(`\x1B[${idx + 2};${x}H`); process.stdout.write(line); }); } @@ -220,9 +220,9 @@ class Clef { const catLines = this.frames.standing.split("\n"); // Erase from bottom to top - // Cat starts at line 1 (no padding), so fade from line 4 to line 1 + // Cat starts at line 2 (1 line padding), so fade from line 5 to line 2 for (let i = catLines.length - 1; i >= 0; i--) { - process.stdout.write(`\x1B[${1 + i};${x}H`); + process.stdout.write(`\x1B[${2 + i};${x}H`); process.stdout.write(" ".repeat(20)); // Clear line with spaces await sleep(80); } @@ -279,9 +279,9 @@ class Clef { this.hideCursor(); this.clearScreen(); - const catX = 0; // Start at column 0 (accounts for leading space in ASCII art) + const catX = 1; // Start at column 1 (adds 1 column of left padding) const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame - const textY = 2; // Align text with cat's face (line 2 of cat is the face/eyes) + const textY = 3; // Align text with cat's face (line 3 with 1-line top padding, face is at idx 1 + 2) // Messages to type const messages = [ From e518b60fc2fa07ed2f5f1d8caaa328f17e111e3c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:20:04 -0600 Subject: [PATCH 43/77] feat: add dialog-style text layout with blue label and pure white text - Added labelBlue color (#547fef) for character name labels - Added pureWhite color (#FFF, ANSI 231) for message text - Changed intro text layout: 'Clef:' on line 2, message on line 3 - 'Clef:' label is now static and colored blue - Message text types out with pure white for maximum brightness - Dialog-style format with clear character identification --- src/cli/commands/init/clef.ts | 20 ++++++++++++-------- src/cli/commands/init/colors.ts | 12 ++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index fc2b5ae..bc7670a 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -180,8 +180,7 @@ class Clef { for (let i = 0; i < text.length; i++) { // Reposition cursor for each character (handles concurrent animations) process.stdout.write(`\x1B[${y};${x + i}H`); - // Use normal white for clean, readable intro text - process.stdout.write(textColors.white(text[i])); + process.stdout.write(textColors.pureWhite(text[i])); await sleep(delay); } } @@ -281,7 +280,8 @@ class Clef { const catX = 1; // Start at column 1 (adds 1 column of left padding) const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame - const textY = 3; // Align text with cat's face (line 3 with 1-line top padding, face is at idx 1 + 2) + const labelY = 3; // Line 2 of cat output - label "Clef:" + const messageY = 4; // Line 3 of cat output - message text // Messages to type const messages = [ @@ -293,16 +293,20 @@ class Clef { let isAnimating = true; const animationPromise = this.animateLegs(catX, () => isAnimating); - // Type first message - await this.typeText(messages[0], textX, textY); + // Write static label "Clef:" in blue + process.stdout.write(`\x1B[${labelY};${textX}H`); + process.stdout.write(textColors.labelBlue("Clef: ")); + + // Type first message on line below + await this.typeText(messages[0], textX, messageY); await sleep(1000); - // Clear first message - this.clearLine(textY, textX); + // Clear message only (keep label) + this.clearLine(messageY, textX); await sleep(300); // Type second message - await this.typeText(messages[1], textX, textY); + await this.typeText(messages[1], textX, messageY); await sleep(1200); // Stop leg animation diff --git a/src/cli/commands/init/colors.ts b/src/cli/commands/init/colors.ts index afe694e..82b7531 100644 --- a/src/cli/commands/init/colors.ts +++ b/src/cli/commands/init/colors.ts @@ -88,6 +88,18 @@ export const textColors = { * Perfect for intro/outro messages (normal weight, not bold) */ white: (text: string) => `\x1b[37m${text}\x1b[0m`, + + /** + * Pure White (#FFF) - Maximum brightness, perfect white + * For typed message text that needs to be crystal clear + */ + pureWhite: (text: string) => `\x1b[38;5;231m${text}\x1b[0m`, + + /** + * Label Blue (#547fef) - Character name/speaker label color + * Perfect for "Clef:" label + */ + labelBlue: (text: string) => `\x1b[38;5;75m${text}\x1b[0m`, }; /** From 70e66ed9c702ddd3d3f41db91436de4672f5fef6 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:22:08 -0600 Subject: [PATCH 44/77] feat: make all prompt questions pure white for maximum visibility - Wrapped all question text with textColors.pureWhite() (#FFF) - Applied to preset, emoji, scope, and types prompts - Questions now have maximum brightness for optimal readability - Consistent white text across all prompts --- src/cli/commands/init/prompts.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 138f21e..1aa98b5 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -54,7 +54,7 @@ function handleCancel(value: unknown): void { */ export async function promptPreset(): Promise { const preset = await select({ - message: `${label("preset", "magenta")} Which commit style fits your project?`, + message: `${label("preset", "magenta")} ${textColors.pureWhite("Which commit style fits your project?")}`, options: [ { value: "conventional", @@ -88,7 +88,7 @@ export async function promptPreset(): Promise { */ export async function promptEmoji(): Promise { const emoji = await select({ - message: `${label("emoji", "cyan")} Enable emoji support?`, + message: `${label("emoji", "cyan")} ${textColors.pureWhite("Enable emoji support?")}`, options: [ { value: false, @@ -114,7 +114,7 @@ export async function promptScope(): Promise< "optional" | "selective" | "always" | "never" > { const scope = await select({ - message: `${label("scope", "blue")} How should scopes work?`, + message: `${label("scope", "blue")} ${textColors.pureWhite("How should scopes work?")}`, options: [ { value: "optional", @@ -151,7 +151,7 @@ export async function promptScopeTypes( types: Array<{ id: string; description: string }>, ): Promise { const selected = await multiselect({ - message: `${label("types", "blue")} Which types require a scope?`, + message: `${label("types", "blue")} ${textColors.pureWhite("Which types require a scope?")}`, options: types.map((type) => ({ value: type.id, label: type.id, From cb6d6e628e216685cb8b29bc4835bf83a2a48306 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:26:08 -0600 Subject: [PATCH 45/77] feat: update outro to use dialog-style format with blue label and pure white text - Changed outro to display 'Clef:' label in blue on line 2 - Message text types out in pure white below on line 3 - Matches the intro's dialog-style format - Adds typewriter effect to the outro message - Consistent character identification and maximum visibility --- .labcommitr.config.yaml | 63 +++++++++++++++++++++++++++++++++++ src/cli/commands/init/clef.ts | 27 ++++++++++----- 2 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 .labcommitr.config.yaml diff --git a/.labcommitr.config.yaml b/.labcommitr.config.yaml new file mode 100644 index 0000000..74616d3 --- /dev/null +++ b/.labcommitr.config.yaml @@ -0,0 +1,63 @@ +# Labcommitr Configuration +# Generated by: lab init +# Edit this file to customize your commit workflow +# Documentation: https://github.com/labcatr/labcommitr#config + +version: "1.0" +config: + emoji_enabled: false + force_emoji_detection: null +format: + template: "{type}({scope}): {subject}" + subject_max_length: 50 +types: + - id: feat + description: Introduce new features + emoji: ✨ + - id: fix + description: Fix a bug + emoji: 🐛 + - id: docs + description: Add or update documentation + emoji: 📚 + - id: style + description: Improve structure or format of code + emoji: 🎨 + - id: refactor + description: Refactor code + emoji: ♻️ + - id: perf + description: Improve performance + emoji: ⚡ + - id: test + description: Add or update tests + emoji: ✅ + - id: build + description: Add or update build scripts + emoji: 👷 + - id: ci + description: Add or update CI configuration + emoji: 💚 + - id: chore + description: Miscellaneous chores + emoji: 🔧 +validation: + require_scope_for: + - feat + - fix + - docs + - style + - refactor + - perf + - test + - build + - ci + - chore + allowed_scopes: [] + subject_min_length: 3 + prohibited_words: [] +advanced: + aliases: {} + git: + auto_stage: false + sign_commits: false diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index bc7670a..0e75da8 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -374,16 +374,27 @@ class Clef { // Split cat into lines for side-by-side display const catLines = this.frames.waving.split("\n"); - const message = ` ${textColors.white("You're all set! Happy committing!")}`; + const catX = 1; // Start at column 1 (adds 1 column of left padding) + const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame + const labelY = 3; // Line 2 of cat output - label "Clef:" + const messageY = 4; // Line 3 of cat output - message text - // Display cat and message side by side (line by line) + // Display cat lines for (let i = 0; i < catLines.length; i++) { - if (i === 2) { - // Show message on the middle line of the cat (vertically centered) - console.log(catLines[i] + message); - } else { - console.log(catLines[i]); - } + console.log(catLines[i]); + } + + // Display label and message below cat + console.log(); // Blank line after cat + process.stdout.write(`\x1B[${labelY};${textX}H`); + process.stdout.write(textColors.labelBlue("Clef: ")); + + // Type out message in pure white + const message = "You're all set! Happy committing!"; + for (let i = 0; i < message.length; i++) { + process.stdout.write(`\x1B[${messageY};${textX + i}H`); + process.stdout.write(textColors.pureWhite(message[i])); + await sleep(30); // Slight pause for typewriter effect } console.log(); // Extra line at end From 4fbe3cc3b2fdd46ddbc6557c4efb1cfc211848ce Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:28:08 -0600 Subject: [PATCH 46/77] fix: correct outro label and message positioning to align with cat - Changed labelY from 3 to 6 (cat ends at line 5, label starts at line 6) - Changed messageY from 4 to 7 (message below label at line 7) - Label and message now properly aligned with cat's right edge - Removed unnecessary blank line before positioning --- .labcommitr.config.yaml | 39 +++++++++-------------------------- src/cli/commands/init/clef.ts | 10 +++++---- 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/.labcommitr.config.yaml b/.labcommitr.config.yaml index 74616d3..68afd7b 100644 --- a/.labcommitr.config.yaml +++ b/.labcommitr.config.yaml @@ -12,47 +12,28 @@ format: subject_max_length: 50 types: - id: feat - description: Introduce new features + description: A new feature for the user emoji: ✨ - id: fix - description: Fix a bug + description: A bug fix for the user emoji: 🐛 - id: docs - description: Add or update documentation + description: Documentation changes emoji: 📚 - id: style - description: Improve structure or format of code - emoji: 🎨 + description: Code style changes (formatting, semicolons, etc.) + emoji: 💄 - id: refactor - description: Refactor code + description: Code refactoring without changing functionality emoji: ♻️ - - id: perf - description: Improve performance - emoji: ⚡ - id: test - description: Add or update tests - emoji: ✅ - - id: build - description: Add or update build scripts - emoji: 👷 - - id: ci - description: Add or update CI configuration - emoji: 💚 + description: Adding or updating tests + emoji: 🧪 - id: chore - description: Miscellaneous chores + description: Maintenance tasks, build changes, etc. emoji: 🔧 validation: - require_scope_for: - - feat - - fix - - docs - - style - - refactor - - perf - - test - - build - - ci - - chore + require_scope_for: [] allowed_scopes: [] subject_min_length: 3 prohibited_words: [] diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 0e75da8..7fa388b 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -376,16 +376,18 @@ class Clef { const catLines = this.frames.waving.split("\n"); const catX = 1; // Start at column 1 (adds 1 column of left padding) const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame - const labelY = 3; // Line 2 of cat output - label "Clef:" - const messageY = 4; // Line 3 of cat output - message text + + // Calculate positions: cat is at lines 2-5 (1 line padding + 4 cat lines) + // Label and message go right after cat (lines 6-7) + const labelY = 6; // Line right after cat + const messageY = 7; // Line below label // Display cat lines for (let i = 0; i < catLines.length; i++) { console.log(catLines[i]); } - // Display label and message below cat - console.log(); // Blank line after cat + // Display label process.stdout.write(`\x1B[${labelY};${textX}H`); process.stdout.write(textColors.labelBlue("Clef: ")); From 1ee4fb96b4cda92fab3ad3f7e588278282e389bd Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:30:29 -0600 Subject: [PATCH 47/77] fix: simplify outro to use console.log instead of absolute positioning - Removed complex absolute positioning calculations - Now uses simple console.log for cat lines and label/message - Label and message appear directly below cat without positioning issues - Much simpler and more reliable approach --- src/cli/commands/init/clef.ts | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 7fa388b..11bed9b 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -374,30 +374,17 @@ class Clef { // Split cat into lines for side-by-side display const catLines = this.frames.waving.split("\n"); - const catX = 1; // Start at column 1 (adds 1 column of left padding) - const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame - - // Calculate positions: cat is at lines 2-5 (1 line padding + 4 cat lines) - // Label and message go right after cat (lines 6-7) - const labelY = 6; // Line right after cat - const messageY = 7; // Line below label // Display cat lines for (let i = 0; i < catLines.length; i++) { console.log(catLines[i]); } - // Display label - process.stdout.write(`\x1B[${labelY};${textX}H`); - process.stdout.write(textColors.labelBlue("Clef: ")); - - // Type out message in pure white - const message = "You're all set! Happy committing!"; - for (let i = 0; i < message.length; i++) { - process.stdout.write(`\x1B[${messageY};${textX + i}H`); - process.stdout.write(textColors.pureWhite(message[i])); - await sleep(30); // Slight pause for typewriter effect - } + // Display label and message below cat + console.log( + textColors.labelBlue("Clef: ") + + textColors.pureWhite("You're all set! Happy committing!"), + ); console.log(); // Extra line at end From 78da06d81bf0acf40e1294005330a6410d72c33c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:32:27 -0600 Subject: [PATCH 48/77] fix: position outro text beside cat using absolute positioning - Changed from console.log to renderFrame + absolute positioning - Label and message now appear beside cat at lines 3-4 - Matches intro positioning behavior - Text appears to the right of cat instead of below --- src/cli/commands/init/clef.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 11bed9b..0b22e7a 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -372,19 +372,22 @@ class Clef { return; // No clear - message stays visible } - // Split cat into lines for side-by-side display - const catLines = this.frames.waving.split("\n"); + // Use renderFrame to position cat (starts at line 2 with 1-line padding) + const catX = 1; // Start at column 1 (adds 1 column of left padding) + this.renderFrame(this.frames.waving, catX); - // Display cat lines - for (let i = 0; i < catLines.length; i++) { - console.log(catLines[i]); - } + const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame + const labelY = 3; // Line 2 of cat (face line) - label "Clef:" + const messageY = 4; // Line 3 of cat - message text - // Display label and message below cat - console.log( - textColors.labelBlue("Clef: ") + - textColors.pureWhite("You're all set! Happy committing!"), - ); + // Display label + process.stdout.write(`\x1B[${labelY};${textX}H`); + process.stdout.write(textColors.labelBlue("Clef: ")); + + // Display message in pure white + const message = "You're all set! Happy committing!"; + process.stdout.write(`\x1B[${messageY};${textX}H`); + process.stdout.write(textColors.pureWhite(message)); console.log(); // Extra line at end From d058873b8e21861b65e0e25f62452c11d348076b Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:40:30 -0600 Subject: [PATCH 49/77] fix: position outro after processing checklist using relative positioning - Cat now displays below all prompts and processing steps - Uses console.log to naturally position cat at end of output - Uses ANSI cursor movement to position text beside cat - Text appears to the right of cat's face and body - Works regardless of which prompt path is taken --- src/cli/commands/init/clef.ts | 46 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 0b22e7a..0d07085 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -372,23 +372,51 @@ class Clef { return; // No clear - message stays visible } - // Use renderFrame to position cat (starts at line 2 with 1-line padding) + // Add spacing before outro + console.log(); + + // Get cat position - appears after processing checklist const catX = 1; // Start at column 1 (adds 1 column of left padding) - this.renderFrame(this.frames.waving, catX); + const catLines = this.frames.waving.split("\n"); - const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame - const labelY = 3; // Line 2 of cat (face line) - label "Clef:" - const messageY = 4; // Line 3 of cat - message text + // Display cat starting at the next available line + // We use getCursorPosition to find where we are + // For simplicity, just use console.log which naturally goes to the next line + for (let i = 0; i < catLines.length; i++) { + console.log(catLines[i]); + } - // Display label - process.stdout.write(`\x1B[${labelY};${textX}H`); + // Display label and message on lines next to cat (beside cat's face) + // Cat is now on lines: current+1, current+2, current+3, current+4 + // Label should be beside line 2 (face line) + const textX = catX + this.frameWidth + 1; // 1 space padding after cat + + // Use relative positioning: we're currently after displaying the cat + // Need to go back up to position text beside the cat + const linesToMove = catLines.length; // Move up by cat height + const faceLineRelativeToCat = 1; // Face is on the 2nd line of cat (idx 1) + + // Calculate absolute line numbers + // After console.log, cursor is at line: current + cat height + 1 + // We need to write at: current + faceLineRelativeToCat (i.e., 2nd line of cat display) + // So we need to move up by: (cat height - faceLineRelativeToCat + 1) + const linesUp = catLines.length - faceLineRelativeToCat + 1; + const messageLinesUp = catLines.length - faceLineRelativeToCat; + + // Move up to cat's face line and display label + process.stdout.write(`\x1B[${linesUp}A`); // Move up + process.stdout.write(`\r${" ".repeat(textX)}`); // Clear to textX position process.stdout.write(textColors.labelBlue("Clef: ")); - // Display message in pure white + // Move up to next line and display message + process.stdout.write(`\x1B[${linesUp}A`); // Move up one more line + process.stdout.write(`\r${" ".repeat(textX)}`); // Clear to textX position const message = "You're all set! Happy committing!"; - process.stdout.write(`\x1B[${messageY};${textX}H`); process.stdout.write(textColors.pureWhite(message)); + // Move cursor back down to end + process.stdout.write(`\x1B[${linesUp}B`); // Move down + process.stdout.write(`\x1B[${linesUp}B`); // Move down again console.log(); // Extra line at end // Small pause to let user see the message From 48814b8688b6ee66a61743994ee6043f2b1f1220 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:43:08 -0600 Subject: [PATCH 50/77] fix: simplify outro to display cat and text side-by-side using console.log - Cat displays with all lines intact (ears preserved) - 'Clef:' label appears beside cat's face line (line 1) - Message text appears beside cat's body line (line 2) - No complex cursor repositioning needed - Cat and message display after all prompts naturally --- src/cli/commands/init/clef.ts | 56 ++++++++++++----------------------- 1 file changed, 19 insertions(+), 37 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 0d07085..488f986 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -375,48 +375,30 @@ class Clef { // Add spacing before outro console.log(); - // Get cat position - appears after processing checklist - const catX = 1; // Start at column 1 (adds 1 column of left padding) + // Display cat and text side by side + // Cat on left, "Clef:" label and message on right const catLines = this.frames.waving.split("\n"); + const catX = 1; // Start at column 1 (adds 1 column of left padding) + const textX = catX + this.frameWidth + 1; // 1 space padding after cat - // Display cat starting at the next available line - // We use getCursorPosition to find where we are - // For simplicity, just use console.log which naturally goes to the next line + // Display cat lines with label/message beside appropriate lines for (let i = 0; i < catLines.length; i++) { - console.log(catLines[i]); + if (i === 1) { + // Line 1: Face line - display "Clef:" label + console.log(catLines[i] + " " + textColors.labelBlue("Clef:")); + } else if (i === 2) { + // Line 2: Body line - display message text + console.log( + catLines[i] + + " " + + textColors.pureWhite("You're all set! Happy committing!"), + ); + } else { + // Other lines: just the cat + console.log(catLines[i]); + } } - // Display label and message on lines next to cat (beside cat's face) - // Cat is now on lines: current+1, current+2, current+3, current+4 - // Label should be beside line 2 (face line) - const textX = catX + this.frameWidth + 1; // 1 space padding after cat - - // Use relative positioning: we're currently after displaying the cat - // Need to go back up to position text beside the cat - const linesToMove = catLines.length; // Move up by cat height - const faceLineRelativeToCat = 1; // Face is on the 2nd line of cat (idx 1) - - // Calculate absolute line numbers - // After console.log, cursor is at line: current + cat height + 1 - // We need to write at: current + faceLineRelativeToCat (i.e., 2nd line of cat display) - // So we need to move up by: (cat height - faceLineRelativeToCat + 1) - const linesUp = catLines.length - faceLineRelativeToCat + 1; - const messageLinesUp = catLines.length - faceLineRelativeToCat; - - // Move up to cat's face line and display label - process.stdout.write(`\x1B[${linesUp}A`); // Move up - process.stdout.write(`\r${" ".repeat(textX)}`); // Clear to textX position - process.stdout.write(textColors.labelBlue("Clef: ")); - - // Move up to next line and display message - process.stdout.write(`\x1B[${linesUp}A`); // Move up one more line - process.stdout.write(`\r${" ".repeat(textX)}`); // Clear to textX position - const message = "You're all set! Happy committing!"; - process.stdout.write(textColors.pureWhite(message)); - - // Move cursor back down to end - process.stdout.write(`\x1B[${linesUp}B`); // Move down - process.stdout.write(`\x1B[${linesUp}B`); // Move down again console.log(); // Extra line at end // Small pause to let user see the message From a8e5d3be543165ba8d0ec1068440242a23f4611d Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:46:01 -0600 Subject: [PATCH 51/77] feat: make intro and outro cats display in pure white - Updated renderFrame to wrap each line with textColors.pureWhite() - Intro cat now displays in bright white for maximum visibility - Outro cat also displays in bright white with all lines colored - Consistent white cats throughout intro and outro animations --- src/cli/commands/init/clef.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index 488f986..e2793cd 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -160,7 +160,8 @@ class Clef { // Move cursor to position (row, column) // Start at line 2 (1 line padding above) process.stdout.write(`\x1B[${idx + 2};${x}H`); - process.stdout.write(line); + // Make cat white for better visibility + process.stdout.write(textColors.pureWhite(line)); }); } @@ -385,17 +386,21 @@ class Clef { for (let i = 0; i < catLines.length; i++) { if (i === 1) { // Line 1: Face line - display "Clef:" label - console.log(catLines[i] + " " + textColors.labelBlue("Clef:")); + console.log( + textColors.pureWhite(catLines[i]) + + " " + + textColors.labelBlue("Clef:"), + ); } else if (i === 2) { // Line 2: Body line - display message text console.log( - catLines[i] + + textColors.pureWhite(catLines[i]) + " " + textColors.pureWhite("You're all set! Happy committing!"), ); } else { - // Other lines: just the cat - console.log(catLines[i]); + // Other lines: just the cat in white + console.log(textColors.pureWhite(catLines[i])); } } From 99b2eeb53075fe680360be644ac8238f5cfccdee Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 19:48:27 -0600 Subject: [PATCH 52/77] feat: unify all step labels to single light blue color (#77c0f7) - Changed all label colors (magenta, cyan, blue, yellow, green) to use ANSI 256 code 117 - Represents light blue #77c0f7 for consistent visual hierarchy - All step labels now appear in the same uniform color - Can be reverted to unique colors in future if needed --- src/cli/commands/init/colors.ts | 34 +++++++-------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/cli/commands/init/colors.ts b/src/cli/commands/init/colors.ts index 82b7531..37d650b 100644 --- a/src/cli/commands/init/colors.ts +++ b/src/cli/commands/init/colors.ts @@ -18,34 +18,14 @@ */ export const labelColors = { /** - * Bright Magenta - Vibrant, energetic, attention-grabbing - * Perfect for the first step (preset selection) + * Uniform Light Blue (#77c0f7) - Consistent label color + * Used for all step labels for unified appearance */ - bgBrightMagenta: (text: string) => `\x1b[48;5;201m\x1b[30m${text}\x1b[0m`, - - /** - * Bright Cyan - Fresh, modern, tech-forward - * Great for emoji and next steps - */ - bgBrightCyan: (text: string) => `\x1b[48;5;51m\x1b[30m${text}\x1b[0m`, - - /** - * Bright Blue - Clear, professional, confident - * Ideal for scope and types configuration - */ - bgBrightBlue: (text: string) => `\x1b[48;5;39m\x1b[30m${text}\x1b[0m`, - - /** - * Bright Yellow - Attention-grabbing, action-oriented - * Perfect for call-to-action (next steps) - */ - bgBrightYellow: (text: string) => `\x1b[48;5;226m\x1b[30m${text}\x1b[0m`, - - /** - * Bright Green - Success, completion, positive - * Excellent for config result and success messages - */ - bgBrightGreen: (text: string) => `\x1b[48;5;46m\x1b[30m${text}\x1b[0m`, + bgBrightMagenta: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, + bgBrightCyan: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, + bgBrightBlue: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, + bgBrightYellow: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, + bgBrightGreen: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, }; /** From b4dcee04ece8d6efb98045e7cbe404547fd8d478 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Tue, 28 Oct 2025 23:58:07 -0600 Subject: [PATCH 53/77] feat(init): simplify flow and add git integration options - Preset options now show multi-line examples for clarity - Restored emoji prompt (independent of preset) - Added auto-stage prompt (No default; uses git add -u when enabled) - Removed scope prompt; default to optional in generated config - buildConfig now accepts autoStage and defaults sign_commits=true - Init flow builds config with scope optional and chosen options --- .labcommitr.config.yaml | 17 +++-------- src/cli/commands/init/index.ts | 19 ++++--------- src/cli/commands/init/prompts.ts | 49 ++++++++++++++++++++++++-------- src/lib/presets/index.ts | 9 ++++-- 4 files changed, 54 insertions(+), 40 deletions(-) diff --git a/.labcommitr.config.yaml b/.labcommitr.config.yaml index 68afd7b..2fa8a9d 100644 --- a/.labcommitr.config.yaml +++ b/.labcommitr.config.yaml @@ -12,25 +12,16 @@ format: subject_max_length: 50 types: - id: feat - description: A new feature for the user + description: New feature emoji: ✨ - id: fix - description: A bug fix for the user + description: Bug fix emoji: 🐛 - id: docs - description: Documentation changes + description: Documentation emoji: 📚 - - id: style - description: Code style changes (formatting, semicolons, etc.) - emoji: 💄 - - id: refactor - description: Code refactoring without changing functionality - emoji: ♻️ - - id: test - description: Adding or updating tests - emoji: 🧪 - id: chore - description: Maintenance tasks, build changes, etc. + description: Maintenance emoji: 🔧 validation: require_scope_for: [] diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts index 35f0ea9..b571518 100644 --- a/src/cli/commands/init/index.ts +++ b/src/cli/commands/init/index.ts @@ -7,7 +7,7 @@ * * Flow (Astro-style): * 1. Intro animation (Clef introduces tool, then clears) - * 2. User prompts (preset, emoji, scope choices) + * 2. User prompts (preset, emoji, auto-stage) * 3. Summary display (show choices, then clear) * 4. Processing checklist (compact steps, stays visible) * 5. Outro animation (Clef appears below, stays on screen) @@ -20,8 +20,7 @@ import { clef } from "./clef.js"; import { promptPreset, promptEmoji, - promptScope, - promptScopeTypes, + promptAutoStage, displayProcessingSteps, } from "./prompts.js"; import { buildConfig, getPreset } from "../../../lib/presets/index.js"; @@ -99,16 +98,10 @@ async function initAction(options: { // Prompts: Clean labels, no cat // Note: @clack/prompts clears each prompt after selection (their default behavior) const presetId = options.preset || (await promptPreset()); - const preset = getPreset(presetId); + getPreset(presetId); const emojiEnabled = await promptEmoji(); - const scopeMode = await promptScope(); - - // If selective scope mode, ask which types require scopes - let scopeRequiredFor: string[] = []; - if (scopeMode === "selective") { - scopeRequiredFor = await promptScopeTypes(preset.types); - } + const autoStage = await promptAutoStage(); // Small pause before processing await new Promise((resolve) => setTimeout(resolve, 800)); @@ -119,8 +112,8 @@ async function initAction(options: { // Build config from choices const config = buildConfig(presetId, { emoji: emojiEnabled, - scope: scopeMode, - scopeRequiredFor, + scope: "optional", + autoStage, }); // Show title "Labcommitr initializing..." diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 1aa98b5..8ad584d 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -58,23 +58,23 @@ export async function promptPreset(): Promise { options: [ { value: "conventional", - label: "Conventional Commits", - hint: "Industry-standard format (recommended)", + label: + "Conventional Commits (Recommended): Popular across open-source and personal projects.\n e.g., fix(dining): add security to treat container", }, { value: "gitmoji", - label: "Gitmoji Style", - hint: "Visual commits with emojis", + label: + "Gitmoji Style: Visual commits with emojis for better scannability.\n e.g., 🐛 fix(dining): add security to treat container", }, { value: "angular", - label: "Angular Convention", - hint: "Strict enterprise format", + label: + "Angular Convention: Strict format used by Angular and enterprise teams.\n e.g., fix(snacks): add security to treat container", }, { value: "minimal", - label: "Minimal Setup", - hint: "Start basic, customize later", + label: + "Minimal Setup: Start with basics, customize everything yourself later.\n e.g., fix: add security to treat container", }, ], }); @@ -88,17 +88,17 @@ export async function promptPreset(): Promise { */ export async function promptEmoji(): Promise { const emoji = await select({ - message: `${label("emoji", "cyan")} ${textColors.pureWhite("Enable emoji support?")}`, + message: `${label("emoji", "cyan")} ${textColors.pureWhite("Enable emoji support in commits?")}`, options: [ { value: false, - label: "No", - hint: "Text-only (recommended)", + label: "No (Recommended)", + hint: "Text-only commits", }, { value: true, label: "Yes", - hint: "Visual emojis in commits", + hint: "Include emojis for better visibility", }, ], }); @@ -107,6 +107,31 @@ export async function promptEmoji(): Promise { return emoji as boolean; } +/** + * Prompt for auto-stage behavior + * When enabled, stages modified/deleted tracked files automatically (git add -u) + */ +export async function promptAutoStage(): Promise { + const autoStage = await select({ + message: `${label("stage", "yellow")} ${textColors.pureWhite("Stage files automatically?")}`, + options: [ + { + value: false, + label: "No (Recommended)", + hint: "I'll stage files manually with 'git add'", + }, + { + value: true, + label: "Yes", + hint: "Auto-stage modified files before committing", + }, + ], + }); + + handleCancel(autoStage); + return autoStage as boolean; +} + /** * Prompt for scope configuration mode */ diff --git a/src/lib/presets/index.ts b/src/lib/presets/index.ts index 56df7ef..3b34b07 100644 --- a/src/lib/presets/index.ts +++ b/src/lib/presets/index.ts @@ -70,8 +70,11 @@ export function buildConfig( presetId: string, customizations: { emoji?: boolean; + // Scope prompt removed in init; default to optional unless provided scope?: "optional" | "selective" | "always" | "never"; scopeRequiredFor?: string[]; + // Git integration + autoStage?: boolean; }, ): LabcommitrConfig { const preset = getPreset(presetId); @@ -93,6 +96,7 @@ export function buildConfig( force_emoji_detection: null, }, format: { + // Template is determined by style; emoji is handled at render time template: "{type}({scope}): {subject}", subject_max_length: 50, }, @@ -106,8 +110,9 @@ export function buildConfig( advanced: { aliases: {}, git: { - auto_stage: false, - sign_commits: false, + auto_stage: customizations.autoStage ?? false, + // Security best-practice: enable signed commits by default + sign_commits: true, }, }, }; From b6706eaf7998e0483187113ffcb4add8178f89a2 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Wed, 29 Oct 2025 00:00:47 -0600 Subject: [PATCH 54/77] feat(init): improve preset UX and remove Gitmoji option - Added visual spacing between preset options for readability - Removed Gitmoji preset from selection (emoji prompt now controls visuals) - Conventional with emoji enabled provides equivalent output --- src/cli/commands/init/prompts.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 8ad584d..0028ddb 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -59,22 +59,17 @@ export async function promptPreset(): Promise { { value: "conventional", label: - "Conventional Commits (Recommended): Popular across open-source and personal projects.\n e.g., fix(dining): add security to treat container", - }, - { - value: "gitmoji", - label: - "Gitmoji Style: Visual commits with emojis for better scannability.\n e.g., 🐛 fix(dining): add security to treat container", + "Conventional Commits (Recommended): Popular across open-source and personal projects.\n e.g., fix(dining): add security to treat container\n", }, { value: "angular", label: - "Angular Convention: Strict format used by Angular and enterprise teams.\n e.g., fix(snacks): add security to treat container", + "\nAngular Convention: Strict format used by Angular and enterprise teams.\n e.g., fix(snacks): add security to treat container\n", }, { value: "minimal", label: - "Minimal Setup: Start with basics, customize everything yourself later.\n e.g., fix: add security to treat container", + "\nMinimal Setup: Start with basics, customize everything yourself later.\n e.g., fix: add security to treat container\n", }, ], }); From 473e0d2f028570b28d6e6ed487dd27cfd2ed8f3c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 12:45:25 -0600 Subject: [PATCH 55/77] feat(init): simplify preset options display - Removed descriptions from preset option labels for cleaner UI - Created PRESET_OPTIONS data structure to preserve descriptions for future use - Labels now show only preset name and example (e.g., 'Conventional Commits (Recommended) - e.g., fix(dining): add security') - Keeps connector line continuous by using single-line labels --- .labcommitr.config.yaml | 21 ++++++++++---- src/cli/commands/init/prompts.ts | 47 ++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/.labcommitr.config.yaml b/.labcommitr.config.yaml index 2fa8a9d..519229f 100644 --- a/.labcommitr.config.yaml +++ b/.labcommitr.config.yaml @@ -5,23 +5,32 @@ version: "1.0" config: - emoji_enabled: false + emoji_enabled: true force_emoji_detection: null format: template: "{type}({scope}): {subject}" subject_max_length: 50 types: - id: feat - description: New feature + description: A new feature for the user emoji: ✨ - id: fix - description: Bug fix + description: A bug fix for the user emoji: 🐛 - id: docs - description: Documentation + description: Documentation changes emoji: 📚 + - id: style + description: Code style changes (formatting, semicolons, etc.) + emoji: 💄 + - id: refactor + description: Code refactoring without changing functionality + emoji: ♻️ + - id: test + description: Adding or updating tests + emoji: 🧪 - id: chore - description: Maintenance + description: Maintenance tasks, build changes, etc. emoji: 🔧 validation: require_scope_for: [] @@ -32,4 +41,4 @@ advanced: aliases: {} git: auto_stage: false - sign_commits: false + sign_commits: true diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 0028ddb..f740361 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -49,29 +49,42 @@ function handleCancel(value: unknown): void { } } +/** + * Preset option data structure + * Keeps descriptions for future use while labels only show examples + */ +const PRESET_OPTIONS = [ + { + value: "conventional", + name: "Conventional Commits (Recommended)", + description: "Popular across open-source and personal projects.", + example: "fix(dining): add security to treat container", + }, + { + value: "angular", + name: "Angular Convention", + description: "Strict format used by Angular and enterprise teams.", + example: "fix(snacks): add security to treat container", + }, + { + value: "minimal", + name: "Minimal Setup", + description: "Start with basics, customize everything yourself later.", + example: "fix: add security to treat container", + }, +] as const; + /** * Prompt for commit style preset selection */ export async function promptPreset(): Promise { const preset = await select({ message: `${label("preset", "magenta")} ${textColors.pureWhite("Which commit style fits your project?")}`, - options: [ - { - value: "conventional", - label: - "Conventional Commits (Recommended): Popular across open-source and personal projects.\n e.g., fix(dining): add security to treat container\n", - }, - { - value: "angular", - label: - "\nAngular Convention: Strict format used by Angular and enterprise teams.\n e.g., fix(snacks): add security to treat container\n", - }, - { - value: "minimal", - label: - "\nMinimal Setup: Start with basics, customize everything yourself later.\n e.g., fix: add security to treat container\n", - }, - ], + options: PRESET_OPTIONS.map((option) => ({ + value: option.value, + label: `${option.name} - e.g., ${option.example}`, + // description is kept in PRESET_OPTIONS for future use + })), }); handleCancel(preset); From 19ead2aa321aac1a6db7757f7907d941b2f3e4b3 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 21:49:50 -0600 Subject: [PATCH 56/77] feat(config): add editor preference support for commit body - Add BodyConfig interface with editor_preference field - Support 'auto', 'inline', and 'editor' modes - Update defaults and presets to include body configuration --- src/lib/config/defaults.ts | 21 +++++++++++++++++++++ src/lib/config/types.ts | 19 +++++++++++++++++++ src/lib/presets/index.ts | 8 ++++++++ 3 files changed, 48 insertions(+) diff --git a/src/lib/config/defaults.ts b/src/lib/config/defaults.ts index ebecc58..5eab263 100644 --- a/src/lib/config/defaults.ts +++ b/src/lib/config/defaults.ts @@ -35,6 +35,17 @@ export const DEFAULT_CONFIG: LabcommitrConfig = { template: "{emoji}{type}({scope}): {subject}", // Standard 50-character limit for commit subjects (git best practice) subject_max_length: 50, + // Commit message body configuration + body: { + // Body is optional by default (user can provide if needed) + required: false, + // No minimum length when body is provided (user decides) + min_length: 0, + // No maximum length (unlimited) + max_length: null, + // Auto-detect best method (inline for short, editor for long) + editor_preference: "auto", + }, }, /** Empty types array - must be provided by user or preset */ @@ -50,6 +61,8 @@ export const DEFAULT_CONFIG: LabcommitrConfig = { subject_min_length: 3, // No prohibited words by default (user can customize) prohibited_words: [], + // No prohibited words in body by default (separate from subject) + prohibited_words_body: [], }, /** Conservative advanced settings - minimal automation by default */ @@ -136,6 +149,14 @@ export function mergeWithDefaults(rawConfig: RawConfig): LabcommitrConfig { if (rawConfig.format) { merged.format = { ...merged.format, ...rawConfig.format }; + + // Handle nested body configuration if provided + if (rawConfig.format.body) { + merged.format.body = { + ...merged.format.body, + ...rawConfig.format.body, + }; + } } if (rawConfig.validation) { diff --git a/src/lib/config/types.ts b/src/lib/config/types.ts index fa419df..c9bf283 100644 --- a/src/lib/config/types.ts +++ b/src/lib/config/types.ts @@ -18,6 +18,21 @@ export interface CommitType { emoji?: string; } +/** + * Commit message body configuration + * Controls how commit message body/description is collected and validated + */ +export interface BodyConfig { + /** Whether commit body is required (default: false) */ + required: boolean; + /** Minimum length when body is provided (default: 0 = no minimum) */ + min_length: number; + /** Maximum length (null = unlimited, default: null) */ + max_length: number | null; + /** Preferred editor for body input (default: "auto") */ + editor_preference: "auto" | "inline" | "editor"; +} + /** * Main configuration interface - fully resolved with all defaults applied * This represents the complete configuration structure after processing @@ -38,6 +53,8 @@ export interface LabcommitrConfig { template: string; /** Maximum length for commit subject line */ subject_max_length: number; + /** Configuration for commit message body/description */ + body: BodyConfig; }; /** Array of available commit types (presence = enabled) */ types: CommitType[]; @@ -51,6 +68,8 @@ export interface LabcommitrConfig { subject_min_length: number; /** Words prohibited in commit subjects */ prohibited_words: string[]; + /** Words prohibited in commit body (separate from subject) */ + prohibited_words_body: string[]; }; /** Advanced configuration options */ advanced: { diff --git a/src/lib/presets/index.ts b/src/lib/presets/index.ts index 3b34b07..0c60803 100644 --- a/src/lib/presets/index.ts +++ b/src/lib/presets/index.ts @@ -99,6 +99,13 @@ export function buildConfig( // Template is determined by style; emoji is handled at render time template: "{type}({scope}): {subject}", subject_max_length: 50, + // Default body configuration (optional, auto-detect editor preference) + body: { + required: false, + min_length: 0, + max_length: null, + editor_preference: "auto", + }, }, types: preset.types, validation: { @@ -106,6 +113,7 @@ export function buildConfig( allowed_scopes: [], subject_min_length: 3, prohibited_words: [], + prohibited_words_body: [], }, advanced: { aliases: {}, From 44b8e0956407a451275e4b2c4933b8d4eba86f51 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 21:59:54 -0600 Subject: [PATCH 57/77] feat(commit): implement editor support for commit body input - Add editor detection (nvim, vim, vi) with / support - Implement editInEditor function with temp file management - Integrate editor option into promptBody based on editor_preference config - Support 'editor', 'inline', and 'auto' preference modes - Add validation and retry logic for editor input - Handle editor cancellation and fallback to inline input --- src/cli/commands/commit/editor.ts | 159 +++++ src/cli/commands/commit/formatter.ts | 30 + src/cli/commands/commit/git.ts | 266 +++++++++ src/cli/commands/commit/index.ts | 302 ++++++++++ src/cli/commands/commit/prompts.ts | 831 +++++++++++++++++++++++++++ src/cli/commands/commit/types.ts | 76 +++ 6 files changed, 1664 insertions(+) create mode 100644 src/cli/commands/commit/editor.ts create mode 100644 src/cli/commands/commit/formatter.ts create mode 100644 src/cli/commands/commit/git.ts create mode 100644 src/cli/commands/commit/index.ts create mode 100644 src/cli/commands/commit/prompts.ts create mode 100644 src/cli/commands/commit/types.ts diff --git a/src/cli/commands/commit/editor.ts b/src/cli/commands/commit/editor.ts new file mode 100644 index 0000000..be0ab7c --- /dev/null +++ b/src/cli/commands/commit/editor.ts @@ -0,0 +1,159 @@ +/** + * Editor Support for Commit Body + * + * Handles spawning external editors (nvim, vim, vi) for commit message body input + */ + +import { spawnSync } from "child_process"; +import { writeFileSync, readFileSync, unlinkSync, mkdtempSync, rmdirSync } from "fs"; +import { join, dirname } from "path"; +import { tmpdir } from "os"; +import { Logger } from "../../../lib/logger.js"; + +/** + * Detect available editor in priority order: nvim → vim → vi + * Also checks $EDITOR and $VISUAL environment variables + */ +export function detectEditor(): string | null { + // Check environment variables first (user preference) + const envEditor = process.env.EDITOR || process.env.VISUAL; + if (envEditor) { + // Verify the editor exists + const check = spawnSync("which", [envEditor], { encoding: "utf-8" }); + if (check.status === 0) { + return envEditor.trim(); + } + } + + // Try nvim, vim, vi in order + const editors = ["nvim", "vim", "vi"]; + for (const editor of editors) { + const check = spawnSync("which", [editor], { encoding: "utf-8" }); + if (check.status === 0) { + return editor; + } + } + + return null; +} + +/** + * Open editor with content and return edited text + * + * @param initialContent - Initial content to show in editor + * @param editor - Editor command to use (if null, auto-detect) + * @returns Edited content or null if cancelled/failed + */ +export function editInEditor( + initialContent: string = "", + editor?: string | null, +): string | null { + const editorCommand = editor || detectEditor(); + + if (!editorCommand) { + Logger.error("No editor found"); + console.error("\n No editor available (nvim, vim, or vi)"); + console.error(" Set $EDITOR environment variable to your preferred editor\n"); + return null; + } + + // Create temporary file + let tempFile: string; + try { + const tempDir = mkdtempSync(join(tmpdir(), "labcommitr-")); + tempFile = join(tempDir, "COMMIT_BODY"); + writeFileSync(tempFile, initialContent, "utf-8"); + } catch (error) { + Logger.error("Failed to create temporary file"); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`\n Error: ${errorMessage}\n`); + return null; + } + + // Spawn editor + try { + // Determine editor arguments based on editor type + let editorArgs: string[]; + const isNeovim = editorCommand.includes("nvim"); + + if (isNeovim) { + // Neovim: use -f flag to run in foreground (don't detach) + editorArgs = ["-f", tempFile]; + } else { + // Vim/Vi: just pass the file + editorArgs = [tempFile]; + } + + const result = spawnSync(editorCommand, editorArgs, { + stdio: "inherit", + shell: false, + }); + + // Editor returned - check if successful + if (result.error) { + Logger.error(`Editor execution failed: ${editorCommand}`); + const errorMessage = + result.error instanceof Error + ? result.error.message + : String(result.error); + console.error(`\n Editor error: ${errorMessage}\n`); + + // Cleanup + try { + unlinkSync(tempFile); + } catch { + // Ignore cleanup errors + } + + return null; + } + + // Read back the file + try { + const content = readFileSync(tempFile, "utf-8"); + + // Cleanup + try { + unlinkSync(tempFile); + // Also try to remove temp dir if empty + const tempDir = dirname(tempFile); + try { + rmdirSync(tempDir); + } catch { + // Ignore if not empty + } + } catch { + // Ignore cleanup errors + } + + return content.trim(); + } catch (error) { + Logger.error("Failed to read edited file"); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`\n Error reading file: ${errorMessage}\n`); + + // Cleanup + try { + unlinkSync(tempFile); + } catch { + // Ignore cleanup errors + } + + return null; + } + } catch (error) { + Logger.error(`Failed to spawn editor: ${editorCommand}`); + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(`\n Error: ${errorMessage}\n`); + + // Cleanup + try { + unlinkSync(tempFile); + } catch { + // Ignore cleanup errors + } + + return null; + } +} + diff --git a/src/cli/commands/commit/formatter.ts b/src/cli/commands/commit/formatter.ts new file mode 100644 index 0000000..89847c4 --- /dev/null +++ b/src/cli/commands/commit/formatter.ts @@ -0,0 +1,30 @@ +/** + * Message Formatter + * + * Formats commit messages according to configuration template + */ + +import type { LabcommitrConfig } from "../../../lib/config/types.js"; + +/** + * Format commit message from template + */ +export function formatCommitMessage( + config: LabcommitrConfig, + type: string, + typeEmoji: string | undefined, + scope: string | undefined, + subject: string, +): string { + let message = config.format.template; + + // Replace variables + const emoji = config.config.emoji_enabled && typeEmoji ? typeEmoji : ""; + message = message.replace("{emoji}", emoji); + message = message.replace("{type}", type); + message = message.replace("{scope}", scope || ""); + message = message.replace("{subject}", subject); + + return message; +} + diff --git a/src/cli/commands/commit/git.ts b/src/cli/commands/commit/git.ts new file mode 100644 index 0000000..d169424 --- /dev/null +++ b/src/cli/commands/commit/git.ts @@ -0,0 +1,266 @@ +/** + * Git Operations + * + * Handles all git operations for the commit command: + * - Checking git status + * - Staging files + * - Getting staged file information + * - Executing commits + * - Cleanup (unstaging) + */ + +import { execSync, spawnSync } from "child_process"; +import { Logger } from "../../../lib/logger.js"; +import type { StagedFileInfo, GitStatus } from "./types.js"; + +/** + * Execute git command and return stdout + * Uses spawnSync with separate command and args to avoid shell interpretation + * This prevents issues with special characters like parentheses and colons + */ +function execGit(args: string[]): string { + try { + const result = spawnSync("git", args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + const stderr = result.stderr?.toString() || "Unknown error"; + const error = new Error(stderr); + (error as any).code = result.status; + throw error; + } + + return result.stdout?.toString().trim() || ""; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.error(`Git command failed: git ${args.join(" ")}`); + Logger.error(errorMessage); + throw error; + } +} + +/** + * Check if current directory is a git repository + */ +export function isGitRepository(): boolean { + try { + execGit(["rev-parse", "--git-dir"]); + return true; + } catch { + return false; + } +} + +/** + * Get staged files (files already staged before auto-stage) + */ +function getStagedFiles(): string[] { + try { + const output = execGit(["diff", "--cached", "--name-only"]); + return output ? output.split("\n").filter((f) => f.trim()) : []; + } catch { + return []; + } +} + +/** + * Get unstaged tracked files (modified/deleted) + */ +function getUnstagedTrackedFiles(): string[] { + try { + const output = execGit(["diff", "--name-only"]); + return output ? output.split("\n").filter((f) => f.trim()) : []; + } catch { + return []; + } +} + +/** + * Check if there are untracked files + */ +function hasUntrackedFiles(): boolean { + try { + const output = execGit(["ls-files", "--others", "--exclude-standard"]); + return output.trim().length > 0; + } catch { + return false; + } +} + +/** + * Stage all modified/deleted tracked files (git add -u) + */ +export function stageAllTrackedFiles(): string[] { + const beforeStaged = getStagedFiles(); + execGit(["add", "-u"]); + const afterStaged = getStagedFiles(); + + // Return files that were newly staged + return afterStaged.filter((file) => !beforeStaged.includes(file)); +} + +/** + * Get detailed information about staged files + */ +export function getStagedFilesInfo(): StagedFileInfo[] { + try { + // Get file statuses + const statusOutput = execGit(["diff", "--cached", "--name-status"]); + if (!statusOutput) return []; + + // Get line statistics + const statsOutput = execGit([ + "diff", + "--cached", + "--numstat", + "--format=", + ]); + + const statusLines = statusOutput.split("\n").filter((l) => l.trim()); + const statsMap = new Map(); + + if (statsOutput) { + const statsLines = statsOutput.split("\n").filter((l) => l.trim()); + for (const line of statsLines) { + const parts = line.split(/\s+/); + if (parts.length >= 3) { + const additions = parseInt(parts[0], 10) || 0; + const deletions = parseInt(parts[1], 10) || 0; + const path = parts.slice(2).join(" "); + statsMap.set(path, { additions, deletions }); + } + } + } + + const files: StagedFileInfo[] = []; + const alreadyStaged = new Set(getStagedFiles()); + + for (const line of statusLines) { + const match = line.match(/^([MADRC])\s+(.+)$/); + if (match) { + const [, statusCode, path] = match; + const stats = statsMap.get(path); + + // Determine status type + let status: StagedFileInfo["status"] = "M"; + if (statusCode === "A") status = "A"; + else if (statusCode === "D") status = "D"; + else if (statusCode === "R") status = "R"; + else if (statusCode === "C") status = "C"; + else if (statusCode === "M") status = "M"; + + files.push({ + path, + status, + additions: stats?.additions, + deletions: stats?.deletions, + }); + } + } + + return files; + } catch { + return []; + } +} + +/** + * Get git status information + */ +export function getGitStatus(alreadyStagedPaths: string[]): GitStatus { + const alreadyStaged = alreadyStagedPaths; + const allStagedInfo = getStagedFilesInfo(); + + // Separate already staged from newly staged + const alreadyStagedSet = new Set(alreadyStaged); + const alreadyStagedInfo: StagedFileInfo[] = []; + const newlyStagedInfo: StagedFileInfo[] = []; + + for (const file of allStagedInfo) { + const info = { ...file, wasAlreadyStaged: alreadyStagedSet.has(file.path) }; + if (alreadyStagedSet.has(file.path)) { + alreadyStagedInfo.push(info); + } else { + newlyStagedInfo.push(info); + } + } + + return { + alreadyStaged: alreadyStagedInfo, + newlyStaged: newlyStagedInfo, + totalStaged: allStagedInfo.length, + hasUnstagedTracked: getUnstagedTrackedFiles().length > 0, + hasUntracked: hasUntrackedFiles(), + }; +} + +/** + * Check if there are any staged files + */ +export function hasStagedFiles(): boolean { + return getStagedFiles().length > 0; +} + +/** + * Get count of staged files + */ +export function getStagedFilesCount(): number { + return getStagedFiles().length; +} + +/** + * Execute git commit + */ +export function createCommit( + subject: string, + body: string | undefined, + sign: boolean, + noVerify: boolean, +): string { + const args: string[] = ["commit"]; + + // Add subject + args.push("-m", subject); + + // Add body (each -m adds a paragraph) + if (body) { + const bodyLines = body.split("\n"); + for (const line of bodyLines) { + args.push("-m", line); + } + } + + // Sign commit if enabled + if (sign) { + args.push("-S"); + } + + // Bypass hooks if requested + if (noVerify) { + args.push("--no-verify"); + } + + execGit(args); + + // Get commit hash + try { + return execGit(["rev-parse", "HEAD"]).substring(0, 7); + } catch { + return "unknown"; + } +} + +/** + * Unstage specific files + */ +export function unstageFiles(files: string[]): void { + if (files.length === 0) return; + execGit(["reset", "HEAD", "--", ...files]); +} + diff --git a/src/cli/commands/commit/index.ts b/src/cli/commands/commit/index.ts new file mode 100644 index 0000000..f602d7d --- /dev/null +++ b/src/cli/commands/commit/index.ts @@ -0,0 +1,302 @@ +/** + * Commit Command Main Handler + * + * Orchestrates the complete commit workflow: + * 1. Load configuration + * 2. Check/stage files (early, before prompts) + * 3. Display file verification + * 4. Collect commit data via prompts + * 5. Preview and confirm + * 6. Execute commit + * 7. Cleanup on cancellation/failure + */ + +import { loadConfig, ConfigError } from "../../../lib/config/index.js"; +import { Logger } from "../../../lib/logger.js"; +import { isGitRepository } from "./git.js"; +import { + stageAllTrackedFiles, + hasStagedFiles, + getGitStatus, + createCommit, + unstageFiles, +} from "./git.js"; +import { + promptType, + promptScope, + promptSubject, + promptBody, + displayStagedFiles, + displayPreview, +} from "./prompts.js"; +import { formatCommitMessage } from "./formatter.js"; +import type { CommitState } from "./types.js"; +import * as readline from "readline"; +import { success } from "../init/colors.js"; + +/** + * Wait for user to press Enter (for file verification step) + */ +function waitForEnter(): Promise { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + rl.on("line", () => { + rl.close(); + resolve(); + }); + + // Handle Ctrl+C + rl.on("SIGINT", () => { + rl.close(); + console.log("\nCommit cancelled."); + process.exit(0); + }); + }); +} + +/** + * Handle cleanup: unstage files we staged + */ +async function cleanup(state: CommitState): Promise { + if (state.newlyStagedFiles.length > 0) { + console.log(); + console.log("◐ Cleaning up..."); + unstageFiles(state.newlyStagedFiles); + + const preservedCount = state.alreadyStagedFiles.length; + if (preservedCount > 0) { + console.log( + `✓ Unstaged ${state.newlyStagedFiles.length} file${state.newlyStagedFiles.length !== 1 ? "s" : ""} (preserved ${preservedCount} already-staged file${preservedCount !== 1 ? "s" : ""})`, + ); + } else { + console.log( + `✓ Unstaged files successfully`, + ); + } + } +} + +/** + * Main commit action handler + */ +export async function commitAction(options: { + type?: string; + scope?: string; + message?: string; + verify?: boolean; +}): Promise { + try { + // Step 1: Load configuration + const configResult = await loadConfig(); + + if (!configResult.config) { + Logger.error("Configuration not found"); + console.error("\n Run 'lab init' to create configuration file.\n"); + process.exit(1); + } + + const config = configResult.config; + + // Step 2: Verify git repository + if (!isGitRepository()) { + Logger.error("Not a git repository"); + console.error("\n Initialize git first: git init\n"); + process.exit(1); + } + + // Step 3: Early file check/staging + const autoStageEnabled = config.advanced.git.auto_stage; + let alreadyStagedFiles: string[] = []; + let newlyStagedFiles: string[] = []; + + // Get already staged files (before we do anything) + if (autoStageEnabled) { + // Check what's already staged + const { execSync } = await import("child_process"); + try { + const stagedOutput = execSync("git diff --cached --name-only", { + encoding: "utf-8", + }).trim(); + alreadyStagedFiles = stagedOutput + ? stagedOutput.split("\n").filter((f) => f.trim()) + : []; + } catch { + alreadyStagedFiles = []; + } + + // Check if there are unstaged tracked files + try { + const unstagedOutput = execSync("git diff --name-only", { + encoding: "utf-8", + }).trim(); + const hasUnstagedTracked = unstagedOutput.length > 0; + + if (!hasUnstagedTracked && alreadyStagedFiles.length === 0) { + // Check for untracked files + try { + const untrackedOutput = execSync( + "git ls-files --others --exclude-standard", + { encoding: "utf-8" }, + ).trim(); + const hasUntracked = untrackedOutput.length > 0; + + if (hasUntracked) { + console.error("\n⚠ No tracked files to stage"); + console.error( + "\n Only untracked files exist. Stage them manually with 'git add '\n", + ); + process.exit(1); + } else { + console.error("\n⚠ No modified files to stage"); + console.error( + "\n All files are already committed or there are no changes.", + ); + console.error(" Nothing to commit.\n"); + process.exit(1); + } + } catch { + console.error("\n⚠ No modified files to stage"); + console.error( + "\n All files are already committed or there are no changes.", + ); + console.error(" Nothing to commit.\n"); + process.exit(1); + } + return; + } + + // Stage remaining files + if (hasUnstagedTracked) { + console.log("◐ Staging files..."); + if (alreadyStagedFiles.length > 0) { + console.log( + ` Found ${alreadyStagedFiles.length} file${alreadyStagedFiles.length !== 1 ? "s" : ""} already staged, ${unstagedOutput.split("\n").filter((f) => f.trim()).length} file${unstagedOutput.split("\n").filter((f) => f.trim()).length !== 1 ? "s" : ""} unstaged`, + ); + } + newlyStagedFiles = stageAllTrackedFiles(); + console.log( + `✓ Staged ${newlyStagedFiles.length} file${newlyStagedFiles.length !== 1 ? "s" : ""}${alreadyStagedFiles.length > 0 ? " (preserved existing staging)" : ""}`, + ); + } + } catch { + // Error getting unstaged files, continue + } + } else { + // auto_stage: false - check if anything is staged + if (!hasStagedFiles()) { + console.error("\n✗ Error: No files staged for commit"); + console.error("\n Nothing has been staged. Please stage files first:"); + console.error(" • Use 'git add ' to stage specific files"); + console.error(" • Use 'git add -u' to stage all modified files"); + console.error(" • Or enable auto_stage in your config\n"); + process.exit(1); + } + + // Get already staged files for tracking + const { execSync } = await import("child_process"); + try { + const stagedOutput = execSync("git diff --cached --name-only", { + encoding: "utf-8", + }).trim(); + alreadyStagedFiles = stagedOutput + ? stagedOutput.split("\n").filter((f) => f.trim()) + : []; + } catch { + alreadyStagedFiles = []; + } + } + + // Step 4: Display staged files verification + const gitStatus = getGitStatus(alreadyStagedFiles); + displayStagedFiles(gitStatus); + + // Wait for user confirmation + await waitForEnter(); + + // Step 5: Collect commit data via prompts + const { type, emoji } = await promptType(config, options.type); + const scope = await promptScope(config, type, options.scope); + const subject = await promptSubject(config, options.message); + const body = await promptBody(config); + + // Step 6: Format and preview message + const formattedMessage = formatCommitMessage( + config, + type, + emoji, + scope, + subject, + ); + + const confirmed = await displayPreview(formattedMessage, body); + + if (!confirmed) { + // User selected "No, let me edit" + await cleanup({ + config, + autoStageEnabled, + alreadyStagedFiles, + newlyStagedFiles, + type, + typeEmoji: emoji, + scope, + subject, + body, + formattedMessage, + }); + console.log("\nCommit cancelled."); + process.exit(0); + } + + // Step 7: Execute commit + console.log(); + console.log("◐ Creating commit..."); + + try { + const commitHash = createCommit( + formattedMessage, + body, + config.advanced.git.sign_commits, + options.verify === false, + ); + + console.log(`${success("✓")} Commit created successfully!`); + console.log(` ${commitHash} ${formattedMessage}`); + } catch (error: unknown) { + // Cleanup on failure + await cleanup({ + config, + autoStageEnabled, + alreadyStagedFiles, + newlyStagedFiles, + type, + typeEmoji: emoji, + scope, + subject, + body, + formattedMessage, + }); + + const errorMessage = + error instanceof Error ? error.message : String(error); + console.error(`\n✗ Error: Git commit failed`); + console.error(`\n ${errorMessage}\n`); + process.exit(1); + } + } catch (error: unknown) { + if (error instanceof ConfigError) { + Logger.error("Configuration error"); + console.error(error.formatForUser()); + process.exit(1); + } + + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.error(`Unexpected error: ${errorMessage}`); + process.exit(1); + } +} + diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts new file mode 100644 index 0000000..5bc707a --- /dev/null +++ b/src/cli/commands/commit/prompts.ts @@ -0,0 +1,831 @@ +/** + * Commit Command Prompts + * + * Interactive prompts for commit creation + * Uses same styling as init command for consistency + */ + +import { select, text, isCancel } from "@clack/prompts"; +import { + labelColors, + textColors, + success, + attention, +} from "../init/colors.js"; +import type { LabcommitrConfig, CommitType } from "../../../lib/config/types.js"; +import type { ValidationError } from "./types.js"; +import { editInEditor, detectEditor } from "./editor.js"; + +/** + * Create compact color-coded label + * Labels are 7 characters wide (6 chars + padding) for alignment + */ +function label( + text: string, + color: "magenta" | "cyan" | "blue" | "yellow" | "green", +): string { + const colorFn = { + magenta: labelColors.bgBrightMagenta, + cyan: labelColors.bgBrightCyan, + blue: labelColors.bgBrightBlue, + yellow: labelColors.bgBrightYellow, + green: labelColors.bgBrightGreen, + }[color]; + + return colorFn(` ${text.padEnd(6)} `); +} + +/** + * Handle prompt cancellation + */ +function handleCancel(value: unknown): void { + if (isCancel(value)) { + console.log("\nCommit cancelled."); + process.exit(0); + } +} + +/** + * Prompt for commit type selection + */ +export async function promptType( + config: LabcommitrConfig, + providedType?: string, +): Promise<{ type: string; emoji?: string }> { + // If type provided via CLI flag, validate it + if (providedType) { + const typeConfig = config.types.find((t) => t.id === providedType); + if (!typeConfig) { + const available = config.types.map((t) => ` • ${t.id} - ${t.description}`).join("\n"); + console.error(`\n✗ Error: Invalid commit type '${providedType}'`); + console.error("\n The commit type is not defined in your configuration."); + console.error("\n Available types:"); + console.error(available); + console.error("\n Solutions:"); + console.error(" • Use one of the available types listed above"); + console.error(" • Check your configuration file for custom types\n"); + process.exit(1); + } + return { + type: providedType, + emoji: typeConfig.emoji, + }; + } + + const selected = await select({ + message: `${label("type", "magenta")} ${textColors.pureWhite("Select commit type:")}`, + options: config.types.map((type) => ({ + value: type.id, + label: `${type.id.padEnd(8)} ${type.description}`, + hint: type.description, + })), + }); + + handleCancel(selected); + const typeId = selected as string; + const typeConfig = config.types.find((t) => t.id === typeId)!; + + return { + type: typeId, + emoji: typeConfig.emoji, + }; +} + +/** + * Prompt for scope input + */ +export async function promptScope( + config: LabcommitrConfig, + selectedType: string, + providedScope?: string, +): Promise { + const isRequired = config.validation.require_scope_for.includes(selectedType); + const allowedScopes = config.validation.allowed_scopes; + + // If scope provided via CLI flag, validate it + if (providedScope !== undefined) { + if (providedScope === "" && isRequired) { + console.error(`\n✗ Error: Scope is required for commit type '${selectedType}'`); + process.exit(1); + } + if (allowedScopes.length > 0 && !allowedScopes.includes(providedScope)) { + console.error(`\n✗ Error: Invalid scope '${providedScope}'`); + console.error(`\n Allowed scopes: ${allowedScopes.join(", ")}\n`); + process.exit(1); + } + return providedScope || undefined; + } + + // Use select if allowed scopes are defined + if (allowedScopes.length > 0) { + const options = [ + ...allowedScopes.map((scope) => ({ + value: scope, + label: scope, + })), + { + value: "__custom__", + label: "(custom) Type a custom scope", + }, + ]; + + const selected = await select({ + message: `${label("scope", "blue")} ${textColors.pureWhite( + `Enter scope ${isRequired ? "(required for '" + selectedType + "')" : "(optional)"}:`, + )}`, + options, + }); + + handleCancel(selected); + + if (selected === "__custom__") { + const custom = await text({ + message: `${label("scope", "blue")} ${textColors.pureWhite("Enter custom scope:")}`, + placeholder: "", + validate: (value) => { + if (isRequired && !value) { + return "Scope is required for this commit type"; + } + return undefined; + }, + }); + + handleCancel(custom); + return custom ? (custom as string) : undefined; + } + + return selected as string; + } + + // Use text input for free-form scope + const scope = await text({ + message: `${label("scope", "blue")} ${textColors.pureWhite( + `Enter scope ${isRequired ? "(required)" : "(optional)"}:`, + )}`, + placeholder: "", + validate: (value) => { + if (isRequired && !value) { + return "Scope is required for this commit type"; + } + return undefined; + }, + }); + + handleCancel(scope); + return scope ? (scope as string) : undefined; +} + +/** + * Validate subject against config rules + */ +function validateSubject( + config: LabcommitrConfig, + subject: string, +): ValidationError[] { + const errors: ValidationError[] = []; + + // Check min length + if (subject.length < config.validation.subject_min_length) { + errors.push({ + message: `Subject too short (${subject.length} characters)`, + context: `Minimum length: ${config.validation.subject_min_length}`, + }); + } + + // Check max length + if (subject.length > config.format.subject_max_length) { + errors.push({ + message: `Subject too long (${subject.length} characters)`, + context: `Maximum length: ${config.format.subject_max_length}`, + }); + } + + // Check prohibited words (case-insensitive) + const lowerSubject = subject.toLowerCase(); + const foundWords: string[] = []; + for (const word of config.validation.prohibited_words) { + if (lowerSubject.includes(word.toLowerCase())) { + foundWords.push(word); + } + } + + if (foundWords.length > 0) { + errors.push({ + message: `Subject contains prohibited words: ${foundWords.join(", ")}`, + context: "Please rephrase your commit message", + }); + } + + return errors; +} + +/** + * Prompt for subject input + */ +export async function promptSubject( + config: LabcommitrConfig, + providedMessage?: string, +): Promise { + if (providedMessage) { + const errors = validateSubject(config, providedMessage); + if (errors.length > 0) { + console.error("\n✗ Validation failed:"); + for (const error of errors) { + console.error(` • ${error.message}`); + if (error.context) { + console.error(` ${error.context}`); + } + } + console.error(); + process.exit(1); + } + return providedMessage; + } + + let subject: string | symbol = ""; + let errors: ValidationError[] = []; + + do { + if (errors.length > 0) { + console.log(); + console.log(`${attention("⚠")} ${attention("Validation failed:")}`); + for (const error of errors) { + console.log(` • ${error.message}`); + if (error.context) { + console.log(` ${error.context}`); + } + } + console.log(); + } + + subject = await text({ + message: `${label("subject", "cyan")} ${textColors.pureWhite( + `Enter commit subject (max ${config.format.subject_max_length} chars):`, + )}`, + placeholder: "", + validate: (value) => { + const validationErrors = validateSubject(config, value); + if (validationErrors.length > 0) { + // Return first error message for inline display + const firstError = validationErrors[0]; + let message = firstError.message; + if (firstError.context) { + message += `\n ${firstError.context}`; + } + return message; + } + return undefined; + }, + }); + + handleCancel(subject); + + if (typeof subject === "string") { + errors = validateSubject(config, subject); + } + } while (errors.length > 0 && typeof subject === "string"); + + return subject as string; +} + +/** + * Validate body against config rules + */ +function validateBody( + config: LabcommitrConfig, + body: string, +): ValidationError[] { + const errors: ValidationError[] = []; + const bodyConfig = config.format.body; + + // Check required + if (bodyConfig.required && !body) { + errors.push({ + message: "Body is required", + context: "Please provide a commit body", + }); + return errors; + } + + // Skip other checks if body is empty and not required + if (!body) { + return errors; + } + + // Check min length + if (body.length < bodyConfig.min_length) { + errors.push({ + message: `Body too short (${body.length} characters)`, + context: `Minimum length: ${bodyConfig.min_length}`, + }); + } + + // Check max length + if (bodyConfig.max_length !== null && body.length > bodyConfig.max_length) { + errors.push({ + message: `Body too long (${body.length} characters)`, + context: `Maximum length: ${bodyConfig.max_length}`, + }); + } + + // Check prohibited words (case-insensitive) + const lowerBody = body.toLowerCase(); + const foundWords: string[] = []; + for (const word of config.validation.prohibited_words_body) { + if (lowerBody.includes(word.toLowerCase())) { + foundWords.push(word); + } + } + + if (foundWords.length > 0) { + errors.push({ + message: `Body contains prohibited words: ${foundWords.join(", ")}`, + context: "Please rephrase your commit message", + }); + } + + return errors; +} +/** + * Prompt for body input with editor support + */ +export async function promptBody( + config: LabcommitrConfig, +): Promise { + const bodyConfig = config.format.body; + const editorAvailable = detectEditor() !== null; + const preference = bodyConfig.editor_preference; + + // If editor preference is "editor" but no editor available, fall back to inline + if (preference === "editor" && !editorAvailable) { + console.log(); + console.log( + `${attention("⚠")} ${attention("Editor not available, using inline input")}`, + ); + console.log(); + // Fall through to inline input + } else if (preference === "editor" && editorAvailable && !bodyConfig.required) { + // Optional body with editor preference - use editor directly + const edited = await promptBodyWithEditor(config, ""); + return edited || undefined; + } else if (preference === "editor" && editorAvailable && bodyConfig.required) { + // Required body with editor preference - use editor with validation loop + return await promptBodyRequiredWithEditor(config); + } + + // Inline input path + if (!bodyConfig.required) { + // Optional body - offer choice if editor available and preference allows + if (editorAvailable && preference === "auto") { + const inputMethod = await select({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + "Enter commit body (optional):", + )}`, + options: [ + { + value: "inline", + label: "Type inline (single/multi-line)", + }, + { + value: "editor", + label: "Open in editor", + }, + { + value: "skip", + label: "Skip (no body)", + }, + ], + }); + + handleCancel(inputMethod); + + if (inputMethod === "skip") { + return undefined; + } else if (inputMethod === "editor") { + return await promptBodyWithEditor(config, ""); + } + // Fall through to inline + } + + const body = await text({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + "Enter commit body (optional):", + )}`, + placeholder: "Press Enter to skip", + validate: (value) => { + if (!value) return undefined; // Empty is OK if optional + const errors = validateBody(config, value); + if (errors.length > 0) { + return errors[0].message; + } + return undefined; + }, + }); + + handleCancel(body); + return body ? (body as string) : undefined; + } + + // Required body + let body: string | symbol = ""; + let errors: ValidationError[] = []; + + do { + if (errors.length > 0) { + console.log(); + console.log(`${attention("⚠")} ${attention("Validation failed:")}`); + for (const error of errors) { + console.log(` • ${error.message}`); + if (error.context) { + console.log(` ${error.context}`); + } + } + console.log(); + } + + // For required body, offer editor option if available and preference allows + if (editorAvailable && (preference === "auto" || preference === "inline")) { + const inputMethod = await select({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + )}`, + options: [ + { + value: "inline", + label: "Type inline", + }, + { + value: "editor", + label: "Open in editor", + }, + ], + }); + + handleCancel(inputMethod); + + if (inputMethod === "editor") { + const editorBody = await promptBodyWithEditor(config, body as string); + if (editorBody !== null && editorBody !== undefined) { + body = editorBody; + } else { + // Editor cancelled or failed, continue loop + continue; + } + } else { + // Inline input + body = await text({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + )}`, + placeholder: "", + validate: (value) => { + const validationErrors = validateBody(config, value); + if (validationErrors.length > 0) { + return validationErrors[0].message; + } + return undefined; + }, + }); + + handleCancel(body); + } + } else { + // No editor choice, just inline + body = await text({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + )}`, + placeholder: "", + validate: (value) => { + const validationErrors = validateBody(config, value); + if (validationErrors.length > 0) { + return validationErrors[0].message; + } + return undefined; + }, + }); + + handleCancel(body); + } + + if (typeof body === "string") { + errors = validateBody(config, body); + } + } while (errors.length > 0 && typeof body === "string"); + + return body as string; +} + +/** + * Prompt for required body using external editor (with validation loop) + */ +async function promptBodyRequiredWithEditor( + config: LabcommitrConfig, +): Promise { + const bodyConfig = config.format.body; + let body: string = ""; + let errors: ValidationError[] = []; + + do { + if (errors.length > 0) { + console.log(); + console.log(`${attention("⚠")} ${attention("Validation failed:")}`); + for (const error of errors) { + console.log(` • ${error.message}`); + if (error.context) { + console.log(` ${error.context}`); + } + } + console.log(); + } + + const edited = await promptBodyWithEditor(config, body); + if (edited === null || edited === undefined) { + // Editor cancelled, ask what to do + const choice = await select({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + "Editor cancelled. What would you like to do?", + )}`, + options: [ + { + value: "retry", + label: "Try editor again", + }, + { + value: "inline", + label: "Switch to inline input", + }, + { + value: "cancel", + label: "Cancel commit", + }, + ], + }); + + handleCancel(choice); + + if (choice === "cancel") { + console.log("\nCommit cancelled."); + process.exit(0); + } else if (choice === "inline") { + // Fall back to inline for required body + const inlineBody = await text({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + )}`, + placeholder: "", + validate: (value) => { + const validationErrors = validateBody(config, value); + if (validationErrors.length > 0) { + return validationErrors[0].message; + } + return undefined; + }, + }); + + handleCancel(inlineBody); + if (typeof inlineBody === "string") { + body = inlineBody; + errors = validateBody(config, body); + } + break; // Exit loop after inline input + } + // Otherwise continue loop (retry editor) + continue; + } + + body = edited; + errors = validateBody(config, body); + } while (errors.length > 0); + + return body; +} + +/** + * Prompt for body using external editor + */ +async function promptBodyWithEditor( + config: LabcommitrConfig, + initialContent: string, +): Promise { + console.log(); + console.log("◐ Opening editor..."); + console.log(); + + const edited = editInEditor(initialContent); + + if (edited === null) { + // Editor failed or was cancelled + console.log(); + console.log("⚠ Editor cancelled or unavailable, returning to prompts"); + console.log(); + return undefined; + } + + // Validate the edited content + const errors = validateBody(config, edited); + if (errors.length > 0) { + console.log(); + console.log(`${attention("⚠")} ${attention("Validation failed:")}`); + for (const error of errors) { + console.log(` • ${error.message}`); + if (error.context) { + console.log(` ${error.context}`); + } + } + console.log(); + + // Ask if user wants to re-edit or go back to inline + const choice = await select({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + "Validation failed. What would you like to do?", + )}`, + options: [ + { + value: "re-edit", + label: "Edit again", + }, + { + value: "inline", + label: "Type inline instead", + }, + { + value: "cancel", + label: "Cancel commit", + }, + ], + }); + + handleCancel(choice); + + if (choice === "cancel") { + console.log("\nCommit cancelled."); + process.exit(0); + } else if (choice === "re-edit") { + return await promptBodyWithEditor(config, edited); + } else { + // Return undefined to trigger inline prompt + return undefined; + } + } + + return edited; +} + +/** + * Display staged files verification + */ +export function displayStagedFiles( + status: { + alreadyStaged: Array<{ path: string; status: string; additions?: number; deletions?: number }>; + newlyStaged: Array<{ path: string; status: string; additions?: number; deletions?: number }>; + totalStaged: number; + }, +): void { + console.log(); + console.log( + `${label("files", "green")} ${textColors.pureWhite( + `Files to be committed (${status.totalStaged} file${status.totalStaged !== 1 ? "s" : ""}):`, + )}`, + ); + console.log(); + + // Group files by status + const groupByStatus = ( + files: Array<{ path: string; status: string; additions?: number; deletions?: number }>, + ) => { + const groups: Record = { + M: [], + A: [], + D: [], + R: [], + C: [], + }; + + for (const file of files) { + const statusCode = file.status as keyof typeof groups; + if (groups[statusCode]) { + groups[statusCode].push(file); + } + } + + return groups; + }; + + const formatStats = (additions?: number, deletions?: number) => { + if (additions === undefined || deletions === undefined) { + return ""; + } + const addStr = additions > 0 ? `+${additions}` : ""; + const delStr = deletions > 0 ? `-${deletions}` : ""; + if (!addStr && !delStr) { + return ""; + } + const parts: string[] = []; + if (addStr) parts.push(addStr); + if (delStr) parts.push(delStr); + return ` (${parts.join(" ")} lines)`; + }; + + const formatStatusName = (status: string) => { + const map: Record = { + M: "Modified", + A: "Added", + D: "Deleted", + R: "Renamed", + C: "Copied", + }; + return map[status] || status; + }; + + // Show already staged if any + if (status.alreadyStaged.length > 0) { + const alreadyPlural = status.alreadyStaged.length !== 1 ? "s" : ""; + console.log(` ${textColors.brightCyan(`Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`)}`); + const groups = groupByStatus(status.alreadyStaged); + for (const [statusCode, files] of Object.entries(groups)) { + if (files.length > 0) { + console.log(` ${formatStatusName(statusCode)} (${files.length}):`); + for (const file of files) { + console.log(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`); + } + } + } + console.log(); + } + + // Show newly staged if any + if (status.newlyStaged.length > 0) { + const newlyPlural = status.newlyStaged.length !== 1 ? "s" : ""; + console.log(` ${textColors.brightYellow(`Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`)}`); + const groups = groupByStatus(status.newlyStaged); + for (const [statusCode, files] of Object.entries(groups)) { + if (files.length > 0) { + console.log(` ${formatStatusName(statusCode)} (${files.length}):`); + for (const file of files) { + console.log(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`); + } + } + } + console.log(); + } + + // If no separation needed, show all together + if (status.alreadyStaged.length === 0 && status.newlyStaged.length > 0) { + const groups = groupByStatus(status.newlyStaged); + for (const [statusCode, files] of Object.entries(groups)) { + if (files.length > 0) { + console.log(` ${formatStatusName(statusCode)} (${files.length}):`); + for (const file of files) { + console.log(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`); + } + } + } + console.log(); + } + + console.log("─────────────────────────────────────────────"); + console.log(` Press Enter to continue, Ctrl+C to cancel`); +} + +/** + * Display commit message preview + */ +export async function displayPreview( + formattedMessage: string, + body: string | undefined, +): Promise { + console.log(); + console.log( + `${label("preview", "green")} ${textColors.pureWhite("Commit message preview:")}`, + ); + console.log(); + console.log(` ${textColors.brightCyan(formattedMessage)}`); + if (body) { + console.log(); + const bodyLines = body.split("\n"); + for (const line of bodyLines) { + console.log(` ${textColors.white(line)}`); + } + } + console.log(); + console.log("─────────────────────────────────────────────"); + + const confirmed = await select({ + message: ` ${success("✓")} ${textColors.pureWhite("Ready to commit?")}`, + options: [ + { + value: true, + label: "Yes, create commit", + }, + { + value: false, + label: "No, let me edit", + }, + ], + }); + + handleCancel(confirmed); + return confirmed as boolean; +} + diff --git a/src/cli/commands/commit/types.ts b/src/cli/commands/commit/types.ts new file mode 100644 index 0000000..0d4fe3a --- /dev/null +++ b/src/cli/commands/commit/types.ts @@ -0,0 +1,76 @@ +/** + * Commit Command Types + * + * Type definitions for commit command state and data structures + */ + +import type { LabcommitrConfig } from "../../../lib/config/types.js"; + +/** + * Staged file information + */ +export interface StagedFileInfo { + /** File path relative to repo root */ + path: string; + /** Git status code: M (modified), A (added), D (deleted), R (renamed), C (copied) */ + status: "M" | "A" | "D" | "R" | "C"; + /** Lines added (undefined if unknown) */ + additions?: number; + /** Lines deleted (undefined if unknown) */ + deletions?: number; + /** Whether file was already staged before we started (auto_stage context) */ + wasAlreadyStaged?: boolean; +} + +/** + * Git status information + */ +export interface GitStatus { + /** Files already staged before auto-stage */ + alreadyStaged: StagedFileInfo[]; + /** Files staged by auto-stage */ + newlyStaged: StagedFileInfo[]; + /** Total staged files */ + totalStaged: number; + /** Whether there are unstaged tracked files */ + hasUnstagedTracked: boolean; + /** Whether there are untracked files */ + hasUntracked: boolean; +} + +/** + * Commit state throughout the workflow + */ +export interface CommitState { + /** Configuration loaded from file */ + config: LabcommitrConfig; + /** Whether auto-stage is enabled */ + autoStageEnabled: boolean; + /** Files staged before we started (for cleanup) */ + alreadyStagedFiles: string[]; + /** Files we staged via auto-stage (for cleanup) */ + newlyStagedFiles: string[]; + /** Selected commit type ID */ + type: string; + /** Selected commit type emoji (if emoji enabled) */ + typeEmoji?: string; + /** Commit scope (optional) */ + scope?: string; + /** Commit subject */ + subject: string; + /** Commit body (optional) */ + body?: string; + /** Formatted commit message (subject line) */ + formattedMessage: string; +} + +/** + * Validation error details + */ +export interface ValidationError { + /** Error message */ + message: string; + /** Additional context */ + context?: string; +} + From 6a1651e97c02c3bcf18ce312f1c24ee594a9d962 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 21:59:57 -0600 Subject: [PATCH 58/77] refactor(commit): update commit command to use new handler module - Remove placeholder implementation - Import and use commit handler from commit/index module --- src/cli/commands/commit.ts | 46 +------------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/src/cli/commands/commit.ts b/src/cli/commands/commit.ts index ca10ef4..c702f63 100644 --- a/src/cli/commands/commit.ts +++ b/src/cli/commands/commit.ts @@ -3,19 +3,10 @@ * * Interactive commit creation with standardized formatting based on * project configuration. - * - * Step 6 Implementation: Interactive commit workflow - * - Type selection from configured commit types - * - Optional scope input - * - Subject and description prompts - * - Git commit execution with formatted message - * - Dynamic emoji support based on terminal capabilities - * - * Current Status: Placeholder for Step 4 */ import { Command } from "commander"; -import { Logger } from "../../lib/logger.js"; +import { commitAction } from "./commit/index.js"; /** * Commit command @@ -28,38 +19,3 @@ export const commitCommand = new Command("commit") .option("-m, --message ", "Commit subject") .option("--no-verify", "Bypass git hooks") .action(commitAction); - -/** - * Commit action handler (placeholder) - * TODO (Step 6): Implement interactive commit workflow - */ -async function commitAction(options: { - type?: string; - scope?: string; - message?: string; - verify?: boolean; -}): Promise { - Logger.info("Commit command - Coming in Step 6!"); - Logger.info("\nPlanned features:"); - console.log(" • Interactive type selection from your config"); - console.log(" • Optional scope and description prompts"); - console.log(" • Automatic emoji detection and fallback"); - console.log(" • Git commit execution with formatted message"); - console.log(" • Validation against configured rules"); - - if (options.type) { - Logger.info(`\nYou specified type: ${options.type}`); - } - - if (options.scope) { - Logger.info(`You specified scope: ${options.scope}`); - } - - if (options.message) { - Logger.info(`You specified message: ${options.message}`); - } - - if (options.verify === false) { - Logger.warn("Git hooks will be bypassed when implemented"); - } -} From 5db11a7895ac80f907f0acb27e4c8a2568d429d0 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 21:59:58 -0600 Subject: [PATCH 59/77] chore: update build configuration and scripts - Exclude scripts directory from TypeScript compilation - Remove unused test scripts from package.json - Keep only sandbox testing script --- package.json | 3 ++- tsconfig.json | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2e9cb14..dc81278 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "format": "pnpm run format:code", "format:ci": "pnpm run format:code", "format:code": "prettier -w \"**/*\" --ignore-unknown --cache", - "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format" + "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format", + "test:commit:sandbox": "bash scripts/test-commit-sandbox.sh" }, "type": "module", "bin": { diff --git a/tsconfig.json b/tsconfig.json index dd9c1c3..51c7342 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -107,5 +107,7 @@ /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "scripts", ".test-temp"] } From 20bb72f8bb8fac54e93b598efd267f457278772d Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 22:00:00 -0600 Subject: [PATCH 60/77] chore: update .gitignore for documentation and testing scripts - Ignore all .md files except README.md and .changeset/*.md - Ignore scripts/ directory (testing utilities) - Keep documentation files local-only for reference --- .gitignore | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index bfed485..ba327b7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ coverage/ # Testing .test-temp/ test-results/ +scripts/ # npm/yarn/pnpm npm-debug.log* @@ -71,10 +72,8 @@ Thumbs.db # Remove rust files for now rust-src/ -### Development -# Development progress tracking (internal use only) -DEVELOPMENT_PROGRESS.md -REQUIREMENTS.md -CONFIG_SCHEMA.md -DEVELOPMENT_GUIDELINES.md -ARCHITECTURE_DECISIONS.md +### Documentation +# Ignore all .md files except README.md and .changeset/*.md (local reference only) +*.md +!README.md +!.changeset/*.md From 67a0f8537c7d2337a2afd43b3a85cea835e22192 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 23:51:09 -0600 Subject: [PATCH 61/77] feat(commit): add connector lines to files display prompt - Add renderWithConnector helper function for manual connector line rendering - Refactor displayStagedFiles to use @clack/prompts log.info() for connector line start - Replace manual waitForEnter with @clack/prompts select() for confirmation - All multi-line file content now displays with connector lines - Removes waitForEnter function and readline dependency from index.ts --- src/cli/commands/commit/index.ts | 32 +--------------- src/cli/commands/commit/prompts.ts | 60 ++++++++++++++++++++++-------- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/cli/commands/commit/index.ts b/src/cli/commands/commit/index.ts index f602d7d..9081fb1 100644 --- a/src/cli/commands/commit/index.ts +++ b/src/cli/commands/commit/index.ts @@ -31,33 +31,8 @@ import { } from "./prompts.js"; import { formatCommitMessage } from "./formatter.js"; import type { CommitState } from "./types.js"; -import * as readline from "readline"; import { success } from "../init/colors.js"; -/** - * Wait for user to press Enter (for file verification step) - */ -function waitForEnter(): Promise { - return new Promise((resolve) => { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - rl.on("line", () => { - rl.close(); - resolve(); - }); - - // Handle Ctrl+C - rl.on("SIGINT", () => { - rl.close(); - console.log("\nCommit cancelled."); - process.exit(0); - }); - }); -} - /** * Handle cleanup: unstage files we staged */ @@ -210,12 +185,9 @@ export async function commitAction(options: { } } - // Step 4: Display staged files verification + // Step 4: Display staged files verification and wait for confirmation const gitStatus = getGitStatus(alreadyStagedFiles); - displayStagedFiles(gitStatus); - - // Wait for user confirmation - await waitForEnter(); + await displayStagedFiles(gitStatus); // Step 5: Collect commit data via prompts const { type, emoji } = await promptType(config, options.type); diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts index 5bc707a..7175b15 100644 --- a/src/cli/commands/commit/prompts.ts +++ b/src/cli/commands/commit/prompts.ts @@ -5,7 +5,7 @@ * Uses same styling as init command for consistency */ -import { select, text, isCancel } from "@clack/prompts"; +import { select, text, isCancel, log } from "@clack/prompts"; import { labelColors, textColors, @@ -673,22 +673,32 @@ async function promptBodyWithEditor( } /** - * Display staged files verification + * Render a line with connector (│) character at the start + * Maintains visual consistency with @clack/prompts connector lines */ -export function displayStagedFiles( +function renderWithConnector(content: string): string { + return `│ ${content}`; +} + +/** + * Display staged files verification with connector line support + * Uses @clack/prompts log.info() to start connector, then manually + * renders connector lines for multi-line content, and ends with + * a confirmation prompt to maintain visual continuity. + */ +export async function displayStagedFiles( status: { alreadyStaged: Array<{ path: string; status: string; additions?: number; deletions?: number }>; newlyStaged: Array<{ path: string; status: string; additions?: number; deletions?: number }>; totalStaged: number; }, -): void { - console.log(); - console.log( +): Promise { + // Start connector line using @clack/prompts + log.info( `${label("files", "green")} ${textColors.pureWhite( `Files to be committed (${status.totalStaged} file${status.totalStaged !== 1 ? "s" : ""}):`, )}`, ); - console.log(); // Group files by status const groupByStatus = ( @@ -738,36 +748,56 @@ export function displayStagedFiles( return map[status] || status; }; + // Render content with connector lines + // Empty line after header + console.log(renderWithConnector("")); + // Show already staged if any if (status.alreadyStaged.length > 0) { const alreadyPlural = status.alreadyStaged.length !== 1 ? "s" : ""; - console.log(` ${textColors.brightCyan(`Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`)}`); + console.log( + renderWithConnector( + textColors.brightCyan(`Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`), + ), + ); const groups = groupByStatus(status.alreadyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log(` ${formatStatusName(statusCode)} (${files.length}):`); + console.log(renderWithConnector(` ${formatStatusName(statusCode)} (${files.length}):`)); for (const file of files) { - console.log(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`); + console.log( + renderWithConnector( + ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, + ), + ); } } } - console.log(); + console.log(renderWithConnector("")); } // Show newly staged if any if (status.newlyStaged.length > 0) { const newlyPlural = status.newlyStaged.length !== 1 ? "s" : ""; - console.log(` ${textColors.brightYellow(`Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`)}`); + console.log( + renderWithConnector( + textColors.brightYellow(`Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`), + ), + ); const groups = groupByStatus(status.newlyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log(` ${formatStatusName(statusCode)} (${files.length}):`); + console.log(renderWithConnector(` ${formatStatusName(statusCode)} (${files.length}):`)); for (const file of files) { - console.log(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`); + console.log( + renderWithConnector( + ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, + ), + ); } } } - console.log(); + console.log(renderWithConnector("")); } // If no separation needed, show all together From 1810c86d5e08257689a741f6ac2c92a3c0279aa3 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 1 Nov 2025 23:51:11 -0600 Subject: [PATCH 62/77] feat(commit): add connector lines to preview display prompt - Refactor displayPreview to use @clack/prompts log.info() for connector line start - Apply renderWithConnector to all preview content lines - Maintain visual consistency with files display prompt --- src/cli/commands/commit/prompts.ts | 53 +++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts index 7175b15..3b66347 100644 --- a/src/cli/commands/commit/prompts.ts +++ b/src/cli/commands/commit/prompts.ts @@ -805,44 +805,67 @@ export async function displayStagedFiles( const groups = groupByStatus(status.newlyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log(` ${formatStatusName(statusCode)} (${files.length}):`); + console.log(renderWithConnector(` ${formatStatusName(statusCode)} (${files.length}):`)); for (const file of files) { - console.log(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`); + console.log( + renderWithConnector(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`), + ); } } } - console.log(); + console.log(renderWithConnector("")); } - console.log("─────────────────────────────────────────────"); - console.log(` Press Enter to continue, Ctrl+C to cancel`); + // Separator line with connector + console.log(renderWithConnector("─────────────────────────────────────────────")); + + // Use select prompt for confirmation (maintains connector continuity) + const confirmation = await select({ + message: "Press Enter to continue, Esc to cancel", + options: [ + { + value: "continue", + label: "Continue", + }, + ], + }); + + handleCancel(confirmation); } /** - * Display commit message preview + * Display commit message preview with connector line support + * Uses @clack/prompts log.info() to start connector, then manually + * renders connector lines for multi-line preview content. */ export async function displayPreview( formattedMessage: string, body: string | undefined, ): Promise { - console.log(); - console.log( + // Start connector line using @clack/prompts + log.info( `${label("preview", "green")} ${textColors.pureWhite("Commit message preview:")}`, ); - console.log(); - console.log(` ${textColors.brightCyan(formattedMessage)}`); + + // Render content with connector lines + // Empty line after header + console.log(renderWithConnector("")); + console.log(renderWithConnector(textColors.brightCyan(formattedMessage))); + if (body) { - console.log(); + console.log(renderWithConnector("")); const bodyLines = body.split("\n"); for (const line of bodyLines) { - console.log(` ${textColors.white(line)}`); + console.log(renderWithConnector(textColors.white(line))); } } - console.log(); - console.log("─────────────────────────────────────────────"); + + console.log(renderWithConnector("")); + // Separator line with connector + console.log(renderWithConnector("─────────────────────────────────────────────")); const confirmed = await select({ - message: ` ${success("✓")} ${textColors.pureWhite("Ready to commit?")}`, + message: `${success("✓")} ${textColors.pureWhite("Ready to commit?")}`, options: [ { value: true, From aebde91c0ec276978f831fb77c4fea13e6382dca Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 09:34:50 -0700 Subject: [PATCH 63/77] refactor(commit): revert spacing changes in prompts Remove manual spacing lines and log.info() calls from commit prompts. All prompts now use direct select()/text() calls with full messages for consistency with @clack/prompts default behavior. --- src/cli/commands/commit/prompts.ts | 70 ++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts index 3b66347..770d69f 100644 --- a/src/cli/commands/commit/prompts.ts +++ b/src/cli/commands/commit/prompts.ts @@ -18,7 +18,8 @@ import { editInEditor, detectEditor } from "./editor.js"; /** * Create compact color-coded label - * Labels are 7 characters wide (6 chars + padding) for alignment + * Labels are 8 characters wide (6 chars + 2 padding spaces) for alignment + * Text is centered within the label */ function label( text: string, @@ -32,7 +33,18 @@ function label( green: labelColors.bgBrightGreen, }[color]; - return colorFn(` ${text.padEnd(6)} `); + // Center text within 6-character width + // For visual centering: when padding is odd, put extra space on LEFT for better balance + const width = 6; + const textLength = Math.min(text.length, width); // Cap at width + const padding = width - textLength; + // For odd padding (1, 3, 5...), ceil puts extra space on LEFT (better visual weight) + // For even padding (2, 4, 6...), floor/ceil both work the same + const leftPad = Math.ceil(padding / 2); + const rightPad = padding - leftPad; + const centeredText = " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + + return colorFn(` ${centeredText} `); } /** @@ -378,9 +390,7 @@ export async function promptBody( // Optional body - offer choice if editor available and preference allows if (editorAvailable && preference === "auto") { const inputMethod = await select({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - "Enter commit body (optional):", - )}`, + message: `${label("body", "yellow")} ${textColors.pureWhite("Enter commit body (optional):")}`, options: [ { value: "inline", @@ -408,9 +418,7 @@ export async function promptBody( } const body = await text({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - "Enter commit body (optional):", - )}`, + message: `${label("body", "yellow")} ${textColors.pureWhite("Enter commit body (optional):")}`, placeholder: "Press Enter to skip", validate: (value) => { if (!value) return undefined; // Empty is OK if optional @@ -443,13 +451,13 @@ export async function promptBody( console.log(); } - // For required body, offer editor option if available and preference allows - if (editorAvailable && (preference === "auto" || preference === "inline")) { - const inputMethod = await select({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required, min ${bodyConfig.min_length} chars):`, - )}`, - options: [ + // For required body, offer editor option if available and preference allows + if (editorAvailable && (preference === "auto" || preference === "inline")) { + const inputMethod = await select({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + )}`, + options: [ { value: "inline", label: "Type inline", @@ -543,9 +551,7 @@ async function promptBodyRequiredWithEditor( if (edited === null || edited === undefined) { // Editor cancelled, ask what to do const choice = await select({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - "Editor cancelled. What would you like to do?", - )}`, + message: `${label("body", "yellow")} ${textColors.pureWhite("Editor cancelled. What would you like to do?")}`, options: [ { value: "retry", @@ -637,9 +643,7 @@ async function promptBodyWithEditor( // Ask if user wants to re-edit or go back to inline const choice = await select({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - "Validation failed. What would you like to do?", - )}`, + message: `${label("body", "yellow")} ${textColors.pureWhite("Validation failed. What would you like to do?")}`, options: [ { value: "re-edit", @@ -748,6 +752,26 @@ export async function displayStagedFiles( return map[status] || status; }; + /** + * Color code git status indicator to match git's default colors + */ + const colorStatusCode = (status: string): string => { + switch (status) { + case "A": + return textColors.gitAdded(status); + case "M": + return textColors.gitModified(status); + case "D": + return textColors.gitDeleted(status); + case "R": + return textColors.gitRenamed(status); + case "C": + return textColors.gitCopied(status); + default: + return status; + } + }; + // Render content with connector lines // Empty line after header console.log(renderWithConnector("")); @@ -767,7 +791,7 @@ export async function displayStagedFiles( for (const file of files) { console.log( renderWithConnector( - ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, + ` ${colorStatusCode(file.status)} ${file.path}${formatStats(file.additions, file.deletions)}`, ), ); } @@ -791,7 +815,7 @@ export async function displayStagedFiles( for (const file of files) { console.log( renderWithConnector( - ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, + ` ${colorStatusCode(file.status)} ${file.path}${formatStats(file.additions, file.deletions)}`, ), ); } From fc25486c9b4d8ade204c874e597700aaf2e6bc2d Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 09:35:19 -0700 Subject: [PATCH 64/77] feat(ui): add Git status colors for file indicators Add color coding for Git file status characters (A, M, D, R, C) to match Git's default color scheme. Colors will be used in commit command file display for better visual distinction. --- src/cli/commands/init/colors.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/cli/commands/init/colors.ts b/src/cli/commands/init/colors.ts index 37d650b..841ee9c 100644 --- a/src/cli/commands/init/colors.ts +++ b/src/cli/commands/init/colors.ts @@ -80,6 +80,24 @@ export const textColors = { * Perfect for "Clef:" label */ labelBlue: (text: string) => `\x1b[38;5;75m${text}\x1b[0m`, + + /** + * Git Status Colors - Match git's default color scheme + */ + // Added (A) - Green (success, positive) + gitAdded: (text: string) => `\x1b[38;5;46m${text}\x1b[0m`, // Bright green + + // Modified (M) - Yellow (caution, change) + gitModified: (text: string) => `\x1b[38;5;226m${text}\x1b[0m`, // Bright yellow + + // Deleted (D) - Red (danger, removal) + gitDeleted: (text: string) => `\x1b[38;5;196m${text}\x1b[0m`, // Bright red + + // Renamed (R) - Cyan (transformation) + gitRenamed: (text: string) => `\x1b[38;5;51m${text}\x1b[0m`, // Bright cyan + + // Copied (C) - Magenta (duplication) + gitCopied: (text: string) => `\x1b[38;5;201m${text}\x1b[0m`, // Bright magenta }; /** From 5d573460c38d3fc0f6d56c518b94283c24c2a2c1 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 09:35:22 -0700 Subject: [PATCH 65/77] fix(commit): improve staged file detection - Add --find-copies-harder -C50 flags to detect copied files - Improve regex parsing to handle renamed/copied file formats - Correctly parse R100\told\tnew and C100\toriginal\tcopy formats - Use new path for renamed/copied files in display --- src/cli/commands/commit/git.ts | 63 ++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/cli/commands/commit/git.ts b/src/cli/commands/commit/git.ts index d169424..a9d3783 100644 --- a/src/cli/commands/commit/git.ts +++ b/src/cli/commands/commit/git.ts @@ -111,7 +111,10 @@ export function stageAllTrackedFiles(): string[] { export function getStagedFilesInfo(): StagedFileInfo[] { try { // Get file statuses - const statusOutput = execGit(["diff", "--cached", "--name-status"]); + // Use --find-copies-harder with threshold to detect copied files (C) + // --find-copies-harder also checks unmodified files as potential sources + // Threshold 50 means 50% similarity required + const statusOutput = execGit(["diff", "--cached", "--name-status", "--find-copies-harder", "-C50"]); if (!statusOutput) return []; // Get line statistics @@ -142,26 +145,48 @@ export function getStagedFilesInfo(): StagedFileInfo[] { const alreadyStaged = new Set(getStagedFiles()); for (const line of statusLines) { - const match = line.match(/^([MADRC])\s+(.+)$/); + // Handle different git diff --cached --name-status formats: + // - Simple: "A file.ts", "M file.ts", "D file.ts" + // - Renamed: "R100\told.ts\tnew.ts" or "R\told.ts\tnew.ts" + // - Copied: "C100\toriginal.ts\tcopy.ts" or "C\toriginal.ts\tcopy.ts" + + let match = line.match(/^([MAD])\s+(.+)$/); + let statusCode: string; + let path: string; + if (match) { - const [, statusCode, path] = match; - const stats = statsMap.get(path); - - // Determine status type - let status: StagedFileInfo["status"] = "M"; - if (statusCode === "A") status = "A"; - else if (statusCode === "D") status = "D"; - else if (statusCode === "R") status = "R"; - else if (statusCode === "C") status = "C"; - else if (statusCode === "M") status = "M"; - - files.push({ - path, - status, - additions: stats?.additions, - deletions: stats?.deletions, - }); + // Simple format: A, M, D + [, statusCode, path] = match; + } else { + // Renamed or Copied format: R100\told\tnew or R\told\tnew + match = line.match(/^([RC])(?:\d+)?\s+(.+)\t(.+)$/); + if (match) { + const [, code, oldPath, newPath] = match; + statusCode = code; + // For renames/copies, use the new path + path = newPath; + } else { + // Skip lines that don't match expected format + continue; + } } + + const stats = statsMap.get(path); + + // Determine status type + let status: StagedFileInfo["status"] = "M"; + if (statusCode === "A") status = "A"; + else if (statusCode === "D") status = "D"; + else if (statusCode === "R") status = "R"; + else if (statusCode === "C") status = "C"; + else if (statusCode === "M") status = "M"; + + files.push({ + path, + status, + additions: stats?.additions, + deletions: stats?.deletions, + }); } return files; From 475bd2c230ce01306dfe05f26431dce328d0a17a Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 09:35:24 -0700 Subject: [PATCH 66/77] feat(commit): clear terminal at command start Add clearTerminal() function to provide clean display and maximum available space for commit prompts. Only clears in TTY environments. --- src/cli/commands/commit/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/cli/commands/commit/index.ts b/src/cli/commands/commit/index.ts index 9081fb1..993c599 100644 --- a/src/cli/commands/commit/index.ts +++ b/src/cli/commands/commit/index.ts @@ -33,6 +33,18 @@ import { formatCommitMessage } from "./formatter.js"; import type { CommitState } from "./types.js"; import { success } from "../init/colors.js"; +/** + * Clear terminal screen for clean prompt display + * Only clears if running in a TTY environment + */ +function clearTerminal(): void { + if (process.stdout.isTTY) { + process.stdout.write("\x1B[2J"); // Clear screen + process.stdout.write("\x1B[H"); // Move cursor to home position + process.stdout.write("\x1B[3J"); // Clear scrollback buffer (optional, for full clear) + } +} + /** * Handle cleanup: unstage files we staged */ @@ -64,6 +76,9 @@ export async function commitAction(options: { message?: string; verify?: boolean; }): Promise { + // Clear terminal for clean prompt display + clearTerminal(); + try { // Step 1: Load configuration const configResult = await loadConfig(); From 90bc3e799336677ba09a2ba4f5caa877c077ef8c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 09:35:25 -0700 Subject: [PATCH 67/77] chore: add test:commit:reset script Add npm script to reset commit testing sandbox environment. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index dc81278..3f62b41 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "format:ci": "pnpm run format:code", "format:code": "prettier -w \"**/*\" --ignore-unknown --cache", "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format", - "test:commit:sandbox": "bash scripts/test-commit-sandbox.sh" + "test:commit:sandbox": "bash scripts/test-commit-sandbox.sh", + "test:commit:reset": "bash scripts/reset-sandbox.sh" }, "type": "module", "bin": { From ec80bed8eafdd3d6cc5665672d34247200e04c86 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 09:35:27 -0700 Subject: [PATCH 68/77] style(init): improve label text centering Update label function to center text within 6-character width for better visual balance. Applies same centering logic as commit prompts for consistency. --- src/cli/commands/init/prompts.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index f740361..4b43f87 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -20,8 +20,9 @@ import { /** * Create compact color-coded label - * Labels are 7 characters wide (6 chars + padding) for alignment + * Labels are 8 characters wide (6 chars + 2 padding spaces) for alignment * Uses bright ANSI 256 colors for high visibility + * Text is centered within the label */ function label( text: string, @@ -35,7 +36,18 @@ function label( green: labelColors.bgBrightGreen, }[color]; - return colorFn(` ${text.padEnd(6)} `); + // Center text within 6-character width + // For visual centering: when padding is odd, put extra space on LEFT for better balance + const width = 6; + const textLength = Math.min(text.length, width); // Cap at width + const padding = width - textLength; + // For odd padding (1, 3, 5...), ceil puts extra space on LEFT (better visual weight) + // For even padding (2, 4, 6...), floor/ceil both work the same + const leftPad = Math.ceil(padding / 2); + const rightPad = padding - leftPad; + const centeredText = " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + + return colorFn(` ${centeredText} `); } /** From 22615f9f671b13d0cc4ffeebdfea9e971c31f708 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 11:36:14 -0700 Subject: [PATCH 69/77] fix(ui): increase label width to accommodate longer labels - Change label width from 6 to 7 characters - Fixes truncation of 'subject' label (was showing 'subjec') - Accommodates all current labels including 'subject' and 'preview' - Applied to both commit and init command prompts --- src/cli/commands/commit/prompts.ts | 23 ++++++++++++---------- src/cli/commands/init/prompts.ts | 31 +++++++++++++++++++++++++++--- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts index 770d69f..47b30f5 100644 --- a/src/cli/commands/commit/prompts.ts +++ b/src/cli/commands/commit/prompts.ts @@ -18,7 +18,7 @@ import { editInEditor, detectEditor } from "./editor.js"; /** * Create compact color-coded label - * Labels are 8 characters wide (6 chars + 2 padding spaces) for alignment + * Labels are 9 characters wide (7 chars + 2 padding spaces) for alignment * Text is centered within the label */ function label( @@ -33,9 +33,9 @@ function label( green: labelColors.bgBrightGreen, }[color]; - // Center text within 6-character width + // Center text within 7-character width (accommodates "subject" and "preview") // For visual centering: when padding is odd, put extra space on LEFT for better balance - const width = 6; + const width = 7; const textLength = Math.min(text.length, width); // Cap at width const padding = width - textLength; // For odd padding (1, 3, 5...), ceil puts extra space on LEFT (better visual weight) @@ -367,6 +367,9 @@ export async function promptBody( const bodyConfig = config.format.body; const editorAvailable = detectEditor() !== null; const preference = bodyConfig.editor_preference; + + // Explicitly check if body is required (handle potential type coercion) + const isRequired = bodyConfig.required === true; // If editor preference is "editor" but no editor available, fall back to inline if (preference === "editor" && !editorAvailable) { @@ -376,17 +379,17 @@ export async function promptBody( ); console.log(); // Fall through to inline input - } else if (preference === "editor" && editorAvailable && !bodyConfig.required) { + } else if (preference === "editor" && editorAvailable && !isRequired) { // Optional body with editor preference - use editor directly const edited = await promptBodyWithEditor(config, ""); return edited || undefined; - } else if (preference === "editor" && editorAvailable && bodyConfig.required) { + } else if (preference === "editor" && editorAvailable && isRequired) { // Required body with editor preference - use editor with validation loop return await promptBodyRequiredWithEditor(config); } // Inline input path - if (!bodyConfig.required) { + if (!isRequired) { // Optional body - offer choice if editor available and preference allows if (editorAvailable && preference === "auto") { const inputMethod = await select({ @@ -455,7 +458,7 @@ export async function promptBody( if (editorAvailable && (preference === "auto" || preference === "inline")) { const inputMethod = await select({ message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, )}`, options: [ { @@ -483,7 +486,7 @@ export async function promptBody( // Inline input body = await text({ message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, )}`, placeholder: "", validate: (value) => { @@ -501,7 +504,7 @@ export async function promptBody( // No editor choice, just inline body = await text({ message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, )}`, placeholder: "", validate: (value) => { @@ -577,7 +580,7 @@ async function promptBodyRequiredWithEditor( // Fall back to inline for required body const inlineBody = await text({ message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required, min ${bodyConfig.min_length} chars):`, + `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, )}`, placeholder: "", validate: (value) => { diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 4b43f87..8a72bd7 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -20,7 +20,7 @@ import { /** * Create compact color-coded label - * Labels are 8 characters wide (6 chars + 2 padding spaces) for alignment + * Labels are 9 characters wide (7 chars + 2 padding spaces) for alignment * Uses bright ANSI 256 colors for high visibility * Text is centered within the label */ @@ -36,9 +36,9 @@ function label( green: labelColors.bgBrightGreen, }[color]; - // Center text within 6-character width + // Center text within 7-character width (accommodates all current labels) // For visual centering: when padding is odd, put extra space on LEFT for better balance - const width = 6; + const width = 7; const textLength = Math.min(text.length, width); // Cap at width const padding = width - textLength; // For odd padding (1, 3, 5...), ceil puts extra space on LEFT (better visual weight) @@ -152,6 +152,31 @@ export async function promptAutoStage(): Promise { return autoStage as boolean; } +/** + * Prompt for commit body requirement + * When enabled, commit body becomes required during commit creation + */ +export async function promptBodyRequired(): Promise { + const bodyRequired = await select({ + message: `${label("body", "green")} ${textColors.pureWhite("Require commit body?")}`, + options: [ + { + value: true, + label: "Yes (Recommended)", + hint: "Always require body with commit", + }, + { + value: false, + label: "No", + hint: "Body is optional", + }, + ], + }); + + handleCancel(bodyRequired); + return bodyRequired as boolean; +} + /** * Prompt for scope configuration mode */ From 0836e557361608ef7e5e3538975cfef7a5911335 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 11:37:56 -0700 Subject: [PATCH 70/77] feat(init): add prompt for commit body requirement - Add promptBodyRequired() function to init prompts - Integrate body requirement prompt into init flow (after auto-stage) - Update buildConfig() to accept and use bodyRequired customization - Default to false (optional) for backward compatibility - 'Yes' option marked as recommended for better commit practices --- src/cli/commands/init/index.ts | 3 +++ src/lib/presets/index.ts | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts index b571518..e228add 100644 --- a/src/cli/commands/init/index.ts +++ b/src/cli/commands/init/index.ts @@ -21,6 +21,7 @@ import { promptPreset, promptEmoji, promptAutoStage, + promptBodyRequired, displayProcessingSteps, } from "./prompts.js"; import { buildConfig, getPreset } from "../../../lib/presets/index.js"; @@ -102,6 +103,7 @@ async function initAction(options: { const emojiEnabled = await promptEmoji(); const autoStage = await promptAutoStage(); + const bodyRequired = await promptBodyRequired(); // Small pause before processing await new Promise((resolve) => setTimeout(resolve, 800)); @@ -114,6 +116,7 @@ async function initAction(options: { emoji: emojiEnabled, scope: "optional", autoStage, + bodyRequired, }); // Show title "Labcommitr initializing..." diff --git a/src/lib/presets/index.ts b/src/lib/presets/index.ts index 0c60803..e0ef966 100644 --- a/src/lib/presets/index.ts +++ b/src/lib/presets/index.ts @@ -75,6 +75,8 @@ export function buildConfig( scopeRequiredFor?: string[]; // Git integration autoStage?: boolean; + // Body requirement + bodyRequired?: boolean; }, ): LabcommitrConfig { const preset = getPreset(presetId); @@ -99,9 +101,9 @@ export function buildConfig( // Template is determined by style; emoji is handled at render time template: "{type}({scope}): {subject}", subject_max_length: 50, - // Default body configuration (optional, auto-detect editor preference) + // Body configuration (respects user choice, defaults to optional) body: { - required: false, + required: customizations.bodyRequired ?? false, min_length: 0, max_length: null, editor_preference: "auto", From 16c0a4fc6ba12fdbc3baf2a6c6cb12207ef90a42 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:03:05 -0700 Subject: [PATCH 71/77] ci: use built-in GITHUB_TOKEN instead of custom PAT - Replace VOXEL_GITHUB_TOKEN with secrets.GITHUB_TOKEN - Uses automatically available token with workflow permissions - Removes dependency on manually managed Personal Access Token - Fixes 'Bad credentials' error in release workflow - Aligns with GitHub Actions best practices --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6584392..41d8fb1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: commit: "[ci] release" title: "[ci] release" env: - # Needs access to push to main - GITHUB_TOKEN: ${{ secrets.VOXEL_GITHUB_TOKEN }} + # Uses built-in GITHUB_TOKEN (automatically available, no secret needed) + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needs access to publish to npm NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} From fd74494fa11c454ac88110783c555e7cfa910082 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:03:08 -0700 Subject: [PATCH 72/77] config: set changeset access to public - Change access from 'restricted' to 'public' in changeset config - Matches package.json publishConfig.access setting - Ensures consistent configuration across project --- .changeset/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/config.json b/.changeset/config.json index 91b6a95..fce1c26 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,7 +4,7 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] From 5b707eb8b4753ac309b438ed188ace440c9b7957 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:03:11 -0700 Subject: [PATCH 73/77] docs: add changesets for recent features and fixes - Add changeset for body requirement prompt feature - Add changeset for editor support in commit body - Add changeset for commit command UX improvements - Add changeset for label truncation fix - Documents user-facing changes for upcoming release --- .changeset/add-body-requirement-prompt.md | 12 ++++++++++++ .changeset/add-commit-editor-support.md | 12 ++++++++++++ .changeset/fix-label-truncation.md | 11 +++++++++++ .changeset/improve-commit-command-ux.md | 12 ++++++++++++ 4 files changed, 47 insertions(+) create mode 100644 .changeset/add-body-requirement-prompt.md create mode 100644 .changeset/add-commit-editor-support.md create mode 100644 .changeset/fix-label-truncation.md create mode 100644 .changeset/improve-commit-command-ux.md diff --git a/.changeset/add-body-requirement-prompt.md b/.changeset/add-body-requirement-prompt.md new file mode 100644 index 0000000..4b65e27 --- /dev/null +++ b/.changeset/add-body-requirement-prompt.md @@ -0,0 +1,12 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: add body requirement prompt to init command + +- New prompt in init flow to set commit body as required or optional +- "Yes" option marked as recommended for better commit practices +- Configuration properly respected in commit prompts +- When body is required, commit prompts show "required" and remove "Skip" option +- Defaults to optional for backward compatibility + diff --git a/.changeset/add-commit-editor-support.md b/.changeset/add-commit-editor-support.md new file mode 100644 index 0000000..6c70807 --- /dev/null +++ b/.changeset/add-commit-editor-support.md @@ -0,0 +1,12 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: add editor support for commit body input + +- Users can now open their preferred editor for writing commit bodies +- Supports both inline and editor input methods +- Automatically detects available editors (VS Code, Vim, Nano, etc.) +- Improved experience for multi-line commit bodies +- Configurable editor preference in configuration files + diff --git a/.changeset/fix-label-truncation.md b/.changeset/fix-label-truncation.md new file mode 100644 index 0000000..12af765 --- /dev/null +++ b/.changeset/fix-label-truncation.md @@ -0,0 +1,11 @@ +--- +"@labcatr/labcommitr": patch +--- + +fix: prevent label text truncation in prompts + +- Increased label width from 6 to 7 characters to accommodate longer labels +- Fixes issue where "subject" label was being truncated to "subjec" +- Applied to both commit and init command prompts for consistency +- All labels now properly display full text with centered alignment + diff --git a/.changeset/improve-commit-command-ux.md b/.changeset/improve-commit-command-ux.md new file mode 100644 index 0000000..478537b --- /dev/null +++ b/.changeset/improve-commit-command-ux.md @@ -0,0 +1,12 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: enhance commit command user experience + +- Terminal automatically clears at command start for maximum available space +- Improved staged file detection with support for renamed and copied files +- Color-coded Git status indicators (A, M, D, R, C) matching Git's default colors +- Connector lines added to files and preview displays for better visual flow +- More accurate file status reporting with copy detection using -C50 flag + From af5122ff862551a3c0e552f321959fb59f1c1a18 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:03:14 -0700 Subject: [PATCH 74/77] chore: bump version to 0.1.0 and update changelog - Version bumped from 0.0.1 to 0.1.0 - Generated CHANGELOG.md with all consumed changesets - Removed consumed changeset files (cli-framework, config-loading, config-validation, init-command) - Applied formatting to source files - Result of running 'changeset version' command --- .changeset/cli-framework-implementation.md | 13 --- .changeset/config-loading-system.md | 11 -- .changeset/config-validation-system.md | 11 -- .changeset/init-command-with-clef.md | 14 --- package.json | 2 +- src/cli/commands/commit/editor.ts | 18 +++- src/cli/commands/commit/formatter.ts | 1 - src/cli/commands/commit/git.ts | 33 +++--- src/cli/commands/commit/index.ts | 9 +- src/cli/commands/commit/prompts.ts | 115 ++++++++++++++------- src/cli/commands/commit/types.ts | 1 - src/cli/commands/init/colors.ts | 8 +- src/cli/commands/init/prompts.ts | 3 +- 13 files changed, 118 insertions(+), 121 deletions(-) delete mode 100644 .changeset/cli-framework-implementation.md delete mode 100644 .changeset/config-loading-system.md delete mode 100644 .changeset/config-validation-system.md delete mode 100644 .changeset/init-command-with-clef.md diff --git a/.changeset/cli-framework-implementation.md b/.changeset/cli-framework-implementation.md deleted file mode 100644 index 51a796c..0000000 --- a/.changeset/cli-framework-implementation.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -"@labcatr/labcommitr": minor ---- - -feat: add working CLI framework with basic commands - -- Tool now provides functional command-line interface with help system -- Both `labcommitr` and `lab` command aliases are now available -- Added `--version` flag to display current tool version -- Added `config show` command to display and debug configuration -- Interactive help system guides users through available commands -- Clear error messages when invalid commands are used -- Foundation ready for init and commit commands in upcoming releases diff --git a/.changeset/config-loading-system.md b/.changeset/config-loading-system.md deleted file mode 100644 index 9216ff1..0000000 --- a/.changeset/config-loading-system.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@labcatr/labcommitr": minor ---- - -feat: add intelligent configuration file discovery - -- Tool now automatically finds configuration files in project roots -- Prioritizes git repositories and supports monorepo structures -- Provides clear error messages when configuration files have issues -- Improved performance with smart caching for faster subsequent runs -- Works reliably across different project structures and environments diff --git a/.changeset/config-validation-system.md b/.changeset/config-validation-system.md deleted file mode 100644 index 4abdd46..0000000 --- a/.changeset/config-validation-system.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"@labcatr/labcommitr": minor ---- - -feat: add configuration file validation - -- Configuration files are now validated for syntax and required fields -- Clear error messages help users fix configuration issues quickly -- Tool prevents common mistakes like missing commit types or invalid IDs -- Improved reliability when loading project-specific configurations -- Validates commit type IDs contain only lowercase letters as required diff --git a/.changeset/init-command-with-clef.md b/.changeset/init-command-with-clef.md deleted file mode 100644 index 589c4fd..0000000 --- a/.changeset/init-command-with-clef.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -"@labcatr/labcommitr": minor ---- - -feat: add interactive init command with Clef mascot - -- Introduced Clef, an animated cat mascot for enhanced user experience -- Implemented clean, minimal CLI prompts following modern design patterns -- Added four configuration presets: Conventional Commits, Gitmoji, Angular, and Minimal -- Created interactive setup flow with preset selection, emoji support, and scope configuration -- Integrated animated character that appears at key moments: intro, processing, and outro -- Automatic YAML configuration file generation with validation -- Non-intrusive design with clean labels and compact spacing -- Graceful degradation for terminals without animation support diff --git a/package.json b/package.json index 3f62b41..b91d1ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@labcatr/labcommitr", - "version": "0.0.1", + "version": "0.1.0", "description": "Labcommitr is a solution for building standardized git commits, hassle-free!", "main": "dist/index.js", "scripts": { diff --git a/src/cli/commands/commit/editor.ts b/src/cli/commands/commit/editor.ts index be0ab7c..37a4995 100644 --- a/src/cli/commands/commit/editor.ts +++ b/src/cli/commands/commit/editor.ts @@ -5,7 +5,13 @@ */ import { spawnSync } from "child_process"; -import { writeFileSync, readFileSync, unlinkSync, mkdtempSync, rmdirSync } from "fs"; +import { + writeFileSync, + readFileSync, + unlinkSync, + mkdtempSync, + rmdirSync, +} from "fs"; import { join, dirname } from "path"; import { tmpdir } from "os"; import { Logger } from "../../../lib/logger.js"; @@ -53,7 +59,9 @@ export function editInEditor( if (!editorCommand) { Logger.error("No editor found"); console.error("\n No editor available (nvim, vim, or vi)"); - console.error(" Set $EDITOR environment variable to your preferred editor\n"); + console.error( + " Set $EDITOR environment variable to your preferred editor\n", + ); return null; } @@ -75,7 +83,7 @@ export function editInEditor( // Determine editor arguments based on editor type let editorArgs: string[]; const isNeovim = editorCommand.includes("nvim"); - + if (isNeovim) { // Neovim: use -f flag to run in foreground (don't detach) editorArgs = ["-f", tempFile]; @@ -129,7 +137,8 @@ export function editInEditor( return content.trim(); } catch (error) { Logger.error("Failed to read edited file"); - const errorMessage = error instanceof Error ? error.message : String(error); + const errorMessage = + error instanceof Error ? error.message : String(error); console.error(`\n Error reading file: ${errorMessage}\n`); // Cleanup @@ -156,4 +165,3 @@ export function editInEditor( return null; } } - diff --git a/src/cli/commands/commit/formatter.ts b/src/cli/commands/commit/formatter.ts index 89847c4..0635c6a 100644 --- a/src/cli/commands/commit/formatter.ts +++ b/src/cli/commands/commit/formatter.ts @@ -27,4 +27,3 @@ export function formatCommitMessage( return message; } - diff --git a/src/cli/commands/commit/git.ts b/src/cli/commands/commit/git.ts index a9d3783..3d45a95 100644 --- a/src/cli/commands/commit/git.ts +++ b/src/cli/commands/commit/git.ts @@ -24,18 +24,18 @@ function execGit(args: string[]): string { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], }); - + if (result.error) { throw result.error; } - + if (result.status !== 0) { const stderr = result.stderr?.toString() || "Unknown error"; const error = new Error(stderr); (error as any).code = result.status; throw error; } - + return result.stdout?.toString().trim() || ""; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -100,7 +100,7 @@ export function stageAllTrackedFiles(): string[] { const beforeStaged = getStagedFiles(); execGit(["add", "-u"]); const afterStaged = getStagedFiles(); - + // Return files that were newly staged return afterStaged.filter((file) => !beforeStaged.includes(file)); } @@ -114,19 +114,23 @@ export function getStagedFilesInfo(): StagedFileInfo[] { // Use --find-copies-harder with threshold to detect copied files (C) // --find-copies-harder also checks unmodified files as potential sources // Threshold 50 means 50% similarity required - const statusOutput = execGit(["diff", "--cached", "--name-status", "--find-copies-harder", "-C50"]); - if (!statusOutput) return []; - - // Get line statistics - const statsOutput = execGit([ + const statusOutput = execGit([ "diff", "--cached", - "--numstat", - "--format=", + "--name-status", + "--find-copies-harder", + "-C50", ]); + if (!statusOutput) return []; + + // Get line statistics + const statsOutput = execGit(["diff", "--cached", "--numstat", "--format="]); const statusLines = statusOutput.split("\n").filter((l) => l.trim()); - const statsMap = new Map(); + const statsMap = new Map< + string, + { additions: number; deletions: number } + >(); if (statsOutput) { const statsLines = statsOutput.split("\n").filter((l) => l.trim()); @@ -149,7 +153,7 @@ export function getStagedFilesInfo(): StagedFileInfo[] { // - Simple: "A file.ts", "M file.ts", "D file.ts" // - Renamed: "R100\told.ts\tnew.ts" or "R\told.ts\tnew.ts" // - Copied: "C100\toriginal.ts\tcopy.ts" or "C\toriginal.ts\tcopy.ts" - + let match = line.match(/^([MAD])\s+(.+)$/); let statusCode: string; let path: string; @@ -201,7 +205,7 @@ export function getStagedFilesInfo(): StagedFileInfo[] { export function getGitStatus(alreadyStagedPaths: string[]): GitStatus { const alreadyStaged = alreadyStagedPaths; const allStagedInfo = getStagedFilesInfo(); - + // Separate already staged from newly staged const alreadyStagedSet = new Set(alreadyStaged); const alreadyStagedInfo: StagedFileInfo[] = []; @@ -288,4 +292,3 @@ export function unstageFiles(files: string[]): void { if (files.length === 0) return; execGit(["reset", "HEAD", "--", ...files]); } - diff --git a/src/cli/commands/commit/index.ts b/src/cli/commands/commit/index.ts index 993c599..c1256c9 100644 --- a/src/cli/commands/commit/index.ts +++ b/src/cli/commands/commit/index.ts @@ -53,16 +53,14 @@ async function cleanup(state: CommitState): Promise { console.log(); console.log("◐ Cleaning up..."); unstageFiles(state.newlyStagedFiles); - + const preservedCount = state.alreadyStagedFiles.length; if (preservedCount > 0) { console.log( `✓ Unstaged ${state.newlyStagedFiles.length} file${state.newlyStagedFiles.length !== 1 ? "s" : ""} (preserved ${preservedCount} already-staged file${preservedCount !== 1 ? "s" : ""})`, ); } else { - console.log( - `✓ Unstaged files successfully`, - ); + console.log(`✓ Unstaged files successfully`); } } } @@ -82,7 +80,7 @@ export async function commitAction(options: { try { // Step 1: Load configuration const configResult = await loadConfig(); - + if (!configResult.config) { Logger.error("Configuration not found"); console.error("\n Run 'lab init' to create configuration file.\n"); @@ -286,4 +284,3 @@ export async function commitAction(options: { process.exit(1); } } - diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts index 47b30f5..8fe4f0e 100644 --- a/src/cli/commands/commit/prompts.ts +++ b/src/cli/commands/commit/prompts.ts @@ -6,13 +6,11 @@ */ import { select, text, isCancel, log } from "@clack/prompts"; -import { - labelColors, - textColors, - success, - attention, -} from "../init/colors.js"; -import type { LabcommitrConfig, CommitType } from "../../../lib/config/types.js"; +import { labelColors, textColors, success, attention } from "../init/colors.js"; +import type { + LabcommitrConfig, + CommitType, +} from "../../../lib/config/types.js"; import type { ValidationError } from "./types.js"; import { editInEditor, detectEditor } from "./editor.js"; @@ -42,7 +40,8 @@ function label( // For even padding (2, 4, 6...), floor/ceil both work the same const leftPad = Math.ceil(padding / 2); const rightPad = padding - leftPad; - const centeredText = " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + const centeredText = + " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); return colorFn(` ${centeredText} `); } @@ -68,9 +67,13 @@ export async function promptType( if (providedType) { const typeConfig = config.types.find((t) => t.id === providedType); if (!typeConfig) { - const available = config.types.map((t) => ` • ${t.id} - ${t.description}`).join("\n"); + const available = config.types + .map((t) => ` • ${t.id} - ${t.description}`) + .join("\n"); console.error(`\n✗ Error: Invalid commit type '${providedType}'`); - console.error("\n The commit type is not defined in your configuration."); + console.error( + "\n The commit type is not defined in your configuration.", + ); console.error("\n Available types:"); console.error(available); console.error("\n Solutions:"); @@ -117,7 +120,9 @@ export async function promptScope( // If scope provided via CLI flag, validate it if (providedScope !== undefined) { if (providedScope === "" && isRequired) { - console.error(`\n✗ Error: Scope is required for commit type '${selectedType}'`); + console.error( + `\n✗ Error: Scope is required for commit type '${selectedType}'`, + ); process.exit(1); } if (allowedScopes.length > 0 && !allowedScopes.includes(providedScope)) { @@ -367,7 +372,7 @@ export async function promptBody( const bodyConfig = config.format.body; const editorAvailable = detectEditor() !== null; const preference = bodyConfig.editor_preference; - + // Explicitly check if body is required (handle potential type coercion) const isRequired = bodyConfig.required === true; @@ -454,13 +459,13 @@ export async function promptBody( console.log(); } - // For required body, offer editor option if available and preference allows - if (editorAvailable && (preference === "auto" || preference === "inline")) { - const inputMethod = await select({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, - )}`, - options: [ + // For required body, offer editor option if available and preference allows + if (editorAvailable && (preference === "auto" || preference === "inline")) { + const inputMethod = await select({ + message: `${label("body", "yellow")} ${textColors.pureWhite( + `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, + )}`, + options: [ { value: "inline", label: "Type inline", @@ -693,13 +698,21 @@ function renderWithConnector(content: string): string { * renders connector lines for multi-line content, and ends with * a confirmation prompt to maintain visual continuity. */ -export async function displayStagedFiles( - status: { - alreadyStaged: Array<{ path: string; status: string; additions?: number; deletions?: number }>; - newlyStaged: Array<{ path: string; status: string; additions?: number; deletions?: number }>; - totalStaged: number; - }, -): Promise { +export async function displayStagedFiles(status: { + alreadyStaged: Array<{ + path: string; + status: string; + additions?: number; + deletions?: number; + }>; + newlyStaged: Array<{ + path: string; + status: string; + additions?: number; + deletions?: number; + }>; + totalStaged: number; +}): Promise { // Start connector line using @clack/prompts log.info( `${label("files", "green")} ${textColors.pureWhite( @@ -709,7 +722,12 @@ export async function displayStagedFiles( // Group files by status const groupByStatus = ( - files: Array<{ path: string; status: string; additions?: number; deletions?: number }>, + files: Array<{ + path: string; + status: string; + additions?: number; + deletions?: number; + }>, ) => { const groups: Record = { M: [], @@ -784,13 +802,19 @@ export async function displayStagedFiles( const alreadyPlural = status.alreadyStaged.length !== 1 ? "s" : ""; console.log( renderWithConnector( - textColors.brightCyan(`Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`), + textColors.brightCyan( + `Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`, + ), ), ); const groups = groupByStatus(status.alreadyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log(renderWithConnector(` ${formatStatusName(statusCode)} (${files.length}):`)); + console.log( + renderWithConnector( + ` ${formatStatusName(statusCode)} (${files.length}):`, + ), + ); for (const file of files) { console.log( renderWithConnector( @@ -808,13 +832,19 @@ export async function displayStagedFiles( const newlyPlural = status.newlyStaged.length !== 1 ? "s" : ""; console.log( renderWithConnector( - textColors.brightYellow(`Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`), + textColors.brightYellow( + `Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`, + ), ), ); const groups = groupByStatus(status.newlyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log(renderWithConnector(` ${formatStatusName(statusCode)} (${files.length}):`)); + console.log( + renderWithConnector( + ` ${formatStatusName(statusCode)} (${files.length}):`, + ), + ); for (const file of files) { console.log( renderWithConnector( @@ -832,10 +862,16 @@ export async function displayStagedFiles( const groups = groupByStatus(status.newlyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log(renderWithConnector(` ${formatStatusName(statusCode)} (${files.length}):`)); + console.log( + renderWithConnector( + ` ${formatStatusName(statusCode)} (${files.length}):`, + ), + ); for (const file of files) { console.log( - renderWithConnector(` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`), + renderWithConnector( + ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, + ), ); } } @@ -844,7 +880,9 @@ export async function displayStagedFiles( } // Separator line with connector - console.log(renderWithConnector("─────────────────────────────────────────────")); + console.log( + renderWithConnector("─────────────────────────────────────────────"), + ); // Use select prompt for confirmation (maintains connector continuity) const confirmation = await select({ @@ -878,7 +916,7 @@ export async function displayPreview( // Empty line after header console.log(renderWithConnector("")); console.log(renderWithConnector(textColors.brightCyan(formattedMessage))); - + if (body) { console.log(renderWithConnector("")); const bodyLines = body.split("\n"); @@ -886,10 +924,12 @@ export async function displayPreview( console.log(renderWithConnector(textColors.white(line))); } } - + console.log(renderWithConnector("")); // Separator line with connector - console.log(renderWithConnector("─────────────────────────────────────────────")); + console.log( + renderWithConnector("─────────────────────────────────────────────"), + ); const confirmed = await select({ message: `${success("✓")} ${textColors.pureWhite("Ready to commit?")}`, @@ -908,4 +948,3 @@ export async function displayPreview( handleCancel(confirmed); return confirmed as boolean; } - diff --git a/src/cli/commands/commit/types.ts b/src/cli/commands/commit/types.ts index 0d4fe3a..48f5d17 100644 --- a/src/cli/commands/commit/types.ts +++ b/src/cli/commands/commit/types.ts @@ -73,4 +73,3 @@ export interface ValidationError { /** Additional context */ context?: string; } - diff --git a/src/cli/commands/init/colors.ts b/src/cli/commands/init/colors.ts index 841ee9c..1f01723 100644 --- a/src/cli/commands/init/colors.ts +++ b/src/cli/commands/init/colors.ts @@ -86,16 +86,16 @@ export const textColors = { */ // Added (A) - Green (success, positive) gitAdded: (text: string) => `\x1b[38;5;46m${text}\x1b[0m`, // Bright green - + // Modified (M) - Yellow (caution, change) gitModified: (text: string) => `\x1b[38;5;226m${text}\x1b[0m`, // Bright yellow - + // Deleted (D) - Red (danger, removal) gitDeleted: (text: string) => `\x1b[38;5;196m${text}\x1b[0m`, // Bright red - + // Renamed (R) - Cyan (transformation) gitRenamed: (text: string) => `\x1b[38;5;51m${text}\x1b[0m`, // Bright cyan - + // Copied (C) - Magenta (duplication) gitCopied: (text: string) => `\x1b[38;5;201m${text}\x1b[0m`, // Bright magenta }; diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 8a72bd7..e0b0d72 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -45,7 +45,8 @@ function label( // For even padding (2, 4, 6...), floor/ceil both work the same const leftPad = Math.ceil(padding / 2); const rightPad = padding - leftPad; - const centeredText = " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + const centeredText = + " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); return colorFn(` ${centeredText} `); } From a04c11764b1cf81eeebe3251302f03aed096effb Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:07:28 -0700 Subject: [PATCH 75/77] chore: track CHANGELOG.md in git - Remove CHANGELOG.md from .gitignore exceptions - CHANGELOG.md should be tracked to preserve release history - Allows Changesets to append to existing changelog entries - Ensures full changelog is available on GitHub and in releases --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ba327b7..6f6f124 100644 --- a/.gitignore +++ b/.gitignore @@ -73,7 +73,8 @@ Thumbs.db rust-src/ ### Documentation -# Ignore all .md files except README.md and .changeset/*.md (local reference only) +# Ignore all .md files except README.md, CHANGELOG.md, and .changeset/*.md (local reference only) *.md !README.md +!CHANGELOG.md !.changeset/*.md From 3ad5c48c29af15082d6214c54a5915a70629e768 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:07:31 -0700 Subject: [PATCH 76/77] docs: add CHANGELOG.md with v0.1.0 release notes - Includes all consumed changesets from initial development - Documents CLI framework, config system, validation, and init command - Preserves release history for future reference - Generated by changeset version command --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..de6d50d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,67 @@ +# @labcatr/labcommitr + +## 0.1.0 + +### Minor Changes + +- feat: add body requirement prompt to init command + - New prompt in init flow to set commit body as required or optional + - "Yes" option marked as recommended for better commit practices + - Configuration properly respected in commit prompts + - When body is required, commit prompts show "required" and remove "Skip" option + - Defaults to optional for backward compatibility + +- feat: add editor support for commit body input + - Users can now open their preferred editor for writing commit bodies + - Supports both inline and editor input methods + - Automatically detects available editors (VS Code, Vim, Nano, etc.) + - Improved experience for multi-line commit bodies + - Configurable editor preference in configuration files + +- 8837714: feat: add working CLI framework with basic commands + - Tool now provides functional command-line interface with help system + - Both `labcommitr` and `lab` command aliases are now available + - Added `--version` flag to display current tool version + - Added `config show` command to display and debug configuration + - Interactive help system guides users through available commands + - Clear error messages when invalid commands are used + - Foundation ready for init and commit commands in upcoming releases + +- 20ab2ee: feat: add intelligent configuration file discovery + - Tool now automatically finds configuration files in project roots + - Prioritizes git repositories and supports monorepo structures + - Provides clear error messages when configuration files have issues + - Improved performance with smart caching for faster subsequent runs + - Works reliably across different project structures and environments + +- e041576: feat: add configuration file validation + - Configuration files are now validated for syntax and required fields + - Clear error messages help users fix configuration issues quickly + - Tool prevents common mistakes like missing commit types or invalid IDs + - Improved reliability when loading project-specific configurations + - Validates commit type IDs contain only lowercase letters as required + +- feat: enhance commit command user experience + - Terminal automatically clears at command start for maximum available space + - Improved staged file detection with support for renamed and copied files + - Color-coded Git status indicators (A, M, D, R, C) matching Git's default colors + - Connector lines added to files and preview displays for better visual flow + - More accurate file status reporting with copy detection using -C50 flag + +- 677a4ad: feat: add interactive init command with Clef mascot + - Introduced Clef, an animated cat mascot for enhanced user experience + - Implemented clean, minimal CLI prompts following modern design patterns + - Added four configuration presets: Conventional Commits, Gitmoji, Angular, and Minimal + - Created interactive setup flow with preset selection, emoji support, and scope configuration + - Integrated animated character that appears at key moments: intro, processing, and outro + - Automatic YAML configuration file generation with validation + - Non-intrusive design with clean labels and compact spacing + - Graceful degradation for terminals without animation support + +### Patch Changes + +- fix: prevent label text truncation in prompts + - Increased label width from 6 to 7 characters to accommodate longer labels + - Fixes issue where "subject" label was being truncated to "subjec" + - Applied to both commit and init command prompts for consistency + - All labels now properly display full text with centered alignment From 78edd07e36957aed2b021bc0b1d3ad24d5ae9898 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 2 Nov 2025 13:18:55 -0700 Subject: [PATCH 77/77] chore: stop tracking user-specific config file - Remove .labcommitr.config.yaml from git tracking - Add .labcommitr.config.yaml and .labcommitr.config.yml to .gitignore - Config file is user-specific and should not be committed - Similar to .env files - each developer generates their own config --- .gitignore | 5 +++++ .labcommitr.config.yaml | 44 ----------------------------------------- 2 files changed, 5 insertions(+), 44 deletions(-) delete mode 100644 .labcommitr.config.yaml diff --git a/.gitignore b/.gitignore index 6f6f124..9c44b80 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,8 @@ rust-src/ !README.md !CHANGELOG.md !.changeset/*.md + +### Labcommitr Configuration +# User-specific configuration file (generated by 'lab init') +.labcommitr.config.yaml +.labcommitr.config.yml diff --git a/.labcommitr.config.yaml b/.labcommitr.config.yaml deleted file mode 100644 index 519229f..0000000 --- a/.labcommitr.config.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# Labcommitr Configuration -# Generated by: lab init -# Edit this file to customize your commit workflow -# Documentation: https://github.com/labcatr/labcommitr#config - -version: "1.0" -config: - emoji_enabled: true - force_emoji_detection: null -format: - template: "{type}({scope}): {subject}" - subject_max_length: 50 -types: - - id: feat - description: A new feature for the user - emoji: ✨ - - id: fix - description: A bug fix for the user - emoji: 🐛 - - id: docs - description: Documentation changes - emoji: 📚 - - id: style - description: Code style changes (formatting, semicolons, etc.) - emoji: 💄 - - id: refactor - description: Code refactoring without changing functionality - emoji: ♻️ - - id: test - description: Adding or updating tests - emoji: 🧪 - - id: chore - description: Maintenance tasks, build changes, etc. - emoji: 🔧 -validation: - require_scope_for: [] - allowed_scopes: [] - subject_min_length: 3 - prohibited_words: [] -advanced: - aliases: {} - git: - auto_stage: false - sign_commits: true