@@ -436,59 +436,82 @@ async function listDir(payload: ListDirPayload): Promise<LocalOpResult> {
436436 return { ok : true , data : { entries } } ;
437437}
438438
439- function readFiles ( payload : ReadFilesPayload ) : LocalOpResult {
440- const { cwd, params } = payload ;
441- const maxBytes = params . maxBytes ?? MAX_FILE_BYTES ;
442- const files : Record < string , string | null > = { } ;
443-
444- for ( const filePath of params . paths ) {
445- try {
446- const absPath = safePath ( cwd , filePath ) ;
447- const stat = fs . statSync ( absPath ) ;
448- let content : string ;
449- if ( stat . size > maxBytes ) {
450- // Read only up to maxBytes
439+ async function readSingleFile (
440+ cwd : string ,
441+ filePath : string ,
442+ maxBytes : number
443+ ) : Promise < string | null > {
444+ try {
445+ const absPath = safePath ( cwd , filePath ) ;
446+ const stat = await fs . promises . stat ( absPath ) ;
447+ let content : string ;
448+ if ( stat . size > maxBytes ) {
449+ const fh = await fs . promises . open ( absPath , "r" ) ;
450+ try {
451451 const buffer = Buffer . alloc ( maxBytes ) ;
452- const fd = fs . openSync ( absPath , "r" ) ;
453- try {
454- fs . readSync ( fd , buffer , 0 , maxBytes , 0 ) ;
455- } finally {
456- fs . closeSync ( fd ) ;
457- }
452+ await fh . read ( buffer , 0 , maxBytes , 0 ) ;
458453 content = buffer . toString ( "utf-8" ) ;
459- } else {
460- content = fs . readFileSync ( absPath , "utf-8" ) ;
454+ } finally {
455+ await fh . close ( ) ;
461456 }
457+ } else {
458+ content = await fs . promises . readFile ( absPath , "utf-8" ) ;
459+ }
462460
463- // Minify JSON files by stripping whitespace/formatting
464- if ( filePath . endsWith ( ".json" ) ) {
465- try {
466- content = JSON . stringify ( JSON . parse ( content ) ) ;
467- } catch {
468- // Not valid JSON (truncated, JSONC, etc.) — send as-is
469- }
461+ // Minify JSON files by stripping whitespace/formatting
462+ if ( filePath . endsWith ( ".json" ) ) {
463+ try {
464+ content = JSON . stringify ( JSON . parse ( content ) ) ;
465+ } catch {
466+ // Not valid JSON (truncated, JSONC, etc.) — send as-is
470467 }
471-
472- files [ filePath ] = content ;
473- } catch {
474- files [ filePath ] = null ;
475468 }
469+
470+ return content ;
471+ } catch {
472+ return null ;
473+ }
474+ }
475+
476+ async function readFiles ( payload : ReadFilesPayload ) : Promise < LocalOpResult > {
477+ const { cwd, params } = payload ;
478+ const maxBytes = params . maxBytes ?? MAX_FILE_BYTES ;
479+
480+ const results = await Promise . all (
481+ params . paths . map ( async ( filePath ) => {
482+ const content = await readSingleFile ( cwd , filePath , maxBytes ) ;
483+ return [ filePath , content ] as const ;
484+ } )
485+ ) ;
486+
487+ const files : Record < string , string | null > = { } ;
488+ for ( const [ filePath , content ] of results ) {
489+ files [ filePath ] = content ;
476490 }
477491
478492 return { ok : true , data : { files } } ;
479493}
480494
481- function fileExistsBatch ( payload : FileExistsBatchPayload ) : LocalOpResult {
495+ async function fileExistsBatch (
496+ payload : FileExistsBatchPayload
497+ ) : Promise < LocalOpResult > {
482498 const { cwd, params } = payload ;
483- const exists : Record < string , boolean > = { } ;
484499
485- for ( const filePath of params . paths ) {
486- try {
487- const absPath = safePath ( cwd , filePath ) ;
488- exists [ filePath ] = fs . existsSync ( absPath ) ;
489- } catch {
490- exists [ filePath ] = false ;
491- }
500+ const results = await Promise . all (
501+ params . paths . map ( async ( filePath ) => {
502+ try {
503+ const absPath = safePath ( cwd , filePath ) ;
504+ await fs . promises . access ( absPath ) ;
505+ return [ filePath , true ] as const ;
506+ } catch {
507+ return [ filePath , false ] as const ;
508+ }
509+ } )
510+ ) ;
511+
512+ const exists : Record < string , boolean > = { } ;
513+ for ( const [ filePath , found ] of results ) {
514+ exists [ filePath ] = found ;
492515 }
493516
494517 return { ok : true , data : { exists } } ;
@@ -626,24 +649,56 @@ function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult {
626649 * indentation style is detected and preserved. For `create` actions, a default
627650 * of 2-space indentation is used.
628651 */
629- function resolvePatchContent (
652+ async function resolvePatchContent (
630653 absPath : string ,
631654 patch : ApplyPatchsetPayload [ "params" ] [ "patches" ] [ number ]
632- ) : string {
655+ ) : Promise < string > {
633656 if ( ! patch . path . endsWith ( ".json" ) ) {
634657 return patch . patch ;
635658 }
636659 if ( patch . action === "modify" ) {
637- const existing = fs . readFileSync ( absPath , "utf-8" ) ;
660+ const existing = await fs . promises . readFile ( absPath , "utf-8" ) ;
638661 return prettyPrintJson ( patch . patch , detectJsonIndent ( existing ) ) ;
639662 }
640663 return prettyPrintJson ( patch . patch , DEFAULT_JSON_INDENT ) ;
641664}
642665
643- function applyPatchset (
666+ type Patch = ApplyPatchsetPayload [ "params" ] [ "patches" ] [ number ] ;
667+
668+ const VALID_PATCH_ACTIONS = new Set ( [ "create" , "modify" , "delete" ] ) ;
669+
670+ async function applySinglePatch ( absPath : string , patch : Patch ) : Promise < void > {
671+ switch ( patch . action ) {
672+ case "create" : {
673+ await fs . promises . mkdir ( path . dirname ( absPath ) , { recursive : true } ) ;
674+ const content = await resolvePatchContent ( absPath , patch ) ;
675+ await fs . promises . writeFile ( absPath , content , "utf-8" ) ;
676+ break ;
677+ }
678+ case "modify" : {
679+ const content = await resolvePatchContent ( absPath , patch ) ;
680+ await fs . promises . writeFile ( absPath , content , "utf-8" ) ;
681+ break ;
682+ }
683+ case "delete" : {
684+ try {
685+ await fs . promises . unlink ( absPath ) ;
686+ } catch ( err ) {
687+ if ( ( err as NodeJS . ErrnoException ) . code !== "ENOENT" ) {
688+ throw err ;
689+ }
690+ }
691+ break ;
692+ }
693+ default :
694+ break ;
695+ }
696+ }
697+
698+ async function applyPatchset (
644699 payload : ApplyPatchsetPayload ,
645700 dryRun ?: boolean
646- ) : LocalOpResult {
701+ ) : Promise < LocalOpResult > {
647702 if ( dryRun ) {
648703 return applyPatchsetDryRun ( payload ) ;
649704 }
@@ -653,56 +708,34 @@ function applyPatchset(
653708 // Phase 1: Validate all paths and actions before writing anything
654709 for ( const patch of params . patches ) {
655710 safePath ( cwd , patch . path ) ;
656- if ( ! [ "create" , "modify" , "delete" ] . includes ( patch . action ) ) {
711+ if ( ! VALID_PATCH_ACTIONS . has ( patch . action ) ) {
657712 return {
658713 ok : false ,
659714 error : `Unknown patch action: "${ patch . action } " for path "${ patch . path } "` ,
660715 } ;
661716 }
662717 }
663718
664- // Phase 2: Apply patches
719+ // Phase 2: Apply patches (sequential — later patches may depend on earlier creates)
665720 const applied : Array < { path : string ; action : string } > = [ ] ;
666721
667722 for ( const patch of params . patches ) {
668723 const absPath = safePath ( cwd , patch . path ) ;
669724
670- switch ( patch . action ) {
671- case "create" : {
672- const dir = path . dirname ( absPath ) ;
673- fs . mkdirSync ( dir , { recursive : true } ) ;
674- const content = resolvePatchContent ( absPath , patch ) ;
675- fs . writeFileSync ( absPath , content , "utf-8" ) ;
676- applied . push ( { path : patch . path , action : "create" } ) ;
677- break ;
678- }
679- case "modify" : {
680- if ( ! fs . existsSync ( absPath ) ) {
681- return {
682- ok : false ,
683- error : `Cannot modify "${ patch . path } ": file does not exist` ,
684- data : { applied } ,
685- } ;
686- }
687- const content = resolvePatchContent ( absPath , patch ) ;
688- fs . writeFileSync ( absPath , content , "utf-8" ) ;
689- applied . push ( { path : patch . path , action : "modify" } ) ;
690- break ;
691- }
692- case "delete" : {
693- if ( fs . existsSync ( absPath ) ) {
694- fs . unlinkSync ( absPath ) ;
695- }
696- applied . push ( { path : patch . path , action : "delete" } ) ;
697- break ;
698- }
699- default :
725+ if ( patch . action === "modify" ) {
726+ try {
727+ await fs . promises . access ( absPath ) ;
728+ } catch {
700729 return {
701730 ok : false ,
702- error : `Unknown patch action: "${ patch . action } " for path " ${ patch . path } " ` ,
731+ error : `Cannot modify "${ patch . path } ": file does not exist ` ,
703732 data : { applied } ,
704733 } ;
734+ }
705735 }
736+
737+ await applySinglePatch ( absPath , patch ) ;
738+ applied . push ( { path : patch . path , action : patch . action } ) ;
706739 }
707740
708741 return { ok : true , data : { applied } } ;
0 commit comments