Compare commits

..

6 Commits

Author SHA1 Message Date
74a8229e43 1.16.8
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 06:25:40 +00:00
859cbc733d fix(format): Improve concurrency control in cache and rollback management with mutex locking and refine formatting details 2025-08-08 06:25:40 +00:00
d32d47b706 1.16.7
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:48:41 +00:00
fd90cfe895 fix(core): Improve formatting, logging, and rollback integrity in core modules 2025-08-08 05:48:41 +00:00
c48f48fc8b 1.16.6
Some checks failed
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-08 05:43:34 +00:00
e21e7f0850 fix(changecache): Improve cache manifest validation and atomic file writes; add local settings and overrides 2025-08-08 05:43:34 +00:00
44 changed files with 943 additions and 683 deletions

1
assets/overrides.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -1,5 +1,29 @@
# Changelog # Changelog
## 2025-08-08 - 1.16.8 - fix(format)
Improve concurrency control in cache and rollback management with mutex locking and refine formatting details
- Added 'withMutex' functions in ChangeCache and RollbackManager to synchronize file I/O operations
- Introduced static mutex maps to prevent race conditions during manifest updates
- Fixed minor formatting issues in commit info and package.json
## 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
- Add manifest structure validation and default fallback in getManifest
- Implement atomic write in saveManifest using a temporary file and rename strategy
- Enhance error handling and cleanup for corrupted manifest files
- Introduce new .claude/settings.local.json for project-specific permission configuration
- Add an empty assets/overrides.json file for future overrides
## 2025-08-08 - 1.16.5 - fix(prettier) ## 2025-08-08 - 1.16.5 - fix(prettier)
Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation Improve file selection in Prettier formatter, remove legacy package overrides, and update CI template indentation

View File

@@ -1,7 +1,7 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "1.16.5", "version": "1.16.8",
"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.",
"main": "dist_ts/index.ts", "main": "dist_ts/index.ts",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",

View File

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

@@ -40,7 +40,9 @@ export class GitzoneConfig {
public async readConfigFromCwd() { public async readConfigFromCwd() {
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd); const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('gitzone', {}); this.data = npmextraInstance.dataFor<IGitzoneConfigData>('gitzone', {});
this.data.npmciOptions = npmextraInstance.dataFor<IGitzoneConfigData['npmciOptions']>('npmci', { this.data.npmciOptions = npmextraInstance.dataFor<
IGitzoneConfigData['npmciOptions']
>('npmci', {
npmAccessLevel: 'public', npmAccessLevel: 'public',
}); });
} }

View File

@@ -89,7 +89,7 @@ export let run = async () => {
detailed: argvArg.detailed, detailed: argvArg.detailed,
interactive: argvArg.interactive !== false, interactive: argvArg.interactive !== false,
parallel: argvArg.parallel !== false, parallel: argvArg.parallel !== false,
verbose: argvArg.verbose verbose: argvArg.verbose,
}); });
}); });

View File

@@ -5,7 +5,8 @@ import * as plugins from './plugins.js';
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo); export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
// Add console destination // Add console destination
const consoleDestination = new plugins.smartlogDestinationLocal.DestinationLocal(); const consoleDestination =
new plugins.smartlogDestinationLocal.DestinationLocal();
logger.addLogDestination(consoleDestination); logger.addLogDestination(consoleDestination);
// Verbose logging helper // Verbose logging helper

View File

