Compare commits

..

2 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
38 changed files with 784 additions and 726 deletions

View File

@@ -1,5 +1,12 @@
# 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) ## 2025-08-08 - 1.16.7 - fix(core)
Improve formatting, logging, and rollback integrity in core modules Improve formatting, logging, and rollback integrity in core modules

View File

@@ -1,7 +1,7 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "1.16.7", "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.7', 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

@@ -2,32 +2,29 @@ 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();
} }
abstract get name(): string; abstract get name(): string;
abstract analyze(): Promise<IPlannedChange[]>; abstract analyze(): Promise<IPlannedChange[]>;
abstract applyChange(change: IPlannedChange): Promise<void>; abstract applyChange(change: IPlannedChange): Promise<void>;
async execute(changes: IPlannedChange[]): Promise<void> { async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name); const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name); this.stats.startModule(this.name);
try { try {
await this.preExecute(); await this.preExecute();
for (const change of changes) { for (const change of changes) {
try { try {
await this.applyChange(change); await this.applyChange(change);
@@ -37,57 +34,37 @@ export abstract class BaseFormatter {
throw error; throw error;
} }
} }
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);
} }
} }
protected async preExecute(): Promise<void> { protected async preExecute(): Promise<void> {
// Override in subclasses if needed // Override in subclasses if needed
} }
protected async postExecute(): Promise<void> { protected async postExecute(): Promise<void> {
// Override in subclasses if needed // Override in subclasses if needed
} }
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

