2025-02-02 00:42:30 +00:00
import { json } from '@remix-run/node' ;
import type { ActionFunction } from '@remix-run/node' ;
2025-02-02 15:17:33 +00:00
import { exec } from 'child_process' ;
import { promisify } from 'util' ;
const execAsync = promisify ( exec ) ;
2025-02-02 00:42:30 +00:00
interface UpdateRequestBody {
branch : string ;
}
2025-02-02 15:17:33 +00:00
interface UpdateProgress {
stage : 'fetch' | 'pull' | 'install' | 'build' | 'complete' ;
message : string ;
progress? : number ;
error? : string ;
details ? : {
changedFiles? : string [ ] ;
additions? : number ;
deletions? : number ;
commitMessages? : string [ ] ;
2025-02-02 15:42:36 +00:00
totalSize? : string ;
currentCommit? : string ;
remoteCommit? : string ;
2025-02-02 15:17:33 +00:00
} ;
}
2025-02-02 00:42:30 +00:00
export const action : ActionFunction = async ( { request } ) = > {
if ( request . method !== 'POST' ) {
return json ( { error : 'Method not allowed' } , { status : 405 } ) ;
}
try {
const body = await request . json ( ) ;
if ( ! body || typeof body !== 'object' || ! ( 'branch' in body ) || typeof body . branch !== 'string' ) {
return json ( { error : 'Invalid request body: branch is required and must be a string' } , { status : 400 } ) ;
}
const { branch } = body as UpdateRequestBody ;
2025-02-02 15:17:33 +00:00
// Create a ReadableStream to send progress updates
const stream = new ReadableStream ( {
async start ( controller ) {
const encoder = new TextEncoder ( ) ;
const sendProgress = ( update : UpdateProgress ) = > {
controller . enqueue ( encoder . encode ( JSON . stringify ( update ) + '\n' ) ) ;
} ;
try {
2025-02-02 15:50:43 +00:00
// Initial check stage
sendProgress ( {
stage : 'fetch' ,
message : 'Checking repository status...' ,
progress : 0 ,
} ) ;
2025-02-02 15:25:22 +00:00
// Check if remote exists
let defaultBranch = branch || 'main' ; // Make branch mutable
try {
2025-02-02 15:58:41 +00:00
await execAsync ( 'git remote get-url upstream' ) ;
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Repository remote verified' ,
progress : 10 ,
} ) ;
2025-02-02 15:25:22 +00:00
} catch {
throw new Error (
2025-02-02 15:58:41 +00:00
'No upstream repository found. Please set up the upstream repository first by running:\ngit remote add upstream https://github.com/stackblitz-labs/bolt.diy.git' ,
2025-02-02 15:25:22 +00:00
) ;
}
// Get default branch if not specified
if ( ! branch ) {
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Detecting default branch...' ,
progress : 20 ,
} ) ;
2025-02-02 15:25:22 +00:00
try {
2025-02-02 15:58:41 +00:00
const { stdout } = await execAsync ( 'git remote show upstream | grep "HEAD branch" | cut -d" " -f5' ) ;
2025-02-02 15:25:22 +00:00
defaultBranch = stdout . trim ( ) || 'main' ;
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : ` Using branch: ${ defaultBranch } ` ,
progress : 30 ,
} ) ;
2025-02-02 15:25:22 +00:00
} catch {
defaultBranch = 'main' ; // Fallback to main if we can't detect
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Using default branch: main' ,
progress : 30 ,
} ) ;
2025-02-02 15:25:22 +00:00
}
}
2025-02-02 15:17:33 +00:00
// Fetch stage
sendProgress ( {
stage : 'fetch' ,
message : 'Fetching latest changes...' ,
2025-02-02 15:50:43 +00:00
progress : 40 ,
2025-02-02 15:17:33 +00:00
} ) ;
2025-02-02 15:25:22 +00:00
// Fetch all remotes
await execAsync ( 'git fetch --all' ) ;
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Remote changes fetched' ,
progress : 50 ,
} ) ;
2025-02-02 15:25:22 +00:00
// Check if remote branch exists
try {
2025-02-02 15:58:41 +00:00
await execAsync ( ` git rev-parse --verify upstream/ ${ defaultBranch } ` ) ;
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Remote branch verified' ,
progress : 60 ,
} ) ;
2025-02-02 15:25:22 +00:00
} catch {
2025-02-02 15:58:41 +00:00
throw new Error (
` Remote branch 'upstream/ ${ defaultBranch } ' not found. Please ensure the upstream repository is properly configured. ` ,
) ;
2025-02-02 15:25:22 +00:00
}
2025-02-02 15:42:36 +00:00
// Get current commit hash and remote commit hash
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Comparing versions...' ,
progress : 70 ,
} ) ;
2025-02-02 15:17:33 +00:00
const { stdout : currentCommit } = await execAsync ( 'git rev-parse HEAD' ) ;
2025-02-02 15:58:41 +00:00
const { stdout : remoteCommit } = await execAsync ( ` git rev-parse upstream/ ${ defaultBranch } ` ) ;
2025-02-02 15:42:36 +00:00
// If we're on the same commit, no update is available
if ( currentCommit . trim ( ) === remoteCommit . trim ( ) ) {
sendProgress ( {
stage : 'complete' ,
message : 'No updates available. You are on the latest version.' ,
progress : 100 ,
2025-02-02 15:50:43 +00:00
details : {
currentCommit : currentCommit.trim ( ) . substring ( 0 , 7 ) ,
remoteCommit : remoteCommit.trim ( ) . substring ( 0 , 7 ) ,
} ,
2025-02-02 15:42:36 +00:00
} ) ;
return ;
}
2025-02-02 15:17:33 +00:00
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : 'Analyzing changes...' ,
progress : 80 ,
} ) ;
2025-02-02 15:25:22 +00:00
// Initialize variables
let changedFiles : string [ ] = [ ] ;
let commitMessages : string [ ] = [ ] ;
let stats : RegExpMatchArray | null = null ;
2025-02-02 15:42:36 +00:00
let totalSizeInBytes = 0 ;
// Format size for display
const formatSize = ( bytes : number ) = > {
if ( bytes === 0 ) {
return '0 B' ;
}
const k = 1024 ;
const sizes = [ 'B' , 'KB' , 'MB' , 'GB' ] ;
const i = Math . floor ( Math . log ( bytes ) / Math . log ( k ) ) ;
2025-02-02 15:17:33 +00:00
2025-02-02 15:42:36 +00:00
return ` ${ parseFloat ( ( bytes / Math . pow ( k , i ) ) . toFixed ( 2 ) ) } ${ sizes [ i ] } ` ;
} ;
// Get list of changed files and their sizes
2025-02-02 15:25:22 +00:00
try {
2025-02-02 15:42:36 +00:00
const { stdout : diffOutput } = await execAsync (
` git diff --name-status ${ currentCommit . trim ( ) } .. ${ remoteCommit . trim ( ) } ` ,
) ;
const files = diffOutput . split ( '\n' ) . filter ( Boolean ) ;
if ( files . length === 0 ) {
sendProgress ( {
stage : 'complete' ,
2025-02-02 15:58:41 +00:00
message : ` No file changes detected between your version and upstream/ ${ defaultBranch } . You might be on a different branch. ` ,
2025-02-02 15:42:36 +00:00
progress : 100 ,
2025-02-02 15:50:43 +00:00
details : {
currentCommit : currentCommit.trim ( ) . substring ( 0 , 7 ) ,
remoteCommit : remoteCommit.trim ( ) . substring ( 0 , 7 ) ,
} ,
2025-02-02 15:25:22 +00:00
} ) ;
2025-02-02 15:42:36 +00:00
return ;
}
2025-02-02 15:50:43 +00:00
sendProgress ( {
stage : 'fetch' ,
message : ` Found ${ files . length } changed files, calculating sizes... ` ,
progress : 90 ,
} ) ;
2025-02-02 15:42:36 +00:00
// Get size information for each changed file
for ( const line of files ) {
const [ status , file ] = line . split ( '\t' ) ;
if ( status !== 'D' ) {
// Skip deleted files
try {
const { stdout : sizeOutput } = await execAsync ( ` git cat-file -s ${ remoteCommit . trim ( ) } : ${ file } ` ) ;
const size = parseInt ( sizeOutput ) || 0 ;
totalSizeInBytes += size ;
} catch {
console . debug ( ` Could not get size for file: ${ file } ` ) ;
}
}
}
changedFiles = files . map ( ( line ) = > {
const [ status , file ] = line . split ( '\t' ) ;
return ` ${ status === 'M' ? 'Modified' : status === 'A' ? 'Added' : 'Deleted' } : ${ file } ` ;
} ) ;
} catch ( err ) {
console . debug ( 'Failed to get changed files:' , err ) ;
2025-02-02 15:58:41 +00:00
throw new Error ( ` Failed to compare changes with upstream/ ${ defaultBranch } . Are you on the correct branch? ` ) ;
2025-02-02 15:25:22 +00:00
}
2025-02-02 15:17:33 +00:00
2025-02-02 15:42:36 +00:00
// Get commit messages between current and remote
2025-02-02 15:25:22 +00:00
try {
const { stdout : logOutput } = await execAsync (
2025-02-02 15:42:36 +00:00
` git log --oneline ${ currentCommit . trim ( ) } .. ${ remoteCommit . trim ( ) } ` ,
2025-02-02 15:25:22 +00:00
) ;
commitMessages = logOutput . split ( '\n' ) . filter ( Boolean ) ;
} catch {
// Handle silently - empty commitMessages array will be used
}
2025-02-02 15:17:33 +00:00
2025-02-02 15:42:36 +00:00
// Get diff stats using the specific commits
2025-02-02 15:25:22 +00:00
try {
2025-02-02 15:42:36 +00:00
const { stdout : diffStats } = await execAsync (
` git diff --shortstat ${ currentCommit . trim ( ) } .. ${ remoteCommit . trim ( ) } ` ,
) ;
2025-02-02 15:25:22 +00:00
stats = diffStats . match (
/(\d+) files? changed(?:, (\d+) insertions?\\(\\+\\))?(?:, (\d+) deletions?\\(-\\))?/ ,
) ;
} catch {
// Handle silently - null stats will be used
}
2025-02-02 15:42:36 +00:00
// If we somehow still have no changes detected
2025-02-02 15:25:22 +00:00
if ( ! stats && changedFiles . length === 0 ) {
sendProgress ( {
stage : 'complete' ,
2025-02-02 15:58:41 +00:00
message : ` No changes detected between your version and upstream/ ${ defaultBranch } . This might be unexpected - please check your git status. ` ,
2025-02-02 15:25:22 +00:00
progress : 100 ,
} ) ;
return ;
}
2025-02-02 15:17:33 +00:00
2025-02-02 15:42:36 +00:00
// We have changes, send the details
2025-02-02 15:17:33 +00:00
sendProgress ( {
stage : 'fetch' ,
2025-02-02 15:58:41 +00:00
message : ` Changes detected on upstream/ ${ defaultBranch } ` ,
2025-02-02 15:17:33 +00:00
progress : 100 ,
details : {
changedFiles ,
additions : stats?. [ 2 ] ? parseInt ( stats [ 2 ] ) : 0 ,
deletions : stats?. [ 3 ] ? parseInt ( stats [ 3 ] ) : 0 ,
commitMessages ,
2025-02-02 15:42:36 +00:00
totalSize : formatSize ( totalSizeInBytes ) ,
currentCommit : currentCommit.trim ( ) . substring ( 0 , 7 ) ,
remoteCommit : remoteCommit.trim ( ) . substring ( 0 , 7 ) ,
2025-02-02 15:17:33 +00:00
} ,
} ) ;
// Pull stage
sendProgress ( {
stage : 'pull' ,
2025-02-02 15:58:41 +00:00
message : ` Pulling changes from upstream/ ${ defaultBranch } ... ` ,
2025-02-02 15:17:33 +00:00
progress : 0 ,
} ) ;
2025-02-02 15:58:41 +00:00
await execAsync ( ` git pull upstream ${ defaultBranch } ` ) ;
2025-02-02 15:17:33 +00:00
sendProgress ( {
stage : 'pull' ,
message : 'Changes pulled successfully' ,
progress : 100 ,
} ) ;
// Install stage
sendProgress ( {
stage : 'install' ,
message : 'Installing dependencies...' ,
progress : 0 ,
} ) ;
await execAsync ( 'pnpm install' ) ;
sendProgress ( {
stage : 'install' ,
message : 'Dependencies installed successfully' ,
progress : 100 ,
} ) ;
// Build stage
sendProgress ( {
stage : 'build' ,
message : 'Building application...' ,
progress : 0 ,
} ) ;
await execAsync ( 'pnpm build' ) ;
sendProgress ( {
stage : 'build' ,
message : 'Build completed successfully' ,
progress : 100 ,
} ) ;
// Complete
sendProgress ( {
stage : 'complete' ,
message : 'Update completed successfully! Click Restart to apply changes.' ,
progress : 100 ,
} ) ;
2025-02-02 15:25:22 +00:00
} catch ( err ) {
2025-02-02 15:17:33 +00:00
sendProgress ( {
stage : 'complete' ,
message : 'Update failed' ,
2025-02-02 15:25:22 +00:00
error : err instanceof Error ? err . message : 'Unknown error occurred' ,
2025-02-02 15:17:33 +00:00
} ) ;
} finally {
controller . close ( ) ;
}
} ,
} ) ;
return new Response ( stream , {
headers : {
'Content-Type' : 'text/event-stream' ,
'Cache-Control' : 'no-cache' ,
Connection : 'keep-alive' ,
} ,
2025-02-02 00:42:30 +00:00
} ) ;
2025-02-02 15:25:22 +00:00
} catch ( err ) {
console . error ( 'Update preparation failed:' , err ) ;
2025-02-02 00:42:30 +00:00
return json (
{
success : false ,
2025-02-02 15:25:22 +00:00
error : err instanceof Error ? err . message : 'Unknown error occurred while preparing update' ,
2025-02-02 00:42:30 +00:00
} ,
{ status : 500 } ,
) ;
}
} ;