@@ -10,20 +10,22 @@ export const run = async (argvArg: any) => {
await formatMod.run(); await formatMod.run();
} }
logger.log('info', `gathering facts...`); logger.log('info', `gathering facts...`);
const aidoc = new plugins.tsdoc.AiDoc(); const aidoc = new plugins.tsdoc.AiDoc();
await aidoc.start(); await aidoc.start();
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd); const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
logger.log('info', `--------- logger.log(
'info',
`---------
Next recommended commit would be: Next recommended commit would be:
=========== ===========
-> ${nextCommitObject.recommendedNextVersion}: -> ${nextCommitObject.recommendedNextVersion}:
-> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage} -> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage}
=========== ===========
`); `,
);
const commitInteract = new plugins.smartinteract.SmartInteract(); const commitInteract = new plugins.smartinteract.SmartInteract();
commitInteract.addQuestions([ commitInteract.addQuestions([
{ {
@@ -72,32 +74,55 @@ export const run = async (argvArg: any) => {
}); });
logger.log('info', `Baking commitinfo into code ...`); 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(); await commitInfo.writeIntoPotentialDirs();
logger.log('info', `Writing changelog.md ...`); logger.log('info', `Writing changelog.md ...`);
let changelog = nextCommitObject.changelog; let changelog = nextCommitObject.changelog;
changelog = changelog.replaceAll('{{nextVersion}}', (await commitInfo.getNextPlannedVersion()).versionString); changelog = changelog.replaceAll(
changelog = changelog.replaceAll('{{nextVersionScope}}', `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`); '{{nextVersion}}',
changelog = changelog.replaceAll('{{nextVersionMessage}}', nextCommitObject.recommendedNextVersionMessage); (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) { if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
changelog = changelog.replaceAll('{{nextVersionDetails}}', '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- ')); changelog = changelog.replaceAll(
'{{nextVersionDetails}}',
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '),
);
} else { } else {
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', ''); 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:`); logger.log('info', `Staging files for commit:`);
await smartshellInstance.exec(`git add -A`); await smartshellInstance.exec(`git add -A`);
await smartshellInstance.exec(`git commit -m "${commitString}"`); await smartshellInstance.exec(`git commit -m "${commitString}"`);
await smartshellInstance.exec(`npm version ${commitVersionType}`); 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`); 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 commitType = answerBucket.getAnswerFor('commitType');
const commitScope = answerBucket.getAnswerFor('commitScope'); const commitScope = answerBucket.getAnswerFor('commitScope');
const commitDescription = answerBucket.getAnswerFor('commitDescription'); const commitDescription = answerBucket.getAnswerFor('commitDescription');

View File

@@ -36,7 +36,10 @@ export const run = async () => {
const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(','); const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(',');
const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`); const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`);
const newPackageName = answerBucket.getAnswerFor(`newPackageName`); 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({ const smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
}); });

View File

@@ -2,18 +2,15 @@ import * as plugins from './mod.plugins.js';
import { FormatContext } from './classes.formatcontext.js'; import { FormatContext } from './classes.formatcontext.js';
import type { IPlannedChange } from './interfaces.format.js'; import type { IPlannedChange } from './interfaces.format.js';
import { Project } from '../classes.project.js'; import { Project } from '../classes.project.js';
import { ChangeCache } from './classes.changecache.js';
export abstract class BaseFormatter { export abstract class BaseFormatter {
protected context: FormatContext; protected context: FormatContext;
protected project: Project; protected project: Project;
protected cache: ChangeCache;
protected stats: any; // Will be FormatStats from context protected stats: any; // Will be FormatStats from context
constructor(context: FormatContext, project: Project) { constructor(context: FormatContext, project: Project) {
this.context = context; this.context = context;
this.project = project; this.project = project;
this.cache = context.getChangeCache();
this.stats = context.getFormatStats(); this.stats = context.getFormatStats();
} }
@@ -40,7 +37,7 @@ export abstract class BaseFormatter {
await this.postExecute(); await this.postExecute();
} catch (error) { } catch (error) {
await this.context.rollbackOperation(); // Don't rollback here - let the FormatPlanner handle it
throw error; throw error;
} finally { } finally {
this.stats.endModule(this.name, startTime); this.stats.endModule(this.name, startTime);
@@ -56,38 +53,18 @@ export abstract class BaseFormatter {
} }
protected async modifyFile(filepath: string, content: string): Promise<void> { protected async modifyFile(filepath: string, content: string): Promise<void> {
await this.context.trackFileChange(filepath);
await plugins.smartfile.memory.toFs(content, filepath); await plugins.smartfile.memory.toFs(content, filepath);
await this.cache.updateFileCache(filepath);
} }
protected async createFile(filepath: string, content: string): Promise<void> { protected async createFile(filepath: string, content: string): Promise<void> {
await plugins.smartfile.memory.toFs(content, filepath); await plugins.smartfile.memory.toFs(content, filepath);
await this.cache.updateFileCache(filepath);
} }
protected async deleteFile(filepath: string): Promise<void> { protected async deleteFile(filepath: string): Promise<void> {
await this.context.trackFileChange(filepath);
await plugins.smartfile.fs.remove(filepath); await plugins.smartfile.fs.remove(filepath);
} }
protected async shouldProcessFile(filepath: string): Promise<boolean> { protected async shouldProcessFile(filepath: string): Promise<boolean> {
const config = new plugins.npmextra.Npmextra(); return true;
const useCache = config.dataFor('gitzone.format.cache.enabled', true);
if (!useCache) {
return true; // Process all files if cache is disabled
}
const hasChanged = await this.cache.hasFileChanged(filepath);
// Record cache statistics
if (hasChanged) {
this.stats.recordCacheMiss();
} else {
this.stats.recordCacheHit();
}
return hasChanged;
} }
} }

View File

@@ -29,21 +29,54 @@ export class ChangeCache {
} }
async getManifest(): Promise<ICacheManifest> { async getManifest(): Promise<ICacheManifest> {
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); const defaultManifest: ICacheManifest = {
if (!exists) {
return {
version: this.cacheVersion, version: this.cacheVersion,
lastFormat: 0, lastFormat: 0,
files: [] files: [],
}; };
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) {
return defaultManifest;
} }
try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath); const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
return JSON.parse(content); const manifest = JSON.parse(content);
// Validate the manifest structure
if (this.isValidManifest(manifest)) {
return manifest;
} else {
console.warn('Invalid manifest structure, returning default manifest');
return defaultManifest;
}
} catch (error) {
console.warn(
`Failed to read cache 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;
}
} }
async saveManifest(manifest: ICacheManifest): Promise<void> { async saveManifest(manifest: ICacheManifest): Promise<void> {
await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); // Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid manifest structure, cannot save');
}
// Ensure directory exists
await plugins.smartfile.fs.ensureDir(this.cacheDir);
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
} }
async hasFileChanged(filePath: string): Promise<boolean> { async hasFileChanged(filePath: string): Promise<boolean> {
@@ -70,16 +103,18 @@ export class ChangeCache {
// Get cached info // Get cached info
const manifest = await this.getManifest(); const manifest = await this.getManifest();
const cachedFile = manifest.files.find(f => f.path === filePath); const cachedFile = manifest.files.find((f) => f.path === filePath);
if (!cachedFile) { if (!cachedFile) {
return true; // Not in cache, so it's changed return true; // Not in cache, so it's changed
} }
// Compare checksums // Compare checksums
return cachedFile.checksum !== currentChecksum || return (
cachedFile.checksum !== currentChecksum ||
cachedFile.size !== stats.size || cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtimeMs; cachedFile.modified !== stats.mtimeMs
);
} }
async updateFileCache(filePath: string): Promise<void> { async updateFileCache(filePath: string): Promise<void> {
@@ -95,18 +130,18 @@ export class ChangeCache {
return; // Don't cache directories return; // Don't cache directories
} }
const content = await plugins.smartfile.fs.toStringSync(absolutePath); const content = plugins.smartfile.fs.toStringSync(absolutePath);
const checksum = this.calculateChecksum(content); const checksum = this.calculateChecksum(content);
// Update manifest // Update manifest
const manifest = await this.getManifest(); const manifest = await this.getManifest();
const existingIndex = manifest.files.findIndex(f => f.path === filePath); const existingIndex = manifest.files.findIndex((f) => f.path === filePath);
const cacheEntry: IFileCache = { const cacheEntry: IFileCache = {
path: filePath, path: filePath,
checksum, checksum,
modified: stats.mtimeMs, modified: stats.mtimeMs,
size: stats.size size: stats.size,
}; };
if (existingIndex !== -1) { if (existingIndex !== -1) {
@@ -153,4 +188,36 @@ export class ChangeCache {
private calculateChecksum(content: string | Buffer): string { private calculateChecksum(content: string | Buffer): string {
return plugins.crypto.createHash('sha256').update(content).digest('hex'); return plugins.crypto.createHash('sha256').update(content).digest('hex');
} }
private isValidManifest(manifest: any): manifest is ICacheManifest {
// Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') {
return false;
}
// Check required fields
if (
typeof manifest.version !== 'string' ||
typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)
) {
return false;
}
// Check each file entry
for (const file of manifest.files) {
if (
!file ||
typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number'
) {
return false;
}
}
return true;
}
} }

View File

@@ -17,16 +17,23 @@ export class DependencyAnalyzer {
private initializeDependencies(): void { private initializeDependencies(): void {
// Define dependencies between format modules // Define dependencies between format modules
const dependencies = { const dependencies = {
'cleanup': [], // No dependencies cleanup: [], // No dependencies
'npmextra': [], // No dependencies npmextra: [], // No dependencies
'license': ['npmextra'], // Depends on npmextra for config license: ['npmextra'], // Depends on npmextra for config
'packagejson': ['npmextra'], // Depends on npmextra for config packagejson: ['npmextra'], // Depends on npmextra for config
'templates': ['npmextra', 'packagejson'], // Depends on both templates: ['npmextra', 'packagejson'], // Depends on both
'gitignore': ['templates'], // Depends on templates gitignore: ['templates'], // Depends on templates
'tsconfig': ['packagejson'], // Depends on package.json tsconfig: ['packagejson'], // Depends on package.json
'prettier': ['cleanup', 'npmextra', 'packagejson', 'templates', 'gitignore', 'tsconfig'], // Runs after most others prettier: [
'readme': ['npmextra', 'packagejson'], // Depends on project metadata 'cleanup',
'copy': ['npmextra'], // Depends on config 'npmextra',
'packagejson',
'templates',
'gitignore',
'tsconfig',
], // Runs after most others
readme: ['npmextra', 'packagejson'], // Depends on project metadata
copy: ['npmextra'], // Depends on config
}; };
// Initialize all modules // Initialize all modules
@@ -34,7 +41,7 @@ export class DependencyAnalyzer {
this.moduleDependencies.set(module, { this.moduleDependencies.set(module, {
module, module,
dependencies: new Set(deps), dependencies: new Set(deps),
dependents: new Set() dependents: new Set(),
}); });
} }
@@ -50,7 +57,7 @@ export class DependencyAnalyzer {
} }
getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] { getExecutionGroups(modules: BaseFormatter[]): BaseFormatter[][] {
const modulesMap = new Map(modules.map(m => [m.name, m])); const modulesMap = new Map(modules.map((m) => [m.name, m]));
const executed = new Set<string>(); const executed = new Set<string>();
const groups: BaseFormatter[][] = []; const groups: BaseFormatter[][] = [];
@@ -68,8 +75,9 @@ export class DependencyAnalyzer {
} }
// Check if all dependencies have been executed // Check if all dependencies have been executed
const allDepsExecuted = Array.from(dependency.dependencies) const allDepsExecuted = Array.from(dependency.dependencies).every(
.every(dep => executed.has(dep) || !modulesMap.has(dep)); (dep) => executed.has(dep) || !modulesMap.has(dep),
);
if (allDepsExecuted) { if (allDepsExecuted) {
currentGroup.push(module); currentGroup.push(module);
@@ -85,7 +93,7 @@ export class DependencyAnalyzer {
} }
} }
currentGroup.forEach(m => executed.add(m.name)); currentGroup.forEach((m) => executed.add(m.name));
groups.push(currentGroup); groups.push(currentGroup);
} }
@@ -99,9 +107,11 @@ export class DependencyAnalyzer {
if (!dep1 || !dep2) return false; if (!dep1 || !dep2) return false;
// Check if module1 depends on module2 or vice versa // Check if module1 depends on module2 or vice versa
return !dep1.dependencies.has(module2) && return (
!dep1.dependencies.has(module2) &&
!dep2.dependencies.has(module1) && !dep2.dependencies.has(module1) &&
!dep1.dependents.has(module2) && !dep1.dependents.has(module2) &&
!dep2.dependents.has(module1); !dep2.dependents.has(module1)
);
} }
} }

View File