@@ -18,32 +18,32 @@ export class ChangeCache {
private cacheDir: string; private cacheDir: string;
private manifestPath: string; private manifestPath: string;
private cacheVersion = '1.0.0'; private cacheVersion = '1.0.0';
constructor() { constructor() {
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache'); this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json'); this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
await plugins.smartfile.fs.ensureDir(this.cacheDir); await plugins.smartfile.fs.ensureDir(this.cacheDir);
} }
async getManifest(): Promise<ICacheManifest> { async getManifest(): Promise<ICacheManifest> {
const defaultManifest: ICacheManifest = { const defaultManifest: ICacheManifest = {
version: this.cacheVersion, version: this.cacheVersion,
lastFormat: 0, lastFormat: 0,
files: [] files: [],
}; };
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath); const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
if (!exists) { if (!exists) {
return defaultManifest; return defaultManifest;
} }
try { try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath); const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content); const manifest = JSON.parse(content);
// Validate the manifest structure // Validate the manifest structure
if (this.isValidManifest(manifest)) { if (this.isValidManifest(manifest)) {
return manifest; return manifest;
@@ -52,7 +52,9 @@ export class ChangeCache {
return defaultManifest; return defaultManifest;
} }
} catch (error) { } catch (error) {
console.warn(`Failed to read cache manifest: ${error.message}, returning default manifest`); console.warn(
`Failed to read cache manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file // Try to delete the corrupted file
try { try {
await plugins.smartfile.fs.remove(this.manifestPath); await plugins.smartfile.fs.remove(this.manifestPath);
@@ -62,168 +64,160 @@ export class ChangeCache {
return defaultManifest; return defaultManifest;
} }
} }
async saveManifest(manifest: ICacheManifest): Promise<void> { async saveManifest(manifest: ICacheManifest): Promise<void> {
// Validate before saving // Validate before saving
if (!this.isValidManifest(manifest)) { if (!this.isValidManifest(manifest)) {
throw new Error('Invalid manifest structure, cannot save'); throw new Error('Invalid manifest structure, cannot save');
} }
// Use atomic write: write to temp file, then move it // Ensure directory exists
const tempPath = `${this.manifestPath}.tmp`; await plugins.smartfile.fs.ensureDir(this.cacheDir);
try { // Write directly with proper JSON stringification
// Write to temporary file const jsonContent = JSON.stringify(manifest, null, 2);
const jsonContent = JSON.stringify(manifest, null, 2); await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
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;
}
} }
async hasFileChanged(filePath: string): Promise<boolean> { async hasFileChanged(filePath: string): Promise<boolean> {
const absolutePath = plugins.path.isAbsolute(filePath) const absolutePath = plugins.path.isAbsolute(filePath)
? filePath ? filePath
: plugins.path.join(paths.cwd, filePath); : plugins.path.join(paths.cwd, filePath);
// Check if file exists // Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath); const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) { if (!exists) {
return true; // File doesn't exist, so it's "changed" (will be created) return true; // File doesn't exist, so it's "changed" (will be created)
} }
// Get current file stats // Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath); const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories // Skip directories
if (stats.isDirectory()) { if (stats.isDirectory()) {
return false; // Directories are not processed return false; // Directories are not processed
} }
const content = plugins.smartfile.fs.toStringSync(absolutePath); const content = plugins.smartfile.fs.toStringSync(absolutePath);
const currentChecksum = this.calculateChecksum(content); const currentChecksum = this.calculateChecksum(content);
// 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.size !== stats.size || cachedFile.checksum !== currentChecksum ||
cachedFile.modified !== stats.mtimeMs; cachedFile.size !== stats.size ||
cachedFile.modified !== stats.mtimeMs
);
} }
async updateFileCache(filePath: string): Promise<void> { async updateFileCache(filePath: string): Promise<void> {
const absolutePath = plugins.path.isAbsolute(filePath) const absolutePath = plugins.path.isAbsolute(filePath)
? filePath ? filePath
: plugins.path.join(paths.cwd, filePath); : plugins.path.join(paths.cwd, filePath);
// Get current file stats // Get current file stats
const stats = await plugins.smartfile.fs.stat(absolutePath); const stats = await plugins.smartfile.fs.stat(absolutePath);
// Skip directories // Skip directories
if (stats.isDirectory()) { if (stats.isDirectory()) {
return; // Don't cache directories return; // Don't cache directories
} }
const content = 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) {
manifest.files[existingIndex] = cacheEntry; manifest.files[existingIndex] = cacheEntry;
} else { } else {
manifest.files.push(cacheEntry); manifest.files.push(cacheEntry);
} }
manifest.lastFormat = Date.now(); manifest.lastFormat = Date.now();
await this.saveManifest(manifest); await this.saveManifest(manifest);
} }
async getChangedFiles(filePaths: string[]): Promise<string[]> { async getChangedFiles(filePaths: string[]): Promise<string[]> {
const changedFiles: string[] = []; const changedFiles: string[] = [];
for (const filePath of filePaths) { for (const filePath of filePaths) {
if (await this.hasFileChanged(filePath)) { if (await this.hasFileChanged(filePath)) {
changedFiles.push(filePath); changedFiles.push(filePath);
} }
} }
return changedFiles; return changedFiles;
} }
async clean(): Promise<void> { async clean(): Promise<void> {
const manifest = await this.getManifest(); const manifest = await this.getManifest();
const validFiles: IFileCache[] = []; const validFiles: IFileCache[] = [];
// Remove entries for files that no longer exist // Remove entries for files that no longer exist
for (const file of manifest.files) { for (const file of manifest.files) {
const absolutePath = plugins.path.isAbsolute(file.path) const absolutePath = plugins.path.isAbsolute(file.path)
? file.path ? file.path
: plugins.path.join(paths.cwd, file.path); : plugins.path.join(paths.cwd, file.path);
if (await plugins.smartfile.fs.fileExists(absolutePath)) { if (await plugins.smartfile.fs.fileExists(absolutePath)) {
validFiles.push(file); validFiles.push(file);
} }
} }
manifest.files = validFiles; manifest.files = validFiles;
await this.saveManifest(manifest); await this.saveManifest(manifest);
} }
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 { private isValidManifest(manifest: any): manifest is ICacheManifest {
// Check if manifest has the required structure // Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') { if (!manifest || typeof manifest !== 'object') {
return false; return false;
} }
// Check required fields // Check required fields
if (typeof manifest.version !== 'string' || if (
typeof manifest.lastFormat !== 'number' || typeof manifest.version !== 'string' ||
!Array.isArray(manifest.files)) { typeof manifest.lastFormat !== 'number' ||
!Array.isArray(manifest.files)
) {
return false; return false;
} }
// Check each file entry // Check each file entry
for (const file of manifest.files) { for (const file of manifest.files) {
if (!file || typeof file !== 'object' || if (
typeof file.path !== 'string' || !file ||
typeof file.checksum !== 'string' || typeof file !== 'object' ||
typeof file.modified !== 'number' || typeof file.path !== 'string' ||
typeof file.size !== 'number') { typeof file.checksum !== 'string' ||
typeof file.modified !== 'number' ||
typeof file.size !== 'number'
) {
return false; return false;
} }
} }
return true; return true;
} }
} }

View File

@@ -9,35 +9,42 @@ export interface IModuleDependency {
export class DependencyAnalyzer { export class DependencyAnalyzer {
private moduleDependencies: Map<string, IModuleDependency> = new Map(); private moduleDependencies: Map<string, IModuleDependency> = new Map();
constructor() { constructor() {
this.initializeDependencies(); this.initializeDependencies();
} }
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
for (const [module, deps] of Object.entries(dependencies)) { for (const [module, deps] of Object.entries(dependencies)) {
this.moduleDependencies.set(module, { this.moduleDependencies.set(module, {
module, module,
dependencies: new Set(deps), dependencies: new Set(deps),
dependents: new Set() dependents: new Set(),
}); });
} }
// Build reverse dependencies (dependents) // Build reverse dependencies (dependents)
for (const [module, deps] of Object.entries(dependencies)) { for (const [module, deps] of Object.entries(dependencies)) {
for (const dep of deps) { for (const dep of deps) {
@@ -48,34 +55,35 @@ 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[][] = [];
while (executed.size < modules.length) { while (executed.size < modules.length) {
const currentGroup: BaseFormatter[] = []; const currentGroup: BaseFormatter[] = [];
for (const module of modules) { for (const module of modules) {
if (executed.has(module.name)) continue; if (executed.has(module.name)) continue;
const dependency = this.moduleDependencies.get(module.name); const dependency = this.moduleDependencies.get(module.name);
if (!dependency) { if (!dependency) {
// Unknown module, execute in isolation // Unknown module, execute in isolation
currentGroup.push(module); currentGroup.push(module);
continue; continue;
} }
// 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);
} }
} }
if (currentGroup.length === 0) { if (currentGroup.length === 0) {
// Circular dependency or error - execute remaining modules // Circular dependency or error - execute remaining modules
for (const module of modules) { for (const module of modules) {
@@ -84,24 +92,26 @@ export class DependencyAnalyzer {
} }
} }
} }
currentGroup.forEach(m => executed.add(m.name)); currentGroup.forEach((m) => executed.add(m.name));
groups.push(currentGroup); groups.push(currentGroup);
} }
return groups; return groups;
} }
canRunInParallel(module1: string, module2: string): boolean { canRunInParallel(module1: string, module2: string): boolean {
const dep1 = this.moduleDependencies.get(module1); const dep1 = this.moduleDependencies.get(module1);
const dep2 = this.moduleDependencies.get(module2); const dep2 = this.moduleDependencies.get(module2);
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 (
!dep2.dependencies.has(module1) && !dep1.dependencies.has(module2) &&
!dep1.dependents.has(module2) && !dep2.dependencies.has(module1) &&
!dep2.dependents.has(module1); !dep1.dependents.has(module2) &&
!dep2.dependents.has(module1)
);
} }
} }

View File

@@ -4,72 +4,85 @@ 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;
} }
async generateDiffForChange(change: IPlannedChange): Promise<string | null> { async generateDiffForChange(change: IPlannedChange): Promise<string | null> {
if (change.type !== 'modify') { if (change.type !== 'modify') {
return null; return null;
} }
try { try {
const exists = await plugins.smartfile.fs.fileExists(change.path); const exists = await plugins.smartfile.fs.fileExists(change.path);
if (!exists) { if (!exists) {
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;
} }
} }
displayDiff(filePath: string, diff?: string): void { displayDiff(filePath: string, diff?: string): void {
const diffToShow = diff || this.diffs.get(filePath); const diffToShow = diff || this.diffs.get(filePath);
if (!diffToShow) { if (!diffToShow) {
logger.log('warn', `No diff available for ${filePath}`); logger.log('warn', `No diff available for ${filePath}`);
return; return;
} }
console.log(`\n${this.formatDiffHeader(filePath)}`); console.log(`\n${this.formatDiffHeader(filePath)}`);
console.log(this.colorDiff(diffToShow)); console.log(this.colorDiff(diffToShow));
console.log('━'.repeat(50)); console.log('━'.repeat(50));
} }
displayAllDiffs(): void { displayAllDiffs(): void {
if (this.diffs.size === 0) { if (this.diffs.size === 0) {
logger.log('info', 'No diffs to display'); logger.log('info', 'No diffs to display');
return; return;
} }
console.log('\nFile Changes:'); console.log('\nFile Changes:');
console.log('═'.repeat(50)); console.log('═'.repeat(50));
for (const [filePath, diff] of this.diffs) { for (const [filePath, diff] of this.diffs) {
this.displayDiff(filePath, diff); this.displayDiff(filePath, diff);
} }
} }
private formatDiffHeader(filePath: string): string { private formatDiffHeader(filePath: string): string {
return `📄 ${filePath}`; return `📄 ${filePath}`;
} }
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('---')) {
@@ -80,29 +93,32 @@ export class DiffReporter {
return line; return line;
} }
}); });
return coloredLines.join('\n'); return coloredLines.join('\n');
} }
async saveDiffReport(outputPath: string): Promise<void> { async saveDiffReport(outputPath: string): Promise<void> {
const report = { const report = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
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}`);
} }
hasAnyDiffs(): boolean { hasAnyDiffs(): boolean {
return this.diffs.size > 0; return this.diffs.size > 0;
} }
getDiffCount(): number { getDiffCount(): number {
return this.diffs.size; return this.diffs.size;
} }
} }

