import React, { useState, useEffect } from 'react'; import { motion, AnimatePresence } 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 UpdateInfo { currentVersion: string; latestVersion: string; branch: string; hasUpdate: boolean; releaseNotes?: string; downloadSize?: string; changelog?: string[]; currentCommit?: string; latestCommit?: string; downloadProgress?: number; installProgress?: number; estimatedTimeRemaining?: number; error?: { type: string; message: string; }; } interface UpdateSettings { autoUpdate: boolean; notifyInApp: boolean; checkInterval: number; } interface UpdateResponse { success: boolean; error?: string; message?: string; instructions?: string[]; } const categorizeChangelog = (messages: string[]) => { const categories = new Map(); 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 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 [retryCount, setRetryCount] = useState(0); 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 stored = localStorage.getItem('update_settings'); return stored ? JSON.parse(stored) : { autoUpdate: false, notifyInApp: true, checkInterval: 24, }; }); const [lastChecked, setLastChecked] = useState(null); const [showUpdateDialog, setShowUpdateDialog] = useState(false); const [updateChangelog, setUpdateChangelog] = useState([]); useEffect(() => { localStorage.setItem('update_settings', JSON.stringify(updateSettings)); }, [updateSettings]); const checkForUpdates = async () => { 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); let currentRetry = 0; const maxRetries = 3; const attemptUpdate = async (): Promise => { try { const response = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ branch: isLatestBranch ? 'main' : 'stable', }), }); if (!response.ok) { const errorData = (await response.json()) as { error: string }; throw new Error(errorData.error || 'Failed to initiate update'); } const result = (await response.json()) as UpdateResponse; if (result.success) { logStore.logSuccess('Update instructions ready', { type: 'update', message: result.message || 'Update instructions ready', }); // Show manual update instructions setShowManualInstructions(true); setUpdateChangelog( result.instructions || [ 'Failed to get update instructions. Please update manually:', '1. git pull origin main', '2. pnpm install', '3. pnpm build', '4. Restart the application', ], ); return; } throw new Error(result.error || 'Update failed'); } catch (err) { currentRetry++; const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; if (currentRetry < maxRetries) { toast.warning(`Update attempt ${currentRetry} failed. Retrying...`, { autoClose: 2000 }); setRetryCount(currentRetry); await new Promise((resolve) => setTimeout(resolve, 2000)); await attemptUpdate(); return; } setError('Failed to get update instructions. Please update manually.'); console.error('Update failed:', err); logStore.logSystem('Update failed: ' + errorMessage); toast.error('Update failed: ' + errorMessage); setUpdateFailed(true); } }; await attemptUpdate(); setIsUpdating(false); setRetryCount(0); }; 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. 2

    Install dependencies:

    pnpm install
  3. 3

    Build the application:

    pnpm build
  4. 4

    Restart the application

)}
{/* 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 && updateInfo?.downloadProgress !== undefined && (
Downloading Update {Math.round(updateInfo.downloadProgress)}%
{retryCount > 0 &&

Retry attempt {retryCount}/3...

}
)} {/* Update Confirmation Dialog */} { setShowUpdateDialog(false); setHasUserRespondedToUpdate(true); logStore.logSystem('Update cancelled by user'); }} >
Update Available A new version is available. Would you like to update now?

Update Information:

{updateChangelog.map((log, index) => (
{log.startsWith('Compare changes:') ? ( View changes on GitHub ) : ( log )}
))}
{ setShowUpdateDialog(false); setHasUserRespondedToUpdate(true); logStore.logSystem('Update cancelled by user'); }} > Cancel { setShowUpdateDialog(false); setHasUserRespondedToUpdate(true); await initiateUpdate(); }} > Update Now
); }; export default UpdateTab;