Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
74a8229e43 | |||
859cbc733d | |||
d32d47b706 | |||
fd90cfe895 |
15
changelog.md
15
changelog.md
@@ -1,5 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-08 - 1.16.8 - fix(format)
|
||||
Improve concurrency control in cache and rollback management with mutex locking and refine formatting details
|
||||
|
||||
- Added 'withMutex' functions in ChangeCache and RollbackManager to synchronize file I/O operations
|
||||
- Introduced static mutex maps to prevent race conditions during manifest updates
|
||||
- Fixed minor formatting issues in commit info and package.json
|
||||
|
||||
## 2025-08-08 - 1.16.7 - fix(core)
|
||||
Improve formatting, logging, and rollback integrity in core modules
|
||||
|
||||
- Add .claude/settings.local.json with defined permissions for allowed commands
|
||||
- Standardize formatting in package.json, commit info, and configuration files
|
||||
- Refactor rollback manager to use atomic manifest writes and validate manifest structure
|
||||
- Enhance logging messages and overall code clarity in CLI and commit modules
|
||||
|
||||
## 2025-08-08 - 1.16.6 - fix(changecache)
|
||||
Improve cache manifest validation and atomic file writes; add local settings and overrides
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@git.zone/cli",
|
||||
"private": false,
|
||||
"version": "1.16.6",
|
||||
"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.",
|
||||
"main": "dist_ts/index.ts",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/cli',
|
||||
version: '1.16.6',
|
||||
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.'
|
||||
}
|
||||
|
@@ -40,7 +40,9 @@ export class GitzoneConfig {
|
||||
public async readConfigFromCwd() {
|
||||
const npmextraInstance = new plugins.npmextra.Npmextra(paths.cwd);
|
||||
this.data = npmextraInstance.dataFor<IGitzoneConfigData>('gitzone', {});
|
||||
this.data.npmciOptions = npmextraInstance.dataFor<IGitzoneConfigData['npmciOptions']>('npmci', {
|
||||
this.data.npmciOptions = npmextraInstance.dataFor<
|
||||
IGitzoneConfigData['npmciOptions']
|
||||
>('npmci', {
|
||||
npmAccessLevel: 'public',
|
||||
});
|
||||
}
|
||||
|
@@ -62,23 +62,23 @@ export let run = async () => {
|
||||
gitzoneSmartcli.addCommand('format').subscribe(async (argvArg) => {
|
||||
const config = GitzoneConfig.fromCwd();
|
||||
const modFormat = await import('./mod_format/index.js');
|
||||
|
||||
|
||||
// Handle rollback commands
|
||||
if (argvArg.rollback) {
|
||||
await modFormat.handleRollback(argvArg.rollback);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (argvArg['list-backups']) {
|
||||
await modFormat.handleListBackups();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (argvArg['clean-backups']) {
|
||||
await modFormat.handleCleanBackups();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Handle format with options
|
||||
await modFormat.run({
|
||||
dryRun: argvArg['dry-run'],
|
||||
@@ -89,7 +89,7 @@ export let run = async () => {
|
||||
detailed: argvArg.detailed,
|
||||
interactive: argvArg.interactive !== false,
|
||||
parallel: argvArg.parallel !== false,
|
||||
verbose: argvArg.verbose
|
||||
verbose: argvArg.verbose,
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -5,7 +5,8 @@ import * as plugins from './plugins.js';
|
||||
export const logger = plugins.smartlog.Smartlog.createForCommitinfo(commitinfo);
|
||||
|
||||
// Add console destination
|
||||
const consoleDestination = new plugins.smartlogDestinationLocal.DestinationLocal();
|
||||
const consoleDestination =
|
||||
new plugins.smartlogDestinationLocal.DestinationLocal();
|
||||
logger.addLogDestination(consoleDestination);
|
||||
|
||||
// Verbose logging helper
|
||||
|
@@ -10,20 +10,22 @@ export const run = async (argvArg: any) => {
|
||||
await formatMod.run();
|
||||
}
|
||||
|
||||
|
||||
logger.log('info', `gathering facts...`);
|
||||
const aidoc = new plugins.tsdoc.AiDoc();
|
||||
await aidoc.start();
|
||||
|
||||
const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
|
||||
|
||||
logger.log('info', `---------
|
||||
logger.log(
|
||||
'info',
|
||||
`---------
|
||||
Next recommended commit would be:
|
||||
===========
|
||||
-> ${nextCommitObject.recommendedNextVersion}:
|
||||
-> ${nextCommitObject.recommendedNextVersionLevel}(${nextCommitObject.recommendedNextVersionScope}): ${nextCommitObject.recommendedNextVersionMessage}
|
||||
===========
|
||||
`);
|
||||
`,
|
||||
);
|
||||
const commitInteract = new plugins.smartinteract.SmartInteract();
|
||||
commitInteract.addQuestions([
|
||||
{
|
||||
@@ -72,32 +74,55 @@ export const run = async (argvArg: any) => {
|
||||
});
|
||||
|
||||
logger.log('info', `Baking commitinfo into code ...`);
|
||||
const commitInfo = new plugins.commitinfo.CommitInfo(paths.cwd, commitVersionType);
|
||||
const commitInfo = new plugins.commitinfo.CommitInfo(
|
||||
paths.cwd,
|
||||
commitVersionType,
|
||||
);
|
||||
await commitInfo.writeIntoPotentialDirs();
|
||||
|
||||
logger.log('info', `Writing changelog.md ...`);
|
||||
let changelog = nextCommitObject.changelog;
|
||||
changelog = changelog.replaceAll('{{nextVersion}}', (await commitInfo.getNextPlannedVersion()).versionString);
|
||||
changelog = changelog.replaceAll('{{nextVersionScope}}', `${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`);
|
||||
changelog = changelog.replaceAll('{{nextVersionMessage}}', nextCommitObject.recommendedNextVersionMessage);
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersion}}',
|
||||
(await commitInfo.getNextPlannedVersion()).versionString,
|
||||
);
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersionScope}}',
|
||||
`${await answerBucket.getAnswerFor('commitType')}(${await answerBucket.getAnswerFor('commitScope')})`,
|
||||
);
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersionMessage}}',
|
||||
nextCommitObject.recommendedNextVersionMessage,
|
||||
);
|
||||
if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
|
||||
changelog = changelog.replaceAll('{{nextVersionDetails}}', '- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '));
|
||||
changelog = changelog.replaceAll(
|
||||
'{{nextVersionDetails}}',
|
||||
'- ' + nextCommitObject.recommendedNextVersionDetails.join('\n- '),
|
||||
);
|
||||
} else {
|
||||
changelog = changelog.replaceAll('\n{{nextVersionDetails}}', '');
|
||||
}
|
||||
|
||||
await plugins.smartfile.memory.toFs(changelog, plugins.path.join(paths.cwd, `changelog.md`));
|
||||
await plugins.smartfile.memory.toFs(
|
||||
changelog,
|
||||
plugins.path.join(paths.cwd, `changelog.md`),
|
||||
);
|
||||
|
||||
logger.log('info', `Staging files for commit:`);
|
||||
await smartshellInstance.exec(`git add -A`);
|
||||
await smartshellInstance.exec(`git commit -m "${commitString}"`);
|
||||
await smartshellInstance.exec(`npm version ${commitVersionType}`);
|
||||
if (answerBucket.getAnswerFor('pushToOrigin') && !(process.env.CI === 'true')) {
|
||||
if (
|
||||
answerBucket.getAnswerFor('pushToOrigin') &&
|
||||
!(process.env.CI === 'true')
|
||||
) {
|
||||
await smartshellInstance.exec(`git push origin master --follow-tags`);
|
||||
}
|
||||
};
|
||||
|
||||
const createCommitStringFromAnswerBucket = (answerBucket: plugins.smartinteract.AnswerBucket) => {
|
||||
const createCommitStringFromAnswerBucket = (
|
||||
answerBucket: plugins.smartinteract.AnswerBucket,
|
||||
) => {
|
||||
const commitType = answerBucket.getAnswerFor('commitType');
|
||||
const commitScope = answerBucket.getAnswerFor('commitScope');
|
||||
const commitDescription = answerBucket.getAnswerFor('commitDescription');
|
||||
|
@@ -36,7 +36,10 @@ export const run = async () => {
|
||||
const registryUrls = answerBucket.getAnswerFor(`registryUrls`).split(',');
|
||||
const oldPackageName = answerBucket.getAnswerFor(`oldPackageName`);
|
||||
const newPackageName = answerBucket.getAnswerFor(`newPackageName`);
|
||||
logger.log('info', `Deprecating package ${oldPackageName} in favour of ${newPackageName}`);
|
||||
logger.log(
|
||||
'info',
|
||||
`Deprecating package ${oldPackageName} in favour of ${newPackageName}`,
|
||||
);
|
||||
const smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
});
|
||||
|
@@ -2,32 +2,29 @@ import * as plugins from './mod.plugins.js';
|
||||
import { FormatContext } from './classes.formatcontext.js';
|
||||
import type { IPlannedChange } from './interfaces.format.js';
|
||||
import { Project } from '../classes.project.js';
|
||||
import { ChangeCache } from './classes.changecache.js';
|
||||
|
||||
export abstract class BaseFormatter {
|
||||
protected context: FormatContext;
|
||||
protected project: Project;
|
||||
protected cache: ChangeCache;
|
||||
protected stats: any; // Will be FormatStats from context
|
||||
|
||||
|
||||
constructor(context: FormatContext, project: Project) {
|
||||
this.context = context;
|
||||
this.project = project;
|
||||
this.cache = context.getChangeCache();
|
||||
this.stats = context.getFormatStats();
|
||||
}
|
||||
|
||||
|
||||
abstract get name(): string;
|
||||
abstract analyze(): Promise<IPlannedChange[]>;
|
||||
abstract applyChange(change: IPlannedChange): Promise<void>;
|
||||
|
||||
|
||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||
const startTime = this.stats.moduleStartTime(this.name);
|
||||
this.stats.startModule(this.name);
|
||||
|
||||
|
||||
try {
|
||||
await this.preExecute();
|
||||
|
||||
|
||||
for (const change of changes) {
|
||||
try {
|
||||
await this.applyChange(change);
|
||||
@@ -37,57 +34,37 @@ export abstract class BaseFormatter {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await this.postExecute();
|
||||
} catch (error) {
|
||||
await this.context.rollbackOperation();
|
||||
// Don't rollback here - let the FormatPlanner handle it
|
||||
throw error;
|
||||
} finally {
|
||||
this.stats.endModule(this.name, startTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected async preExecute(): Promise<void> {
|
||||
// Override in subclasses if needed
|
||||
}
|
||||
|
||||
|
||||
protected async postExecute(): Promise<void> {
|
||||
// Override in subclasses if needed
|
||||
}
|
||||
|
||||
|
||||
protected async modifyFile(filepath: string, content: string): Promise<void> {
|
||||
await this.context.trackFileChange(filepath);
|
||||
await plugins.smartfile.memory.toFs(content, filepath);
|
||||
await this.cache.updateFileCache(filepath);
|
||||
}
|
||||
|
||||
|
||||
protected async createFile(filepath: string, content: string): Promise<void> {
|
||||
await plugins.smartfile.memory.toFs(content, filepath);
|
||||
await this.cache.updateFileCache(filepath);
|
||||
}
|
||||
|
||||
|
||||
protected async deleteFile(filepath: string): Promise<void> {
|
||||
await this.context.trackFileChange(filepath);
|
||||
await plugins.smartfile.fs.remove(filepath);
|
||||
}
|
||||
|
||||
|
||||
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
||||
const config = new plugins.npmextra.Npmextra();
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,32 +18,32 @@ export class ChangeCache {
|
||||
private cacheDir: string;
|
||||
private manifestPath: string;
|
||||
private cacheVersion = '1.0.0';
|
||||
|
||||
|
||||
constructor() {
|
||||
this.cacheDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-cache');
|
||||
this.manifestPath = plugins.path.join(this.cacheDir, 'manifest.json');
|
||||
}
|
||||
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await plugins.smartfile.fs.ensureDir(this.cacheDir);
|
||||
}
|
||||
|
||||
|
||||
async getManifest(): Promise<ICacheManifest> {
|
||||
const defaultManifest: ICacheManifest = {
|
||||
version: this.cacheVersion,
|
||||
lastFormat: 0,
|
||||
files: []
|
||||
files: [],
|
||||
};
|
||||
|
||||
|
||||
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
|
||||
if (!exists) {
|
||||
return defaultManifest;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
|
||||
const manifest = JSON.parse(content);
|
||||
|
||||
|
||||
// Validate the manifest structure
|
||||
if (this.isValidManifest(manifest)) {
|
||||
return manifest;
|
||||
@@ -52,7 +52,9 @@ export class ChangeCache {
|
||||
return defaultManifest;
|
||||
}
|
||||
} 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 {
|
||||
await plugins.smartfile.fs.remove(this.manifestPath);
|
||||
@@ -62,168 +64,160 @@ export class ChangeCache {
|
||||
return defaultManifest;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async saveManifest(manifest: ICacheManifest): Promise<void> {
|
||||
// Validate before saving
|
||||
if (!this.isValidManifest(manifest)) {
|
||||
throw new Error('Invalid manifest structure, cannot save');
|
||||
}
|
||||
|
||||
// Use atomic write: write to temp file, then move it
|
||||
const tempPath = `${this.manifestPath}.tmp`;
|
||||
|
||||
try {
|
||||
// Write to temporary file
|
||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
||||
await plugins.smartfile.memory.toFs(jsonContent, tempPath);
|
||||
|
||||
// Move temp file to actual manifest (atomic-like operation)
|
||||
// Since smartfile doesn't have rename, we copy and delete
|
||||
await plugins.smartfile.fs.copy(tempPath, this.manifestPath);
|
||||
await plugins.smartfile.fs.remove(tempPath);
|
||||
} catch (error) {
|
||||
// Clean up temp file if it exists
|
||||
try {
|
||||
await plugins.smartfile.fs.remove(tempPath);
|
||||
} catch (removeError) {
|
||||
// Ignore removal errors
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
await plugins.smartfile.fs.ensureDir(this.cacheDir);
|
||||
|
||||
// Write directly with proper JSON stringification
|
||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
||||
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
|
||||
}
|
||||
|
||||
|
||||
async hasFileChanged(filePath: string): Promise<boolean> {
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: plugins.path.join(paths.cwd, filePath);
|
||||
|
||||
|
||||
// Check if file exists
|
||||
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
|
||||
if (!exists) {
|
||||
return true; // File doesn't exist, so it's "changed" (will be created)
|
||||
}
|
||||
|
||||
|
||||
// Get current file stats
|
||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
||||
|
||||
|
||||
// Skip directories
|
||||
if (stats.isDirectory()) {
|
||||
return false; // Directories are not processed
|
||||
}
|
||||
|
||||
|
||||
const content = plugins.smartfile.fs.toStringSync(absolutePath);
|
||||
const currentChecksum = this.calculateChecksum(content);
|
||||
|
||||
|
||||
// Get cached info
|
||||
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) {
|
||||
return true; // Not in cache, so it's changed
|
||||
}
|
||||
|
||||
|
||||
// Compare checksums
|
||||
return cachedFile.checksum !== currentChecksum ||
|
||||
cachedFile.size !== stats.size ||
|
||||
cachedFile.modified !== stats.mtimeMs;
|
||||
return (
|
||||
cachedFile.checksum !== currentChecksum ||
|
||||
cachedFile.size !== stats.size ||
|
||||
cachedFile.modified !== stats.mtimeMs
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
async updateFileCache(filePath: string): Promise<void> {
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
const absolutePath = plugins.path.isAbsolute(filePath)
|
||||
? filePath
|
||||
: plugins.path.join(paths.cwd, filePath);
|
||||
|
||||
|
||||
// Get current file stats
|
||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
||||
|
||||
|
||||
// Skip directories
|
||||
if (stats.isDirectory()) {
|
||||
return; // Don't cache directories
|
||||
}
|
||||
|
||||
|
||||
const content = plugins.smartfile.fs.toStringSync(absolutePath);
|
||||
const checksum = this.calculateChecksum(content);
|
||||
|
||||
|
||||
// Update manifest
|
||||
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 = {
|
||||
path: filePath,
|
||||
checksum,
|
||||
modified: stats.mtimeMs,
|
||||
size: stats.size
|
||||
size: stats.size,
|
||||
};
|
||||
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
manifest.files[existingIndex] = cacheEntry;
|
||||
} else {
|
||||
manifest.files.push(cacheEntry);
|
||||
}
|
||||
|
||||
|
||||
manifest.lastFormat = Date.now();
|
||||
await this.saveManifest(manifest);
|
||||
}
|
||||
|
||||
|
||||
async getChangedFiles(filePaths: string[]): Promise<string[]> {
|
||||
const changedFiles: string[] = [];
|
||||
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
if (await this.hasFileChanged(filePath)) {
|
||||
changedFiles.push(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return changedFiles;
|
||||
}
|
||||
|
||||
|
||||
async clean(): Promise<void> {
|
||||
const manifest = await this.getManifest();
|
||||
const validFiles: IFileCache[] = [];
|
||||
|
||||
|
||||
// Remove entries for files that no longer exist
|
||||
for (const file of manifest.files) {
|
||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
||||
? file.path
|
||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
||||
? file.path
|
||||
: plugins.path.join(paths.cwd, file.path);
|
||||
|
||||
|
||||
if (await plugins.smartfile.fs.fileExists(absolutePath)) {
|
||||
validFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
manifest.files = validFiles;
|
||||
await this.saveManifest(manifest);
|
||||
}
|
||||
|
||||
|
||||
private calculateChecksum(content: string | Buffer): string {
|
||||
return plugins.crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
private isValidManifest(manifest: any): manifest is ICacheManifest {
|
||||
// Check if manifest has the required structure
|
||||
if (!manifest || typeof manifest !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check required fields
|
||||
if (typeof manifest.version !== 'string' ||
|
||||
typeof manifest.lastFormat !== 'number' ||
|
||||
!Array.isArray(manifest.files)) {
|
||||
if (
|
||||
typeof manifest.version !== 'string' ||
|
||||
typeof manifest.lastFormat !== 'number' ||
|
||||
!Array.isArray(manifest.files)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Check each file entry
|
||||
for (const file of manifest.files) {
|
||||
if (!file || typeof file !== 'object' ||
|
||||
typeof file.path !== 'string' ||
|
||||
typeof file.checksum !== 'string' ||
|
||||
typeof file.modified !== 'number' ||
|
||||
typeof file.size !== 'number') {
|
||||
if (
|
||||
!file ||
|
||||
typeof file !== 'object' ||
|
||||
typeof file.path !== 'string' ||
|
||||
typeof file.checksum !== 'string' ||
|
||||
typeof file.modified !== 'number' ||
|
||||
typeof file.size !== 'number'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -9,35 +9,42 @@ export interface IModuleDependency {
|
||||
|
||||
export class DependencyAnalyzer {
|
||||
private moduleDependencies: Map<string, IModuleDependency> = new Map();
|
||||
|
||||
|
||||
constructor() {
|
||||
this.initializeDependencies();
|
||||
}
|
||||
|
||||
|
||||
private initializeDependencies(): void {
|
||||
// Define dependencies between format modules
|
||||
const dependencies = {
|
||||
'cleanup': [], // No dependencies
|
||||
'npmextra': [], // No dependencies
|
||||
'license': ['npmextra'], // Depends on npmextra for config
|
||||
'packagejson': ['npmextra'], // Depends on npmextra for config
|
||||
'templates': ['npmextra', 'packagejson'], // Depends on both
|
||||
'gitignore': ['templates'], // Depends on templates
|
||||
'tsconfig': ['packagejson'], // Depends on package.json
|
||||
'prettier': ['cleanup', 'npmextra', 'packagejson', 'templates', 'gitignore', 'tsconfig'], // Runs after most others
|
||||
'readme': ['npmextra', 'packagejson'], // Depends on project metadata
|
||||
'copy': ['npmextra'], // Depends on config
|
||||
cleanup: [], // No dependencies
|
||||
npmextra: [], // No dependencies
|
||||
license: ['npmextra'], // Depends on npmextra for config
|
||||
packagejson: ['npmextra'], // Depends on npmextra for config
|
||||
templates: ['npmextra', 'packagejson'], // Depends on both
|
||||
gitignore: ['templates'], // Depends on templates
|
||||
tsconfig: ['packagejson'], // Depends on package.json
|
||||
prettier: [
|
||||
'cleanup',
|
||||
'npmextra',
|
||||
'packagejson',
|
||||
'templates',
|
||||
'gitignore',
|
||||
'tsconfig',
|
||||
], // Runs after most others
|
||||
readme: ['npmextra', 'packagejson'], // Depends on project metadata
|
||||
copy: ['npmextra'], // Depends on config
|
||||
};
|
||||
|
||||
|
||||
// Initialize all modules
|
||||
for (const [module, deps] of Object.entries(dependencies)) {
|
||||
this.moduleDependencies.set(module, {
|
||||
module,
|
||||
dependencies: new Set(deps),
|
||||
dependents: new Set()
|
||||
dependents: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Build reverse dependencies (dependents)
|
||||
for (const [module, deps] of Object.entries(dependencies)) {
|
||||
for (const dep of deps) {
|
||||
@@ -48,34 +55,35 @@ export class DependencyAnalyzer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 groups: BaseFormatter[][] = [];
|
||||
|
||||
|
||||
while (executed.size < modules.length) {
|
||||
const currentGroup: BaseFormatter[] = [];
|
||||
|
||||
|
||||
for (const module of modules) {
|
||||
if (executed.has(module.name)) continue;
|
||||
|
||||
|
||||
const dependency = this.moduleDependencies.get(module.name);
|
||||
if (!dependency) {
|
||||
// Unknown module, execute in isolation
|
||||
currentGroup.push(module);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Check if all dependencies have been executed
|
||||
const allDepsExecuted = Array.from(dependency.dependencies)
|
||||
.every(dep => executed.has(dep) || !modulesMap.has(dep));
|
||||
|
||||
const allDepsExecuted = Array.from(dependency.dependencies).every(
|
||||
(dep) => executed.has(dep) || !modulesMap.has(dep),
|
||||
);
|
||||
|
||||
if (allDepsExecuted) {
|
||||
currentGroup.push(module);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (currentGroup.length === 0) {
|
||||
// Circular dependency or error - execute remaining 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);
|
||||
}
|
||||
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
||||
canRunInParallel(module1: string, module2: string): boolean {
|
||||
const dep1 = this.moduleDependencies.get(module1);
|
||||
const dep2 = this.moduleDependencies.get(module2);
|
||||
|
||||
|
||||
if (!dep1 || !dep2) return false;
|
||||
|
||||
|
||||
// Check if module1 depends on module2 or vice versa
|
||||
return !dep1.dependencies.has(module2) &&
|
||||
!dep2.dependencies.has(module1) &&
|
||||
!dep1.dependents.has(module2) &&
|
||||
!dep2.dependents.has(module1);
|
||||
return (
|
||||
!dep1.dependencies.has(module2) &&
|
||||
!dep2.dependencies.has(module1) &&
|
||||
!dep1.dependents.has(module2) &&
|
||||
!dep2.dependents.has(module1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,72 +4,85 @@ import { logger } from '../gitzone.logging.js';
|
||||
|
||||
export class DiffReporter {
|
||||
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);
|
||||
this.diffs.set(filePath, diff);
|
||||
return diff;
|
||||
}
|
||||
|
||||
|
||||
async generateDiffForChange(change: IPlannedChange): Promise<string | null> {
|
||||
if (change.type !== 'modify') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const exists = await plugins.smartfile.fs.fileExists(change.path);
|
||||
if (!exists) {
|
||||
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
|
||||
if (!change.content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.generateDiff(change.path, currentContent, change.content);
|
||||
|
||||
return await this.generateDiff(
|
||||
change.path,
|
||||
currentContent,
|
||||
change.content,
|
||||
);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
displayDiff(filePath: string, diff?: string): void {
|
||||
const diffToShow = diff || this.diffs.get(filePath);
|
||||
|
||||
|
||||
if (!diffToShow) {
|
||||
logger.log('warn', `No diff available for ${filePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log(`\n${this.formatDiffHeader(filePath)}`);
|
||||
console.log(this.colorDiff(diffToShow));
|
||||
console.log('━'.repeat(50));
|
||||
}
|
||||
|
||||
|
||||
displayAllDiffs(): void {
|
||||
if (this.diffs.size === 0) {
|
||||
logger.log('info', 'No diffs to display');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
console.log('\nFile Changes:');
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
|
||||
for (const [filePath, diff] of this.diffs) {
|
||||
this.displayDiff(filePath, diff);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private formatDiffHeader(filePath: string): string {
|
||||
return `📄 ${filePath}`;
|
||||
}
|
||||
|
||||
|
||||
private colorDiff(diff: string): string {
|
||||
const lines = diff.split('\n');
|
||||
const coloredLines = lines.map(line => {
|
||||
const coloredLines = lines.map((line) => {
|
||||
if (line.startsWith('+') && !line.startsWith('+++')) {
|
||||
return `\x1b[32m${line}\x1b[0m`; // Green for additions
|
||||
} else if (line.startsWith('-') && !line.startsWith('---')) {
|
||||
@@ -80,29 +93,32 @@ export class DiffReporter {
|
||||
return line;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return coloredLines.join('\n');
|
||||
}
|
||||
|
||||
|
||||
async saveDiffReport(outputPath: string): Promise<void> {
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
totalFiles: this.diffs.size,
|
||||
diffs: Array.from(this.diffs.entries()).map(([path, diff]) => ({
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
hasAnyDiffs(): boolean {
|
||||
return this.diffs.size > 0;
|
||||
}
|
||||
|
||||
|
||||
getDiffCount(): number {
|
||||
return this.diffs.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,65 +1,14 @@
|
||||
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 type { IFormatOperation, IFormatPlan } from './interfaces.format.js';
|
||||
|
||||
export class FormatContext {
|
||||
private rollbackManager: RollbackManager;
|
||||
private currentOperation: IFormatOperation | null = null;
|
||||
private changeCache: ChangeCache;
|
||||
private formatStats: FormatStats;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.rollbackManager = new RollbackManager();
|
||||
this.changeCache = new ChangeCache();
|
||||
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 {
|
||||
return this.formatStats;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ export class FormatPlanner {
|
||||
private plannedChanges: Map<string, IPlannedChange[]> = new Map();
|
||||
private dependencyAnalyzer = new DependencyAnalyzer();
|
||||
private diffReporter = new DiffReporter();
|
||||
|
||||
|
||||
async planFormat(modules: BaseFormatter[]): Promise<IFormatPlan> {
|
||||
const plan: IFormatPlan = {
|
||||
summary: {
|
||||
@@ -18,20 +18,20 @@ export class FormatPlanner {
|
||||
filesAdded: 0,
|
||||
filesModified: 0,
|
||||
filesRemoved: 0,
|
||||
estimatedTime: 0
|
||||
estimatedTime: 0,
|
||||
},
|
||||
changes: [],
|
||||
warnings: []
|
||||
warnings: [],
|
||||
};
|
||||
|
||||
|
||||
for (const module of modules) {
|
||||
try {
|
||||
const changes = await module.analyze();
|
||||
this.plannedChanges.set(module.name, changes);
|
||||
|
||||
|
||||
for (const change of changes) {
|
||||
plan.changes.push(change);
|
||||
|
||||
|
||||
// Update summary
|
||||
switch (change.type) {
|
||||
case 'create':
|
||||
@@ -49,67 +49,51 @@ export class FormatPlanner {
|
||||
plan.warnings.push({
|
||||
level: 'error',
|
||||
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
|
||||
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
async executePlan(plan: IFormatPlan, modules: BaseFormatter[], context: FormatContext, parallel: boolean = true): Promise<void> {
|
||||
await context.beginOperation();
|
||||
|
||||
async executePlan(
|
||||
plan: IFormatPlan,
|
||||
modules: BaseFormatter[],
|
||||
context: FormatContext,
|
||||
parallel: boolean = false,
|
||||
): Promise<void> {
|
||||
const startTime = Date.now();
|
||||
|
||||
|
||||
try {
|
||||
if (parallel) {
|
||||
// Get execution groups based on dependencies
|
||||
const executionGroups = this.dependencyAnalyzer.getExecutionGroups(modules);
|
||||
|
||||
logger.log('info', `Executing formatters in ${executionGroups.length} groups...`);
|
||||
|
||||
for (let i = 0; i < executionGroups.length; i++) {
|
||||
const group = executionGroups[i];
|
||||
logger.log('info', `Executing group ${i + 1}: ${group.map(m => m.name).join(', ')}`);
|
||||
|
||||
// Execute modules in this group in parallel
|
||||
const promises = group.map(async (module) => {
|
||||
const changes = this.plannedChanges.get(module.name) || [];
|
||||
if (changes.length > 0) {
|
||||
logger.log('info', `Executing ${module.name} formatter...`);
|
||||
await module.execute(changes);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
} else {
|
||||
// Sequential execution (original implementation)
|
||||
for (const module of modules) {
|
||||
const changes = this.plannedChanges.get(module.name) || [];
|
||||
|
||||
if (changes.length > 0) {
|
||||
logger.log('info', `Executing ${module.name} formatter...`);
|
||||
await module.execute(changes);
|
||||
}
|
||||
// Always use sequential execution to avoid race conditions
|
||||
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 duration = endTime - startTime;
|
||||
logger.log('info', `Format operations completed in ${duration}ms`);
|
||||
|
||||
await context.commitOperation();
|
||||
} catch (error) {
|
||||
await context.rollbackOperation();
|
||||
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('━'.repeat(50));
|
||||
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('');
|
||||
console.log('Changes by module:');
|
||||
|
||||
|
||||
// Group changes by module
|
||||
const changesByModule = new Map<string, IPlannedChange[]>();
|
||||
for (const change of plan.changes) {
|
||||
@@ -126,14 +110,16 @@ export class FormatPlanner {
|
||||
moduleChanges.push(change);
|
||||
changesByModule.set(change.module, moduleChanges);
|
||||
}
|
||||
|
||||
|
||||
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) {
|
||||
const icon = this.getChangeIcon(change.type);
|
||||
console.log(` ${icon} ${change.path} - ${change.description}`);
|
||||
|
||||
|
||||
// Show diff for modified files if detailed view is requested
|
||||
if (detailed && change.type === 'modify') {
|
||||
const diff = await this.diffReporter.generateDiffForChange(change);
|
||||
@@ -143,7 +129,7 @@ export class FormatPlanner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (plan.warnings.length > 0) {
|
||||
console.log('\nWarnings:');
|
||||
for (const warning of plan.warnings) {
|
||||
@@ -151,26 +137,26 @@ export class FormatPlanner {
|
||||
console.log(` ${icon} ${warning.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('\n' + '━'.repeat(50));
|
||||
}
|
||||
|
||||
|
||||
private getModuleIcon(module: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
'packagejson': '📦',
|
||||
'license': '📝',
|
||||
'tsconfig': '🔧',
|
||||
'cleanup': '🚮',
|
||||
'gitignore': '🔒',
|
||||
'prettier': '✨',
|
||||
'readme': '📖',
|
||||
'templates': '📄',
|
||||
'npmextra': '⚙️',
|
||||
'copy': '📋'
|
||||
packagejson: '📦',
|
||||
license: '📝',
|
||||
tsconfig: '🔧',
|
||||
cleanup: '🚮',
|
||||
gitignore: '🔒',
|
||||
prettier: '✨',
|
||||
readme: '📖',
|
||||
templates: '📄',
|
||||
npmextra: '⚙️',
|
||||
copy: '📋',
|
||||
};
|
||||
return icons[module] || '📁';
|
||||
}
|
||||
|
||||
|
||||
private getChangeIcon(type: 'create' | 'modify' | 'delete'): string {
|
||||
switch (type) {
|
||||
case 'create':
|
||||
@@ -181,4 +167,4 @@ export class FormatPlanner {
|
||||
return '❌';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -30,7 +30,7 @@ export interface IFormatStats {
|
||||
|
||||
export class FormatStats {
|
||||
private stats: IFormatStats;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.stats = {
|
||||
totalExecutionTime: 0,
|
||||
@@ -44,11 +44,11 @@ export class FormatStats {
|
||||
totalDeleted: 0,
|
||||
totalErrors: 0,
|
||||
cacheHits: 0,
|
||||
cacheMisses: 0
|
||||
}
|
||||
cacheMisses: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
startModule(moduleName: string): void {
|
||||
this.stats.moduleStats.set(moduleName, {
|
||||
name: moduleName,
|
||||
@@ -58,31 +58,35 @@ export class FormatStats {
|
||||
successes: 0,
|
||||
filesCreated: 0,
|
||||
filesModified: 0,
|
||||
filesDeleted: 0
|
||||
filesDeleted: 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
moduleStartTime(moduleName: string): number {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
|
||||
endModule(moduleName: string, startTime: number): void {
|
||||
const moduleStats = this.stats.moduleStats.get(moduleName);
|
||||
if (moduleStats) {
|
||||
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);
|
||||
if (!moduleStats) return;
|
||||
|
||||
|
||||
moduleStats.filesProcessed++;
|
||||
|
||||
|
||||
if (success) {
|
||||
moduleStats.successes++;
|
||||
this.stats.overallStats.totalFiles++;
|
||||
|
||||
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
moduleStats.filesCreated++;
|
||||
@@ -102,53 +106,66 @@ export class FormatStats {
|
||||
this.stats.overallStats.totalErrors++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
recordCacheHit(): void {
|
||||
this.stats.overallStats.cacheHits++;
|
||||
}
|
||||
|
||||
|
||||
recordCacheMiss(): void {
|
||||
this.stats.overallStats.cacheMisses++;
|
||||
}
|
||||
|
||||
|
||||
finish(): void {
|
||||
this.stats.endTime = Date.now();
|
||||
this.stats.totalExecutionTime = this.stats.endTime - this.stats.startTime;
|
||||
}
|
||||
|
||||
|
||||
displayStats(): void {
|
||||
console.log('\n📊 Format Operation Statistics:');
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
|
||||
// Overall stats
|
||||
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(` • Created: ${this.stats.overallStats.totalCreated}`);
|
||||
console.log(` • Modified: ${this.stats.overallStats.totalModified}`);
|
||||
console.log(` • Deleted: ${this.stats.overallStats.totalDeleted}`);
|
||||
console.log(` Errors: ${this.stats.overallStats.totalErrors}`);
|
||||
|
||||
if (this.stats.overallStats.cacheHits > 0 || this.stats.overallStats.cacheMisses > 0) {
|
||||
const cacheHitRate = this.stats.overallStats.cacheHits /
|
||||
(this.stats.overallStats.cacheHits + this.stats.overallStats.cacheMisses) * 100;
|
||||
|
||||
if (
|
||||
this.stats.overallStats.cacheHits > 0 ||
|
||||
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(` • Hits: ${this.stats.overallStats.cacheHits}`);
|
||||
console.log(` • Misses: ${this.stats.overallStats.cacheMisses}`);
|
||||
}
|
||||
|
||||
|
||||
// Module stats
|
||||
console.log('\nModule Breakdown:');
|
||||
console.log('─'.repeat(50));
|
||||
|
||||
const sortedModules = Array.from(this.stats.moduleStats.values())
|
||||
.sort((a, b) => b.filesProcessed - a.filesProcessed);
|
||||
|
||||
|
||||
const sortedModules = Array.from(this.stats.moduleStats.values()).sort(
|
||||
(a, b) => b.filesProcessed - a.filesProcessed,
|
||||
);
|
||||
|
||||
for (const moduleStats of sortedModules) {
|
||||
console.log(`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`);
|
||||
console.log(` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`);
|
||||
console.log(
|
||||
`\n${this.getModuleIcon(moduleStats.name)} ${moduleStats.name}:`,
|
||||
);
|
||||
console.log(
|
||||
` Execution Time: ${this.formatDuration(moduleStats.executionTime)}`,
|
||||
);
|
||||
console.log(` Files Processed: ${moduleStats.filesProcessed}`);
|
||||
|
||||
|
||||
if (moduleStats.filesCreated > 0) {
|
||||
console.log(` • Created: ${moduleStats.filesCreated}`);
|
||||
}
|
||||
@@ -158,27 +175,30 @@ export class FormatStats {
|
||||
if (moduleStats.filesDeleted > 0) {
|
||||
console.log(` • Deleted: ${moduleStats.filesDeleted}`);
|
||||
}
|
||||
|
||||
|
||||
if (moduleStats.errors > 0) {
|
||||
console.log(` ❌ Errors: ${moduleStats.errors}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('\n' + '═'.repeat(50));
|
||||
}
|
||||
|
||||
|
||||
async saveReport(outputPath: string): Promise<void> {
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
executionTime: this.stats.totalExecutionTime,
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
private formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
@@ -190,20 +210,20 @@ export class FormatStats {
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private getModuleIcon(module: string): string {
|
||||
const icons: Record<string, string> = {
|
||||
'packagejson': '📦',
|
||||
'license': '📝',
|
||||
'tsconfig': '🔧',
|
||||
'cleanup': '🚮',
|
||||
'gitignore': '🔒',
|
||||
'prettier': '✨',
|
||||
'readme': '📖',
|
||||
'templates': '📄',
|
||||
'npmextra': '⚙️',
|
||||
'copy': '📋'
|
||||
packagejson: '📦',
|
||||
license: '📝',
|
||||
tsconfig: '🔧',
|
||||
cleanup: '🚮',
|
||||
gitignore: '🔒',
|
||||
prettier: '✨',
|
||||
readme: '📖',
|
||||
templates: '📄',
|
||||
npmextra: '⚙️',
|
||||
copy: '📋',
|
||||
};
|
||||
return icons[module] || '📁';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,214 +5,314 @@ import type { IFormatOperation } from './interfaces.format.js';
|
||||
export class RollbackManager {
|
||||
private backupDir: string;
|
||||
private manifestPath: string;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.backupDir = plugins.path.join(paths.cwd, '.nogit', 'gitzone-backups');
|
||||
this.manifestPath = plugins.path.join(this.backupDir, 'manifest.json');
|
||||
}
|
||||
|
||||
|
||||
async createOperation(): Promise<IFormatOperation> {
|
||||
await this.ensureBackupDir();
|
||||
|
||||
|
||||
const operation: IFormatOperation = {
|
||||
id: this.generateOperationId(),
|
||||
timestamp: Date.now(),
|
||||
files: [],
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
};
|
||||
|
||||
|
||||
await this.updateManifest(operation);
|
||||
return operation;
|
||||
}
|
||||
|
||||
|
||||
async backupFile(filepath: string, operationId: string): Promise<void> {
|
||||
const operation = await this.getOperation(operationId);
|
||||
if (!operation) {
|
||||
throw new Error(`Operation ${operationId} not found`);
|
||||
}
|
||||
|
||||
const absolutePath = plugins.path.isAbsolute(filepath)
|
||||
? filepath
|
||||
|
||||
const absolutePath = plugins.path.isAbsolute(filepath)
|
||||
? filepath
|
||||
: plugins.path.join(paths.cwd, filepath);
|
||||
|
||||
|
||||
// Check if file exists
|
||||
const exists = await plugins.smartfile.fs.fileExists(absolutePath);
|
||||
if (!exists) {
|
||||
// File doesn't exist yet (will be created), so we skip backup
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Read file content and metadata
|
||||
const content = await plugins.smartfile.fs.toStringSync(absolutePath);
|
||||
const content = plugins.smartfile.fs.toStringSync(absolutePath);
|
||||
const stats = await plugins.smartfile.fs.stat(absolutePath);
|
||||
const checksum = this.calculateChecksum(content);
|
||||
|
||||
|
||||
// Create backup
|
||||
const backupPath = this.getBackupPath(operationId, filepath);
|
||||
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(backupPath));
|
||||
await plugins.smartfile.memory.toFs(content, backupPath);
|
||||
|
||||
|
||||
// Update operation
|
||||
operation.files.push({
|
||||
path: filepath,
|
||||
originalContent: content,
|
||||
checksum,
|
||||
permissions: stats.mode.toString(8)
|
||||
permissions: stats.mode.toString(8),
|
||||
});
|
||||
|
||||
|
||||
await this.updateManifest(operation);
|
||||
}
|
||||
|
||||
|
||||
async rollback(operationId: string): Promise<void> {
|
||||
const operation = await this.getOperation(operationId);
|
||||
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') {
|
||||
throw new Error(`Operation ${operationId} has already been rolled back`);
|
||||
}
|
||||
|
||||
|
||||
// Restore files in reverse order
|
||||
for (let i = operation.files.length - 1; i >= 0; i--) {
|
||||
const file = operation.files[i];
|
||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
||||
? file.path
|
||||
const absolutePath = plugins.path.isAbsolute(file.path)
|
||||
? file.path
|
||||
: plugins.path.join(paths.cwd, file.path);
|
||||
|
||||
|
||||
// Verify backup integrity
|
||||
const backupPath = this.getBackupPath(operationId, file.path);
|
||||
const backupContent = await plugins.smartfile.fs.toStringSync(backupPath);
|
||||
const backupContent = plugins.smartfile.fs.toStringSync(backupPath);
|
||||
const backupChecksum = this.calculateChecksum(backupContent);
|
||||
|
||||
|
||||
if (backupChecksum !== file.checksum) {
|
||||
throw new Error(`Backup integrity check failed for ${file.path}`);
|
||||
}
|
||||
|
||||
|
||||
// Restore file
|
||||
await plugins.smartfile.memory.toFs(file.originalContent, absolutePath);
|
||||
|
||||
|
||||
// Restore permissions
|
||||
const mode = parseInt(file.permissions, 8);
|
||||
// Note: Permissions restoration may not work on all platforms
|
||||
}
|
||||
|
||||
|
||||
// Update operation status
|
||||
operation.status = 'rolled-back';
|
||||
await this.updateManifest(operation);
|
||||
}
|
||||
|
||||
|
||||
async markComplete(operationId: string): Promise<void> {
|
||||
const operation = await this.getOperation(operationId);
|
||||
if (!operation) {
|
||||
throw new Error(`Operation ${operationId} not found`);
|
||||
}
|
||||
|
||||
|
||||
operation.status = 'completed';
|
||||
await this.updateManifest(operation);
|
||||
}
|
||||
|
||||
|
||||
async cleanOldBackups(retentionDays: number): Promise<void> {
|
||||
const manifest = await this.getManifest();
|
||||
const cutoffTime = Date.now() - (retentionDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const operationsToDelete = manifest.operations.filter(op =>
|
||||
op.timestamp < cutoffTime && op.status === 'completed'
|
||||
const cutoffTime = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
const operationsToDelete = manifest.operations.filter(
|
||||
(op) => op.timestamp < cutoffTime && op.status === 'completed',
|
||||
);
|
||||
|
||||
|
||||
for (const operation of operationsToDelete) {
|
||||
// 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);
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
async verifyBackup(operationId: string): Promise<boolean> {
|
||||
const operation = await this.getOperation(operationId);
|
||||
if (!operation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
for (const file of operation.files) {
|
||||
const backupPath = this.getBackupPath(operationId, file.path);
|
||||
const exists = await plugins.smartfile.fs.fileExists(backupPath);
|
||||
|
||||
|
||||
if (!exists) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = await plugins.smartfile.fs.toStringSync(backupPath);
|
||||
|
||||
const content = plugins.smartfile.fs.toStringSync(backupPath);
|
||||
const checksum = this.calculateChecksum(content);
|
||||
|
||||
|
||||
if (checksum !== file.checksum) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
async listBackups(): Promise<IFormatOperation[]> {
|
||||
const manifest = await this.getManifest();
|
||||
return manifest.operations;
|
||||
}
|
||||
|
||||
|
||||
private async ensureBackupDir(): Promise<void> {
|
||||
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 {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const random = Math.random().toString(36).substring(2, 8);
|
||||
return `${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
|
||||
private getBackupPath(operationId: string, filepath: string): string {
|
||||
const filename = plugins.path.basename(filepath);
|
||||
const dir = plugins.path.dirname(filepath);
|
||||
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 {
|
||||
return plugins.crypto.createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
|
||||
private async getManifest(): Promise<{ operations: IFormatOperation[] }> {
|
||||
const defaultManifest = { operations: [] };
|
||||
|
||||
const exists = await plugins.smartfile.fs.fileExists(this.manifestPath);
|
||||
if (!exists) {
|
||||
return { operations: [] };
|
||||
return defaultManifest;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = plugins.smartfile.fs.toStringSync(this.manifestPath);
|
||||
const manifest = JSON.parse(content);
|
||||
|
||||
// Validate the manifest structure
|
||||
if (this.isValidManifest(manifest)) {
|
||||
return manifest;
|
||||
} else {
|
||||
console.warn(
|
||||
'Invalid rollback manifest structure, returning default manifest',
|
||||
);
|
||||
return defaultManifest;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to read rollback manifest: ${error.message}, returning default manifest`,
|
||||
);
|
||||
// Try to delete the corrupted file
|
||||
try {
|
||||
await plugins.smartfile.fs.remove(this.manifestPath);
|
||||
} catch (removeError) {
|
||||
// Ignore removal errors
|
||||
}
|
||||
return defaultManifest;
|
||||
}
|
||||
|
||||
const content = await plugins.smartfile.fs.toStringSync(this.manifestPath);
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
private async saveManifest(manifest: { operations: IFormatOperation[] }): Promise<void> {
|
||||
await plugins.smartfile.memory.toFs(JSON.stringify(manifest, null, 2), this.manifestPath);
|
||||
|
||||
private async saveManifest(manifest: {
|
||||
operations: IFormatOperation[];
|
||||
}): Promise<void> {
|
||||
// Validate before saving
|
||||
if (!this.isValidManifest(manifest)) {
|
||||
throw new Error('Invalid rollback manifest structure, cannot save');
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
await this.ensureBackupDir();
|
||||
|
||||
// Write directly with proper JSON stringification
|
||||
const jsonContent = JSON.stringify(manifest, null, 2);
|
||||
await plugins.smartfile.memory.toFs(jsonContent, this.manifestPath);
|
||||
}
|
||||
|
||||
private async getOperation(operationId: string): Promise<IFormatOperation | null> {
|
||||
|
||||
private async getOperation(
|
||||
operationId: string,
|
||||
): Promise<IFormatOperation | null> {
|
||||
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> {
|
||||
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) {
|
||||
manifest.operations[existingIndex] = operation;
|
||||
} else {
|
||||
manifest.operations.push(operation);
|
||||
}
|
||||
|
||||
|
||||
await this.saveManifest(manifest);
|
||||
}
|
||||
}
|
||||
|
||||
private isValidManifest(
|
||||
manifest: any,
|
||||
): manifest is { operations: IFormatOperation[] } {
|
||||
// Check if manifest has the required structure
|
||||
if (!manifest || typeof manifest !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check required fields
|
||||
if (!Array.isArray(manifest.operations)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check each operation entry
|
||||
for (const operation of manifest.operations) {
|
||||
if (
|
||||
!operation ||
|
||||
typeof operation !== 'object' ||
|
||||
typeof operation.id !== 'string' ||
|
||||
typeof operation.timestamp !== 'number' ||
|
||||
typeof operation.status !== 'string' ||
|
||||
!Array.isArray(operation.files)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check each file in the operation
|
||||
for (const file of operation.files) {
|
||||
if (
|
||||
!file ||
|
||||
typeof file !== 'object' ||
|
||||
typeof file.path !== 'string' ||
|
||||
typeof file.checksum !== 'string'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -4,14 +4,21 @@ import * as paths from '../paths.js';
|
||||
import { logger } from '../gitzone.logging.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) => {
|
||||
for (const relativeFilePath of filesToDelete) {
|
||||
const fileExists = plugins.smartfile.fs.fileExistsSync(relativeFilePath);
|
||||
if (fileExists) {
|
||||
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 {
|
||||
logger.log('info', `Project is free of ${relativeFilePath}`);
|
||||
}
|
||||
|
@@ -4,56 +4,59 @@ import { logger } from '../gitzone.logging.js';
|
||||
|
||||
export const run = async (projectArg: Project) => {
|
||||
const gitzoneConfig = await projectArg.gitzoneConfig;
|
||||
|
||||
|
||||
// Get copy configuration from npmextra.json
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||
const copyConfig = npmextraConfig.dataFor<any>('gitzone.format.copy', {
|
||||
patterns: []
|
||||
patterns: [],
|
||||
});
|
||||
|
||||
|
||||
if (!copyConfig.patterns || copyConfig.patterns.length === 0) {
|
||||
logger.log('info', 'No copy patterns configured in npmextra.json');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (const pattern of copyConfig.patterns) {
|
||||
if (!pattern.from || !pattern.to) {
|
||||
logger.log('warn', 'Invalid copy pattern - missing "from" or "to" field');
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Handle glob patterns
|
||||
const files = await plugins.smartfile.fs.listFileTree('.', pattern.from);
|
||||
|
||||
|
||||
for (const file of files) {
|
||||
const sourcePath = file;
|
||||
let destPath = pattern.to;
|
||||
|
||||
|
||||
// If destination is a directory, preserve filename
|
||||
if (pattern.to.endsWith('/')) {
|
||||
const filename = plugins.path.basename(file);
|
||||
destPath = plugins.path.join(pattern.to, filename);
|
||||
}
|
||||
|
||||
|
||||
// Handle template variables in destination path
|
||||
if (pattern.preservePath) {
|
||||
const relativePath = plugins.path.relative(
|
||||
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
|
||||
file
|
||||
plugins.path.dirname(pattern.from.replace(/\*/g, '')),
|
||||
file,
|
||||
);
|
||||
destPath = plugins.path.join(pattern.to, relativePath);
|
||||
}
|
||||
|
||||
|
||||
// Ensure destination directory exists
|
||||
await plugins.smartfile.fs.ensureDir(plugins.path.dirname(destPath));
|
||||
|
||||
|
||||
// Copy file
|
||||
await plugins.smartfile.fs.copy(sourcePath, destPath);
|
||||
logger.log('info', `Copied ${sourcePath} to ${destPath}`);
|
||||
}
|
||||
} 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) => {
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
*/
|
||||
|
@@ -12,7 +12,8 @@ export const run = async (projectArg: Project) => {
|
||||
const ciTemplate = await templateModule.getTemplate('gitignore');
|
||||
if (gitignoreExists) {
|
||||
// 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];
|
||||
customPart ? null : (customPart = '');
|
||||
}
|
||||
|
@@ -24,7 +24,9 @@ export const run = async (projectArg: Project) => {
|
||||
} else {
|
||||
logger.log('error', 'Error -> licenses failed. Here is why:');
|
||||
for (const failedModule of licenseCheckResult.failingModules) {
|
||||
console.log(`${failedModule.name} fails with license ${failedModule.license}`);
|
||||
console.log(
|
||||
`${failedModule.name} fails with license ${failedModule.license}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -29,7 +29,12 @@ export const run = async (projectArg: Project) => {
|
||||
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||
if (!plugins.smartobject.smartGet(npmextraJson.gitzone, expectedRepoInformationItem)) {
|
||||
if (
|
||||
!plugins.smartobject.smartGet(
|
||||
npmextraJson.gitzone,
|
||||
expectedRepoInformationItem,
|
||||
)
|
||||
) {
|
||||
interactInstance.addQuestions([
|
||||
{
|
||||
message: `What is the value of ${expectedRepoInformationItem}`,
|
||||
@@ -43,7 +48,9 @@ export const run = async (projectArg: Project) => {
|
||||
|
||||
const answerbucket = await interactInstance.runQueue();
|
||||
for (const expectedRepoInformationItem of expectedRepoInformation) {
|
||||
const cliProvidedValue = answerbucket.getAnswerFor(expectedRepoInformationItem);
|
||||
const cliProvidedValue = answerbucket.getAnswerFor(
|
||||
expectedRepoInformationItem,
|
||||
);
|
||||
if (cliProvidedValue) {
|
||||
plugins.smartobject.smartAdd(
|
||||
npmextraJson.gitzone,
|
||||
|
@@ -19,7 +19,7 @@ const ensureDependency = async (
|
||||
: [dependencyArg, 'latest'];
|
||||
|
||||
const targetSections: string[] = [];
|
||||
|
||||
|
||||
switch (position) {
|
||||
case 'dep':
|
||||
targetSections.push('dependencies');
|
||||
@@ -43,7 +43,8 @@ const ensureDependency = async (
|
||||
break;
|
||||
case 'include':
|
||||
if (!packageJsonObjectArg[section][packageName]) {
|
||||
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version;
|
||||
packageJsonObjectArg[section][packageName] =
|
||||
version === 'latest' ? '^1.0.0' : version;
|
||||
}
|
||||
break;
|
||||
case 'latest':
|
||||
@@ -54,9 +55,13 @@ const ensureDependency = async (
|
||||
const latestVersion = packageInfo['dist-tags'].latest;
|
||||
packageJsonObjectArg[section][packageName] = `^${latestVersion}`;
|
||||
} 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]) {
|
||||
packageJsonObjectArg[section][packageName] = version === 'latest' ? '^1.0.0' : version;
|
||||
packageJsonObjectArg[section][packageName] =
|
||||
version === 'latest' ? '^1.0.0' : version;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -91,9 +96,15 @@ export const run = async (projectArg: Project) => {
|
||||
|
||||
// Check for private or public
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -101,7 +112,10 @@ export const run = async (projectArg: Project) => {
|
||||
if (packageJson.license) {
|
||||
logger.log('info', 'Success -> found license in package.json!');
|
||||
} 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';
|
||||
}
|
||||
|
||||
@@ -109,13 +123,19 @@ export const run = async (projectArg: Project) => {
|
||||
if (packageJson.scripts.build) {
|
||||
logger.log('info', 'Success -> found build script in package.json!');
|
||||
} 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"`;
|
||||
}
|
||||
|
||||
// Check for buildDocs script
|
||||
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`;
|
||||
}
|
||||
|
||||
@@ -134,9 +154,24 @@ export const run = async (projectArg: Project) => {
|
||||
];
|
||||
|
||||
// check for dependencies
|
||||
await ensureDependency(packageJson, 'devDep', 'latest', '@push.rocks/tapbundle');
|
||||
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tstest');
|
||||
await ensureDependency(packageJson, 'devDep', 'latest', '@git.zone/tsbuild');
|
||||
await ensureDependency(
|
||||
packageJson,
|
||||
'devDep',
|
||||
'latest',
|
||||
'@push.rocks/tapbundle',
|
||||
);
|
||||
await ensureDependency(
|
||||
packageJson,
|
||||
'devDep',
|
||||
'latest',
|
||||
'@git.zone/tstest',
|
||||
);
|
||||
await ensureDependency(
|
||||
packageJson,
|
||||
'devDep',
|
||||
'latest',
|
||||
'@git.zone/tsbuild',
|
||||
);
|
||||
|
||||
// set overrides
|
||||
const overrides = plugins.smartfile.fs.toObjectSync(
|
||||
|
@@ -16,7 +16,12 @@ const prettierDefaultMarkdownConfig: prettier.Options = {
|
||||
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) => {
|
||||
switch (fileArg.parsedPath.ext) {
|
||||
@@ -39,7 +44,10 @@ const prettierTypeScriptPipestop = plugins.through2.obj(
|
||||
cb(null);
|
||||
} else {
|
||||
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);
|
||||
cb(null, fileArg);
|
||||
}
|
||||
|
@@ -18,7 +18,8 @@ export const run = async () => {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
await plugins.smartfile.fs.toFs(
|
||||
'# Project Readme Hints\n\nThis is the initial readme hints file.',
|
||||
|
@@ -26,10 +26,12 @@ export const run = async (project: Project) => {
|
||||
case 'npm':
|
||||
case 'wcc':
|
||||
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);
|
||||
} else {
|
||||
const ciTemplateDefault = await templateModule.getTemplate('ci_default_private');
|
||||
const ciTemplateDefault =
|
||||
await templateModule.getTemplate('ci_default_private');
|
||||
ciTemplateDefault.writeToDisk(paths.cwd);
|
||||
}
|
||||
logger.log('info', 'Updated .gitlabci.yml!');
|
||||
@@ -41,7 +43,8 @@ export const run = async (project: Project) => {
|
||||
logger.log('info', 'Updated CI/CD config files!');
|
||||
|
||||
// lets care about docker
|
||||
const dockerTemplate = await templateModule.getTemplate('dockerfile_service');
|
||||
const dockerTemplate =
|
||||
await templateModule.getTemplate('dockerfile_service');
|
||||
dockerTemplate.writeToDisk(paths.cwd);
|
||||
logger.log('info', 'Updated Dockerfile!');
|
||||
|
||||
@@ -56,17 +59,22 @@ export const run = async (project: Project) => {
|
||||
|
||||
// update html
|
||||
if (project.gitzoneConfig.data.projectType === 'website') {
|
||||
const websiteUpdateTemplate = await templateModule.getTemplate('website_update');
|
||||
const variables ={
|
||||
const websiteUpdateTemplate =
|
||||
await templateModule.getTemplate('website_update');
|
||||
const variables = {
|
||||
assetbrokerUrl: project.gitzoneConfig.data.module.assetbrokerUrl,
|
||||
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);
|
||||
await websiteUpdateTemplate.writeToDisk(paths.cwd);
|
||||
logger.log('info', `Updated html for website!`);
|
||||
} 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);
|
||||
logger.log('info', `Updated html for element template!`);
|
||||
} else if (project.gitzoneConfig.data.projectType === 'wcc') {
|
||||
|
@@ -19,8 +19,12 @@ export const run = async (projectArg: Project) => {
|
||||
const publishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
|
||||
for (const publishModule of Object.keys(publishModules)) {
|
||||
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();
|
||||
};
|
||||
|
@@ -7,13 +7,18 @@ export class CleanupFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'cleanup';
|
||||
}
|
||||
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
|
||||
|
||||
// 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) {
|
||||
const exists = await plugins.smartfile.fs.fileExists(file);
|
||||
if (exists) {
|
||||
@@ -21,14 +26,14 @@ export class CleanupFormatter extends BaseFormatter {
|
||||
type: 'delete',
|
||||
path: file,
|
||||
module: this.name,
|
||||
description: `Remove obsolete file`
|
||||
description: `Remove obsolete file`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
switch (change.type) {
|
||||
case 'delete':
|
||||
@@ -36,4 +41,4 @@ export class CleanupFormatter extends BaseFormatter {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class CopyFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'copy', formatCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class GitignoreFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'gitignore', formatGitignore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,30 +7,37 @@ import * as plugins from '../mod.plugins.js';
|
||||
export class LegacyFormatter extends BaseFormatter {
|
||||
private moduleName: string;
|
||||
private formatModule: any;
|
||||
|
||||
constructor(context: any, project: Project, moduleName: string, formatModule: any) {
|
||||
|
||||
constructor(
|
||||
context: any,
|
||||
project: Project,
|
||||
moduleName: string,
|
||||
formatModule: any,
|
||||
) {
|
||||
super(context, project);
|
||||
this.moduleName = moduleName;
|
||||
this.formatModule = formatModule;
|
||||
}
|
||||
|
||||
|
||||
get name(): string {
|
||||
return this.moduleName;
|
||||
}
|
||||
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
// For legacy modules, we can't easily predict changes
|
||||
// So we'll return a generic change that indicates the module will run
|
||||
return [{
|
||||
type: 'modify',
|
||||
path: '<various files>',
|
||||
module: this.name,
|
||||
description: `Run ${this.name} formatter`
|
||||
}];
|
||||
return [
|
||||
{
|
||||
type: 'modify',
|
||||
path: '<various files>',
|
||||
module: this.name,
|
||||
description: `Run ${this.name} formatter`,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
// Run the legacy format module
|
||||
await this.formatModule.run(this.project);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class LicenseFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'license', formatLicense);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class NpmextraFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'npmextra', formatNpmextra);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class PackageJsonFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'packagejson', formatPackageJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,21 +7,16 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'prettier';
|
||||
}
|
||||
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
const changes: IPlannedChange[] = [];
|
||||
|
||||
|
||||
// Define directories to format (TypeScript directories by default)
|
||||
const includeDirs = [
|
||||
'ts',
|
||||
'ts_*',
|
||||
'test',
|
||||
'tests'
|
||||
];
|
||||
|
||||
const includeDirs = ['ts', 'ts_*', 'test', 'tests'];
|
||||
|
||||
// File extensions to format
|
||||
const extensions = '{ts,tsx,js,jsx,json,md,css,scss,html,xml,yaml,yml}';
|
||||
|
||||
|
||||
// Also format root-level config files
|
||||
const rootConfigFiles = [
|
||||
'package.json',
|
||||
@@ -36,33 +31,36 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
'CHANGELOG.md',
|
||||
'license',
|
||||
'LICENSE',
|
||||
'*.md'
|
||||
'*.md',
|
||||
];
|
||||
|
||||
|
||||
// Collect all files to format
|
||||
const allFiles: string[] = [];
|
||||
|
||||
|
||||
// Add files from TypeScript directories
|
||||
for (const dir of includeDirs) {
|
||||
const globPattern = `${dir}/**/*.${extensions}`;
|
||||
const dirFiles = await plugins.smartfile.fs.listFileTree('.', globPattern);
|
||||
const dirFiles = await plugins.smartfile.fs.listFileTree(
|
||||
'.',
|
||||
globPattern,
|
||||
);
|
||||
allFiles.push(...dirFiles);
|
||||
}
|
||||
|
||||
|
||||
// Add root config files
|
||||
for (const pattern of rootConfigFiles) {
|
||||
const rootFiles = await plugins.smartfile.fs.listFileTree('.', pattern);
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
// Remove duplicates
|
||||
const uniqueFiles = [...new Set(allFiles)];
|
||||
|
||||
|
||||
// Get all files that match the pattern
|
||||
const files = uniqueFiles;
|
||||
|
||||
|
||||
// Ensure we only process actual files (not directories)
|
||||
const validFiles: string[] = [];
|
||||
for (const file of files) {
|
||||
@@ -76,48 +74,52 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
logVerbose(`Skipping ${file} - cannot access: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check which files need formatting
|
||||
for (const file of validFiles) {
|
||||
// Skip files that haven't changed
|
||||
if (!await this.shouldProcessFile(file)) {
|
||||
if (!(await this.shouldProcessFile(file))) {
|
||||
logVerbose(`Skipping ${file} - no changes detected`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
changes.push({
|
||||
type: 'modify',
|
||||
path: file,
|
||||
module: this.name,
|
||||
description: 'Format with Prettier'
|
||||
description: 'Format with Prettier',
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
logger.log('info', `Found ${changes.length} files to format with Prettier`);
|
||||
return changes;
|
||||
}
|
||||
|
||||
|
||||
async execute(changes: IPlannedChange[]): Promise<void> {
|
||||
const startTime = this.stats.moduleStartTime(this.name);
|
||||
this.stats.startModule(this.name);
|
||||
|
||||
|
||||
try {
|
||||
await this.preExecute();
|
||||
|
||||
|
||||
// Batch process files
|
||||
const batchSize = 10; // Process 10 files at a time
|
||||
const batches: IPlannedChange[][] = [];
|
||||
|
||||
|
||||
for (let i = 0; i < changes.length; 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++) {
|
||||
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
|
||||
const promises = batch.map(async (change) => {
|
||||
try {
|
||||
@@ -125,44 +127,45 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
this.stats.recordFileOperation(this.name, change.type, true);
|
||||
} catch (error) {
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
await this.postExecute();
|
||||
} catch (error) {
|
||||
await this.context.rollbackOperation();
|
||||
// Rollback removed - no longer tracking operations
|
||||
throw error;
|
||||
} finally {
|
||||
this.stats.endModule(this.name, startTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
if (change.type !== 'modify') return;
|
||||
|
||||
|
||||
try {
|
||||
// Read current content
|
||||
const content = plugins.smartfile.fs.toStringSync(change.path);
|
||||
|
||||
|
||||
// Format with prettier
|
||||
const prettier = await import('prettier');
|
||||
const formatted = await prettier.format(content, {
|
||||
filepath: change.path,
|
||||
...(await this.getPrettierConfig())
|
||||
...(await this.getPrettierConfig()),
|
||||
});
|
||||
|
||||
|
||||
// Only write if content actually changed
|
||||
if (formatted !== content) {
|
||||
await this.modifyFile(change.path, formatted);
|
||||
logVerbose(`Formatted ${change.path}`);
|
||||
} else {
|
||||
// Still update cache even if content didn't change
|
||||
await this.cache.updateFileCache(change.path);
|
||||
logVerbose(`No formatting changes for ${change.path}`);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -170,7 +173,7 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async getPrettierConfig(): Promise<any> {
|
||||
// Try to load prettier config from the project
|
||||
const prettierConfig = new plugins.npmextra.Npmextra();
|
||||
@@ -181,7 +184,7 @@ export class PrettierFormatter extends BaseFormatter {
|
||||
printWidth: 80,
|
||||
tabWidth: 2,
|
||||
semi: true,
|
||||
arrowParens: 'always'
|
||||
arrowParens: 'always',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,17 +6,19 @@ export class ReadmeFormatter extends BaseFormatter {
|
||||
get name(): string {
|
||||
return 'readme';
|
||||
}
|
||||
|
||||
|
||||
async analyze(): Promise<IPlannedChange[]> {
|
||||
return [{
|
||||
type: 'modify',
|
||||
path: 'readme.md',
|
||||
module: this.name,
|
||||
description: 'Ensure readme files exist'
|
||||
}];
|
||||
return [
|
||||
{
|
||||
type: 'modify',
|
||||
path: 'readme.md',
|
||||
module: this.name,
|
||||
description: 'Ensure readme files exist',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
async applyChange(change: IPlannedChange): Promise<void> {
|
||||
await formatReadme.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class TemplatesFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'templates', formatTemplates);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,4 +5,4 @@ export class TsconfigFormatter extends LegacyFormatter {
|
||||
constructor(context: any, project: any) {
|
||||
super(context, project, 'tsconfig', formatTsconfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -16,27 +16,29 @@ import { PrettierFormatter } from './formatters/prettier.formatter.js';
|
||||
import { ReadmeFormatter } from './formatters/readme.formatter.js';
|
||||
import { CopyFormatter } from './formatters/copy.formatter.js';
|
||||
|
||||
export let run = async (options: {
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
planOnly?: boolean;
|
||||
savePlan?: string;
|
||||
fromPlan?: string;
|
||||
detailed?: boolean;
|
||||
interactive?: boolean;
|
||||
parallel?: boolean;
|
||||
verbose?: boolean;
|
||||
} = {}): Promise<any> => {
|
||||
export let run = async (
|
||||
options: {
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
planOnly?: boolean;
|
||||
savePlan?: string;
|
||||
fromPlan?: string;
|
||||
detailed?: boolean;
|
||||
interactive?: boolean;
|
||||
parallel?: boolean;
|
||||
verbose?: boolean;
|
||||
} = {},
|
||||
): Promise<any> => {
|
||||
// Set verbose mode if requested
|
||||
if (options.verbose) {
|
||||
setVerboseMode(true);
|
||||
}
|
||||
|
||||
|
||||
const project = await Project.fromCwd();
|
||||
const context = new FormatContext();
|
||||
await context.initializeCache(); // Initialize the cache system
|
||||
// Cache system removed - no longer needed
|
||||
const planner = new FormatPlanner();
|
||||
|
||||
|
||||
// Get configuration from npmextra
|
||||
const npmextraConfig = new plugins.npmextra.Npmextra();
|
||||
const formatConfig = npmextraConfig.dataFor<any>('gitzone.format', {
|
||||
@@ -49,30 +51,27 @@ export let run = async (options: {
|
||||
autoRollbackOnError: true,
|
||||
backupRetentionDays: 7,
|
||||
maxBackupSize: '100MB',
|
||||
excludePatterns: ['node_modules/**', '.git/**']
|
||||
excludePatterns: ['node_modules/**', '.git/**'],
|
||||
},
|
||||
modules: {
|
||||
skip: [],
|
||||
only: [],
|
||||
order: []
|
||||
order: [],
|
||||
},
|
||||
parallel: true,
|
||||
cache: {
|
||||
enabled: true,
|
||||
clean: true // Clean invalid entries from cache
|
||||
}
|
||||
clean: true, // Clean invalid entries from cache
|
||||
},
|
||||
});
|
||||
|
||||
// Clean cache if configured
|
||||
if (formatConfig.cache.clean) {
|
||||
await context.getChangeCache().clean();
|
||||
}
|
||||
|
||||
|
||||
// Cache cleaning removed - no longer using cache system
|
||||
|
||||
// Override config with command options
|
||||
const interactive = options.interactive ?? formatConfig.interactive;
|
||||
const autoApprove = options.yes ?? formatConfig.autoApprove;
|
||||
const parallel = options.parallel ?? formatConfig.parallel;
|
||||
|
||||
|
||||
try {
|
||||
// Initialize formatters
|
||||
const formatters = [
|
||||
@@ -87,9 +86,9 @@ export let run = async (options: {
|
||||
new ReadmeFormatter(context, project),
|
||||
new CopyFormatter(context, project),
|
||||
];
|
||||
|
||||
|
||||
// Filter formatters based on configuration
|
||||
const activeFormatters = formatters.filter(formatter => {
|
||||
const activeFormatters = formatters.filter((formatter) => {
|
||||
if (formatConfig.modules.only.length > 0) {
|
||||
return formatConfig.modules.only.includes(formatter.name);
|
||||
}
|
||||
@@ -98,33 +97,36 @@ export let run = async (options: {
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Plan phase
|
||||
logger.log('info', 'Analyzing project for format operations...');
|
||||
let plan = options.fromPlan
|
||||
? JSON.parse(await plugins.smartfile.fs.toStringSync(options.fromPlan))
|
||||
: await planner.planFormat(activeFormatters);
|
||||
|
||||
|
||||
// Display plan
|
||||
await planner.displayPlan(plan, options.detailed);
|
||||
|
||||
|
||||
// Save plan if requested
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
||||
// Exit if plan-only mode
|
||||
if (options.planOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Dry-run mode
|
||||
if (options.dryRun) {
|
||||
logger.log('info', 'Dry-run mode - no changes will be made');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Interactive confirmation
|
||||
if (interactive && !autoApprove) {
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
@@ -132,117 +134,56 @@ export let run = async (options: {
|
||||
type: 'confirm',
|
||||
name: 'proceed',
|
||||
message: 'Proceed with formatting?',
|
||||
default: true
|
||||
default: true,
|
||||
});
|
||||
|
||||
|
||||
if (!(response as any).value) {
|
||||
logger.log('info', 'Format operation cancelled by user');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
// Finish statistics tracking
|
||||
context.getFormatStats().finish();
|
||||
|
||||
|
||||
// Display statistics
|
||||
const showStats = npmextraConfig.dataFor('gitzone.format.showStats', true);
|
||||
if (showStats) {
|
||||
context.getFormatStats().displayStats();
|
||||
}
|
||||
|
||||
|
||||
// Save stats if requested
|
||||
if (options.detailed) {
|
||||
const statsPath = `.nogit/format-stats-${Date.now()}.json`;
|
||||
await context.getFormatStats().saveReport(statsPath);
|
||||
}
|
||||
|
||||
|
||||
logger.log('success', 'Format operations completed successfully!');
|
||||
|
||||
} catch (error) {
|
||||
logger.log('error', `Format operation failed: ${error.message}`);
|
||||
|
||||
// Automatic rollback if enabled
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rollback system has been removed for stability
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Export CLI command handlers
|
||||
export const handleRollback = async (operationId?: string): Promise<void> => {
|
||||
const context = new FormatContext();
|
||||
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;
|
||||
}
|
||||
logger.log('info', 'Rollback system has been disabled for stability');
|
||||
};
|
||||
|
||||
export const handleListBackups = async (): Promise<void> => {
|
||||
const context = new FormatContext();
|
||||
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));
|
||||
}
|
||||
logger.log('info', 'Backup system has been disabled for stability');
|
||||
};
|
||||
|
||||
export const handleCleanBackups = async (): Promise<void> => {
|
||||
const context = new FormatContext();
|
||||
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`);
|
||||
};
|
||||
logger.log('info', 'Backup cleaning has been disabled - backup system removed');
|
||||
};
|
||||
|
@@ -9,7 +9,7 @@ export type IFormatOperation = {
|
||||
}>;
|
||||
status: 'pending' | 'in-progress' | 'completed' | 'failed' | 'rolled-back';
|
||||
error?: Error;
|
||||
}
|
||||
};
|
||||
|
||||
export type IFormatPlan = {
|
||||
summary: {
|
||||
@@ -32,7 +32,7 @@ export type IFormatPlan = {
|
||||
message: string;
|
||||
module: string;
|
||||
}>;
|
||||
}
|
||||
};
|
||||
|
||||
export type IPlannedChange = {
|
||||
type: 'create' | 'modify' | 'delete';
|
||||
@@ -42,4 +42,4 @@ export type IPlannedChange = {
|
||||
content?: string; // For create/modify operations
|
||||
diff?: string;
|
||||
size?: number;
|
||||
}
|
||||
};
|
||||
|
@@ -35,7 +35,10 @@ export class Meta {
|
||||
* sorts the metaRepoData
|
||||
*/
|
||||
public async sortMetaRepoData() {
|
||||
const stringifiedMetadata = plugins.smartjson.stringify(this.metaRepoData, []);
|
||||
const stringifiedMetadata = plugins.smartjson.stringify(
|
||||
this.metaRepoData,
|
||||
[],
|
||||
);
|
||||
this.metaRepoData = plugins.smartjson.parse(stringifiedMetadata);
|
||||
}
|
||||
|
||||
@@ -45,11 +48,15 @@ export class Meta {
|
||||
public async readDirectory() {
|
||||
await this.syncToRemote(true);
|
||||
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) {
|
||||
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,
|
||||
);
|
||||
// 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) {
|
||||
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) {
|
||||
logger.log('info', `cleaning the repository from old directories`);
|
||||
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
|
||||
}
|
||||
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() {
|
||||
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);
|
||||
for (const preExistingFolderArg of preExistingFolders) {
|
||||
if (
|
||||
@@ -107,14 +123,18 @@ export class Meta {
|
||||
projectFolder.startsWith(preExistingFolderArg),
|
||||
)
|
||||
) {
|
||||
const response = await plugins.smartinteraction.SmartInteract.getCliConfirmation(
|
||||
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
|
||||
true,
|
||||
);
|
||||
const response =
|
||||
await plugins.smartinteraction.SmartInteract.getCliConfirmation(
|
||||
`Do you want to delete superfluous directory >>${preExistingFolderArg}<< ?`,
|
||||
true,
|
||||
);
|
||||
if (response) {
|
||||
logger.log('warn', `Deleting >>${preExistingFolderArg}<<!`);
|
||||
} 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() {
|
||||
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) {
|
||||
await plugins.smartfile.memory.toFs(
|
||||
JSON.stringify({
|
||||
@@ -168,7 +190,10 @@ export class Meta {
|
||||
}),
|
||||
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(
|
||||
JSON.stringify({
|
||||
name: this.dirName,
|
||||
@@ -176,9 +201,15 @@ export class Meta {
|
||||
}),
|
||||
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 {
|
||||
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(
|
||||
`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];
|
||||
|
||||
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;
|
||||
@@ -217,7 +250,10 @@ export class Meta {
|
||||
const existingProject = this.metaRepoData.projects[projectNameArg];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -228,7 +264,9 @@ export class Meta {
|
||||
await this.writeToDisk();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@@ -16,7 +16,9 @@ export let run = () => {
|
||||
* create a new project with 'gitzone template [template]'
|
||||
the following templates exist: ${(() => {
|
||||
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`;
|
||||
}
|
||||
return projects;
|
||||
|
@@ -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 pull origin master`);
|
||||
await smartshellInstance.execStrict(
|
||||
`cd ${paths.cwd} && git pull origin master`,
|
||||
);
|
||||
await smartshellInstance.execStrict(`cd ${paths.cwd} && npm ci`);
|
||||
|
||||
await provideNoGitFiles();
|
||||
|
@@ -16,7 +16,9 @@ export const isTemplate = async (templateNameArg: string) => {
|
||||
|
||||
export const getTemplate = async (templateNameArg: string) => {
|
||||
if (isTemplate(templateNameArg)) {
|
||||
const localScafTemplate = new plugins.smartscaf.ScafTemplate(getTemplatePath(templateNameArg));
|
||||
const localScafTemplate = new plugins.smartscaf.ScafTemplate(
|
||||
getTemplatePath(templateNameArg),
|
||||
);
|
||||
await localScafTemplate.readTemplateFromDir();
|
||||
return localScafTemplate;
|
||||
} else {
|
||||
@@ -32,7 +34,8 @@ export const run = async (argvArg: any) => {
|
||||
const answerBucket = await smartinteract.askQuestion({
|
||||
type: 'list',
|
||||
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',
|
||||
choices: ['npm', 'service', 'wcc', 'website'],
|
||||
});
|
||||
|
Reference in New Issue
Block a user