View File

@@ -1,65 +1,14 @@
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

@@ -10,7 +10,7 @@ export class FormatPlanner {
private plannedChanges: Map<string, IPlannedChange[]> = new Map(); private plannedChanges: Map<string, IPlannedChange[]> = new Map();
private dependencyAnalyzer = new DependencyAnalyzer(); private dependencyAnalyzer = new DependencyAnalyzer();
private diffReporter = new DiffReporter(); private diffReporter = new DiffReporter();
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> { async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
const plan: IFormatPlan = { const plan: IFormatPlan = {
summary: { summary: {
@@ -18,20 +18,20 @@ 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) {
try { try {
const changes = await module.analyze(); const changes = await module.analyze();
this.plannedChanges.set(module.name, changes); this.plannedChanges.set(module.name, changes);
for (const change of changes) { for (const change of changes) {
plan.changes.push(change); plan.changes.push(change);
// Update summary // Update summary
switch (change.type) { switch (change.type) {
case 'create': case 'create':
@@ -49,67 +49,51 @@ 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 for (const module of modules) {
const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules); const changes = this.plannedChanges.get(module.name) || [];
logger.log('info', `Executing formatters in ${executionGroups.length} groups...`); if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
for (let i = 0; i < executionGroups.length; i++) { await module.execute(changes);
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) {
const changes = this.plannedChanges.get(module.name) || [];
if (changes.length > 0) {
logger.log('info', `Executing ${module.name} formatter...`);
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`);
@@ -118,7 +102,7 @@ export class FormatPlanner {
console.log(`${plan.summary.filesRemoved} deleted files`); console.log(`${plan.summary.filesRemoved} deleted files`);
console.log(''); console.log('');
console.log('Changes by module:'); console.log('Changes by module:');
// Group changes by module // Group changes by module
const changesByModule = new Map<string, IPlannedChange[]>(); const changesByModule = new Map<string, IPlannedChange[]>();
for (const change of plan.changes) { for (const change of plan.changes) {
@@ -126,14 +110,16 @@ export class FormatPlanner {
moduleChanges.push(change); moduleChanges.push(change);
changesByModule.set(change.module, moduleChanges); changesByModule.set(change.module, moduleChanges);
} }
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);
console.log(` ${icon} ${change.path} - ${change.description}`); console.log(` ${icon} ${change.path} - ${change.description}`);
// Show diff for modified files if detailed view is requested // Show diff for modified files if detailed view is requested
if (detailed && change.type === 'modify') { if (detailed && change.type === 'modify') {
const diff = await this.diffReporter.generateDiffForChange(change); const diff = await this.diffReporter.generateDiffForChange(change);
@@ -143,7 +129,7 @@ export class FormatPlanner {
} }
} }
} }
if (plan.warnings.length > 0) { if (plan.warnings.length > 0) {
console.log('\nWarnings:'); console.log('\nWarnings:');
for (const warning of plan.warnings) { for (const warning of plan.warnings) {
@@ -151,26 +137,26 @@ export class FormatPlanner {
console.log(` ${icon} ${warning.message}`); console.log(` ${icon} ${warning.message}`);
} }
} }
console.log('\n' + '━'.repeat(50)); console.log('\n' + '━'.repeat(50));
} }
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] || '📁';
} }
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string { private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
switch (type) { switch (type) {
case 'create': case 'create':
@@ -181,4 +167,4 @@ export class FormatPlanner {
return '❌'; return '❌';
} }
} }
} }

View File

@@ -30,7 +30,7 @@ export interface IFormatStats {
export class FormatStats { export class FormatStats {
private stats: IFormatStats; private stats: IFormatStats;
constructor() { constructor() {
this.stats = { this.stats = {
totalExecutionTime: 0, totalExecutionTime: 0,
@@ -44,11 +44,11 @@ export class FormatStats {
totalDeleted: 0, totalDeleted: 0,
totalErrors: 0, totalErrors: 0,
cacheHits: 0, cacheHits: 0,
cacheMisses: 0 cacheMisses: 0,
} },
}; };
} }
startModule(moduleName: string): void { startModule(moduleName: string): void {
this.stats.moduleStats.set(moduleName, { this.stats.moduleStats.set(moduleName, {
name: moduleName, name: moduleName,
@@ -58,31 +58,35 @@ export class FormatStats {
successes: 0, successes: 0,
filesCreated: 0, filesCreated: 0,
filesModified: 0, filesModified: 0,
filesDeleted: 0 filesDeleted: 0,
}); });
} }
moduleStartTime(moduleName: string): number { moduleStartTime(moduleName: string): number {
return Date.now(); return Date.now();
} }
endModule(moduleName: string, startTime: number): void { endModule(moduleName: string, startTime: number): void {
const moduleStats = this.stats.moduleStats.get(moduleName); const moduleStats = this.stats.moduleStats.get(moduleName);
if (moduleStats) { if (moduleStats) {
moduleStats.executionTime = Date.now() - startTime; moduleStats.executionTime = Date.now() - startTime;
} }
} }
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;
moduleStats.filesProcessed++; moduleStats.filesProcessed++;
if (success) { if (success) {
moduleStats.successes++; moduleStats.successes++;
this.stats.overallStats.totalFiles++; this.stats.overallStats.totalFiles++;
switch (operation) { switch (operation) {
case 'create': case 'create':
moduleStats.filesCreated++; moduleStats.filesCreated++;
@@ -102,53 +106,66 @@ export class FormatStats {
this.stats.overallStats.totalErrors++; this.stats.overallStats.totalErrors++;
} }
} }
recordCacheHit(): void { recordCacheHit(): void {
this.stats.overallStats.cacheHits++; this.stats.overallStats.cacheHits++;
} }
recordCacheMiss(): void { recordCacheMiss(): void {
this.stats.overallStats.cacheMisses++; this.stats.overallStats.cacheMisses++;
} }
finish(): void { finish(): void {
this.stats.endTime = Date.now(); this.stats.endTime = Date.now();
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime; this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
} }
displayStats(): void { displayStats(): void {
console.log('\n📊 Format Operation Statistics:'); console.log('\n📊 Format Operation Statistics:');
console.log('═'.repeat(50)); console.log('═'.repeat(50));
// 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}`);
} }
// Module stats // Module stats
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) {
console.log(` • Created: ${moduleStats.filesCreated}`); console.log(` • Created: ${moduleStats.filesCreated}`);
} }
@@ -158,27 +175,30 @@ export class FormatStats {
if (moduleStats.filesDeleted > 0) { if (moduleStats.filesDeleted > 0) {
console.log(` • Deleted: ${moduleStats.filesDeleted}`); console.log(` • Deleted: ${moduleStats.filesDeleted}`);
} }
if (moduleStats.errors > 0) { if (moduleStats.errors > 0) {
console.log(` ❌ Errors: ${moduleStats.errors}`); console.log(` ❌ Errors: ${moduleStats.errors}`);
} }
} }
console.log('\n' + '═'.repeat(50)); console.log('\n' + '═'.repeat(50));
} }
async saveReport(outputPath: string): Promise<void> { async saveReport(outputPath: string): Promise<void> {
const report = { const report = {
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}`);
} }
private formatDuration(ms: number): string { private formatDuration(ms: number): string {
if (ms < 1000) { if (ms < 1000) {
return `${ms}ms`; return `${ms}ms`;
@@ -190,20 +210,20 @@ export class FormatStats {
return `${minutes}m ${seconds}s`; return `${minutes}m ${seconds}s`;
} }
} }
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

@@ -5,206 +5,227 @@ import type { IFormatOperation } from './interfaces.format.js';
export class RollbackManager { export class RollbackManager {
private backupDir: string; private backupDir: string;
private manifestPath: string; private manifestPath: string;
constructor() { constructor() {
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups'); this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json'); this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
} }
async createOperation(): Promise<IFormatOperation> { async createOperation(): Promise<IFormatOperation> {
await this.ensureBackupDir(); await this.ensureBackupDir();
const operation: IFormatOperation = { const operation: IFormatOperation = {
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);
return operation; return operation;
} }
async backupFile(filepath: string, operationId: string): Promise<void> { async backupFile(filepath: string, 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`); throw new Error(`Operation ${operationId} not found`);
} }
const absolutePath = plugins.path.isAbsolute(filepath) const absolutePath = plugins.path.isAbsolute(filepath)
? filepath ? filepath
: plugins.path.join(paths.cwd, filepath); : plugins.path.join(paths.cwd, filepath);
// Check if file exists // Check if file exists
const exists = await plugins.smartfile.fs.fileExists(absolutePath); const exists = await plugins.smartfile.fs.fileExists(absolutePath);
if (!exists) { if (!exists) {
// File doesn't exist yet (will be created), so we skip backup // File doesn't exist yet (will be created), so we skip backup
return; return;
} }
// Read file content and metadata // Read file content and metadata
const content = 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);
// Create backup // Create backup
const backupPath = this.getBackupPath(operationId, filepath); const backupPath = this.getBackupPath(operationId, filepath);
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath)); await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
await plugins.smartfile.memory.toFs(content, backupPath); await plugins.smartfile.memory.toFs(content, backupPath);
// Update operation // Update operation
operation.files.push({ operation.files.push({
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);
} }
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') {
throw new Error(`Operation ${operationId} has already been rolled back`); throw new Error(`Operation ${operationId} has already been rolled back`);
} }
// Restore files in reverse order // Restore files in reverse order
for (let i = operation.files.length - 1; i >= 0; i--) { for (let i = operation.files.length - 1; i >= 0; i--) {
const file = operation.files[i]; const file = operation.files[i];
const absolutePath = plugins.path.isAbsolute(file.path) const absolutePath = plugins.path.isAbsolute(file.path)
? file.path ? file.path
: plugins.path.join(paths.cwd, file.path); : plugins.path.join(paths.cwd, file.path);
// Verify backup integrity // Verify backup integrity
const backupPath = this.getBackupPath(operationId, file.path); const backupPath = this.getBackupPath(operationId, file.path);
const backupContent = 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) {
throw new Error(`Backup integrity check failed for ${file.path}`); throw new Error(`Backup integrity check failed for ${file.path}`);
} }
// Restore file // Restore file
await plugins.smartfile.memory.toFs(file.originalContent, absolutePath); await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
// Restore permissions // Restore permissions
const mode = parseInt(file.permissions, 8); const mode = parseInt(file.permissions, 8);
// Note: Permissions restoration may not work on all platforms // Note: Permissions restoration may not work on all platforms
} }
// Update operation status // Update operation status
operation.status = 'rolled-back'; operation.status = 'rolled-back';
await this.updateManifest(operation); await this.updateManifest(operation);
} }
async markComplete(operationId: string): Promise<void> { async markComplete(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`); throw new Error(`Operation ${operationId} not found`);
} }
operation.status = 'completed'; operation.status = 'completed';
await this.updateManifest(operation); await this.updateManifest(operation);
} }
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);
} }
async verifyBackup(operationId: string): Promise<boolean> { async verifyBackup(operationId: string): Promise<boolean> {
const operation = await this.getOperation(operationId); const operation = await this.getOperation(operationId);
if (!operation) { if (!operation) {
return false; return false;
} }
for (const file of operation.files) { for (const file of operation.files) {
const backupPath = this.getBackupPath(operationId, file.path); const backupPath = this.getBackupPath(operationId, file.path);
const exists = await plugins.smartfile.fs.fileExists(backupPath); const exists = await plugins.smartfile.fs.fileExists(backupPath);
if (!exists) { if (!exists) {
return false; return false;
} }
const content = 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) {
return false; return false;
} }
} }
return true; return true;
} }
async listBackups(): Promise<IFormatOperation[]> { async listBackups(): Promise<IFormatOperation[]> {
const manifest = await this.getManifest(); const manifest = await this.getManifest();
return manifest.operations; return manifest.operations;
} }
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 {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const random = Math.random().toString(36).substring(2, 8); const random = Math.random().toString(36).substring(2, 8);
return `${timestamp}-${random}`; return `${timestamp}-${random}`;
} }
private getBackupPath(operationId: string, filepath: string): string { private getBackupPath(operationId: string, filepath: string): string {
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 {
return plugins.crypto.createHash('sha256').update(content).digest('hex'); return plugins.crypto.createHash('sha256').update(content).digest('hex');
} }
private async getManifest(): Promise<{ operations: IFormatOperation[] }> { private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
const defaultManifest = { operations: [] }; 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 defaultManifest; return defaultManifest;
} }
try { try {
const content = plugins.smartfile.fs.toStringSync(this.manifestPath); const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
const manifest = JSON.parse(content); const manifest = JSON.parse(content);
// Validate the manifest structure // Validate the manifest structure
if (this.isValidManifest(manifest)) { if (this.isValidManifest(manifest)) {
return manifest; return manifest;
} else { } else {
console.warn('Invalid rollback manifest structure, returning default manifest'); console.warn(
'Invalid rollback manifest structure, returning default manifest',
);
return defaultManifest; return defaultManifest;
} }
} catch (error) { } catch (error) {
console.warn(`Failed to read rollback manifest: ${error.message}, returning default manifest`); console.warn(
`Failed to read rollback manifest: ${error.message}, returning default manifest`,
);
// Try to delete the corrupted file // Try to delete the corrupted file
try { try {
await plugins.smartfile.fs.remove(this.manifestPath); await plugins.smartfile.fs.remove(this.manifestPath);
@@ -214,85 +235,84 @@ export class RollbackManager {
return defaultManifest; return defaultManifest;
} }
} }
private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> { private async saveManifest(manifest: {
operations: IFormatOperation[];
}): Promise<void> {
// Validate before saving // Validate before saving
if (!this.isValidManifest(manifest)) { if (!this.isValidManifest(manifest)) {
throw new Error('Invalid rollback manifest structure, cannot save'); throw new Error('Invalid rollback manifest structure, cannot save');
} }
// Use atomic write: write to temp file, then move it // Ensure directory exists
const tempPath = `${this.manifestPath}.tmp`; await this.ensureBackupDir();
try { // Write directly with proper JSON stringification
// Write to temporary file const jsonContent = JSON.stringify(manifest, null, 2);
const jsonContent = JSON.stringify(manifest, null, 2); await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
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<IFormatOperation | null> { 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;
} else { } else {
manifest.operations.push(operation); manifest.operations.push(operation);
} }
await this.saveManifest(manifest); await this.saveManifest(manifest);
} }
private isValidManifest(manifest: any): manifest is { operations: IFormatOperation[] } { private isValidManifest(
manifest: any,
): manifest is { operations: IFormatOperation[] } {
// Check if manifest has the required structure // Check if manifest has the required structure
if (!manifest || typeof manifest !== 'object') { if (!manifest || typeof manifest !== 'object') {
return false; return false;
} }
// Check required fields // Check required fields
if (!Array.isArray(manifest.operations)) { if (!Array.isArray(manifest.operations)) {
return false; return false;
} }
// Check each operation entry // Check each operation entry
for (const operation of manifest.operations) { for (const operation of manifest.operations) {
if (!operation || typeof operation !== 'object' || if (
typeof operation.id !== 'string' || !operation ||
typeof operation.timestamp !== 'number' || typeof operation !== 'object' ||
typeof operation.status !== 'string' || typeof operation.id !== 'string' ||
!Array.isArray(operation.files)) { typeof operation.timestamp !== 'number' ||
typeof operation.status !== 'string' ||
!Array.isArray(operation.files)
) {
return false; return false;
} }
// Check each file in the operation // Check each file in the operation
for (const file of operation.files) { for (const file of operation.files) {
if (!file || typeof file !== 'object' || if (
typeof file.path !== 'string' || !file ||
typeof file.checksum !== 'string') { typeof file !== 'object' ||
typeof file.path !== 'string' ||
typeof file.checksum !== 'string'
) {
return false; return false;
} }
} }
} }
return true; 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

@@ -4,56 +4,59 @@ import { logger } from '../gitzone.logging.js';
export const run = async (projectArg: Project) => { export const run = async (projectArg: Project) => {
const gitzoneConfig = await projectArg.gitzoneConfig; const gitzoneConfig = await projectArg.gitzoneConfig;
// 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) {
logger.log('info', 'No copy patterns configured in npmextra.json'); logger.log('info', 'No copy patterns configured in npmextra.json');
return; return;
} }
for (const pattern of copyConfig.patterns) { for (const pattern of copyConfig.patterns) {
if (!pattern.from || !pattern.to) { if (!pattern.from || !pattern.to) {
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field'); logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
continue; continue;
} }
try { try {
// Handle glob patterns // Handle glob patterns
const files = await plugins.smartfile.fs.listFileTree('.', pattern.from); const files = await plugins.smartfile.fs.listFileTree('.', pattern.from);
for (const file of files) { for (const file of files) {
const sourcePath = file; const sourcePath = file;
let destPath = pattern.to; let destPath = pattern.to;
// If destination is a directory, preserve filename // If destination is a directory, preserve filename
if (pattern.to.endsWith('/')) { if (pattern.to.endsWith('/')) {
const filename = plugins.path.basename(file); const filename = plugins.path.basename(file);
destPath = plugins.path.join(pattern.to, filename); destPath = plugins.path.join(pattern.to, filename);
} }
// Handle template variables in destination path // Handle template variables in destination path
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);
} }
// Ensure destination directory exists // Ensure destination directory exists
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath)); await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath));
// Copy file // Copy file
await plugins.smartfile.fs.copy(sourcePath, destPath); await plugins.smartfile.fs.copy(sourcePath, destPath);
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}`,
);
} }
} }
}; };
@@ -79,4 +82,4 @@ export const run = async (projectArg: Project) => {
* } * }
* } * }
* } * }
*/ */

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