@@ -5,7 +5,11 @@ import { logger } from '../gitzone.logging.js';
export class DiffReporter { export class DiffReporter {
private diffs: Map<string, string> = new Map(); private diffs: Map<string, string> = new Map();
async generateDiff(filePath: string, oldContent: string, newContent: string): Promise<string> { async generateDiff(
filePath: string,
oldContent: string,
newContent: string,
): Promise<string> {
const diff = plugins.smartdiff.createDiff(oldContent, newContent); const diff = plugins.smartdiff.createDiff(oldContent, newContent);
this.diffs.set(filePath, diff); this.diffs.set(filePath, diff);
return diff; return diff;
@@ -22,16 +26,25 @@ export class DiffReporter {
return null; return null;
} }
const currentContent = await plugins.smartfile.fs.toStringSync(change.path); const currentContent = await plugins.smartfile.fs.toStringSync(
change.path,
);
// For planned changes, we need the new content // For planned changes, we need the new content
if (!change.content) { if (!change.content) {
return null; return null;
} }
return await this.generateDiff(change.path, currentContent, change.content); return await this.generateDiff(
change.path,
currentContent,
change.content,
);
} catch (error) { } catch (error) {
logger.log('error', `Failed to generate diff for ${change.path}: ${error.message}`); logger.log(
'error',
`Failed to generate diff for ${change.path}: ${error.message}`,
);
return null; return null;
} }
} }
@@ -69,7 +82,7 @@ export class DiffReporter {
private colorDiff(diff: string): string { private colorDiff(diff: string): string {
const lines = diff.split('\n'); const lines = diff.split('\n');
const coloredLines = lines.map(line => { const coloredLines = lines.map((line) => {
if (line.startsWith('+') && !line.startsWith('+++')) { if (line.startsWith('+') && !line.startsWith('+++')) {
return `\x1b[32m${line}\x1b[0m`; // Green for additions return `\x1b[32m${line}\x1b[0m`; // Green for additions
} else if (line.startsWith('-') && !line.startsWith('---')) { } else if (line.startsWith('-') && !line.startsWith('---')) {
@@ -90,11 +103,14 @@ export class DiffReporter {
totalFiles: this.diffs.size, totalFiles: this.diffs.size,
diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({ diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({
path, path,
diff diff,
})) })),
}; };
await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath); await plugins.smartfile.memory.toFs(
JSON.stringify(report, null, 2),
outputPath,
);
logger.log('info', `Diff report saved to ${outputPath}`); logger.log('info', `Diff report saved to ${outputPath}`);
} }

View File

@@ -1,64 +1,13 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import { RollbackManager } from './classes.rollbackmanager.js';
import { ChangeCache } from './classes.changecache.js';
import { FormatStats } from './classes.formatstats.js'; import { FormatStats } from './classes.formatstats.js';
import type { IFormatOperation, IFormatPlan } from './interfaces.format.js';
export class FormatContext { export class FormatContext {
private rollbackManager: RollbackManager;
private currentOperation: IFormatOperation | null = null;
private changeCache: ChangeCache;
private formatStats: FormatStats; private formatStats: FormatStats;
constructor() { constructor() {
this.rollbackManager = new RollbackManager();
this.changeCache = new ChangeCache();
this.formatStats = new FormatStats(); this.formatStats = new FormatStats();
} }
async beginOperation(): Promise<void> {
this.currentOperation = await this.rollbackManager.createOperation();
}
async trackFileChange(filepath: string): Promise<void> {
if (!this.currentOperation) {
throw new Error('No operation in progress. Call beginOperation() first.');
}
await this.rollbackManager.backupFile(filepath, this.currentOperation.id);
}
async commitOperation(): Promise<void> {
if (!this.currentOperation) {
throw new Error('No operation in progress. Call beginOperation() first.');
}
await this.rollbackManager.markComplete(this.currentOperation.id);
this.currentOperation = null;
}
async rollbackOperation(): Promise<void> {
if (!this.currentOperation) {
throw new Error('No operation in progress. Call beginOperation() first.');
}
await this.rollbackManager.rollback(this.currentOperation.id);
this.currentOperation = null;
}
async rollbackTo(operationId: string): Promise<void> {
await this.rollbackManager.rollback(operationId);
}
getRollbackManager(): RollbackManager {
return this.rollbackManager;
}
getChangeCache(): ChangeCache {
return this.changeCache;
}
async initializeCache(): Promise<void> {
await this.changeCache.initialize();
}
getFormatStats(): FormatStats { getFormatStats(): FormatStats {
return this.formatStats; return this.formatStats;
} }

View File

@@ -18,10 +18,10 @@ export class FormatPlanner {
filesAdded: 0, filesAdded: 0,
filesModified: 0, filesModified: 0,
filesRemoved: 0, filesRemoved: 0,
estimatedTime: 0 estimatedTime: 0,
}, },
changes: [], changes: [],
warnings: [] warnings: [],
}; };
for (const module of modules) { for (const module of modules) {
@@ -49,45 +49,30 @@ export class FormatPlanner {
plan.warnings.push({ plan.warnings.push({
level: 'error', level: 'error',
message: `Failed to analyze module ${module.name}: ${error.message}`, message: `Failed to analyze module ${module.name}: ${error.message}`,
module: module.name module: module.name,
}); });
} }
} }
plan.summary.totalFiles = plan.summary.filesAdded + plan.summary.filesModified + plan.summary.filesRemoved; plan.summary.totalFiles =
plan.summary.filesAdded +
plan.summary.filesModified +
plan.summary.filesRemoved;
plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate plan.summary.estimatedTime = plan.summary.totalFiles * 100; // 100ms per file estimate
return plan; return plan;
} }
async executePlan(plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, parallel: boolean = true): Promise<void> { async executePlan(
await context.beginOperation(); plan: IFormatPlan,
modules: BaseFormatter[],
context: FormatContext,
parallel: boolean = false,
): Promise<void> {
const startTime = Date.now(); const startTime = Date.now();
try { try {
if (parallel) { // Always use sequential execution to avoid race conditions
// Get execution groups based on dependencies
const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules);
logger.log('info', `Executing formatters in ${executionGroups.length} groups...`);
for (let i = 0; i < executionGroups.length; i++) {
const group = executionGroups[i];
logger.log('info', `Executing group ${i + 1}: ${group.map(m => m.name).join(', ')}`);
// Execute modules in this group in parallel
const promises = group.map(async (module) => {
const changes = this.plannedChanges.get(module.name) || [];
if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
await module.execute(changes);
}
});
await Promise.all(promises);
}
} else {
// Sequential execution (original implementation)
for (const module of modules) { for (const module of modules) {
const changes = this.plannedChanges.get(module.name) || []; const changes = this.plannedChanges.get(module.name) || [];
@@ -96,20 +81,19 @@ export class FormatPlanner {
await module.execute(changes); await module.execute(changes);
} }
} }
}
const endTime = Date.now(); const endTime = Date.now();
const duration = endTime - startTime; const duration = endTime - startTime;
logger.log('info', `Format operations completed in ${duration}ms`); logger.log('info', `Format operations completed in ${duration}ms`);
await context.commitOperation();
} catch (error) { } catch (error) {
await context.rollbackOperation();
throw error; throw error;
} }
} }
async displayPlan(plan: IFormatPlan, detailed: boolean = false): Promise<void> { async displayPlan(
plan: IFormatPlan,
detailed: boolean = false,
): Promise<void> {
console.log('\nFormat Plan:'); console.log('\nFormat Plan:');
console.log('━'.repeat(50)); console.log('━'.repeat(50));
console.log(`Summary: ${plan.summary.totalFiles} files will be changed`); console.log(`Summary: ${plan.summary.totalFiles} files will be changed`);
@@ -128,7 +112,9 @@ export class FormatPlanner {
} }
for (const [module, changes] of changesByModule) { for (const [module, changes] of changesByModule) {
console.log(`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`); console.log(
`\n${this.getModuleIcon(module)} ${module} (${changes.length} ${changes.length === 1 ? 'file' : 'files'})`,
);
for (const change of changes) { for (const change of changes) {
const icon = this.getChangeIcon(change.type); const icon = this.getChangeIcon(change.type);
@@ -157,16 +143,16 @@ export class FormatPlanner {
private getModuleIcon(module: string): string { private getModuleIcon(module: string): string {
const icons: Record<string, string> = { const icons: Record<string, string> = {
'packagejson': '📦', packagejson: '📦',
'license': '📝', license: '📝',
'tsconfig': '🔧', tsconfig: '🔧',
'cleanup': '🚮', cleanup: '🚮',
'gitignore': '🔒', gitignore: '🔒',
'prettier': '✨', prettier: '✨',
'readme': '📖', readme: '📖',
'templates': '📄', templates: '📄',
'npmextra': '⚙️', npmextra: '⚙️',
'copy': '📋' copy: '📋',
}; };
return icons[module] || '📁'; return icons[module] || '📁';
} }

View File

@@ -44,8 +44,8 @@ export class FormatStats {
totalDeleted: 0, totalDeleted: 0,
totalErrors: 0, totalErrors: 0,
cacheHits: 0, cacheHits: 0,
cacheMisses: 0 cacheMisses: 0,
} },
}; };
} }
@@ -58,7 +58,7 @@ export class FormatStats {
successes: 0, successes: 0,
filesCreated: 0, filesCreated: 0,
filesModified: 0, filesModified: 0,
filesDeleted: 0 filesDeleted: 0,
}); });
} }
@@ -73,7 +73,11 @@ export class FormatStats {
} }
} }
recordFileOperation(moduleName: string, operation: 'create' | 'modify' | 'delete', success: boolean = true): void { recordFileOperation(
moduleName: string,
operation: 'create' | 'modify' | 'delete',
success: boolean = true,
): void {
const moduleStats = this.stats.moduleStats.get(moduleName); const moduleStats = this.stats.moduleStats.get(moduleName);
if (!moduleStats) return; if (!moduleStats) return;
@@ -122,16 +126,24 @@ export class FormatStats {
// Overall stats // Overall stats
console.log('\nOverall Summary:'); console.log('\nOverall Summary:');
console.log(` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`); console.log(
` Total Execution Time: ${this.formatDuration(this.stats.totalExecutionTime)}`,
);
console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`); console.log(` Files Processed: ${this.stats.overallStats.totalFiles}`);
console.log(` • Created: ${this.stats.overallStats.totalCreated}`); console.log(` • Created: ${this.stats.overallStats.totalCreated}`);
console.log(` • Modified: ${this.stats.overallStats.totalModified}`); console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`); console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
console.log(` Errors: ${this.stats.overallStats.totalErrors}`); console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
if (this.stats.overallStats.cacheHits > 0 || this.stats.overallStats.cacheMisses > 0) { if (
const cacheHitRate = this.stats.overallStats.cacheHits / this.stats.overallStats.cacheHits > 0 ||
(this.stats.overallStats.cacheHits + this.stats.overallStats.cacheMisses) * 100; this.stats.overallStats.cacheMisses > 0
) {
const cacheHitRate =
(this.stats.overallStats.cacheHits /
(this.stats.overallStats.cacheHits +
this.stats.overallStats.cacheMisses)) *
100;
console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`); console.log(` Cache Hit Rate: ${cacheHitRate.toFixed(1)}%`);
console.log(` • Hits: ${this.stats.overallStats.cacheHits}`); console.log(` • Hits: ${this.stats.overallStats.cacheHits}`);
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`); console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
@@ -141,12 +153,17 @@ export class FormatStats {
console.log('\nModule Breakdown:'); console.log('\nModule Breakdown:');
console.log('─'.repeat(50)); console.log('─'.repeat(50));
const sortedModules = Array.from(this.stats.moduleStats.values()) const sortedModules = Array.from(this.stats.moduleStats.values()).sort(
.sort((a, b) => b.filesProcessed - a.filesProcessed); (a, b) => b.filesProcessed - a.filesProcessed,
);
for (const moduleStats of sortedModules) { for (const moduleStats of sortedModules) {
console.log(`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`); console.log(
console.log(` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`); `\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
);
console.log(
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
);
console.log(` Files Processed: ${moduleStats.filesProcessed}`); console.log(` Files Processed: ${moduleStats.filesProcessed}`);
if (moduleStats.filesCreated > 0) { if (moduleStats.filesCreated > 0) {
@@ -172,10 +189,13 @@ export class FormatStats {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
executionTime: this.stats.totalExecutionTime, executionTime: this.stats.totalExecutionTime,
overallStats: this.stats.overallStats, overallStats: this.stats.overallStats,
moduleStats: Array.from(this.stats.moduleStats.values()) moduleStats: Array.from(this.stats.moduleStats.values()),
}; };
await plugins.smartfile.memory.toFs(JSON.stringify(report, null, 2), outputPath); await plugins.smartfile.memory.toFs(
JSON.stringify(report, null, 2),
outputPath,
);
logger.log('info', `Statistics report saved to ${outputPath}`); logger.log('info', `Statistics report saved to ${outputPath}`);
} }
@@ -193,16 +213,16 @@ export class FormatStats {
private getModuleIcon(module: string): string { private getModuleIcon(module: string): string {
const icons: Record<string, string> = { const icons: Record<string, string> = {
'packagejson': '📦', packagejson: '📦',
'license': '📝', license: '📝',
'tsconfig': '🔧', tsconfig: '🔧',
'cleanup': '🚮', cleanup: '🚮',
'gitignore': '🔒', gitignore: '🔒',
'prettier': '✨', prettier: '✨',
'readme': '📖', readme: '📖',
'templates': '📄', templates: '📄',
'npmextra': '⚙️', npmextra: '⚙️',
'copy': '📋' copy: '📋',
}; };
return icons[module] || '📁'; return icons[module] || '📁';
} }

View File

@@ -18,7 +18,7 @@ export class RollbackManager {
id: this.generateOperationId(), id: this.generateOperationId(),
timestamp: Date.now(), timestamp: Date.now(),
files: [], files: [],
status: 'pending' status: 'pending',
}; };
await this.updateManifest(operation); await this.updateManifest(operation);
@@ -43,7 +43,7 @@ export class RollbackManager {
} }
// Read file content and metadata // 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 stats = await plugins.smartfile.fs.stat(absolutePath);
const checksum = this.calculateChecksum(content); const checksum = this.calculateChecksum(content);
@@ -57,7 +57,7 @@ export class RollbackManager {
path: filepath, path: filepath,
originalContent: content, originalContent: content,
checksum, checksum,
permissions: stats.mode.toString(8) permissions: stats.mode.toString(8),
}); });
await this.updateManifest(operation); await this.updateManifest(operation);
@@ -66,7 +66,9 @@ export class RollbackManager {
async rollback(operationId: string): Promise<void> { async rollback(operationId: string): Promise<void> {
const operation = await this.getOperation(operationId); const operation = await this.getOperation(operationId);
if (!operation) { if (!operation) {
throw new Error(`Operation ${operationId} not found`); // Operation doesn't exist, might have already been rolled back or never created
console.warn(`Operation ${operationId} not found for rollback, skipping`);
return;
} }
if (operation.status === 'rolled-back') { if (operation.status === 'rolled-back') {
@@ -82,7 +84,7 @@ export class RollbackManager {
// Verify backup integrity // Verify backup integrity
const backupPath = this.getBackupPath(operationId, file.path); 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); const backupChecksum = this.calculateChecksum(backupContent);
if (backupChecksum !== file.checksum) { if (backupChecksum !== file.checksum) {
@@ -114,19 +116,25 @@ export class RollbackManager {
async cleanOldBackups(retentionDays: number): Promise<void> { async cleanOldBackups(retentionDays: number): Promise<void> {
const manifest = await this.getManifest(); const manifest = await this.getManifest();
const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000); const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
const operationsToDelete = manifest.operations.filter(op => const operationsToDelete = manifest.operations.filter(
op.timestamp < cutoffTime && op.status === 'completed' (op) => op.timestamp < cutoffTime && op.status === 'completed',
); );
for (const operation of operationsToDelete) { for (const operation of operationsToDelete) {
// Remove backup files // Remove backup files
const operationDir = plugins.path.join(this.backupDir, 'operations', operation.id); const operationDir = plugins.path.join(
this.backupDir,
'operations',
operation.id,
);
await plugins.smartfile.fs.remove(operationDir); await plugins.smartfile.fs.remove(operationDir);
// Remove from manifest // Remove from manifest
manifest.operations = manifest.operations.filter(op => op.id !== operation.id); manifest.operations = manifest.operations.filter(
(op) => op.id !== operation.id,
);
} }
await this.saveManifest(manifest); await this.saveManifest(manifest);
@@ -146,7 +154,7 @@ export class RollbackManager {
return false; return false;
} }
const content = await plugins.smartfile.fs.toStringSync(backupPath); const content = plugins.smartfile.fs.toStringSync(backupPath);
const checksum = this.calculateChecksum(content); const checksum = this.calculateChecksum(content);
if (checksum !== file.checksum) { if (checksum !== file.checksum) {
@@ -164,7 +172,9 @@ export class RollbackManager {
private async ensureBackupDir(): Promise<void> { private async ensureBackupDir(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.backupDir); await plugins.smartfile.fs.ensureDir(this.backupDir);
await plugins.smartfile.fs.ensureDir(plugins.path.join(this.backupDir, 'operations')); await plugins.smartfile.fs.ensureDir(
plugins.path.join(this.backupDir, 'operations'),
);
} }
private generateOperationId(): string { private generateOperationId(): string {
@@ -177,7 +187,14 @@ export class RollbackManager {
const filename = plugins.path.basename(filepath); const filename = plugins.path.basename(filepath);
const dir = plugins.path.dirname(filepath); const dir = plugins.path.dirname(filepath);
const safeDir = dir.replace(/[/\\]/g, '__'); const safeDir = dir.replace(/[/\\]/g, '__');
return plugins.path.join(this.backupDir, 'operations', operationId, 'files', safeDir, `${filename}.backup`); return plugins.path.join(
this.backupDir,
'operations',
operationId,
'files',
safeDir,
`${filename}.backup`,
);
} }
private calculateChecksum(content: string | Buffer): string { private calculateChecksum(content: string | Buffer): string {
@@ -185,27 +202,68 @@ export class RollbackManager {
} }
private async getManifest(): Promise<{ operations: IFormatOperation[] }> { private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
const defaultManifest = { operations: [] };
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) { if (!exists) {
return { operations: [] }; return defaultManifest;
} }
const content = await plugins.smartfile.fs.toStringSync(this.manifestPath); try {
return JSON.parse(content); 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<void> { private async saveManifest(manifest: {
await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath); operations: IFormatOperation[];
}): Promise<void> {
// Validate before saving
if (!this.isValidManifest(manifest)) {
throw new Error('Invalid rollback manifest structure, cannot save');
} }
private async getOperation(operationId: string): Promise<IFormatOperation | null> { // Ensure directory exists
await this.ensureBackupDir();
// Write directly with proper JSON stringification
const jsonContent = JSON.stringify(manifest, null, 2);
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
}
private async getOperation(
operationId: string,
): Promise<IFormatOperation | null> {
const manifest = await this.getManifest(); const manifest = await this.getManifest();
return manifest.operations.find(op => op.id === operationId) || null; return manifest.operations.find((op) => op.id === operationId) || null;
} }
private async updateManifest(operation: IFormatOperation): Promise<void> { private async updateManifest(operation: IFormatOperation): Promise<void> {
const manifest = await this.getManifest(); const manifest = await this.getManifest();
const existingIndex = manifest.operations.findIndex(op => op.id === operation.id); const existingIndex = manifest.operations.findIndex(
(op) => op.id === operation.id,
);
if (existingIndex !== -1) { if (existingIndex !== -1) {
manifest.operations[existingIndex] = operation; manifest.operations[existingIndex] = operation;
@@ -215,4 +273,46 @@ export class RollbackManager {
await this.saveManifest(manifest); 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;
}
} }

View File

@@ -4,14 +4,21 @@ import * as paths from '../paths.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
import { Project } from '../classes.project.js'; import { Project } from '../classes.project.js';
const filesToDelete = ['defaults.yml', 'yarn.lock', 'package-lock.json', 'tslint.json']; const filesToDelete = [
'defaults.yml',
'yarn.lock',
'package-lock.json',
'tslint.json',
];
export const run = async (projectArg: Project) => { export const run = async (projectArg: Project) => {
for (const relativeFilePath of filesToDelete) { for (const relativeFilePath of filesToDelete) {
const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath); const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath);
if (fileExists) { if (fileExists) {
logger.log('info', `Found ${relativeFilePath}! Removing it!`); logger.log('info', `Found ${relativeFilePath}! Removing it!`);
plugins.smartfile.fs.removeSync(plugins.path.join(paths.cwd, relativeFilePath)); plugins.smartfile.fs.removeSync(
plugins.path.join(paths.cwd, relativeFilePath),
);
} else { } else {
logger.log('info', `Project is free of ${relativeFilePath}`); logger.log('info', `Project is free of ${relativeFilePath}`);
} }

View File

@@ -8,7 +8,7 @@ export const run = async (projectArg: Project) => {
// Get copy configuration from npmextra.json // Get copy configuration from npmextra.json
const npmextraConfig = new plugins.npmextra.Npmextra(); const npmextraConfig = new plugins.npmextra.Npmextra();
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', { const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
patterns: [] patterns: [],
}); });
if (!copyConfig.patterns || copyConfig.patterns.length === 0) { if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
@@ -40,7 +40,7 @@ export const run = async (projectArg: Project) => {
if (pattern.preservePath) { if (pattern.preservePath) {
const relativePath = plugins.path.relative( const relativePath = plugins.path.relative(
plugins.path.dirname(pattern.from.replace(/\*/g, '')), plugins.path.dirname(pattern.from.replace(/\*/g, '')),
file file,
); );
destPath = plugins.path.join(pattern.to, relativePath); destPath = plugins.path.join(pattern.to, relativePath);
} }
@@ -53,7 +53,10 @@ export const run = async (projectArg: Project) => {
logger.log('info', `Copied ${sourcePath} to ${destPath}`); logger.log('info', `Copied ${sourcePath} to ${destPath}`);
} }
} catch (error) { } catch (error) {
logger.log('error', `Failed to copy pattern ${pattern.from}: ${error.message}`); logger.log(
'error',
`Failed to copy pattern ${pattern.from}: ${error.message}`,
);
} }
} }
}; };

View File

@@ -12,7 +12,8 @@ export const run = async (projectArg: Project) => {
const ciTemplate = await templateModule.getTemplate('gitignore'); const ciTemplate = await templateModule.getTemplate('gitignore');
if (gitignoreExists) { if (gitignoreExists) {
// lets get the existing gitignore file // lets get the existing gitignore file
const existingGitIgnoreString = plugins.smartfile.fs.toStringSync(gitignorePath); const existingGitIgnoreString =
plugins.smartfile.fs.toStringSync(gitignorePath);
let customPart = existingGitIgnoreString.split('# custom\n')[1]; let customPart = existingGitIgnoreString.split('# custom\n')[1];
customPart ? null : (customPart = ''); customPart ? null : (customPart = '');
} }

View File

@@ -24,7 +24,9 @@ export const run = async (projectArg: Project) => {
} else { } else {
logger.log('error', 'Error -> licenses failed. Here is why:'); logger.log('error', 'Error -> licenses failed. Here is why:');
for (const failedModule of licenseCheckResult.failingModules) { for (const failedModule of licenseCheckResult.failingModules) {
console.log(`${failedModule.name} fails with license ${failedModule.license}`); console.log(
`${failedModule.name} fails with license ${failedModule.license}`,
);
} }
} }
}; };

View File

@@ -29,7 +29,12 @@ export const run = async (projectArg: Project) => {
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
for (const expectedRepoInformationItem of expectedRepoInformation) { for (const expectedRepoInformationItem of expectedRepoInformation) {
if (!plugins.smartobject.smartGet(npmextraJson.gitzone, expectedRepoInformationItem)) { if (
!plugins.smartobject.smartGet(
npmextraJson.gitzone,
expectedRepoInformationItem,
)
) {
interactInstance.addQuestions([ interactInstance.addQuestions([
{ {
message: `What is the value of ${expectedRepoInformationItem}`, message: `What is the value of ${expectedRepoInformationItem}`,
@@ -43,7 +48,9 @@ export const run = async (projectArg: Project) => {
const answerbucket = await interactInstance.runQueue(); const answerbucket = await interactInstance.runQueue();
for (const expectedRepoInformationItem of expectedRepoInformation) { for (const expectedRepoInformationItem of expectedRepoInformation) {
const cliProvidedValue = answerbucket.getAnswerFor(expectedRepoInformationItem); const cliProvidedValue = answerbucket.getAnswerFor(
expectedRepoInformationItem,
);
if (cliProvidedValue) { if (cliProvidedValue) {
plugins.smartobject.smartAdd( plugins.smartobject.smartAdd(
npmextraJson.gitzone, npmextraJson.gitzone,

View File

@@ -43,7 +43,8 @@ const ensureDependency = async (
break; break;
case 'include': case 'include':
if (!packageJsonObjectArg[section][packageName]) { if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version; packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
} }
break; break;
case 'latest': case 'latest':
@@ -54,9 +55,13 @@ const ensureDependency = async (
const latestVersion = packageInfo['dist-tags'].latest; const latestVersion = packageInfo['dist-tags'].latest;
packageJsonObjectArg[section][packageName] = `^${latestVersion}`; packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
} catch (error) { } catch (error) {
logger.log('warn', `Could not fetch latest version for ${packageName}, using existing or default`); logger.log(
'warn',
`Could not fetch latest version for ${packageName}, using existing or default`,
);
if (!packageJsonObjectArg[section][packageName]) { if (!packageJsonObjectArg[section][packageName]) {
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version; packageJsonObjectArg[section][packageName] =
version === 'latest' ? '^1.0.0' : version;
} }
} }
break; break;
@@ -91,9 +96,15 @@ export const run = async (projectArg: Project) => {
// Check for private or public // Check for private or public
if (packageJson.private !== undefined) { if (packageJson.private !== undefined) {
logger.log('info', 'Success -> found private/public info in package.json!'); logger.log(
'info',
'Success -> found private/public info in package.json!',
);
} else { } else {
logger.log('error', 'found no private boolean! Setting it to private for now!'); logger.log(
'error',
'found no private boolean! Setting it to private for now!',
);
packageJson.private = true; packageJson.private = true;
} }
@@ -101,7 +112,10 @@ export const run = async (projectArg: Project) => {
if (packageJson.license) { if (packageJson.license) {
logger.log('info', 'Success -> found license in package.json!'); logger.log('info', 'Success -> found license in package.json!');
} else { } else {
logger.log('error', 'found no license! Setting it to UNLICENSED for now!'); logger.log(
'error',
'found no license! Setting it to UNLICENSED for now!',
);
packageJson.license = 'UNLICENSED'; packageJson.license = 'UNLICENSED';
} }
@@ -109,13 +123,19 @@ export const run = async (projectArg: Project) => {
if (packageJson.scripts.build) { if (packageJson.scripts.build) {
logger.log('info', 'Success -> found build script in package.json!'); logger.log('info', 'Success -> found build script in package.json!');
} else { } else {
logger.log('error', 'found no build script! Putting a placeholder there for now!'); logger.log(
'error',
'found no build script! Putting a placeholder there for now!',
);
packageJson.scripts.build = `echo "Not needed for now"`; packageJson.scripts.build = `echo "Not needed for now"`;
} }
// Check for buildDocs script // Check for buildDocs script
if (!packageJson.scripts.buildDocs) { if (!packageJson.scripts.buildDocs) {
logger.log('info', 'found no buildDocs script! Putting tsdoc script there now.'); logger.log(
'info',
'found no buildDocs script! Putting tsdoc script there now.',
);
packageJson.scripts.buildDocs = `tsdoc`; packageJson.scripts.buildDocs = `tsdoc`;
} }
@@ -134,9 +154,24 @@ export const run = async (projectArg: Project) => {
]; ];
// check for dependencies // check for dependencies
await ensureDependency(packageJson, 'devDep', 'latest', '@push.rocks/tapbundle'); await ensureDependency(
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest'); packageJson,
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tsbuild'); 'devDep',
'latest',
'@push.rocks/tapbundle',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tstest',
);
await ensureDependency(
packageJson,
'devDep',
'latest',
'@git.zone/tsbuild',
);
// set overrides // set overrides
const overrides = plugins.smartfile.fs.toObjectSync( const overrides = plugins.smartfile.fs.toObjectSync(

View File

@@ -16,7 +16,12 @@ const prettierDefaultMarkdownConfig: prettier.Options = {
parser: 'markdown', parser: 'markdown',
}; };
const filesToFormat = [`ts/**/*.ts`, `test/**/*.ts`, `readme.md`, `docs/**/*.md`]; const filesToFormat = [
`ts/**/*.ts`,
`test/**/*.ts`,
`readme.md`,
`docs/**/*.md`,
];
const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => { const choosePrettierConfig = (fileArg: plugins.smartfile.SmartFile) => {
switch (fileArg.parsedPath.ext) { switch (fileArg.parsedPath.ext) {
@@ -39,7 +44,10 @@ const prettierTypeScriptPipestop = plugins.through2.obj(
cb(null); cb(null);
} else { } else {
logger.log('info', `${fileArg.path} is being reformated!`); logger.log('info', `${fileArg.path} is being reformated!`);
const formatedFileString = await prettier.format(fileString, chosenConfig); const formatedFileString = await prettier.format(
fileString,
chosenConfig,
);
fileArg.setContentsFromString(formatedFileString); fileArg.setContentsFromString(formatedFileString);
cb(null, fileArg); cb(null, fileArg);
} }

View File

@@ -18,7 +18,8 @@ export const run = async () => {
} }
// Check and initialize readme.hints.md if it doesn't exist // Check and initialize readme.hints.md if it doesn't exist
const readmeHintsExists = await plugins.smartfile.fs.fileExists(readmeHintsPath); const readmeHintsExists =
await plugins.smartfile.fs.fileExists(readmeHintsPath);
if (!readmeHintsExists) { if (!readmeHintsExists) {
await plugins.smartfile.fs.toFs( await plugins.smartfile.fs.toFs(
'# Project Readme Hints\n\nThis is the initial readme hints file.', '# Project Readme Hints\n\nThis is the initial readme hints file.',

View File

@@ -26,10 +26,12 @@ export const run = async (project: Project) => {
case 'npm': case 'npm':
case 'wcc': case 'wcc':
if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') { if (project.gitzoneConfig.data.npmciOptions.npmAccessLevel === 'public') {
const ciTemplateDefault = await templateModule.getTemplate('ci_default'); const ciTemplateDefault =
await templateModule.getTemplate('ci_default');
ciTemplateDefault.writeToDisk(paths.cwd); ciTemplateDefault.writeToDisk(paths.cwd);
} else { } else {
const ciTemplateDefault = await templateModule.getTemplate('ci_default_private'); const ciTemplateDefault =
await templateModule.getTemplate('ci_default_private');
ciTemplateDefault.writeToDisk(paths.cwd); ciTemplateDefault.writeToDisk(paths.cwd);
} }
logger.log('info', 'Updated .gitlabci.yml!'); logger.log('info', 'Updated .gitlabci.yml!');
@@ -41,7 +43,8 @@ export const run = async (project: Project) => {
logger.log('info', 'Updated CI/CD config files!'); logger.log('info', 'Updated CI/CD config files!');
// lets care about docker // lets care about docker
const dockerTemplate = await templateModule.getTemplate('dockerfile_service'); const dockerTemplate =
await templateModule.getTemplate('dockerfile_service');
dockerTemplate.writeToDisk(paths.cwd); dockerTemplate.writeToDisk(paths.cwd);
logger.log('info', 'Updated Dockerfile!'); logger.log('info', 'Updated Dockerfile!');
@@ -56,17 +59,22 @@ export const run = async (project: Project) => {
// update html // update html
if (project.gitzoneConfig.data.projectType === 'website') { if (project.gitzoneConfig.data.projectType === 'website') {
const websiteUpdateTemplate = await templateModule.getTemplate('website_update'); const websiteUpdateTemplate =
const variables ={ await templateModule.getTemplate('website_update');
const variables = {
assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl, assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl,
legalUrl: project.gitzoneConfig.data.module.legalUrl, legalUrl: project.gitzoneConfig.data.module.legalUrl,
}; };
console.log('updating website template with variables\n', JSON.stringify(variables, null, 2)); console.log(
'updating website template with variables\n',
JSON.stringify(variables, null, 2),
);
websiteUpdateTemplate.supplyVariables(variables); websiteUpdateTemplate.supplyVariables(variables);
await websiteUpdateTemplate.writeToDisk(paths.cwd); await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for website!`); logger.log('info', `Updated html for website!`);
} else if (project.gitzoneConfig.data.projectType === 'service') { } else if (project.gitzoneConfig.data.projectType === 'service') {
const websiteUpdateTemplate = await templateModule.getTemplate('service_update'); const websiteUpdateTemplate =
await templateModule.getTemplate('service_update');
await websiteUpdateTemplate.writeToDisk(paths.cwd); await websiteUpdateTemplate.writeToDisk(paths.cwd);
logger.log('info', `Updated html for element template!`); logger.log('info', `Updated html for element template!`);
} else if (project.gitzoneConfig.data.projectType === 'wcc') { } else if (project.gitzoneConfig.data.projectType === 'wcc') {

View File

@@ -19,8 +19,12 @@ export const run = async (projectArg: Project) => {
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd); const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
for (const publishModule of Object.keys(publishModules)) { for (const publishModule of Object.keys(publishModules)) {
const publishConfig = publishModules[publishModule]; const publishConfig = publishModules[publishModule];
tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [`./${publishModule}/index.js`]; tsconfigObject.compilerOptions.paths[`${publishConfig.name}`] = [
`./${publishModule}/index.js`,
];
} }
tsconfigSmartfile.setContentsFromString(JSON.stringify(tsconfigObject, null, 2)); tsconfigSmartfile.setContentsFromString(
JSON.stringify(tsconfigObject, null, 2),
);
await tsconfigSmartfile.write(); await tsconfigSmartfile.write();
}; };

View File

@@ -12,7 +12,12 @@ export class CleanupFormatter extends BaseFormatter {
const changes: IPlannedChange[] = []; const changes: IPlannedChange[] = [];
// List of files to remove // List of files to remove
const filesToRemove = ['yarn.lock', 'package-lock.json', 'tslint.json', 'defaults.yml']; const filesToRemove = [
'yarn.lock',
'package-lock.json',
'tslint.json',
'defaults.yml',
];
for (const file of filesToRemove) { for (const file of filesToRemove) {
const exists = await plugins.smartfile.fs.fileExists(file); const exists = await plugins.smartfile.fs.fileExists(file);
@@ -21,7 +26,7 @@ export class CleanupFormatter extends BaseFormatter {
type: 'delete', type: 'delete',
path: file, path: file,
module: this.name, module: this.name,
description: `Remove obsolete file` description: `Remove obsolete file`,
}); });
} }
} }

