bolt.diy/app/routes/api.update.ts

356 lines
11 KiB
TypeScript
Raw Normal View History

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);
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
};
}
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 {
await execAsync('git remote get-url origin');
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(
'No remote repository found. Please set up the remote repository first by running:\ngit remote add origin https://github.com/stackblitz-labs/bolt.diy.git',
);
}
// 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 {
const { stdout } = await execAsync('git remote show origin | grep "HEAD branch" | cut -d" " -f5');
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 {
await execAsync(`git rev-parse --verify origin/${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 {
throw new Error(`Remote branch 'origin/${defaultBranch}' not found. Please push your changes first.`);
}
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:42:36 +00:00
const { stdout: remoteCommit } = await execAsync(`git rev-parse origin/${defaultBranch}`);
// 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',
message: `No file changes detected between your version and origin/${defaultBranch}. You might be on a different branch.`,
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);
throw new Error(`Failed to compare changes with origin/${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:42:36 +00:00
message: `No changes detected between your version and origin/${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:42:36 +00:00
message: `Changes detected on origin/${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:25:22 +00:00
message: `Pulling changes from ${defaultBranch}...`,
2025-02-02 15:17:33 +00:00
progress: 0,
});
2025-02-02 15:25:22 +00:00
await execAsync(`git pull origin ${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 15:25:22 +00:00
} catch (err) {
console.error('Update preparation failed:', err);
return json(
{
success: false,
2025-02-02 15:25:22 +00:00
error: err instanceof Error ? err.message : 'Unknown error occurred while preparing update',
},
{ status: 500 },
);
}
};