import { rm , rename } from 'fs/promises' ; import { join } from 'path' ; import { deleteEditorFile , openEditor , readEditorFile , writeEditorFile } from './editor' ; import { logger } from './logger' ; import { tap } from './utils' ; import match from 'fast-glob' ; export type Operation = 'DELETE' | 'RENAME' | 'NONE' ; export interface Entry { initialName : string ; finalName : string ; operation : Operation ; } export interface Options { /**
     * The directory in which to perform the operation.
     */ directory : string ; /**
     * Whether to perform the operations or not.
     */ dry : boolean ; /**
     * Whether to write on the standard output.
     */ silent : boolean ; /**
     * The glob pattern to match the files to be processed.
     */ glob : string ; /**
     * Only match directories.
     */ onlyDirectories : boolean ; /**
     * Only match files.
     */ onlyFiles : boolean ; /**
     * Defines the maximum depth for globstars.
     */ depth : number ; } /**
 * Merges the given options with the default ones.
 */ export function getOptionsWithDefaults ( options ) { return { dry : false , silent : false , directory : process . cwd ( ) , glob : '*' , onlyDirectories : false , onlyFiles : false , depth : Infinity , ... JSON . parse ( JSON . stringify ( options ) ) , } ; } /**
 * Gets an operation type by comparing the initial and final file names.
 */ export function getOperation ( initial , final ) { if ( initial === final ) { return 'NONE' ; } if ( final . trim ( ) . length === 0 ) { return 'DELETE' ; } return 'RENAME' ; } /**
 * Checks if a parent of the given path has been deleted in the given operations.
 */ function parentHasBeenDeleted ( path , operations ) { const deletions = operations . filter ( ( { operation } ) => operation === 'DELETE' ) ; return Boolean ( deletions . find ( ( { initialName : previouslyDeletedPath } ) => { if ( previouslyDeletedPath === path ) { return false ; } return path . startsWith ( previouslyDeletedPath ) ; } ) ) ; } /**
 * Returns an up-to-date path given a path and an operation.
 */ function getRenamedPath ( path , operations ) { const renames = operations . filter ( ( { operation } ) => operation === 'RENAME' ) ; const previousDirectoryRename = renames . find ( ( { initialName : previouslyRenamedPath } ) => { if ( previouslyRenamedPath === path ) { return false ; } return path . startsWith ( previouslyRenamedPath ) ; } ) ; return previousDirectoryRename ? path . replace ( previousDirectoryRename . initialName , previousDirectoryRename . finalName ) : path ; } /**
 * Applies the given operation with on the given file.
 */ export async function applyOperation ( initialFileName , finalFileName , operation , operations , options ) { logger . operation ( operation , initialFileName , finalFileName ) ; if ( options . dry ) { return ; } if ( parentHasBeenDeleted ( initialFileName , operations ) ) { return ; } if ( operation === 'DELETE' ) { await rm ( join ( options . directory , initialFileName ) , { recursive : true , force : true } ) ; } if ( operation === 'RENAME' ) { await rename ( join ( options . directory , getRenamedPath ( initialFileName , operations ) ) , join ( options . directory , finalFileName ) ) ; } } /**
 * Finds file based on the given options.
 */ export async function getFiles ( options ) { return await match ( options . glob , { cwd : options . directory , onlyDirectories : options . onlyDirectories , onlyFiles : options . onlyFiles , deep : options . depth } ) ; } /**
 * Prompts the user for a rename in the given directory.
 */ export async function startRenameProcess ( options ) { const resolved = getOptionsWithDefaults ( options ) ; const filesToRename = await getFiles ( resolved ) ; if ( filesToRename . length === 0 ) { logger . info ( 'No files to rename.' ) ; return ; } await writeEditorFile ( filesToRename ) ; await openEditor ( ) ; const renamedFiles = await readEditorFile ( ) ; if ( filesToRename . length === renamedFiles . length - 1 && renamedFiles . slice ( - 1 ) . shift ( ) ?. length === 0 ) { renamedFiles . pop ( ) ; } if ( filesToRename . length !== renamedFiles . length ) { throw new Error ( 'Line count mismatch. Operation canceled.' ) ; } const entries = filesToRename . map ( ( initialName , index ) => ( { initialName , finalName : renamedFiles [ index ] ?? '' , operation : getOperation ( initialName , renamedFiles [ index ] ) , } ) ) ; for ( const { initialName , finalName , operation } of entries ) { await applyOperation ( initialName , finalName , operation , entries , resolved ) ; } logger . feedback ( entries , resolved ) ; await deleteEditorFile ( ) ; }