From c200e2f74d90015912df4b7b08b19b979f1b0e79 Mon Sep 17 00:00:00 2001 From: Stijnus <72551117+Stijnus@users.noreply.github.com> Date: Sun, 2 Feb 2025 16:50:43 +0100 Subject: [PATCH] update fixes --- .../@settings/tabs/update/UpdateTab.tsx | 973 +++--------------- app/routes/api.update.ts | 66 +- 2 files changed, 208 insertions(+), 831 deletions(-) diff --git a/app/components/@settings/tabs/update/UpdateTab.tsx b/app/components/@settings/tabs/update/UpdateTab.tsx index 460e625b..c1aba2e0 100644 --- a/app/components/@settings/tabs/update/UpdateTab.tsx +++ b/app/components/@settings/tabs/update/UpdateTab.tsx @@ -1,27 +1,10 @@ import React, { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion } from 'framer-motion'; import { useSettings } from '~/lib/hooks/useSettings'; import { logStore } from '~/lib/stores/logs'; -import { classNames } from '~/utils/classNames'; import { toast } from 'react-toastify'; import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog'; -interface GitHubCommitResponse { - sha: string; - commit: { - message: string; - }; -} - -interface GitHubReleaseResponse { - tag_name: string; - body: string; - assets: Array<{ - size: number; - browser_download_url: string; - }>; -} - interface UpdateProgress { stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete'; message: string; @@ -32,23 +15,9 @@ interface UpdateProgress { additions?: number; deletions?: number; commitMessages?: string[]; - }; -} - -interface UpdateInfo { - currentVersion: string; - latestVersion: string; - branch: string; - hasUpdate: boolean; - releaseNotes?: string; - downloadSize?: string; - changelog?: string[]; - currentCommit?: string; - latestCommit?: string; - updateProgress?: UpdateProgress; - error?: { - type: string; - message: string; + totalSize?: string; + currentCommit?: string; + remoteCommit?: string; }; } @@ -58,125 +27,60 @@ interface UpdateSettings { checkInterval: number; } -const categorizeChangelog = (messages: string[]) => { - const categories = new Map(); +const ProgressBar = ({ progress }: { progress: number }) => ( +
+ +
+); - messages.forEach((message) => { - let category = 'Other'; - - if (message.startsWith('feat:')) { - category = 'Features'; - } else if (message.startsWith('fix:')) { - category = 'Bug Fixes'; - } else if (message.startsWith('docs:')) { - category = 'Documentation'; - } else if (message.startsWith('ci:')) { - category = 'CI Improvements'; - } else if (message.startsWith('refactor:')) { - category = 'Refactoring'; - } else if (message.startsWith('test:')) { - category = 'Testing'; - } else if (message.startsWith('style:')) { - category = 'Styling'; - } else if (message.startsWith('perf:')) { - category = 'Performance'; - } - - if (!categories.has(category)) { - categories.set(category, []); - } - - categories.get(category)!.push(message); - }); - - const order = [ - 'Features', - 'Bug Fixes', - 'Documentation', - 'CI Improvements', - 'Refactoring', - 'Performance', - 'Testing', - 'Styling', - 'Other', - ]; - - return Array.from(categories.entries()) - .sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0])) - .filter(([_, messages]) => messages.length > 0); -}; - -const parseCommitMessage = (message: string) => { - const prMatch = message.match(/#(\d+)/); - const prNumber = prMatch ? prMatch[1] : null; - - let cleanMessage = message.replace(/^[a-z]+:\s*/i, ''); - cleanMessage = cleanMessage.replace(/#\d+/g, '').trim(); - - const parts = cleanMessage.split(/[\n\r]|\s+\*\s+/); - const title = parts[0].trim(); - const description = parts - .slice(1) - .map((p) => p.trim()) - .filter((p) => p && !p.includes('Co-authored-by:')) - .join('\n'); - - return { title, description, prNumber }; -}; - -const GITHUB_URLS = { - commitJson: async (branch: string, headers: HeadersInit = {}): Promise => { - try { - const [commitResponse, releaseResponse, changelogResponse] = await Promise.all([ - fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`, { headers }), - fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest', { headers }), - fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits?sha=${branch}&per_page=10`, { headers }), - ]); - - if (!commitResponse.ok || !releaseResponse.ok || !changelogResponse.ok) { - throw new Error( - `GitHub API error: ${!commitResponse.ok ? await commitResponse.text() : await releaseResponse.text()}`, - ); - } - - const commitData = (await commitResponse.json()) as GitHubCommitResponse; - const releaseData = (await releaseResponse.json()) as GitHubReleaseResponse; - const commits = (await changelogResponse.json()) as GitHubCommitResponse[]; - - const totalSize = releaseData.assets?.reduce((acc, asset) => acc + asset.size, 0) || 0; - const downloadSize = (totalSize / (1024 * 1024)).toFixed(2) + ' MB'; - - const changelog = commits.map((commit) => commit.commit.message); - - return { - currentVersion: process.env.APP_VERSION || 'unknown', - latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7), - branch, - hasUpdate: commitData.sha !== process.env.CURRENT_COMMIT, - releaseNotes: releaseData.body || '', - downloadSize, - changelog, - currentCommit: process.env.CURRENT_COMMIT?.substring(0, 7), - latestCommit: commitData.sha.substring(0, 7), - }; - } catch (error) { - console.error('Error fetching update info:', error); - throw error; - } - }, -}; +const UpdateProgressDisplay = ({ progress }: { progress: UpdateProgress }) => ( +
+
+ {progress.message} + {progress.progress}% +
+ + {progress.details && ( +
+ {progress.details.changedFiles && progress.details.changedFiles.length > 0 && ( +
+
Changed Files:
+
    + {progress.details.changedFiles.map((file, index) => ( +
  • + {file} +
  • + ))} +
+
+ )} + {progress.details.totalSize &&
Total size: {progress.details.totalSize}
} + {progress.details.additions !== undefined && progress.details.deletions !== undefined && ( +
+ Changes: +{progress.details.additions}{' '} + -{progress.details.deletions} +
+ )} + {progress.details.currentCommit && progress.details.remoteCommit && ( +
+ Updating from {progress.details.currentCommit} to {progress.details.remoteCommit} +
+ )} +
+ )} +
+); const UpdateTab = () => { const { isLatestBranch } = useSettings(); - const [updateInfo, setUpdateInfo] = useState(null); const [isChecking, setIsChecking] = useState(false); - const [isUpdating, setIsUpdating] = useState(false); const [error, setError] = useState(null); - const [showChangelog, setShowChangelog] = useState(false); - const [showManualInstructions, setShowManualInstructions] = useState(false); - const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false); - const [updateFailed, setUpdateFailed] = useState(false); - const [updateSettings, setUpdateSettings] = useState(() => { + const [updateSettings] = useState(() => { const stored = localStorage.getItem('update_settings'); return stored ? JSON.parse(stored) @@ -186,9 +90,7 @@ const UpdateTab = () => { checkInterval: 24, }; }); - const [lastChecked, setLastChecked] = useState(null); const [showUpdateDialog, setShowUpdateDialog] = useState(false); - const [updateChangelog, setUpdateChangelog] = useState([]); const [updateProgress, setUpdateProgress] = useState(null); useEffect(() => { @@ -199,97 +101,31 @@ const UpdateTab = () => { console.log('Starting update check...'); setIsChecking(true); setError(null); - setLastChecked(new Date()); - - try { - console.log('Fetching update info...'); - - const branchToCheck = isLatestBranch ? 'main' : 'stable'; - const info = await GITHUB_URLS.commitJson(branchToCheck); - - setUpdateInfo(info); - - if (info.error) { - setError(info.error.message); - logStore.logWarning('Update Check Failed', { - type: 'update', - message: info.error.message, - }); - - return; - } - - if (info.hasUpdate) { - const existingLogs = Object.values(logStore.logs.get()); - const hasUpdateNotification = existingLogs.some( - (log) => - log.level === 'warning' && - log.details?.type === 'update' && - log.details.latestVersion === info.latestVersion, - ); - - if (!hasUpdateNotification && updateSettings.notifyInApp) { - logStore.logWarning('Update Available', { - currentVersion: info.currentVersion, - latestVersion: info.latestVersion, - branch: branchToCheck, - type: 'update', - message: `A new version is available on the ${branchToCheck} branch`, - updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`, - }); - - if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) { - setUpdateChangelog([ - 'New version available.', - `Compare changes: https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`, - '', - 'Click "Update Now" to start the update process.', - ]); - setShowUpdateDialog(true); - } - } - } - } catch (err) { - console.error('Update check failed:', err); - - const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; - setError(`Failed to check for updates: ${errorMessage}`); - setUpdateFailed(true); - } finally { - setIsChecking(false); - } - }; - - const initiateUpdate = async () => { - setIsUpdating(true); - setError(null); setUpdateProgress(null); try { + const branchToCheck = isLatestBranch ? 'main' : 'stable'; + + // Start the update check with streaming progress const response = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ - branch: isLatestBranch ? 'main' : 'stable', - }), + body: JSON.stringify({ branch: branchToCheck }), }); if (!response.ok) { - const errorData = (await response.json()) as { error: string }; - throw new Error(errorData.error || 'Failed to initiate update'); + throw new Error(`Update check failed: ${response.statusText}`); } - // Handle streaming response const reader = response.body?.getReader(); if (!reader) { - throw new Error('Failed to read response stream'); + throw new Error('No response stream available'); } - const decoder = new TextDecoder(); - + // Read the stream while (true) { const { done, value } = await reader.read(); @@ -297,638 +133,115 @@ const UpdateTab = () => { break; } - const chunk = decoder.decode(value); - const updates = chunk.split('\n').filter(Boolean); + // Convert the chunk to text and parse the JSON + const chunk = new TextDecoder().decode(value); + const lines = chunk.split('\n').filter(Boolean); - for (const update of updates) { + for (const line of lines) { try { - const progress = JSON.parse(update) as UpdateProgress; + const progress = JSON.parse(line) as UpdateProgress; setUpdateProgress(progress); if (progress.error) { - throw new Error(progress.error); + setError(progress.error); } + // If we're done, update the UI accordingly if (progress.stage === 'complete') { - logStore.logSuccess('Update completed', { - type: 'update', - message: progress.message, - }); - toast.success(progress.message); - setUpdateFailed(false); + setIsChecking(false); - return; + if (!progress.error) { + // Update was successful + toast.success('Update check completed'); + + if (progress.details?.changedFiles?.length) { + setShowUpdateDialog(true); + } + } } - - logStore.logInfo(`Update progress: ${progress.stage}`, { - type: 'update', - message: progress.message, - }); } catch (e) { - console.error('Failed to parse update progress:', e); + console.error('Error parsing progress update:', e); } } } - } catch (err) { - const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; - setError('Failed to complete update. Please try again or update manually.'); - console.error('Update failed:', err); - logStore.logSystem('Update failed: ' + errorMessage); - toast.error('Update failed: ' + errorMessage); - setUpdateFailed(true); + } catch (error) { + setError(error instanceof Error ? error.message : 'Unknown error occurred'); + logStore.logWarning('Update Check Failed', { + type: 'update', + message: error instanceof Error ? error.message : 'Unknown error occurred', + }); } finally { - setIsUpdating(false); + setIsChecking(false); } }; - const handleRestart = async () => { - // Show confirmation dialog - if (window.confirm('The application needs to restart to apply the update. Proceed?')) { - // Save any necessary state - localStorage.setItem('pendingRestart', 'true'); - - // Reload the page - window.location.reload(); - } - }; - - // Check for pending restart on mount - useEffect(() => { - const pendingRestart = localStorage.getItem('pendingRestart'); - - if (pendingRestart === 'true') { - localStorage.removeItem('pendingRestart'); - toast.success('Update applied successfully!'); - } - }, []); - - useEffect(() => { - const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000; - const intervalId = setInterval(checkForUpdates, checkInterval); - - return () => clearInterval(intervalId); - }, [updateSettings.checkInterval, isLatestBranch]); - - useEffect(() => { - checkForUpdates(); - }, [isLatestBranch]); - return ( -
- -
-
-

Updates

-

Check for and manage application updates

-
- - - {/* Update Settings Card */} - -
-
-

Update Settings

-
- -
-
-
- Automatic Updates -

- Automatically check and apply updates when available -

-
- -
- -
-
- In-App Notifications -

Show notifications when updates are available

-
- -
- -
-
- Check Interval -

How often to check for updates

-
- -
-
- - - {/* Update Status Card */} - -
-
- - Currently on {isLatestBranch ? 'main' : 'stable'} branch - - {updateInfo && ( - - Version: {updateInfo.currentVersion} ({updateInfo.currentCommit}) - - )} -
- -
- - {error && ( -
-
-
-
- {error} - {error.includes('rate limit') && ( - - Try adding a GitHub token in the connections tab to increase the rate limit. - - )} - {error.includes('authentication') && ( - - Please check your GitHub token configuration in the connections tab. - - )} -
-
-
- )} - - {updateInfo && ( -
-
- -
-

- {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'} -

-

- {updateInfo.hasUpdate - ? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available` - : 'You are running the latest version'} -

-
-
-
- )} - {lastChecked && ( -
- - Last checked: {lastChecked.toLocaleString()} - - {error && {error}} -
- )} - - - {/* Update Details Card */} - {updateInfo && updateInfo.hasUpdate && ( - -
-
-
- - Version {updateInfo.latestVersion} - -
- - {updateInfo.downloadSize} - -
- - {/* Update Options */} -
-
- - -
- - {/* Manual Update Instructions */} - - {showManualInstructions && ( - -
-

- Update available from {isLatestBranch ? 'main' : 'stable'} branch! -

-
-

- Current: {updateInfo.currentVersion} ({updateInfo.currentCommit}) -

-

- Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit}) -

-
-
- -
-

To update:

-
    -
  1. -
    - 1 -
    -
    -

    Pull the latest changes:

    - - git pull upstream {isLatestBranch ? 'main' : 'stable'} - -
    -
  2. -
  3. -
    - 2 -
    -
    -

    Install dependencies:

    - - pnpm install - -
    -
  4. -
  5. -
    - 3 -
    -
    -

    Build the application:

    - - pnpm build - -
    -
  6. -
  7. -
    - 4 -
    -

    Restart the application

    -
  8. -
-
-
- )} -
- - {/* Changelog */} - {updateInfo.changelog && updateInfo.changelog.length > 0 && ( -
- - - - {showChangelog && ( - -
- {categorizeChangelog(updateInfo.changelog).map(([category, messages]) => ( -
-
-
- {category} - - ({messages.length}) - -
-
-
- {messages.map((message, index) => { - const { title, description, prNumber } = parseCommitMessage(message); - return ( -
-
-
-
-

- {title} - {prNumber && ( - - #{prNumber} - - )} -

- {description && ( -

{description}

- )} -
-
-
- ); - })} -
-
- ))} -
- - )} - -
- )} -
- - )} - - {/* Update Progress */} - {isUpdating && updateProgress && ( - -
-
-
- - {updateProgress.stage.charAt(0).toUpperCase() + updateProgress.stage.slice(1)} - -

{updateProgress.message}

-
- {updateProgress.progress !== undefined && ( - {Math.round(updateProgress.progress)}% - )} -
- - {/* Show detailed information when available */} - {updateProgress.details && ( -
- {updateProgress.details.commitMessages && updateProgress.details.commitMessages.length > 0 && ( -
-

Commits to be applied:

-
- {updateProgress.details.commitMessages.map((msg, i) => ( -
- {msg} -
- ))} -
-
- )} - - {updateProgress.details.changedFiles && updateProgress.details.changedFiles.length > 0 && ( -
-

Changed Files:

-
- {updateProgress.details.changedFiles.map((file, i) => ( -
- {file} -
- ))} -
-
- )} - - {(updateProgress.details.additions !== undefined || updateProgress.details.deletions !== undefined) && ( -
- {updateProgress.details.additions !== undefined && ( -
- +{updateProgress.details.additions} additions -
- )} - {updateProgress.details.deletions !== undefined && ( -
- -{updateProgress.details.deletions} deletions -
- )} -
- )} -
- )} - - {updateProgress.progress !== undefined && ( -
-
-
- )} - - {/* Show restart button when update is complete */} - {updateProgress.stage === 'complete' && !updateProgress.error && ( -
- -
- )} -
- - )} - - {/* Update Confirmation Dialog */} - - { - setShowUpdateDialog(false); - setHasUserRespondedToUpdate(true); - logStore.logSystem('Update cancelled by user'); +
+
+

Updates

+ +
-
-

Update Information:

-
-
- {updateChangelog.map((log, index) => ( -
- {log.startsWith('Compare changes:') ? ( - - View changes on GitHub - - ) : ( - log - )} -
+ {/* Show progress information */} + {updateProgress && } + + {error &&
{error}
} + + {/* Update dialog */} + + + Update Available + + {updateProgress?.details?.changedFiles && ( +
+

Changes:

+
    + {updateProgress.details.changedFiles.map((file, index) => ( +
  • + {file} +
  • ))} -
+ + {updateProgress.details.totalSize && ( +

Total size: {updateProgress.details.totalSize}

+ )}
-
+ )} + +
+ setShowUpdateDialog(false)}> + Cancel + + { + setShowUpdateDialog(false); -
- { - setShowUpdateDialog(false); - setHasUserRespondedToUpdate(true); - logStore.logSystem('Update cancelled by user'); - }} - > - Cancel - - { - setShowUpdateDialog(false); - setHasUserRespondedToUpdate(true); - await initiateUpdate(); - }} - > - Update Now - -
+ // Handle update initiation here + }} + > + Update Now +
diff --git a/app/routes/api.update.ts b/app/routes/api.update.ts index af777c1b..970a14b0 100644 --- a/app/routes/api.update.ts +++ b/app/routes/api.update.ts @@ -48,11 +48,23 @@ export const action: ActionFunction = async ({ request }) => { }; try { + // Initial check stage + sendProgress({ + stage: 'fetch', + message: 'Checking repository status...', + progress: 0, + }); + // Check if remote exists let defaultBranch = branch || 'main'; // Make branch mutable try { await execAsync('git remote get-url origin'); + sendProgress({ + stage: 'fetch', + message: 'Repository remote verified', + progress: 10, + }); } 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', @@ -61,11 +73,27 @@ export const action: ActionFunction = async ({ request }) => { // Get default branch if not specified if (!branch) { + sendProgress({ + stage: 'fetch', + message: 'Detecting default branch...', + progress: 20, + }); + try { const { stdout } = await execAsync('git remote show origin | grep "HEAD branch" | cut -d" " -f5'); defaultBranch = stdout.trim() || 'main'; + sendProgress({ + stage: 'fetch', + message: `Using branch: ${defaultBranch}`, + progress: 30, + }); } catch { defaultBranch = 'main'; // Fallback to main if we can't detect + sendProgress({ + stage: 'fetch', + message: 'Using default branch: main', + progress: 30, + }); } } @@ -73,20 +101,36 @@ export const action: ActionFunction = async ({ request }) => { sendProgress({ stage: 'fetch', message: 'Fetching latest changes...', - progress: 0, + progress: 40, }); // Fetch all remotes await execAsync('git fetch --all'); + sendProgress({ + stage: 'fetch', + message: 'Remote changes fetched', + progress: 50, + }); // Check if remote branch exists try { await execAsync(`git rev-parse --verify origin/${defaultBranch}`); + sendProgress({ + stage: 'fetch', + message: 'Remote branch verified', + progress: 60, + }); } catch { throw new Error(`Remote branch 'origin/${defaultBranch}' not found. Please push your changes first.`); } // Get current commit hash and remote commit hash + sendProgress({ + stage: 'fetch', + message: 'Comparing versions...', + progress: 70, + }); + const { stdout: currentCommit } = await execAsync('git rev-parse HEAD'); const { stdout: remoteCommit } = await execAsync(`git rev-parse origin/${defaultBranch}`); @@ -96,10 +140,20 @@ export const action: ActionFunction = async ({ request }) => { stage: 'complete', message: 'No updates available. You are on the latest version.', progress: 100, + details: { + currentCommit: currentCommit.trim().substring(0, 7), + remoteCommit: remoteCommit.trim().substring(0, 7), + }, }); return; } + sendProgress({ + stage: 'fetch', + message: 'Analyzing changes...', + progress: 80, + }); + // Initialize variables let changedFiles: string[] = []; let commitMessages: string[] = []; @@ -131,10 +185,20 @@ export const action: ActionFunction = async ({ request }) => { stage: 'complete', message: `No file changes detected between your version and origin/${defaultBranch}. You might be on a different branch.`, progress: 100, + details: { + currentCommit: currentCommit.trim().substring(0, 7), + remoteCommit: remoteCommit.trim().substring(0, 7), + }, }); return; } + sendProgress({ + stage: 'fetch', + message: `Found ${files.length} changed files, calculating sizes...`, + progress: 90, + }); + // Get size information for each changed file for (const line of files) { const [status, file] = line.split('\t');