@@ -19,7 +19,7 @@ const ensureDependency = async (
: [dependencyArg, 'latest']; : [dependencyArg, 'latest'];
const targetSections: string[] = []; const targetSections: string[] = [];
switch (position) { switch (position) {
case 'dep': case 'dep':
targetSections.push('dependencies'); targetSections.push('dependencies');
@@ -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

@@ -7,13 +7,18 @@ export class CleanupFormatter extends BaseFormatter {
get name(): string { get name(): string {
return 'cleanup'; return 'cleanup';
} }
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
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);
if (exists) { if (exists) {
@@ -21,14 +26,14 @@ 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`,
}); });
} }
} }
return changes; return changes;
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
switch (change.type) { switch (change.type) {
case 'delete': case 'delete':
@@ -36,4 +41,4 @@ export class CleanupFormatter extends BaseFormatter {
break; break;
} }
} }
} }

View File

@@ -5,4 +5,4 @@ export class CopyFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'copy', formatCopy); super(context, project, 'copy', formatCopy);
} }
} }

View File

@@ -5,4 +5,4 @@ export class GitignoreFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'gitignore', formatGitignore); super(context, project, 'gitignore', formatGitignore);
} }
} }

View File

@@ -7,30 +7,37 @@ import * as plugins from '../mod.plugins.js';
export class LegacyFormatter extends BaseFormatter { 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;
} }
get name(): string { get name(): string {
return this.moduleName; return this.moduleName;
} }
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', {
path: '<various files>', type: 'modify',
module: this.name, path: '<various files>',
description: `Run ${this.name} formatter` module: this.name,
}]; description: `Run ${this.name} formatter`,
},
];
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
// Run the legacy format module // Run the legacy format module
await this.formatModule.run(this.project); await this.formatModule.run(this.project);
} }
} }

View File

@@ -5,4 +5,4 @@ export class LicenseFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'license', formatLicense); super(context, project, 'license', formatLicense);
} }
} }

View File

@@ -5,4 +5,4 @@ export class NpmextraFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'npmextra', formatNpmextra); super(context, project, 'npmextra', formatNpmextra);
} }
} }

View File

@@ -5,4 +5,4 @@ export class PackageJsonFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'packagejson', formatPackageJson); super(context, project, 'packagejson', formatPackageJson);
} }
} }

View File

@@ -7,21 +7,16 @@ export class PrettierFormatter extends BaseFormatter {
get name(): string { get name(): string {
return 'prettier'; return 'prettier';
} }
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
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}';
// Also format root-level config files // Also format root-level config files
const rootConfigFiles = [ const rootConfigFiles = [
'package.json', 'package.json',
@@ -36,33 +31,36 @@ 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
const allFiles: string[] = []; const allFiles: string[] = [];
// 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);
} }
// Add root config files // Add root config files
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);
} }
// Remove duplicates // Remove duplicates
const uniqueFiles = [...new Set(allFiles)]; const uniqueFiles = [...new Set(allFiles)];
// Get all files that match the pattern // Get all files that match the pattern
const files = uniqueFiles; const files = uniqueFiles;
// Ensure we only process actual files (not directories) // Ensure we only process actual files (not directories)
const validFiles: string[] = []; const validFiles: string[] = [];
for (const file of files) { for (const file of files) {
@@ -76,48 +74,52 @@ export class PrettierFormatter extends BaseFormatter {
logVerbose(`Skipping ${file} - cannot access: ${error.message}`); logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
} }
} }
// 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;
} }
changes.push({ changes.push({
type: 'modify', type: 'modify',
path: file, path: file,
module: this.name, module: this.name,
description: 'Format with Prettier' description: 'Format with Prettier',
}); });
} }
logger.log('info', `Found ${changes.length} files to format with Prettier`); logger.log('info', `Found ${changes.length} files to format with Prettier`);
return changes; return changes;
} }
async execute(changes: IPlannedChange[]): Promise<void> { async execute(changes: IPlannedChange[]): Promise<void> {
const startTime = this.stats.moduleStartTime(this.name); const startTime = this.stats.moduleStartTime(this.name);
this.stats.startModule(this.name); this.stats.startModule(this.name);
try { try {
await this.preExecute(); await this.preExecute();
// Batch process files // Batch process files
const batchSize = 10; // Process 10 files at a time const batchSize = 10; // Process 10 files at a time
const batches: IPlannedChange[][] = []; const batches: IPlannedChange[][] = [];
for (let i = 0; i < changes.length; i += batchSize) { for (let i = 0; i < changes.length; i += batchSize) {
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) => {
try { try {
@@ -125,44 +127,45 @@ 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
} }
}); });
await Promise.all(promises); await Promise.all(promises);
} }
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);
} }
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
if (change.type !== 'modify') return; if (change.type !== 'modify') return;
try { try {
// Read current content // Read current content
const content = plugins.smartfile.fs.toStringSync(change.path); const content = plugins.smartfile.fs.toStringSync(change.path);
// Format with prettier // Format with prettier
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
if (formatted !== content) { if (formatted !== content) {
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) {
@@ -170,7 +173,7 @@ export class PrettierFormatter extends BaseFormatter {
throw error; throw error;
} }
} }
private async getPrettierConfig(): Promise<any> { private async getPrettierConfig(): Promise<any> {
// Try to load prettier config from the project // Try to load prettier config from the project
const prettierConfig = new plugins.npmextra.Npmextra(); const prettierConfig = new plugins.npmextra.Npmextra();
@@ -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

@@ -6,17 +6,19 @@ export class ReadmeFormatter extends BaseFormatter {
get name(): string { get name(): string {
return 'readme'; return 'readme';
} }
async analyze(): Promise<IPlannedChange[]> { async analyze(): Promise<IPlannedChange[]> {
return [{ return [
type: 'modify', {
path: 'readme.md', type: 'modify',
module: this.name, path: 'readme.md',
description: 'Ensure readme files exist' module: this.name,
}]; description: 'Ensure readme files exist',
},
];
} }
async applyChange(change: IPlannedChange): Promise<void> { async applyChange(change: IPlannedChange): Promise<void> {
await formatReadme.run(); await formatReadme.run();
} }
} }

View File

@@ -5,4 +5,4 @@ export class TemplatesFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'templates', formatTemplates); super(context, project, 'templates', formatTemplates);
} }
} }

View File

@@ -5,4 +5,4 @@ export class TsconfigFormatter extends LegacyFormatter {
constructor(context: any, project: any) { constructor(context: any, project: any) {
super(context, project, 'tsconfig', formatTsconfig); super(context, project, 'tsconfig', formatTsconfig);
} }
} }

View File

@@ -16,27 +16,29 @@ 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 (
dryRun?: boolean; options: {
yes?: boolean; dryRun?: boolean;
planOnly?: boolean; yes?: boolean;
savePlan?: string; planOnly?: boolean;
fromPlan?: string; savePlan?: string;
detailed?: boolean; fromPlan?: string;
interactive?: boolean; detailed?: boolean;
parallel?: boolean; interactive?: boolean;
verbose?: boolean; parallel?: boolean;
} = {}): Promise<any> => { verbose?: boolean;
} = {},
): Promise<any> => {
// Set verbose mode if requested // Set verbose mode if requested
if (options.verbose) { if (options.verbose) {
setVerboseMode(true); setVerboseMode(true);
} }
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
const npmextraConfig = new plugins.npmextra.Npmextra(); const npmextraConfig = new plugins.npmextra.Npmextra();
const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', { const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', {
@@ -49,30 +51,27 @@ 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;
const autoApprove = options.yes ?? formatConfig.autoApprove; const autoApprove = options.yes ?? formatConfig.autoApprove;
const parallel = options.parallel ?? formatConfig.parallel; const parallel = options.parallel ?? formatConfig.parallel;
try { try {
// Initialize formatters // Initialize formatters
const formatters = [ const formatters = [
@@ -87,9 +86,9 @@ export let run = async (options: {
new ReadmeFormatter(context, project), new ReadmeFormatter(context, project),
new CopyFormatter(context, project), new CopyFormatter(context, project),
]; ];
// 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);
} }
@@ -98,33 +97,36 @@ export let run = async (options: {
} }
return true; return true;
}); });
// Plan phase // Plan phase
logger.log('info', 'Analyzing project for format operations...'); logger.log('info', 'Analyzing project for format operations...');
let plan = options.fromPlan let plan = options.fromPlan
? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan)) ? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan))
: await planner.planFormat(activeFormatters); : await planner.planFormat(activeFormatters);
// Display plan // Display plan
await planner.displayPlan(plan, options.detailed); await planner.displayPlan(plan, options.detailed);
// 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}`);
} }
// Exit if plan-only mode // Exit if plan-only mode
if (options.planOnly) { if (options.planOnly) {
return; return;
} }
// Dry-run mode // Dry-run mode
if (options.dryRun) { if (options.dryRun) {
logger.log('info', 'Dry-run mode - no changes will be made'); logger.log('info', 'Dry-run mode - no changes will be made');
return; return;
} }
// Interactive confirmation // Interactive confirmation
if (interactive && !autoApprove) { if (interactive && !autoApprove) {
const interactInstance = new plugins.smartinteract.SmartInteract(); const interactInstance = new plugins.smartinteract.SmartInteract();
@@ -132,117 +134,56 @@ 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) {
logger.log('info', 'Format operation cancelled by user'); logger.log('info', 'Format operation cancelled by user');
return; return;
} }
} }
// 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
context.getFormatStats().finish(); context.getFormatStats().finish();
// Display statistics // Display statistics
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true); const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
if (showStats) { if (showStats) {
context.getFormatStats().displayStats(); context.getFormatStats().displayStats();
} }
// Save stats if requested // Save stats if requested
if (options.detailed) { if (options.detailed) {
const statsPath = `.nogit/format-stats-${Date.now()}.json`; const statsPath = `.nogit/format-stats-${Date.now()}.json`;
await context.getFormatStats().saveReport(statsPath); await context.getFormatStats().saveReport(statsPath);
} }
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;
} }
}; };
// 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 =
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`, await plugins.smartinteraction.SmartInteract.getCliConfirmation(
true, `Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
); 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'],
}); });