View File

@@ -8,7 +8,12 @@ export class LegacyFormatter extends BaseFormatter {
private moduleName: string; private moduleName: string;
private formatModule: any; private formatModule: any;
constructor(context: any, project: Project, moduleName: string, formatModule: any) { constructor(
context: any,
project: Project,
moduleName: string,
formatModule: any,
) {
super(context, project); super(context, project);
this.moduleName = moduleName; this.moduleName = moduleName;
this.formatModule = formatModule; this.formatModule = formatModule;
@@ -21,12 +26,14 @@ export class LegacyFormatter extends BaseFormatter {
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
// For legacy modules, we can't easily predict changes // For legacy modules, we can't easily predict changes
// So we'll return a generic change that indicates the module will run // So we'll return a generic change that indicates the module will run
return [{ return [
{
type: 'modify', type: 'modify',
path: '<various files>', path: '<various files>',
module: this.name, module: this.name,
description: `Run ${this.name} formatter` description: `Run ${this.name} formatter`,
}]; },
];
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {

View File

@@ -12,12 +12,7 @@ export class PrettierFormatter extends BaseFormatter {
const changes: IPlannedChange[] = []; const changes: IPlannedChange[] = [];
// Define directories to format (TypeScript directories by default) // Define directories to format (TypeScript directories by default)
const includeDirs = [ const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
'ts',
'ts_*',
'test',
'tests'
];
// File extensions to format // File extensions to format
const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}'; const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
@@ -36,7 +31,7 @@ export class PrettierFormatter extends BaseFormatter {
'CHANGELOG.md', 'CHANGELOG.md',
'license', 'license',
'LICENSE', 'LICENSE',
'*.md' '*.md',
]; ];
// Collect all files to format // Collect all files to format
@@ -45,7 +40,10 @@ export class PrettierFormatter extends BaseFormatter {
// Add files from TypeScript directories // Add files from TypeScript directories
for (const dir of includeDirs) { for (const dir of includeDirs) {
const globPattern = `${dir}/**/*.${extensions}`; const globPattern = `${dir}/**/*.${extensions}`;
const dirFiles = await plugins.smartfile.fs.listFileTree('.', globPattern); const dirFiles = await plugins.smartfile.fs.listFileTree(
'.',
globPattern,
);
allFiles.push(...dirFiles); allFiles.push(...dirFiles);
} }
@@ -53,7 +51,7 @@ export class PrettierFormatter extends BaseFormatter {
for (const pattern of rootConfigFiles) { for (const pattern of rootConfigFiles) {
const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern); const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern);
// Only include files at root level (no slashes in path) // Only include files at root level (no slashes in path)
const rootLevelFiles = rootFiles.filter(f => !f.includes('/')); const rootLevelFiles = rootFiles.filter((f) => !f.includes('/'));
allFiles.push(...rootLevelFiles); allFiles.push(...rootLevelFiles);
} }
@@ -80,7 +78,7 @@ export class PrettierFormatter extends BaseFormatter {
// Check which files need formatting // Check which files need formatting
for (const file of validFiles) { for (const file of validFiles) {
// Skip files that haven't changed // Skip files that haven't changed
if (!await this.shouldProcessFile(file)) { if (!(await this.shouldProcessFile(file))) {
logVerbose(`Skipping ${file} - no changes detected`); logVerbose(`Skipping ${file} - no changes detected`);
continue; continue;
} }
@@ -89,7 +87,7 @@ export class PrettierFormatter extends BaseFormatter {
type: 'modify', type: 'modify',
path: file, path: file,
module: this.name, module: this.name,
description: 'Format with Prettier' description: 'Format with Prettier',
}); });
} }
@@ -112,11 +110,15 @@ export class PrettierFormatter extends BaseFormatter {
batches.push(changes.slice(i, i + batchSize)); batches.push(changes.slice(i, i + batchSize));
} }
logVerbose(`Processing ${changes.length} files in ${batches.length} batches`); logVerbose(
`Processing ${changes.length} files in ${batches.length} batches`,
);
for (let i = 0; i < batches.length; i++) { for (let i = 0; i < batches.length; i++) {
const batch = batches[i]; const batch = batches[i];
logVerbose(`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`); logVerbose(
`Processing batch ${i + 1}/${batches.length} (${batch.length} files)`,
);
// Process batch in parallel // Process batch in parallel
const promises = batch.map(async (change) => { const promises = batch.map(async (change) => {
@@ -125,7 +127,10 @@ export class PrettierFormatter extends BaseFormatter {
this.stats.recordFileOperation(this.name, change.type, true); this.stats.recordFileOperation(this.name, change.type, true);
} catch (error) { } catch (error) {
this.stats.recordFileOperation(this.name, change.type, false); this.stats.recordFileOperation(this.name, change.type, false);
logger.log('error', `Failed to format ${change.path}: ${error.message}`); logger.log(
'error',
`Failed to format ${change.path}: ${error.message}`,
);
// Don't throw - continue with other files // Don't throw - continue with other files
} }
}); });
@@ -135,7 +140,7 @@ export class PrettierFormatter extends BaseFormatter {
await this.postExecute(); await this.postExecute();
} catch (error) { } catch (error) {
await this.context.rollbackOperation(); // Rollback removed - no longer tracking operations
throw error; throw error;
} finally { } finally {
this.stats.endModule(this.name, startTime); this.stats.endModule(this.name, startTime);
@@ -153,7 +158,7 @@ export class PrettierFormatter extends BaseFormatter {
const prettier = await import('prettier'); const prettier = await import('prettier');
const formatted = await prettier.format(content, { const formatted = await prettier.format(content, {
filepath: change.path, filepath: change.path,
...(await this.getPrettierConfig()) ...(await this.getPrettierConfig()),
}); });
// Only write if content actually changed // Only write if content actually changed
@@ -161,8 +166,6 @@ export class PrettierFormatter extends BaseFormatter {
await this.modifyFile(change.path, formatted); await this.modifyFile(change.path, formatted);
logVerbose(`Formatted ${change.path}`); logVerbose(`Formatted ${change.path}`);
} else { } else {
// Still update cache even if content didn't change
await this.cache.updateFileCache(change.path);
logVerbose(`No formatting changes for ${change.path}`); logVerbose(`No formatting changes for ${change.path}`);
} }
} catch (error) { } catch (error) {
@@ -181,7 +184,7 @@ export class PrettierFormatter extends BaseFormatter {
printWidth: 80, printWidth: 80,
tabWidth: 2, tabWidth: 2,
semi: true, semi: true,
arrowParens: 'always' arrowParens: 'always',
}); });
} }
} }

