diff --git a/changelog.md b/changelog.md index bcbe2c2..8ea586d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-12-15 - 2.8.0 - feat(commit) +Add commit configuration and automatic pre-commit tests + +- Add CommitConfig class to manage @git.zone/cli.commit settings in npmextra.json (alwaysTest, alwaysBuild). +- Export CommitConfig from mod_config for use by the CLI. +- Add 'gitzone config commit' subcommand with interactive and direct-setting modes (alwaysTest, alwaysBuild). +- Merge CLI flags and npmextra config: -t/--test and -b/--build now respect commit.alwaysTest and commit.alwaysBuild. +- Run 'pnpm test' early in the commit flow when tests are enabled; abort the commit on failing tests and log results. +- Update commit UI/plan to show the test option and include the test step when enabled. +- Add 'gitzone config services' entry to configure services via ServiceManager. + ## 2025-12-14 - 2.7.0 - feat(mod_format) Add check-only formatting with interactive diff preview; make formatting default to dry-run and extend formatting API diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 9a8ebe7..7d50897 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/cli', - version: '2.7.0', + version: '2.8.0', description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' } diff --git a/ts/mod_commit/index.ts b/ts/mod_commit/index.ts index fdc5099..0653feb 100644 --- a/ts/mod_commit/index.ts +++ b/ts/mod_commit/index.ts @@ -8,9 +8,20 @@ import * as ui from './mod.ui.js'; import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js'; export const run = async (argvArg: any) => { - // Check if release flag is set and validate registries early + // Read commit config from npmextra.json + const npmextraConfig = new plugins.npmextra.Npmextra(); + const gitzoneConfig = npmextraConfig.dataFor<{ + commit?: { + alwaysTest?: boolean; + alwaysBuild?: boolean; + }; + }>('@git.zone/cli', {}); + const commitConfig = gitzoneConfig.commit || {}; + + // Check flags and merge with config options const wantsRelease = !!(argvArg.r || argvArg.release); - const wantsBuild = !!(argvArg.b || argvArg.build); + const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest); + const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild); let releaseConfig: ReleaseConfig | null = null; if (wantsRelease) { @@ -28,6 +39,7 @@ export const run = async (argvArg: any) => { ui.printExecutionPlan({ autoAccept: !!(argvArg.y || argvArg.yes), push: !!(argvArg.p || argvArg.push), + test: wantsTest, build: wantsBuild, release: wantsRelease, format: !!argvArg.format, @@ -39,6 +51,21 @@ export const run = async (argvArg: any) => { await formatMod.run(); } + // Run tests early to fail fast before analysis + if (wantsTest) { + ui.printHeader('๐Ÿงช Running tests...'); + const smartshellForTest = new plugins.smartshell.Smartshell({ + executor: 'bash', + sourceFilePaths: [], + }); + const testResult = await smartshellForTest.exec('pnpm test'); + if (testResult.exitCode !== 0) { + logger.log('error', 'Tests failed. Aborting commit.'); + process.exit(1); + } + logger.log('success', 'All tests passed.'); + } + ui.printHeader('๐Ÿ” Analyzing repository changes...'); const aidoc = new plugins.tsdoc.AiDoc(); @@ -161,6 +188,7 @@ export const run = async (argvArg: any) => { } // Determine total steps based on options + // Note: test runs early (like format) so not counted in numbered steps const willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true'); const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries(); let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version diff --git a/ts/mod_commit/mod.ui.ts b/ts/mod_commit/mod.ui.ts index 5584bc6..d2093f5 100644 --- a/ts/mod_commit/mod.ui.ts +++ b/ts/mod_commit/mod.ui.ts @@ -21,6 +21,7 @@ interface ICommitSummary { interface IExecutionPlanOptions { autoAccept: boolean; push: boolean; + test: boolean; build: boolean; release: boolean; format: boolean; @@ -64,6 +65,7 @@ export function printExecutionPlan(options: IExecutionPlanOptions): void { console.log(' Options:'); console.log(` Auto-accept ${options.autoAccept ? 'โœ“ enabled (-y)' : 'โ—‹ interactive mode'}`); console.log(` Push to remote ${options.push ? 'โœ“ enabled (-p)' : 'โ—‹ disabled'}`); + console.log(` Test first ${options.test ? 'โœ“ enabled (-t)' : 'โ—‹ disabled'}`); console.log(` Build & verify ${options.build ? 'โœ“ enabled (-b)' : 'โ—‹ disabled'}`); console.log(` Release to npm ${options.release ? 'โœ“ enabled (-r)' : 'โ—‹ disabled'}`); if (options.format) { @@ -77,6 +79,9 @@ export function printExecutionPlan(options: IExecutionPlanOptions): void { if (options.format) { console.log(` ${stepNum++}. Format project files`); } + if (options.test) { + console.log(` ${stepNum++}. Run tests`); + } console.log(` ${stepNum++}. Analyze repository changes`); console.log(` ${stepNum++}. Bake commit info into code`); console.log(` ${stepNum++}. Generate changelog.md`); diff --git a/ts/mod_config/classes.commitconfig.ts b/ts/mod_config/classes.commitconfig.ts new file mode 100644 index 0000000..51dde78 --- /dev/null +++ b/ts/mod_config/classes.commitconfig.ts @@ -0,0 +1,104 @@ +import * as plugins from './mod.plugins.js'; + +export interface ICommitConfig { + alwaysTest: boolean; + alwaysBuild: boolean; +} + +/** + * Manages commit configuration stored in npmextra.json + * under @git.zone/cli.commit namespace + */ +export class CommitConfig { + private cwd: string; + private config: ICommitConfig; + + constructor(cwd: string = process.cwd()) { + this.cwd = cwd; + this.config = { alwaysTest: false, alwaysBuild: false }; + } + + /** + * Create a CommitConfig instance from current working directory + */ + public static async fromCwd(cwd: string = process.cwd()): Promise { + const instance = new CommitConfig(cwd); + await instance.load(); + return instance; + } + + /** + * Load configuration from npmextra.json + */ + public async load(): Promise { + const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd); + const gitzoneConfig = npmextraInstance.dataFor('@git.zone/cli', {}); + + this.config = { + alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false, + alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false, + }; + } + + /** + * Save configuration to npmextra.json + */ + public async save(): Promise { + const npmextraPath = plugins.path.join(this.cwd, 'npmextra.json'); + let npmextraData: any = {}; + + // Read existing npmextra.json + if (await plugins.smartfs.file(npmextraPath).exists()) { + const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read(); + npmextraData = JSON.parse(content as string); + } + + // Ensure @git.zone/cli namespace exists + if (!npmextraData['@git.zone/cli']) { + npmextraData['@git.zone/cli'] = {}; + } + + // Ensure commit object exists + if (!npmextraData['@git.zone/cli'].commit) { + npmextraData['@git.zone/cli'].commit = {}; + } + + // Update commit settings + npmextraData['@git.zone/cli'].commit.alwaysTest = this.config.alwaysTest; + npmextraData['@git.zone/cli'].commit.alwaysBuild = this.config.alwaysBuild; + + // Write back to file + await plugins.smartfs + .file(npmextraPath) + .encoding('utf8') + .write(JSON.stringify(npmextraData, null, 2)); + } + + /** + * Get alwaysTest setting + */ + public getAlwaysTest(): boolean { + return this.config.alwaysTest; + } + + /** + * Set alwaysTest setting + */ + public setAlwaysTest(value: boolean): void { + this.config.alwaysTest = value; + } + + /** + * Get alwaysBuild setting + */ + public getAlwaysBuild(): boolean { + return this.config.alwaysBuild; + } + + /** + * Set alwaysBuild setting + */ + public setAlwaysBuild(value: boolean): void { + this.config.alwaysBuild = value; + } +} diff --git a/ts/mod_config/index.ts b/ts/mod_config/index.ts index 1b66df9..ab292d8 100644 --- a/ts/mod_config/index.ts +++ b/ts/mod_config/index.ts @@ -2,9 +2,10 @@ import * as plugins from './mod.plugins.js'; import { ReleaseConfig } from './classes.releaseconfig.js'; +import { CommitConfig } from './classes.commitconfig.js'; import { runFormatter, type ICheckResult } from '../mod_format/index.js'; -export { ReleaseConfig }; +export { ReleaseConfig, CommitConfig }; /** * Format npmextra.json with diff preview @@ -55,6 +56,12 @@ export const run = async (argvArg: any) => { case 'accessLevel': await handleAccessLevel(value); break; + case 'commit': + await handleCommit(argvArg._?.[2], argvArg._?.[3]); + break; + case 'services': + await handleServices(); + break; case 'help': showHelp(); break; @@ -70,7 +77,7 @@ export const run = async (argvArg: any) => { async function handleInteractiveMenu(): Promise { console.log(''); console.log('โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ'); - console.log('โ”‚ gitzone config - Release Configuration โ”‚'); + console.log('โ”‚ gitzone config - Project Configuration โ”‚'); console.log('โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ'); console.log(''); @@ -86,6 +93,8 @@ async function handleInteractiveMenu(): Promise { { name: 'Remove a registry', value: 'remove' }, { name: 'Clear all registries', value: 'clear' }, { name: 'Set access level (public/private)', value: 'access' }, + { name: 'Configure commit options', value: 'commit' }, + { name: 'Configure services', value: 'services' }, { name: 'Show help', value: 'help' }, ], }); @@ -108,6 +117,12 @@ async function handleInteractiveMenu(): Promise { case 'access': await handleAccessLevel(); break; + case 'commit': + await handleCommit(); + break; + case 'services': + await handleServices(); + break; case 'help': showHelp(); break; @@ -278,6 +293,113 @@ async function handleAccessLevel(level?: string): Promise { await formatNpmextraWithDiff(); } +/** + * Handle commit configuration + */ +async function handleCommit(setting?: string, value?: string): Promise { + const config = await CommitConfig.fromCwd(); + + // No setting = interactive mode + if (!setting) { + await handleCommitInteractive(config); + return; + } + + // Direct setting + switch (setting) { + case 'alwaysTest': + await handleCommitSetting(config, 'alwaysTest', value); + break; + case 'alwaysBuild': + await handleCommitSetting(config, 'alwaysBuild', value); + break; + default: + plugins.logger.log('error', `Unknown commit setting: ${setting}`); + showCommitHelp(); + } +} + +/** + * Interactive commit configuration + */ +async function handleCommitInteractive(config: CommitConfig): Promise { + console.log(''); + console.log('โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ'); + console.log('โ”‚ Commit Configuration โ”‚'); + console.log('โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ'); + console.log(''); + + const interactInstance = new plugins.smartinteract.SmartInteract(); + const response = await interactInstance.askQuestion({ + type: 'checkbox', + name: 'commitOptions', + message: 'Select commit options to enable:', + choices: [ + { name: 'Always run tests before commit (-t)', value: 'alwaysTest' }, + { name: 'Always build after commit (-b)', value: 'alwaysBuild' }, + ], + default: [ + ...(config.getAlwaysTest() ? ['alwaysTest'] : []), + ...(config.getAlwaysBuild() ? ['alwaysBuild'] : []), + ], + }); + + const selected = (response as any).value || []; + config.setAlwaysTest(selected.includes('alwaysTest')); + config.setAlwaysBuild(selected.includes('alwaysBuild')); + await config.save(); + + plugins.logger.log('success', 'Commit configuration updated'); + await formatNpmextraWithDiff(); +} + +/** + * Set a specific commit setting + */ +async function handleCommitSetting(config: CommitConfig, setting: string, value?: string): Promise { + // Parse boolean value + const boolValue = value === 'true' || value === '1' || value === 'on'; + + if (setting === 'alwaysTest') { + config.setAlwaysTest(boolValue); + } else if (setting === 'alwaysBuild') { + config.setAlwaysBuild(boolValue); + } + + await config.save(); + plugins.logger.log('success', `Set ${setting} to ${boolValue}`); + await formatNpmextraWithDiff(); +} + +/** + * Show help for commit subcommand + */ +function showCommitHelp(): void { + console.log(''); + console.log('Usage: gitzone config commit [setting] [value]'); + console.log(''); + console.log('Settings:'); + console.log(' alwaysTest [true|false] Always run tests before commit'); + console.log(' alwaysBuild [true|false] Always build after commit'); + console.log(''); + console.log('Examples:'); + console.log(' gitzone config commit # Interactive mode'); + console.log(' gitzone config commit alwaysTest true'); + console.log(' gitzone config commit alwaysBuild false'); + console.log(''); +} + +/** + * Handle services configuration + */ +async function handleServices(): Promise { + // Import and use ServiceManager's configureServices + const { ServiceManager } = await import('../mod_services/classes.servicemanager.js'); + const serviceManager = new ServiceManager(); + await serviceManager.init(); + await serviceManager.configureServices(); +} + /** * Show help for config command */ @@ -291,6 +413,8 @@ function showHelp(): void { console.log(' remove [url] Remove a registry URL'); console.log(' clear Clear all registries'); console.log(' access [public|private] Set npm access level for publishing'); + console.log(' commit [setting] [value] Configure commit options'); + console.log(' services Configure which services are enabled'); console.log(''); console.log('Examples:'); console.log(' gitzone config show'); @@ -300,5 +424,8 @@ function showHelp(): void { console.log(' gitzone config clear'); console.log(' gitzone config access public'); console.log(' gitzone config access private'); + console.log(' gitzone config commit # Interactive'); + console.log(' gitzone config commit alwaysTest true'); + console.log(' gitzone config services # Interactive'); console.log(''); }