@@ -29,6 +29,8 @@ import {
2929 getDyadRunBackendTerminalCmdTags ,
3030 getDyadRunFrontendTerminalCmdTags ,
3131 getDyadRunTerminalCmdTags ,
32+ getWriteToFileTags ,
33+ getSearchReplaceTags ,
3234} from "../utils/dyad_tag_parser" ;
3335import { runShellCommand } from "../utils/runShellCommand" ;
3436import { storeDbTimestampAtCurrentVersion } from "../utils/neon_timestamp_utils" ;
@@ -118,6 +120,8 @@ export async function processFullResponseActions(
118120 try {
119121 // Extract all tags
120122 const dyadWriteTags = getDyadWriteTags ( fullResponse ) ;
123+ const writeToFileTags = getWriteToFileTags ( fullResponse ) ;
124+ const searchReplaceTags = getSearchReplaceTags ( fullResponse ) ;
121125 const dyadRenameTags = getDyadRenameTags ( fullResponse ) ;
122126 const dyadDeletePaths = getDyadDeleteTags ( fullResponse ) ;
123127 const dyadAddDependencyPackages = getDyadAddDependencyTags ( fullResponse ) ;
@@ -456,12 +460,108 @@ export async function processFullResponseActions(
456460 }
457461 }
458462
459- // Process all file writes
463+ // Process all file writes (dyad-write tags)
460464 for ( const tag of dyadWriteTags ) {
461465 const filePath = tag . path ;
462466 let content : string | Buffer = tag . content ;
463467 const fullFilePath = safeJoin ( appPath , filePath ) ;
464468
469+ // Check if this is a search_replace operation
470+ if ( typeof content === "string" && content . startsWith ( "SEARCH_REPLACE:" ) ) {
471+ // Handle search_replace operation
472+ const parts = content . split ( ":" ) ;
473+ if ( parts . length >= 3 ) {
474+ const oldString = parts [ 1 ] ;
475+ const newString = parts . slice ( 2 ) . join ( ":" ) ;
476+
477+ if ( fs . existsSync ( fullFilePath ) ) {
478+ try {
479+ let fileContent = fs . readFileSync ( fullFilePath , 'utf8' ) ;
480+
481+ if ( fileContent . includes ( oldString ) ) {
482+ fileContent = fileContent . replace ( oldString , newString ) ;
483+ fs . writeFileSync ( fullFilePath , fileContent ) ;
484+ logger . log ( `Successfully applied search_replace to file: ${ fullFilePath } ` ) ;
485+ writtenFiles . push ( filePath ) ;
486+ } else {
487+ logger . warn ( `Old string not found in file for search_replace: ${ fullFilePath } ` ) ;
488+ warnings . push ( {
489+ message : `Search string not found in file: ${ filePath } ` ,
490+ error : null ,
491+ } ) ;
492+ }
493+ } catch ( error ) {
494+ logger . error ( `Failed to apply search_replace to file: ${ fullFilePath } ` , error ) ;
495+ errors . push ( {
496+ message : `Failed to apply search_replace to file: ${ filePath } ` ,
497+ error : error ,
498+ } ) ;
499+ }
500+ } else {
501+ logger . warn ( `File not found for search_replace: ${ fullFilePath } ` ) ;
502+ warnings . push ( {
503+ message : `File not found for search_replace: ${ filePath } ` ,
504+ error : null ,
505+ } ) ;
506+ }
507+ }
508+ } else {
509+ // Handle regular file write
510+ // Check if content (stripped of whitespace) exactly matches a file ID and replace with actual file content
511+ if ( fileUploadsMap ) {
512+ const trimmedContent = tag . content . trim ( ) ;
513+ const fileInfo = fileUploadsMap . get ( trimmedContent ) ;
514+ if ( fileInfo ) {
515+ try {
516+ const fileContent = await readFile ( fileInfo . filePath ) ;
517+ content = fileContent ;
518+ logger . log (
519+ `Replaced file ID ${ trimmedContent } with content from ${ fileInfo . originalName } ` ,
520+ ) ;
521+ } catch ( error ) {
522+ logger . error (
523+ `Failed to read uploaded file ${ fileInfo . originalName } :` ,
524+ error ,
525+ ) ;
526+ errors . push ( {
527+ message : `Failed to read uploaded file: ${ fileInfo . originalName } ` ,
528+ error : error ,
529+ } ) ;
530+ }
531+ }
532+ }
533+
534+ // Ensure directory exists
535+ const dirPath = path . dirname ( fullFilePath ) ;
536+ fs . mkdirSync ( dirPath , { recursive : true } ) ;
537+
538+ // Write file content
539+ fs . writeFileSync ( fullFilePath , content ) ;
540+ logger . log ( `Successfully wrote file: ${ fullFilePath } ` ) ;
541+ writtenFiles . push ( filePath ) ;
542+ if ( isServerFunction ( filePath ) && typeof content === "string" ) {
543+ try {
544+ await deploySupabaseFunctions ( {
545+ supabaseProjectId : chatWithApp . app . supabaseProjectId ! ,
546+ functionName : path . basename ( path . dirname ( filePath ) ) ,
547+ content : content ,
548+ } ) ;
549+ } catch ( error ) {
550+ errors . push ( {
551+ message : `Failed to deploy Supabase function: ${ filePath } ` ,
552+ error : error ,
553+ } ) ;
554+ }
555+ }
556+ }
557+ }
558+
559+ // Process write_to_file tags
560+ for ( const tag of writeToFileTags ) {
561+ const filePath = tag . path ;
562+ let content : string | Buffer = tag . content ;
563+ const fullFilePath = safeJoin ( appPath , filePath ) ;
564+
465565 // Check if content (stripped of whitespace) exactly matches a file ID and replace with actual file content
466566 if ( fileUploadsMap ) {
467567 const trimmedContent = tag . content . trim ( ) ;
@@ -492,7 +592,7 @@ export async function processFullResponseActions(
492592
493593 // Write file content
494594 fs . writeFileSync ( fullFilePath , content ) ;
495- logger . log ( `Successfully wrote file: ${ fullFilePath } ` ) ;
595+ logger . log ( `Successfully wrote file via write_to_file tag : ${ fullFilePath } ` ) ;
496596 writtenFiles . push ( filePath ) ;
497597 if ( isServerFunction ( filePath ) && typeof content === "string" ) {
498598 try {
@@ -510,12 +610,55 @@ export async function processFullResponseActions(
510610 }
511611 }
512612
613+ // Process search_replace tags
614+ for ( const tag of searchReplaceTags ) {
615+ const filePath = tag . file ;
616+ const fullFilePath = safeJoin ( appPath , filePath ) ;
617+
618+ if ( fs . existsSync ( fullFilePath ) ) {
619+ try {
620+ let fileContent = fs . readFileSync ( fullFilePath , 'utf8' ) ;
621+
622+ // Replace old_string with new_string
623+ const oldString = tag . old_string . replace ( / " / g, '"' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) . replace ( / & / g, '&' ) ;
624+ const newString = tag . new_string . replace ( / " / g, '"' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) . replace ( / & / g, '&' ) ;
625+
626+ if ( fileContent . includes ( oldString ) ) {
627+ fileContent = fileContent . replace ( oldString , newString ) ;
628+ fs . writeFileSync ( fullFilePath , fileContent ) ;
629+ logger . log ( `Successfully applied search_replace to file: ${ fullFilePath } ` ) ;
630+ writtenFiles . push ( filePath ) ;
631+ } else {
632+ logger . warn ( `Old string not found in file for search_replace: ${ fullFilePath } ` ) ;
633+ warnings . push ( {
634+ message : `Search string not found in file: ${ filePath } ` ,
635+ error : null ,
636+ } ) ;
637+ }
638+ } catch ( error ) {
639+ logger . error ( `Failed to apply search_replace to file: ${ fullFilePath } ` , error ) ;
640+ errors . push ( {
641+ message : `Failed to apply search_replace to file: ${ filePath } ` ,
642+ error : error ,
643+ } ) ;
644+ }
645+ } else {
646+ logger . warn ( `File not found for search_replace: ${ fullFilePath } ` ) ;
647+ warnings . push ( {
648+ message : `File not found for search_replace: ${ filePath } ` ,
649+ error : null ,
650+ } ) ;
651+ }
652+ }
653+
513654 // If we have any file changes, commit them all at once
514655 hasChanges =
515656 writtenFiles . length > 0 ||
516657 renamedFiles . length > 0 ||
517658 deletedFiles . length > 0 ||
518- dyadAddDependencyPackages . length > 0 ;
659+ dyadAddDependencyPackages . length > 0 ||
660+ writeToFileTags . length > 0 ||
661+ searchReplaceTags . length > 0 ;
519662
520663 let uncommittedFiles : string [ ] = [ ] ;
521664 let extraFilesError : string | undefined ;
@@ -545,13 +688,13 @@ export async function processFullResponseActions(
545688 if ( dyadExecuteSqlQueries . length > 0 )
546689 changes . push ( `executed ${ dyadExecuteSqlQueries . length } SQL queries` ) ;
547690
548- let message = chatSummary
691+ const commitMessage = chatSummary
549692 ? `[alifullstack] ${ chatSummary } - ${ changes . join ( ", " ) } `
550693 : `[alifullstack] ${ changes . join ( ", " ) } ` ;
551694 // Use chat summary, if provided, or default for commit message
552695 let commitHash = await gitCommit ( {
553696 path : appPath ,
554- message,
697+ message : commitMessage ,
555698 } ) ;
556699 logger . log ( `Successfully committed changes: ${ changes . join ( ", " ) } ` ) ;
557700
@@ -571,7 +714,7 @@ export async function processFullResponseActions(
571714 try {
572715 commitHash = await gitCommit ( {
573716 path : appPath ,
574- message : message + " + extra files edited outside of AliFullStack" ,
717+ message : commitMessage + " + extra files edited outside of AliFullStack" ,
575718 amend : true ,
576719 } ) ;
577720 logger . log (
0 commit comments