From fd90cfe8951053cbabbd2b53e45a1dfb712d23d9 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Fri, 8 Aug 2025 05:48:41 +0000 Subject: [PATCH] fix(core): Improve formatting, logging, and rollback integrity in core modules --- changelog.md | 8 ++ package.json | 2 +- ts/00_commitinfo_data.ts | 2 +- ts/classes.gitzoneconfig.ts | 4 +- ts/gitzone.cli.ts | 10 +-- ts/gitzone.logging.ts | 3 +- ts/mod_commit/index.ts | 47 +++++++++--- ts/mod_deprecate/index.ts | 5 +- ts/mod_format/classes.rollbackmanager.ts | 94 ++++++++++++++++++++++-- 9 files changed, 147 insertions(+), 28 deletions(-) diff --git a/changelog.md b/changelog.md index 3c30beb..b60ec02 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-08-08 - 1.16.7 - fix(core) +Improve formatting, logging, and rollback integrity in core modules + +- Add .claude/settings.local.json with defined permissions for allowed commands +- Standardize formatting in package.json, commit info, and configuration files +- Refactor rollback manager to use atomic manifest writes and validate manifest structure +- Enhance logging messages and overall code clarity in CLI and commit modules + ## 2025-08-08 - 1.16.6 - fix(changecache) Improve cache manifest validation and atomic file writes; add local settings and overrides diff --git a/package.json b/package.json index 7e765e0..938a6e1 100644 --- a/package.json +++ b/package.json @@ -116,4 +116,4 @@ "overrides": {} }, "packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b871d9244954f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6" -} +} \ No newline at end of file diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 8cc3486..6e800d8 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/cli', - version: '1.16.6', + version: '1.16.7', 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/classes.gitzoneconfig.ts b/ts/classes.gitzoneconfig.ts index 2ee3257..8e9c077 100644 --- a/ts/classes.gitzoneconfig.ts +++ b/ts/classes.gitzoneconfig.ts @@ -40,7 +40,9 @@ export class GitzoneConfig { public async readConfigFromCwd() { const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd); this.data = npmextraInstance.dataFor('gitzone', {}); - this.data.npmciOptions = npmextraInstance.dataFor('npmci', { + this.data.npmciOptions = npmextraInstance.dataFor< + IGitzoneConfigData['npmciOptions'] + >('npmci', { npmAccessLevel: 'public', }); } diff --git a/ts/gitzone.cli.ts b/ts/gitzone.cli.ts index bfbd7d7..e94083a 100644 --- a/ts/gitzone.cli.ts +++ b/ts/gitzone.cli.ts @@ -62,23 +62,23 @@ export let run = async () => { gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => { const config = GitzoneConfig.fromCwd(); const modFormat = await import('./mod_format/index.js'); - + // Handle rollback commands if (argvArg.rollback) { await modFormat.handleRollback(argvArg.rollback); return; } - + if (argvArg['list-backups']) { await modFormat.handleListBackups(); return; } - + if (argvArg['clean-backups']) { await modFormat.handleCleanBackups(); return; } - + // Handle format with options await modFormat.run({ dryRun: argvArg['dry-run'], @@ -89,7 +89,7 @@ export let run = async () => { detailed: argvArg.detailed, interactive: argvArg.interactive !== false, parallel: argvArg.parallel !== false, - verbose: argvArg.verbose + verbose: argvArg.verbose, }); }); diff --git a/ts/gitzone.logging.ts b/ts/gitzone.logging.ts index b419f26..d049749 100644 --- a/ts/gitzone.logging.ts +++ b/ts/gitzone.logging.ts @@ -5,7 +5,8 @@ import * as plugins from './plugins.js'; export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo); // Add console destination -const consoleDestination = new plugins.smartlogDestinationLocal.DestinationLocal(); +const consoleDestination = + new plugins.smartlogDestinationLocal.DestinationLocal(); logger.addLogDestination(consoleDestination); // Verbose logging helper diff --git a/ts/mod_commit/index.ts b/ts/mod_commit/index.ts index 1380775..ca0e04f 100644 --- a/ts/mod_commit/index.ts +++ b/ts/mod_commit/index.ts @@ -10,20 +10,22 @@ export const run = async (argvArg: any) => { await formatMod.run(); } - logger.log('info', `gathering facts...`); const aidoc = new plugins.tsdoc.AiDoc(); await aidoc.start(); const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd); - logger.log('info', `--------- + logger.log( + 'info', + `--------- Next recommended commit would be: =========== -> ${nextCommitObject.recommendedNextVersion}: -> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage} =========== - `); + `, + ); const commitInteract = new plugins.smartinteract.SmartInteract(); commitInteract.addQuestions([ { @@ -72,32 +74,55 @@ export const run = async (argvArg: any) => { }); logger.log('info', `Baking commitinfo into code ...`); - const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, commitVersionType); + const commitInfo = new plugins.commitinfo.CommitInfo( + paths.cwd, + commitVersionType, + ); await commitInfo.writeIntoPotentialDirs(); logger.log('info', `Writing changelog.md ...`); let changelog = nextCommitObject.changelog; - changelog = changelog.replaceAll('{{nextVersion}}', (await commitInfo.getNextPlannedVersion()).versionString); - changelog = changelog.replaceAll('{{nextVersionScope}}', `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`); - changelog = changelog.replaceAll('{{nextVersionMessage}}', nextCommitObject.recommendedNextVersionMessage); + changelog = changelog.replaceAll( + '{{nextVersion}}', + (await commitInfo.getNextPlannedVersion()).versionString, + ); + changelog = changelog.replaceAll( + '{{nextVersionScope}}', + `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`, + ); + changelog = changelog.replaceAll( + '{{nextVersionMessage}}', + nextCommitObject.recommendedNextVersionMessage, + ); if (nextCommitObject.recommendedNextVersionDetails?.length > 0) { - changelog = changelog.replaceAll('{{nextVersionDetails}}', '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- ')); + changelog = changelog.replaceAll( + '{{nextVersionDetails}}', + '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '), + ); } else { changelog = changelog.replaceAll('\n{{nextVersionDetails}}', ''); } - await plugins.smartfile.memory.toFs(changelog, plugins.path.join(paths.cwd, `changelog.md`)); + await plugins.smartfile.memory.toFs( + changelog, + plugins.path.join(paths.cwd, `changelog.md`), + ); logger.log('info', `Staging files for commit:`); await smartshellInstance.exec(`git add -A`); await smartshellInstance.exec(`git commit -m "${commitString}"`); await smartshellInstance.exec(`npm version ${commitVersionType}`); - if (answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true')) { + if ( + answerBucket.getAnswerFor('pushToOrigin') && + !(process.env.CI === 'true') + ) { await smartshellInstance.exec(`git push origin master --follow-tags`); } }; -const createCommitStringFromAnswerBucket = (answerBucket: plugins.smartinteract.AnswerBucket) => { +const createCommitStringFromAnswerBucket = ( + answerBucket: plugins.smartinteract.AnswerBucket, +) => { const commitType = answerBucket.getAnswerFor('commitType'); const commitScope = answerBucket.getAnswerFor('commitScope'); const commitDescription = answerBucket.getAnswerFor('commitDescription'); diff --git a/ts/mod_deprecate/index.ts b/ts/mod_deprecate/index.ts index 089cd26..ab94c33 100644 --- a/ts/mod_deprecate/index.ts +++ b/ts/mod_deprecate/index.ts @@ -36,7 +36,10 @@ export const run = async () => { const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(','); const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`); const newPackageName = answerBucket.getAnswerFor(`newPackageName`); - logger.log('info', `Deprecating package ${oldPackageName} in favour of ${newPackageName}`); + logger.log( + 'info', + `Deprecating package ${oldPackageName} in favour of ${newPackageName}`, + ); const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash', }); diff --git a/ts/mod_format/classes.rollbackmanager.ts b/ts/mod_format/classes.rollbackmanager.ts index 22bbaf5..2777f6e 100644 --- a/ts/mod_format/classes.rollbackmanager.ts +++ b/ts/mod_format/classes.rollbackmanager.ts @@ -43,7 +43,7 @@ export class RollbackManager { } // Read file content and metadata - const content = await plugins.smartfile.fs.toStringSync(absolutePath); + const content = plugins.smartfile.fs.toStringSync(absolutePath); const stats = await plugins.smartfile.fs.stat(absolutePath); const checksum = this.calculateChecksum(content); @@ -82,7 +82,7 @@ export class RollbackManager { // Verify backup integrity const backupPath = this.getBackupPath(operationId, file.path); - const backupContent = await plugins.smartfile.fs.toStringSync(backupPath); + const backupContent = plugins.smartfile.fs.toStringSync(backupPath); const backupChecksum = this.calculateChecksum(backupContent); if (backupChecksum !== file.checksum) { @@ -146,7 +146,7 @@ export class RollbackManager { return false; } - const content = await plugins.smartfile.fs.toStringSync(backupPath); + const content = plugins.smartfile.fs.toStringSync(backupPath); const checksum = this.calculateChecksum(content); if (checksum !== file.checksum) { @@ -185,17 +185,63 @@ export class RollbackManager { } private async getManifest(): Promise<{ operations: IFormatOperation[] }> { + const defaultManifest = { operations: [] }; + const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); if (!exists) { - return { operations: [] }; + return defaultManifest; } - const content = await plugins.smartfile.fs.toStringSync(this.manifestPath); - return JSON.parse(content); + try { + const content = plugins.smartfile.fs.toStringSync(this.manifestPath); + const manifest = JSON.parse(content); + + // Validate the manifest structure + if (this.isValidManifest(manifest)) { + return manifest; + } else { + console.warn('Invalid rollback manifest structure, returning default manifest'); + return defaultManifest; + } + } catch (error) { + console.warn(`Failed to read rollback manifest: ${error.message}, returning default manifest`); + // Try to delete the corrupted file + try { + await plugins.smartfile.fs.remove(this.manifestPath); + } catch (removeError) { + // Ignore removal errors + } + return defaultManifest; + } } private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise { - await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); + // Validate before saving + if (!this.isValidManifest(manifest)) { + throw new Error('Invalid rollback manifest structure, cannot save'); + } + + // Use atomic write: write to temp file, then move it + const tempPath = `${this.manifestPath}.tmp`; + + try { + // Write to temporary file + const jsonContent = JSON.stringify(manifest, null, 2); + await plugins.smartfile.memory.toFs(jsonContent, tempPath); + + // Move temp file to actual manifest (atomic-like operation) + // Since smartfile doesn't have rename, we copy and delete + await plugins.smartfile.fs.copy(tempPath, this.manifestPath); + await plugins.smartfile.fs.remove(tempPath); + } catch (error) { + // Clean up temp file if it exists + try { + await plugins.smartfile.fs.remove(tempPath); + } catch (removeError) { + // Ignore removal errors + } + throw error; + } } private async getOperation(operationId: string): Promise { @@ -215,4 +261,38 @@ export class RollbackManager { await this.saveManifest(manifest); } + + private isValidManifest(manifest: any): manifest is { operations: IFormatOperation[] } { + // Check if manifest has the required structure + if (!manifest || typeof manifest !== 'object') { + return false; + } + + // Check required fields + if (!Array.isArray(manifest.operations)) { + return false; + } + + // Check each operation entry + for (const operation of manifest.operations) { + if (!operation || typeof operation !== 'object' || + typeof operation.id !== 'string' || + typeof operation.timestamp !== 'number' || + typeof operation.status !== 'string' || + !Array.isArray(operation.files)) { + return false; + } + + // Check each file in the operation + for (const file of operation.files) { + if (!file || typeof file !== 'object' || + typeof file.path !== 'string' || + typeof file.checksum !== 'string') { + return false; + } + } + } + + return true; + } } \ No newline at end of file