View File

@@ -8,12 +8,14 @@ export class ReadmeFormatter extends BaseFormatter {
} }
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
return [{ return [
{
type: 'modify', type: 'modify',
path: 'readme.md', path: 'readme.md',
module: this.name, module: this.name,
description: 'Ensure readme files exist' description: 'Ensure readme files exist',
}]; },
];
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {

View File

@@ -16,7 +16,8 @@ import { PrettierFormatter } from './formatters/prettier.formatter.js';
import { ReadmeFormatter } from './formatters/readme.formatter.js'; import { ReadmeFormatter } from './formatters/readme.formatter.js';
import { CopyFormatter } from './formatters/copy.formatter.js'; import { CopyFormatter } from './formatters/copy.formatter.js';
export let run = async (options: { export let run = async (
options: {
dryRun?: boolean; dryRun?: boolean;
yes?: boolean; yes?: boolean;
planOnly?: boolean; planOnly?: boolean;
@@ -26,7 +27,8 @@ export let run = async (options: {
interactive?: boolean; interactive?: boolean;
parallel?: boolean; parallel?: boolean;
verbose?: boolean; verbose?: boolean;
} = {}): Promise<any> => { } = {},
): Promise<any> => {
// Set verbose mode if requested // Set verbose mode if requested
if (options.verbose) { if (options.verbose) {
setVerboseMode(true); setVerboseMode(true);
@@ -34,7 +36,7 @@ export let run = async (options: {
const project = await Project.fromCwd(); const project = await Project.fromCwd();
const context = new FormatContext(); const context = new FormatContext();
await context.initializeCache(); // Initialize the cache system // Cache system removed - no longer needed
const planner = new FormatPlanner(); const planner = new FormatPlanner();
// Get configuration from npmextra // Get configuration from npmextra
@@ -49,24 +51,21 @@ export let run = async (options: {
autoRollbackOnError: true, autoRollbackOnError: true,
backupRetentionDays: 7, backupRetentionDays: 7,
maxBackupSize: '100MB', maxBackupSize: '100MB',
excludePatterns: ['node_modules/**', '.git/**'] excludePatterns: ['node_modules/**', '.git/**'],
}, },
modules: { modules: {
skip: [], skip: [],
only: [], only: [],
order: [] order: [],
}, },
parallel: true, parallel: true,
cache: { cache: {
enabled: true, enabled: true,
clean: true // Clean invalid entries from cache clean: true, // Clean invalid entries from cache
} },
}); });
// Clean cache if configured // Cache cleaning removed - no longer using cache system
if (formatConfig.cache.clean) {
await context.getChangeCache().clean();
}
// Override config with command options // Override config with command options
const interactive = options.interactive ?? formatConfig.interactive; const interactive = options.interactive ?? formatConfig.interactive;
@@ -89,7 +88,7 @@ export let run = async (options: {
]; ];
// Filter formatters based on configuration // Filter formatters based on configuration
const activeFormatters = formatters.filter(formatter => { const activeFormatters = formatters.filter((formatter) => {
if (formatConfig.modules.only.length > 0) { if (formatConfig.modules.only.length > 0) {
return formatConfig.modules.only.includes(formatter.name); return formatConfig.modules.only.includes(formatter.name);
} }
@@ -110,7 +109,10 @@ export let run = async (options: {
// Save plan if requested // Save plan if requested
if (options.savePlan) { if (options.savePlan) {
await plugins.smartfile.memory.toFs(JSON.stringify(plan, null, 2), options.savePlan); await plugins.smartfile.memory.toFs(
JSON.stringify(plan, null, 2),
options.savePlan,
);
logger.log('info', `Plan saved to ${options.savePlan}`); logger.log('info', `Plan saved to ${options.savePlan}`);
} }
@@ -132,7 +134,7 @@ export let run = async (options: {
type: 'confirm', type: 'confirm',
name: 'proceed', name: 'proceed',
message: 'Proceed with formatting?', message: 'Proceed with formatting?',
default: true default: true,
}); });
if (!(response as any).value) { if (!(response as any).value) {
@@ -142,7 +144,10 @@ export let run = async (options: {
} }
// Execute phase // Execute phase
logger.log('info', `Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`); logger.log(
'info',
`Executing format operations${parallel ? ' in parallel' : ' sequentially'}...`,
);
await planner.executePlan(plan, activeFormatters, context, parallel); await planner.executePlan(plan, activeFormatters, context, parallel);
// Finish statistics tracking // Finish statistics tracking
@@ -161,20 +166,10 @@ export let run = async (options: {
} }
logger.log('success', 'Format operations completed successfully!'); logger.log('success', 'Format operations completed successfully!');
} catch (error) { } catch (error) {
logger.log('error', `Format operation failed: ${error.message}`); logger.log('error', `Format operation failed: ${error.message}`);
// Automatic rollback if enabled // Rollback system has been removed for stability
if (formatConfig.rollback.enabled && formatConfig.rollback.autoRollbackOnError) {
logger.log('info', 'Attempting automatic rollback...');
try {
await context.rollbackOperation();
logger.log('success', 'Rollback completed successfully');
} catch (rollbackError) {
logger.log('error', `Rollback failed: ${rollbackError.message}`);
}
}
throw error; throw error;
} }
@@ -182,67 +177,13 @@ export let run = async (options: {
// Export CLI command handlers // Export CLI command handlers
export const handleRollback = async (operationId?: string): Promise<void> => { export const handleRollback = async (operationId?: string): Promise<void> => {
const context = new FormatContext(); logger.log('info', 'Rollback system has been disabled for stability');
const rollbackManager = context.getRollbackManager();
if (!operationId) {
// Rollback to last operation
const backups = await rollbackManager.listBackups();
const lastOperation = backups
.filter(op => op.status !== 'rolled-back')
.sort((a, b) => b.timestamp - a.timestamp)[0];
if (!lastOperation) {
logger.log('warn', 'No operations available for rollback');
return;
}
operationId = lastOperation.id;
}
try {
await rollbackManager.rollback(operationId);
logger.log('success', `Successfully rolled back operation ${operationId}`);
} catch (error) {
logger.log('error', `Rollback failed: ${error.message}`);
throw error;
}
}; };
export const handleListBackups = async (): Promise<void> => { export const handleListBackups = async (): Promise<void> => {
const context = new FormatContext(); logger.log('info', 'Backup system has been disabled for stability');
const rollbackManager = context.getRollbackManager();
const backups = await rollbackManager.listBackups();
if (backups.length === 0) {
logger.log('info', 'No backup operations found');
return;
}
console.log('\nAvailable backups:');
console.log('━'.repeat(50));
for (const backup of backups) {
const date = new Date(backup.timestamp).toLocaleString();
const status = backup.status;
const filesCount = backup.files.length;
console.log(`ID: ${backup.id}`);
console.log(`Date: ${date}`);
console.log(`Status: ${status}`);
console.log(`Files: ${filesCount}`);
console.log('─'.repeat(50));
}
}; };
export const handleCleanBackups = async (): Promise<void> => { export const handleCleanBackups = async (): Promise<void> => {
const context = new FormatContext(); logger.log('info', 'Backup cleaning has been disabled - backup system removed');
const rollbackManager = context.getRollbackManager();
// Get retention days from config
const npmextraConfig = new plugins.npmextra.Npmextra();
const retentionDays = npmextraConfig.dataFor<any>('gitzone.format.rollback.backupRetentionDays', 7);
await rollbackManager.cleanOldBackups(retentionDays);
logger.log('success', `Cleaned backups older than ${retentionDays} days`);
}; };

View File

@@ -9,7 +9,7 @@ export type IFormatOperation = {
}>; }>;
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back'; status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
error?: Error; error?: Error;
} };
export type IFormatPlan = { export type IFormatPlan = {
summary: { summary: {
@@ -32,7 +32,7 @@ export type IFormatPlan = {
message: string; message: string;
module: string; module: string;
}>; }>;
} };
export type IPlannedChange = { export type IPlannedChange = {
type: 'create' | 'modify' | 'delete'; type: 'create' | 'modify' | 'delete';
@@ -42,4 +42,4 @@ export type IPlannedChange = {
content?: string; // For create/modify operations content?: string; // For create/modify operations
diff?: string; diff?: string;
size?: number; size?: number;
} };

View File

@@ -35,7 +35,10 @@ export class Meta {
* sorts the metaRepoData * sorts the metaRepoData
*/ */
public async sortMetaRepoData() { public async sortMetaRepoData() {
const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []); const stringifiedMetadata = plugins.smartjson.stringify(
this.metaRepoData,
[],
);
this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata); this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
} }
@@ -45,11 +48,15 @@ export class Meta {
public async readDirectory() { public async readDirectory() {
await this.syncToRemote(true); await this.syncToRemote(true);
logger.log('info', `reading directory`); logger.log('info', `reading directory`);
const metaFileExists = plugins.smartfile.fs.fileExistsSync(this.filePaths.metaJson); const metaFileExists = plugins.smartfile.fs.fileExistsSync(
this.filePaths.metaJson,
);
if (!metaFileExists) { if (!metaFileExists) {
throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`); throw new Error(`meta file does not exist at ${this.filePaths.metaJson}`);
} }
this.metaRepoData = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson); this.metaRepoData = plugins.smartfile.fs.toObjectSync(
this.filePaths.metaJson,
);
} }
/** /**
@@ -76,7 +83,10 @@ export class Meta {
this.filePaths.metaJson, this.filePaths.metaJson,
); );
// write .gitignore to disk // write .gitignore to disk
plugins.smartfile.memory.toFsSync(await this.generateGitignore(), this.filePaths.gitIgnore); plugins.smartfile.memory.toFsSync(
await this.generateGitignore(),
this.filePaths.gitIgnore,
);
} }
/** /**
@@ -84,13 +94,17 @@ export class Meta {
*/ */
public async syncToRemote(gitCleanArg = false) { public async syncToRemote(gitCleanArg = false) {
logger.log('info', `syncing from origin master`); logger.log('info', `syncing from origin master`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git pull origin master`); await this.smartshellInstance.exec(
`cd ${this.cwd} && git pull origin master`,
);
if (gitCleanArg) { if (gitCleanArg) {
logger.log('info', `cleaning the repository from old directories`); logger.log('info', `cleaning the repository from old directories`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`); await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
} }
logger.log('info', `syncing to remote origin master`); logger.log('info', `syncing to remote origin master`);
await this.smartshellInstance.exec(`cd ${this.cwd} && git push origin master`); await this.smartshellInstance.exec(
`cd ${this.cwd} && git push origin master`,
);
} }
/** /**
@@ -98,7 +112,9 @@ export class Meta {
*/ */
public async updateLocalRepos() { public async updateLocalRepos() {
await this.syncToRemote(); await this.syncToRemote();
const projects = plugins.smartfile.fs.toObjectSync(this.filePaths.metaJson).projects; const projects = plugins.smartfile.fs.toObjectSync(
this.filePaths.metaJson,
).projects;
const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd); const preExistingFolders = plugins.smartfile.fs.listFoldersSync(this.cwd);
for (const preExistingFolderArg of preExistingFolders) { for (const preExistingFolderArg of preExistingFolders) {
if ( if (
@@ -107,14 +123,18 @@ export class Meta {
projectFolder.startsWith(preExistingFolderArg), projectFolder.startsWith(preExistingFolderArg),
) )
) { ) {
const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation( const response =
await plugins.smartinteraction.SmartInteract.getCliConfirmation(
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`, `Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
true, true,
); );
if (response) { if (response) {
logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`); logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
} else { } else {
logger.log('warn', `Not deleting ${preExistingFolderArg} by request!`); logger.log(
'warn',
`Not deleting ${preExistingFolderArg} by request!`,
);
} }
} }
} }
@@ -160,7 +180,9 @@ export class Meta {
*/ */
public async initProject() { public async initProject() {
await this.syncToRemote(true); await this.syncToRemote(true);
const fileExists = await plugins.smartfile.fs.fileExists(this.filePaths.metaJson); const fileExists = await plugins.smartfile.fs.fileExists(
this.filePaths.metaJson,
);
if (!fileExists) { if (!fileExists) {
await plugins.smartfile.memory.toFs( await plugins.smartfile.memory.toFs(
JSON.stringify({ JSON.stringify({
@@ -168,7 +190,10 @@ export class Meta {
}), }),
this.filePaths.metaJson, this.filePaths.metaJson,
); );
logger.log(`success`, `created a new .meta.json in directory ${this.cwd}`); logger.log(
`success`,
`created a new .meta.json in directory ${this.cwd}`,
);
await plugins.smartfile.memory.toFs( await plugins.smartfile.memory.toFs(
JSON.stringify({ JSON.stringify({
name: this.dirName, name: this.dirName,
@@ -176,9 +201,15 @@ export class Meta {
}), }),
this.filePaths.packageJson, this.filePaths.packageJson,
); );
logger.log(`success`, `created a new package.json in directory ${this.cwd}`); logger.log(
`success`,
`created a new package.json in directory ${this.cwd}`,
);
} else { } else {
logger.log(`error`, `directory ${this.cwd} already has a .metaJson file. Doing nothing.`); logger.log(
`error`,
`directory ${this.cwd} already has a .metaJson file. Doing nothing.`,
);
} }
await this.smartshellInstance.exec( await this.smartshellInstance.exec(
`cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`, `cd ${this.cwd} && git add -A && git commit -m "feat(project): init meta project for ${this.dirName}"`,
@@ -195,7 +226,9 @@ export class Meta {
const existingProject = this.metaRepoData.projects[projectNameArg]; const existingProject = this.metaRepoData.projects[projectNameArg];
if (existingProject) { if (existingProject) {
throw new Error('Project already exists! Please remove it first before adding it again.'); throw new Error(
'Project already exists! Please remove it first before adding it again.',
);
} }
this.metaRepoData.projects[projectNameArg] = gitUrlArg; this.metaRepoData.projects[projectNameArg] = gitUrlArg;
@@ -217,7 +250,10 @@ export class Meta {
const existingProject = this.metaRepoData.projects[projectNameArg]; const existingProject = this.metaRepoData.projects[projectNameArg];
if (!existingProject) { if (!existingProject) {
logger.log('error', `Project ${projectNameArg} does not exist! So it cannot be removed`); logger.log(
'error',
`Project ${projectNameArg} does not exist! So it cannot be removed`,
);
return; return;
} }
@@ -228,7 +264,9 @@ export class Meta {
await this.writeToDisk(); await this.writeToDisk();
logger.log('info', 'removing directory from cwd'); logger.log('info', 'removing directory from cwd');
await plugins.smartfile.fs.remove(plugins.path.join(paths.cwd, projectNameArg)); await plugins.smartfile.fs.remove(
plugins.path.join(paths.cwd, projectNameArg),
);
await this.updateLocalRepos(); await this.updateLocalRepos();
} }
} }

View File

@@ -16,7 +16,9 @@ export let run = () => {
* create a new project with 'gitzone template [template]' * create a new project with 'gitzone template [template]'
the following templates exist: ${(() => { the following templates exist: ${(() => {
let projects = `\n`; let projects = `\n`;
for (const template of plugins.smartfile.fs.listFoldersSync(paths.templatesDir)) { for (const template of plugins.smartfile.fs.listFoldersSync(
paths.templatesDir,
)) {
projects += ` - ${template}\n`; projects += ` - ${template}\n`;
} }
return projects; return projects;

View File

@@ -15,7 +15,9 @@ export const run = async (argvArg: any) => {
}); });
await smartshellInstance.execStrict(`cd ${paths.cwd} && git checkout master`); await smartshellInstance.execStrict(`cd ${paths.cwd} && git checkout master`);
await smartshellInstance.execStrict(`cd ${paths.cwd} && git pull origin master`); await smartshellInstance.execStrict(
`cd ${paths.cwd} && git pull origin master`,
);
await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`); await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`);
await provideNoGitFiles(); await provideNoGitFiles();

View File

@@ -16,7 +16,9 @@ export const isTemplate = async (templateNameArg: string) => {
export const getTemplate = async (templateNameArg: string) => { export const getTemplate = async (templateNameArg: string) => {
if (isTemplate(templateNameArg)) { if (isTemplate(templateNameArg)) {
const localScafTemplate = new plugins.smartscaf.ScafTemplate(getTemplatePath(templateNameArg)); const localScafTemplate = new plugins.smartscaf.ScafTemplate(
getTemplatePath(templateNameArg),
);
await localScafTemplate.readTemplateFromDir(); await localScafTemplate.readTemplateFromDir();
return localScafTemplate; return localScafTemplate;
} else { } else {
@@ -32,7 +34,8 @@ export const run = async (argvArg: any) => {
const answerBucket = await smartinteract.askQuestion({ const answerBucket = await smartinteract.askQuestion({
type: 'list', type: 'list',
default: 'npm', default: 'npm',
message: 'What template do you want to scaffold? (Only showing mpost common options)', message:
'What template do you want to scaffold? (Only showing mpost common options)',
name: 'templateName', name: 'templateName',
choices: ['npm', 'service', 'wcc', 'website'], choices: ['npm', 'service', 'wcc', 'website'],
}); });