feat(mod_format): Add check-only formatting with interactive diff preview; make formatting default to dry-run and extend formatting API
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import { FormatContext } from './classes.formatcontext.js';
|
||||
import type { IPlannedChange } from './interfaces.format.js';
|
||||
import type { IPlannedChange, ICheckResult } from './interfaces.format.js';
|
||||
import { Project } from '../classes.project.js';
|
||||
|
||||
export abstract class BaseFormatter {
|
||||
@@ -79,4 +79,94 @@ export abstract class BaseFormatter {
|
||||
protected async shouldProcessFile(filepath: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for diffs without applying changes
|
||||
* Returns information about what would change
|
||||
*/
|
||||
async check(): Promise<ICheckResult> {
|
||||
const changes = await this.analyze();
|
||||
const diffs: ICheckResult['diffs'] = [];
|
||||
|
||||
for (const change of changes) {
|
||||
// Skip generic changes that don't have actual content
|
||||
if (change.path === '<various files>') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (change.type === 'modify' || change.type === 'create') {
|
||||
// Read current content if file exists
|
||||
let currentContent: string | undefined;
|
||||
try {
|
||||
currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
currentContent = undefined;
|
||||
}
|
||||
|
||||
const newContent = change.content;
|
||||
|
||||
// Check if there's an actual diff
|
||||
if (currentContent !== newContent && newContent !== undefined) {
|
||||
diffs.push({
|
||||
path: change.path,
|
||||
type: change.type,
|
||||
before: currentContent,
|
||||
after: newContent,
|
||||
});
|
||||
}
|
||||
} else if (change.type === 'delete') {
|
||||
// Check if file exists before marking for deletion
|
||||
try {
|
||||
const currentContent = await plugins.smartfs.file(change.path).encoding('utf8').read() as string;
|
||||
diffs.push({
|
||||
path: change.path,
|
||||
type: 'delete',
|
||||
before: currentContent,
|
||||
after: undefined,
|
||||
});
|
||||
} catch {
|
||||
// File doesn't exist, nothing to delete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasDiff: diffs.length > 0,
|
||||
diffs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a single diff using smartdiff
|
||||
*/
|
||||
displayDiff(diff: ICheckResult['diffs'][0]): void {
|
||||
console.log(`\n--- ${diff.path}`);
|
||||
if (diff.before && diff.after) {
|
||||
console.log(plugins.smartdiff.formatLineDiffForConsole(diff.before, diff.after));
|
||||
} else if (diff.after && !diff.before) {
|
||||
console.log(' (new file)');
|
||||
// Show first few lines of new content
|
||||
const lines = diff.after.split('\n').slice(0, 10);
|
||||
lines.forEach(line => console.log(` + ${line}`));
|
||||
if (diff.after.split('\n').length > 10) {
|
||||
console.log(' ... (truncated)');
|
||||
}
|
||||
} else if (diff.before && !diff.after) {
|
||||
console.log(' (file will be deleted)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all diffs from a check result
|
||||
*/
|
||||
displayAllDiffs(result: ICheckResult): void {
|
||||
if (!result.hasDiff) {
|
||||
console.log(' No changes detected');
|
||||
return;
|
||||
}
|
||||
for (const diff of result.diffs) {
|
||||
this.displayDiff(diff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ import { CopyFormatter } from './formatters/copy.formatter.js';
|
||||
|
||||
export let run = async (
|
||||
options: {
|
||||
dryRun?: boolean;
|
||||
write?: boolean; // Explicitly write changes (default: false, dry-mode)
|
||||
dryRun?: boolean; // Deprecated, kept for compatibility
|
||||
yes?: boolean;
|
||||
planOnly?: boolean;
|
||||
savePlan?: string;
|
||||
@@ -35,7 +36,11 @@ export let run = async (
|
||||
setVerboseMode(true);
|
||||
}
|
||||
|
||||
const project = await Project.fromCwd();
|
||||
// Determine if we should write changes
|
||||
// Default is dry-mode (no writing) unless --write/-w is specified
|
||||
const shouldWrite = options.write ?? (options.dryRun === false);
|
||||
|
||||
const project = await Project.fromCwd({ requireProjectType: false });
|
||||
const context = new FormatContext();
|
||||
// Cache system removed - no longer needed
|
||||
const planner = new FormatPlanner();
|
||||
@@ -127,9 +132,9 @@ export let run = async (
|
||||
return;
|
||||
}
|
||||
|
||||
// Dry-run mode
|
||||
if (options.dryRun) {
|
||||
logger.log('info', 'Dry-run mode - no changes will be made');
|
||||
// Dry-run mode (default behavior)
|
||||
if (!shouldWrite) {
|
||||
logger.log('info', 'Dry-run mode - use --write (-w) to apply changes');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,14 +202,27 @@ export const handleCleanBackups = async (): Promise<void> => {
|
||||
);
|
||||
};
|
||||
|
||||
// Import the ICheckResult type for external use
|
||||
import type { ICheckResult } from './interfaces.format.js';
|
||||
export type { ICheckResult };
|
||||
|
||||
// Formatters that don't require projectType to be set
|
||||
const formattersNotRequiringProjectType = ['npmextra', 'prettier', 'cleanup', 'packagejson'];
|
||||
|
||||
/**
|
||||
* Run a single formatter by name (for use by other modules)
|
||||
*/
|
||||
export const runFormatter = async (
|
||||
formatterName: string,
|
||||
options: { silent?: boolean } = {}
|
||||
): Promise<void> => {
|
||||
const project = await Project.fromCwd();
|
||||
options: {
|
||||
silent?: boolean;
|
||||
checkOnly?: boolean; // Only check for diffs, don't apply
|
||||
showDiff?: boolean; // Show the diff output
|
||||
} = {}
|
||||
): Promise<ICheckResult | void> => {
|
||||
// Determine if this formatter requires projectType
|
||||
const requireProjectType = !formattersNotRequiringProjectType.includes(formatterName);
|
||||
const project = await Project.fromCwd({ requireProjectType });
|
||||
const context = new FormatContext();
|
||||
|
||||
// Map formatter names to classes
|
||||
@@ -227,6 +245,17 @@ export const runFormatter = async (
|
||||
}
|
||||
|
||||
const formatter = new FormatterClass(context, project);
|
||||
|
||||
// Check-only mode: just check for diffs and optionally display them
|
||||
if (options.checkOnly) {
|
||||
const result = await formatter.check();
|
||||
if (result.hasDiff && options.showDiff) {
|
||||
formatter.displayAllDiffs(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Normal mode: analyze and apply changes
|
||||
const changes = await formatter.analyze();
|
||||
|
||||
for (const change of changes) {
|
||||
|
||||
@@ -39,7 +39,18 @@ export type IPlannedChange = {
|
||||
path: string;
|
||||
module: string;
|
||||
description: string;
|
||||
content?: string; // For create/modify operations
|
||||
content?: string; // New content for create/modify operations
|
||||
originalContent?: string; // Original content for comparison
|
||||
diff?: string;
|
||||
size?: number;
|
||||
};
|
||||
|
||||
export interface ICheckResult {
|
||||
hasDiff: boolean;
|
||||
diffs: Array<{
|
||||
path: string;
|
||||
type: 'create' | 'modify' | 'delete';
|
||||
before?: string;
|
||||
after?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user