feat(commit): Add commit configuration and automatic pre-commit tests

This commit is contained in:
2025-12-15 06:29:32 +00:00
parent 1b328c3045
commit b2d2684895
6 changed files with 280 additions and 5 deletions

View File

@@ -1,5 +1,16 @@
# Changelog # 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) ## 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 Add check-only formatting with interactive diff preview; make formatting default to dry-run and extend formatting API

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', 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.' 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.'
} }

View File

@@ -8,9 +8,20 @@ import * as ui from './mod.ui.js';
import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js'; import { ReleaseConfig } from '../mod_config/classes.releaseconfig.js';
export const run = async (argvArg: any) => { 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 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; let releaseConfig: ReleaseConfig | null = null;
if (wantsRelease) { if (wantsRelease) {
@@ -28,6 +39,7 @@ export const run = async (argvArg: any) => {
ui.printExecutionPlan({ ui.printExecutionPlan({
autoAccept: !!(argvArg.y || argvArg.yes), autoAccept: !!(argvArg.y || argvArg.yes),
push: !!(argvArg.p || argvArg.push), push: !!(argvArg.p || argvArg.push),
test: wantsTest,
build: wantsBuild, build: wantsBuild,
release: wantsRelease, release: wantsRelease,
format: !!argvArg.format, format: !!argvArg.format,
@@ -39,6 +51,21 @@ export const run = async (argvArg: any) => {
await formatMod.run(); 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...'); ui.printHeader('🔍 Analyzing repository changes...');
const aidoc = new plugins.tsdoc.AiDoc(); const aidoc = new plugins.tsdoc.AiDoc();
@@ -161,6 +188,7 @@ export const run = async (argvArg: any) => {
} }
// Determine total steps based on options // 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 willPush = answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true');
const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries(); const willRelease = answerBucket.getAnswerFor('createRelease') && releaseConfig?.hasRegistries();
let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version

View File

@@ -21,6 +21,7 @@ interface ICommitSummary {
interface IExecutionPlanOptions { interface IExecutionPlanOptions {
autoAccept: boolean; autoAccept: boolean;
push: boolean; push: boolean;
test: boolean;
build: boolean; build: boolean;
release: boolean; release: boolean;
format: boolean; format: boolean;
@@ -64,6 +65,7 @@ export function printExecutionPlan(options: IExecutionPlanOptions): void {
console.log(' Options:'); console.log(' Options:');
console.log(` Auto-accept ${options.autoAccept ? '✓ enabled (-y)' : '○ interactive mode'}`); console.log(` Auto-accept ${options.autoAccept ? '✓ enabled (-y)' : '○ interactive mode'}`);
console.log(` Push to remote ${options.push ? '✓ enabled (-p)' : '○ disabled'}`); 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(` Build & verify ${options.build ? '✓ enabled (-b)' : '○ disabled'}`);
console.log(` Release to npm ${options.release ? '✓ enabled (-r)' : '○ disabled'}`); console.log(` Release to npm ${options.release ? '✓ enabled (-r)' : '○ disabled'}`);
if (options.format) { if (options.format) {
@@ -77,6 +79,9 @@ export function printExecutionPlan(options: IExecutionPlanOptions): void {
if (options.format) { if (options.format) {
console.log(` ${stepNum++}. Format project files`); console.log(` ${stepNum++}. Format project files`);
} }
if (options.test) {
console.log(` ${stepNum++}. Run tests`);
}
console.log(` ${stepNum++}. Analyze repository changes`); console.log(` ${stepNum++}. Analyze repository changes`);
console.log(` ${stepNum++}. Bake commit info into code`); console.log(` ${stepNum++}. Bake commit info into code`);
console.log(` ${stepNum++}. Generate changelog.md`); console.log(` ${stepNum++}. Generate changelog.md`);

View File

@@ -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<CommitConfig> {
const instance = new CommitConfig(cwd);
await instance.load();
return instance;
}
/**
* Load configuration from npmextra.json
*/
public async load(): Promise<void> {
const npmextraInstance = new plugins.npmextra.Npmextra(this.cwd);
const gitzoneConfig = npmextraInstance.dataFor<any>('@git.zone/cli', {});
this.config = {
alwaysTest: gitzoneConfig?.commit?.alwaysTest ?? false,
alwaysBuild: gitzoneConfig?.commit?.alwaysBuild ?? false,
};
}
/**
* Save configuration to npmextra.json
*/
public async save(): Promise<void> {
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;
}
}

View File

@@ -2,9 +2,10 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import { ReleaseConfig } from './classes.releaseconfig.js'; import { ReleaseConfig } from './classes.releaseconfig.js';
import { CommitConfig } from './classes.commitconfig.js';
import { runFormatter, type ICheckResult } from '../mod_format/index.js'; import { runFormatter, type ICheckResult } from '../mod_format/index.js';
export { ReleaseConfig }; export { ReleaseConfig, CommitConfig };
/** /**
* Format npmextra.json with diff preview * Format npmextra.json with diff preview
@@ -55,6 +56,12 @@ export const run = async (argvArg: any) => {
case 'accessLevel': case 'accessLevel':
await handleAccessLevel(value); await handleAccessLevel(value);
break; break;
case 'commit':
await handleCommit(argvArg._?.[2], argvArg._?.[3]);
break;
case 'services':
await handleServices();
break;
case 'help': case 'help':
showHelp(); showHelp();
break; break;
@@ -70,7 +77,7 @@ export const run = async (argvArg: any) => {
async function handleInteractiveMenu(): Promise<void> { async function handleInteractiveMenu(): Promise<void> {
console.log(''); console.log('');
console.log('╭─────────────────────────────────────────────────────────────╮'); console.log('╭─────────────────────────────────────────────────────────────╮');
console.log('│ gitzone config - Release Configuration │'); console.log('│ gitzone config - Project Configuration │');
console.log('╰─────────────────────────────────────────────────────────────╯'); console.log('╰─────────────────────────────────────────────────────────────╯');
console.log(''); console.log('');
@@ -86,6 +93,8 @@ async function handleInteractiveMenu(): Promise<void> {
{ name: 'Remove a registry', value: 'remove' }, { name: 'Remove a registry', value: 'remove' },
{ name: 'Clear all registries', value: 'clear' }, { name: 'Clear all registries', value: 'clear' },
{ name: 'Set access level (public/private)', value: 'access' }, { name: 'Set access level (public/private)', value: 'access' },
{ name: 'Configure commit options', value: 'commit' },
{ name: 'Configure services', value: 'services' },
{ name: 'Show help', value: 'help' }, { name: 'Show help', value: 'help' },
], ],
}); });
@@ -108,6 +117,12 @@ async function handleInteractiveMenu(): Promise<void> {
case 'access': case 'access':
await handleAccessLevel(); await handleAccessLevel();
break; break;
case 'commit':
await handleCommit();
break;
case 'services':
await handleServices();
break;
case 'help': case 'help':
showHelp(); showHelp();
break; break;
@@ -278,6 +293,113 @@ async function handleAccessLevel(level?: string): Promise<void> {
await formatNpmextraWithDiff(); await formatNpmextraWithDiff();
} }
/**
* Handle commit configuration
*/
async function handleCommit(setting?: string, value?: string): Promise<void> {
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<void> {
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<void> {
// 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<void> {
// 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 * Show help for config command
*/ */
@@ -291,6 +413,8 @@ function showHelp(): void {
console.log(' remove [url] Remove a registry URL'); console.log(' remove [url] Remove a registry URL');
console.log(' clear Clear all registries'); console.log(' clear Clear all registries');
console.log(' access [public|private] Set npm access level for publishing'); 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('');
console.log('Examples:'); console.log('Examples:');
console.log(' gitzone config show'); console.log(' gitzone config show');
@@ -300,5 +424,8 @@ function showHelp(): void {
console.log(' gitzone config clear'); console.log(' gitzone config clear');
console.log(' gitzone config access public'); console.log(' gitzone config access public');
console.log(' gitzone config access private'); 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(''); console.log('');
} }