Stijnus commited on
Commit
c200e2f
·
1 Parent(s): f50ebc6

update fixes

Browse files
app/components/@settings/tabs/update/UpdateTab.tsx CHANGED
@@ -1,27 +1,10 @@
1
  import React, { useState, useEffect } from 'react';
2
- import { motion, AnimatePresence } from 'framer-motion';
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { logStore } from '~/lib/stores/logs';
5
- import { classNames } from '~/utils/classNames';
6
  import { toast } from 'react-toastify';
7
  import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog';
8
 
9
- interface GitHubCommitResponse {
10
- sha: string;
11
- commit: {
12
- message: string;
13
- };
14
- }
15
-
16
- interface GitHubReleaseResponse {
17
- tag_name: string;
18
- body: string;
19
- assets: Array<{
20
- size: number;
21
- browser_download_url: string;
22
- }>;
23
- }
24
-
25
  interface UpdateProgress {
26
  stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
27
  message: string;
@@ -32,23 +15,9 @@ interface UpdateProgress {
32
  additions?: number;
33
  deletions?: number;
34
  commitMessages?: string[];
35
- };
36
- }
37
-
38
- interface UpdateInfo {
39
- currentVersion: string;
40
- latestVersion: string;
41
- branch: string;
42
- hasUpdate: boolean;
43
- releaseNotes?: string;
44
- downloadSize?: string;
45
- changelog?: string[];
46
- currentCommit?: string;
47
- latestCommit?: string;
48
- updateProgress?: UpdateProgress;
49
- error?: {
50
- type: string;
51
- message: string;
52
  };
53
  }
54
 
@@ -58,125 +27,60 @@ interface UpdateSettings {
58
  checkInterval: number;
59
  }
60
 
61
- const categorizeChangelog = (messages: string[]) => {
62
- const categories = new Map<string, string[]>();
63
-
64
- messages.forEach((message) => {
65
- let category = 'Other';
66
-
67
- if (message.startsWith('feat:')) {
68
- category = 'Features';
69
- } else if (message.startsWith('fix:')) {
70
- category = 'Bug Fixes';
71
- } else if (message.startsWith('docs:')) {
72
- category = 'Documentation';
73
- } else if (message.startsWith('ci:')) {
74
- category = 'CI Improvements';
75
- } else if (message.startsWith('refactor:')) {
76
- category = 'Refactoring';
77
- } else if (message.startsWith('test:')) {
78
- category = 'Testing';
79
- } else if (message.startsWith('style:')) {
80
- category = 'Styling';
81
- } else if (message.startsWith('perf:')) {
82
- category = 'Performance';
83
- }
84
-
85
- if (!categories.has(category)) {
86
- categories.set(category, []);
87
- }
88
-
89
- categories.get(category)!.push(message);
90
- });
91
-
92
- const order = [
93
- 'Features',
94
- 'Bug Fixes',
95
- 'Documentation',
96
- 'CI Improvements',
97
- 'Refactoring',
98
- 'Performance',
99
- 'Testing',
100
- 'Styling',
101
- 'Other',
102
- ];
103
-
104
- return Array.from(categories.entries())
105
- .sort((a, b) => order.indexOf(a[0]) - order.indexOf(b[0]))
106
- .filter(([_, messages]) => messages.length > 0);
107
- };
108
-
109
- const parseCommitMessage = (message: string) => {
110
- const prMatch = message.match(/#(\d+)/);
111
- const prNumber = prMatch ? prMatch[1] : null;
112
-
113
- let cleanMessage = message.replace(/^[a-z]+:\s*/i, '');
114
- cleanMessage = cleanMessage.replace(/#\d+/g, '').trim();
115
-
116
- const parts = cleanMessage.split(/[\n\r]|\s+\*\s+/);
117
- const title = parts[0].trim();
118
- const description = parts
119
- .slice(1)
120
- .map((p) => p.trim())
121
- .filter((p) => p && !p.includes('Co-authored-by:'))
122
- .join('\n');
123
-
124
- return { title, description, prNumber };
125
- };
126
-
127
- const GITHUB_URLS = {
128
- commitJson: async (branch: string, headers: HeadersInit = {}): Promise<UpdateInfo> => {
129
- try {
130
- const [commitResponse, releaseResponse, changelogResponse] = await Promise.all([
131
- fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits/${branch}`, { headers }),
132
- fetch('https://api.github.com/repos/stackblitz-labs/bolt.diy/releases/latest', { headers }),
133
- fetch(`https://api.github.com/repos/stackblitz-labs/bolt.diy/commits?sha=${branch}&per_page=10`, { headers }),
134
- ]);
135
-
136
- if (!commitResponse.ok || !releaseResponse.ok || !changelogResponse.ok) {
137
- throw new Error(
138
- `GitHub API error: ${!commitResponse.ok ? await commitResponse.text() : await releaseResponse.text()}`,
139
- );
140
- }
141
-
142
- const commitData = (await commitResponse.json()) as GitHubCommitResponse;
143
- const releaseData = (await releaseResponse.json()) as GitHubReleaseResponse;
144
- const commits = (await changelogResponse.json()) as GitHubCommitResponse[];
145
-
146
- const totalSize = releaseData.assets?.reduce((acc, asset) => acc + asset.size, 0) || 0;
147
- const downloadSize = (totalSize / (1024 * 1024)).toFixed(2) + ' MB';
148
-
149
- const changelog = commits.map((commit) => commit.commit.message);
150
-
151
- return {
152
- currentVersion: process.env.APP_VERSION || 'unknown',
153
- latestVersion: releaseData.tag_name || commitData.sha.substring(0, 7),
154
- branch,
155
- hasUpdate: commitData.sha !== process.env.CURRENT_COMMIT,
156
- releaseNotes: releaseData.body || '',
157
- downloadSize,
158
- changelog,
159
- currentCommit: process.env.CURRENT_COMMIT?.substring(0, 7),
160
- latestCommit: commitData.sha.substring(0, 7),
161
- };
162
- } catch (error) {
163
- console.error('Error fetching update info:', error);
164
- throw error;
165
- }
166
- },
167
- };
168
 
169
  const UpdateTab = () => {
170
  const { isLatestBranch } = useSettings();
171
- const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null);
172
  const [isChecking, setIsChecking] = useState(false);
173
- const [isUpdating, setIsUpdating] = useState(false);
174
  const [error, setError] = useState<string | null>(null);
175
- const [showChangelog, setShowChangelog] = useState(false);
176
- const [showManualInstructions, setShowManualInstructions] = useState(false);
177
- const [hasUserRespondedToUpdate, setHasUserRespondedToUpdate] = useState(false);
178
- const [updateFailed, setUpdateFailed] = useState(false);
179
- const [updateSettings, setUpdateSettings] = useState<UpdateSettings>(() => {
180
  const stored = localStorage.getItem('update_settings');
181
  return stored
182
  ? JSON.parse(stored)
@@ -186,9 +90,7 @@ const UpdateTab = () => {
186
  checkInterval: 24,
187
  };
188
  });
189
- const [lastChecked, setLastChecked] = useState<Date | null>(null);
190
  const [showUpdateDialog, setShowUpdateDialog] = useState(false);
191
- const [updateChangelog, setUpdateChangelog] = useState<string[]>([]);
192
  const [updateProgress, setUpdateProgress] = useState<UpdateProgress | null>(null);
193
 
194
  useEffect(() => {
@@ -199,97 +101,31 @@ const UpdateTab = () => {
199
  console.log('Starting update check...');
200
  setIsChecking(true);
201
  setError(null);
202
- setLastChecked(new Date());
203
 
204
  try {
205
- console.log('Fetching update info...');
206
-
207
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
208
- const info = await GITHUB_URLS.commitJson(branchToCheck);
209
-
210
- setUpdateInfo(info);
211
-
212
- if (info.error) {
213
- setError(info.error.message);
214
- logStore.logWarning('Update Check Failed', {
215
- type: 'update',
216
- message: info.error.message,
217
- });
218
-
219
- return;
220
- }
221
-
222
- if (info.hasUpdate) {
223
- const existingLogs = Object.values(logStore.logs.get());
224
- const hasUpdateNotification = existingLogs.some(
225
- (log) =>
226
- log.level === 'warning' &&
227
- log.details?.type === 'update' &&
228
- log.details.latestVersion === info.latestVersion,
229
- );
230
-
231
- if (!hasUpdateNotification && updateSettings.notifyInApp) {
232
- logStore.logWarning('Update Available', {
233
- currentVersion: info.currentVersion,
234
- latestVersion: info.latestVersion,
235
- branch: branchToCheck,
236
- type: 'update',
237
- message: `A new version is available on the ${branchToCheck} branch`,
238
- updateUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
239
- });
240
-
241
- if (updateSettings.autoUpdate && !hasUserRespondedToUpdate) {
242
- setUpdateChangelog([
243
- 'New version available.',
244
- `Compare changes: https://github.com/stackblitz-labs/bolt.diy/compare/${info.currentVersion}...${info.latestVersion}`,
245
- '',
246
- 'Click "Update Now" to start the update process.',
247
- ]);
248
- setShowUpdateDialog(true);
249
- }
250
- }
251
- }
252
- } catch (err) {
253
- console.error('Update check failed:', err);
254
-
255
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
256
- setError(`Failed to check for updates: ${errorMessage}`);
257
- setUpdateFailed(true);
258
- } finally {
259
- setIsChecking(false);
260
- }
261
- };
262
-
263
- const initiateUpdate = async () => {
264
- setIsUpdating(true);
265
- setError(null);
266
- setUpdateProgress(null);
267
 
268
- try {
269
  const response = await fetch('/api/update', {
270
  method: 'POST',
271
  headers: {
272
  'Content-Type': 'application/json',
273
  },
274
- body: JSON.stringify({
275
- branch: isLatestBranch ? 'main' : 'stable',
276
- }),
277
  });
278
 
279
  if (!response.ok) {
280
- const errorData = (await response.json()) as { error: string };
281
- throw new Error(errorData.error || 'Failed to initiate update');
282
  }
283
 
284
- // Handle streaming response
285
  const reader = response.body?.getReader();
286
 
287
  if (!reader) {
288
- throw new Error('Failed to read response stream');
289
  }
290
 
291
- const decoder = new TextDecoder();
292
-
293
  while (true) {
294
  const { done, value } = await reader.read();
295
 
@@ -297,638 +133,115 @@ const UpdateTab = () => {
297
  break;
298
  }
299
 
300
- const chunk = decoder.decode(value);
301
- const updates = chunk.split('\n').filter(Boolean);
 
302
 
303
- for (const update of updates) {
304
  try {
305
- const progress = JSON.parse(update) as UpdateProgress;
306
  setUpdateProgress(progress);
307
 
308
  if (progress.error) {
309
- throw new Error(progress.error);
310
  }
311
 
 
312
  if (progress.stage === 'complete') {
313
- logStore.logSuccess('Update completed', {
314
- type: 'update',
315
- message: progress.message,
316
- });
317
- toast.success(progress.message);
318
- setUpdateFailed(false);
319
 
320
- return;
321
- }
 
322
 
323
- logStore.logInfo(`Update progress: ${progress.stage}`, {
324
- type: 'update',
325
- message: progress.message,
326
- });
 
327
  } catch (e) {
328
- console.error('Failed to parse update progress:', e);
329
  }
330
  }
331
  }
332
- } catch (err) {
333
- const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
334
- setError('Failed to complete update. Please try again or update manually.');
335
- console.error('Update failed:', err);
336
- logStore.logSystem('Update failed: ' + errorMessage);
337
- toast.error('Update failed: ' + errorMessage);
338
- setUpdateFailed(true);
339
  } finally {
340
- setIsUpdating(false);
341
- }
342
- };
343
-
344
- const handleRestart = async () => {
345
- // Show confirmation dialog
346
- if (window.confirm('The application needs to restart to apply the update. Proceed?')) {
347
- // Save any necessary state
348
- localStorage.setItem('pendingRestart', 'true');
349
-
350
- // Reload the page
351
- window.location.reload();
352
  }
353
  };
354
 
355
- // Check for pending restart on mount
356
- useEffect(() => {
357
- const pendingRestart = localStorage.getItem('pendingRestart');
358
-
359
- if (pendingRestart === 'true') {
360
- localStorage.removeItem('pendingRestart');
361
- toast.success('Update applied successfully!');
362
- }
363
- }, []);
364
-
365
- useEffect(() => {
366
- const checkInterval = updateSettings.checkInterval * 60 * 60 * 1000;
367
- const intervalId = setInterval(checkForUpdates, checkInterval);
368
-
369
- return () => clearInterval(intervalId);
370
- }, [updateSettings.checkInterval, isLatestBranch]);
371
-
372
- useEffect(() => {
373
- checkForUpdates();
374
- }, [isLatestBranch]);
375
-
376
  return (
377
- <div className="flex flex-col gap-6">
378
- <motion.div
379
- className="flex items-center gap-3"
380
- initial={{ opacity: 0, y: -20 }}
381
- animate={{ opacity: 1, y: 0 }}
382
- transition={{ duration: 0.3 }}
383
- >
384
- <div className="i-ph:arrow-circle-up text-xl text-purple-500" />
385
- <div>
386
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Updates</h3>
387
- <p className="text-sm text-bolt-elements-textSecondary">Check for and manage application updates</p>
388
- </div>
389
- </motion.div>
390
-
391
- {/* Update Settings Card */}
392
- <motion.div
393
- className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
394
- initial={{ opacity: 0, y: 20 }}
395
- animate={{ opacity: 1, y: 0 }}
396
- transition={{ duration: 0.3, delay: 0.1 }}
397
- >
398
- <div className="flex items-center gap-3 mb-6">
399
- <div className="i-ph:gear text-purple-500 w-5 h-5" />
400
- <h3 className="text-lg font-medium text-bolt-elements-textPrimary">Update Settings</h3>
401
- </div>
402
-
403
- <div className="space-y-4">
404
- <div className="flex items-center justify-between">
405
- <div>
406
- <span className="text-sm text-bolt-elements-textPrimary">Automatic Updates</span>
407
- <p className="text-xs text-bolt-elements-textSecondary">
408
- Automatically check and apply updates when available
409
- </p>
410
- </div>
411
- <button
412
- onClick={() => setUpdateSettings((prev) => ({ ...prev, autoUpdate: !prev.autoUpdate }))}
413
- className={classNames(
414
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
415
- updateSettings.autoUpdate ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
416
- )}
417
- >
418
- <span
419
- className={classNames(
420
- 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
421
- updateSettings.autoUpdate ? 'translate-x-6' : 'translate-x-1',
422
- )}
423
- />
424
- </button>
425
- </div>
426
-
427
- <div className="flex items-center justify-between">
428
- <div>
429
- <span className="text-sm text-bolt-elements-textPrimary">In-App Notifications</span>
430
- <p className="text-xs text-bolt-elements-textSecondary">Show notifications when updates are available</p>
431
- </div>
432
- <button
433
- onClick={() => setUpdateSettings((prev) => ({ ...prev, notifyInApp: !prev.notifyInApp }))}
434
- className={classNames(
435
- 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
436
- updateSettings.notifyInApp ? 'bg-purple-500' : 'bg-gray-200 dark:bg-gray-700',
437
- )}
438
- >
439
- <span
440
- className={classNames(
441
- 'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
442
- updateSettings.notifyInApp ? 'translate-x-6' : 'translate-x-1',
443
- )}
444
- />
445
- </button>
446
- </div>
447
-
448
- <div className="flex items-center justify-between">
449
- <div>
450
- <span className="text-sm text-bolt-elements-textPrimary">Check Interval</span>
451
- <p className="text-xs text-bolt-elements-textSecondary">How often to check for updates</p>
452
- </div>
453
- <select
454
- value={updateSettings.checkInterval}
455
- onChange={(e) => setUpdateSettings((prev) => ({ ...prev, checkInterval: Number(e.target.value) }))}
456
- className={classNames(
457
- 'px-3 py-2 rounded-lg text-sm',
458
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
459
- 'border border-[#E5E5E5] dark:border-[#1A1A1A]',
460
- 'text-bolt-elements-textPrimary',
461
- 'hover:bg-[#E5E5E5] dark:hover:bg-[#2A2A2A]',
462
- 'transition-colors duration-200',
463
- )}
464
- >
465
- <option value="6">6 hours</option>
466
- <option value="12">12 hours</option>
467
- <option value="24">24 hours</option>
468
- <option value="48">48 hours</option>
469
- </select>
470
- </div>
471
- </div>
472
- </motion.div>
473
-
474
- {/* Update Status Card */}
475
- <motion.div
476
- className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
477
- initial={{ opacity: 0, y: 20 }}
478
- animate={{ opacity: 1, y: 0 }}
479
- transition={{ duration: 0.3, delay: 0.2 }}
480
- >
481
- <div className="flex items-center justify-between mb-4">
482
- <div className="flex items-center gap-4">
483
- <span className="text-sm text-bolt-elements-textSecondary">
484
- Currently on {isLatestBranch ? 'main' : 'stable'} branch
485
- </span>
486
- {updateInfo && (
487
- <span className="text-xs text-bolt-elements-textTertiary">
488
- Version: {updateInfo.currentVersion} ({updateInfo.currentCommit})
489
- </span>
490
- )}
491
- </div>
492
- <button
493
- onClick={() => {
494
- setHasUserRespondedToUpdate(false);
495
- setUpdateFailed(false);
496
- setError(null);
497
- checkForUpdates();
498
- }}
499
- disabled={isChecking}
500
- className={classNames(
501
- 'flex items-center gap-2 px-3 py-2 rounded-lg text-sm',
502
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
503
- 'hover:bg-purple-500/10 hover:text-purple-500',
504
- 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
505
- 'text-bolt-elements-textPrimary',
506
- 'transition-colors duration-200',
507
- 'disabled:opacity-50 disabled:cursor-not-allowed',
508
- )}
509
- >
510
- <div className={classNames('i-ph:arrows-clockwise w-4 h-4', isChecking ? 'animate-spin' : '')} />
511
- {isChecking ? 'Checking...' : 'Check for Updates'}
512
- </button>
513
- </div>
514
-
515
- {error && (
516
- <div className="p-4 rounded-lg bg-red-500/10 border border-red-500/20 text-red-600 dark:text-red-400">
517
- <div className="flex items-center gap-2">
518
- <div className="i-ph:warning-circle" />
519
- <div className="flex flex-col">
520
- <span className="font-medium">{error}</span>
521
- {error.includes('rate limit') && (
522
- <span className="text-sm mt-1">
523
- Try adding a GitHub token in the connections tab to increase the rate limit.
524
- </span>
525
- )}
526
- {error.includes('authentication') && (
527
- <span className="text-sm mt-1">
528
- Please check your GitHub token configuration in the connections tab.
529
- </span>
530
- )}
531
- </div>
532
- </div>
533
- </div>
534
- )}
535
-
536
- {updateInfo && (
537
- <div
538
- className={classNames(
539
- 'p-4 rounded-lg',
540
- updateInfo.hasUpdate
541
- ? 'bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20'
542
- : 'bg-green-500/5 dark:bg-green-500/10 border border-green-500/20',
543
- )}
544
- >
545
- <div className="flex items-center gap-3">
546
- <span
547
- className={classNames(
548
- 'text-lg',
549
- updateInfo.hasUpdate ? 'i-ph:warning text-purple-500' : 'i-ph:check-circle text-green-500',
550
- )}
551
- />
552
- <div>
553
- <h4 className="font-medium text-bolt-elements-textPrimary">
554
- {updateInfo.hasUpdate ? 'Update Available' : 'Up to Date'}
555
- </h4>
556
- <p className="text-sm text-bolt-elements-textSecondary">
557
- {updateInfo.hasUpdate
558
- ? `Version ${updateInfo.latestVersion} (${updateInfo.latestCommit}) is now available`
559
- : 'You are running the latest version'}
560
- </p>
561
- </div>
562
- </div>
563
- </div>
564
- )}
565
- {lastChecked && (
566
- <div className="flex flex-col items-end mt-2">
567
- <span className="text-xs text-gray-500 dark:text-gray-400">
568
- Last checked: {lastChecked.toLocaleString()}
569
- </span>
570
- {error && <span className="text-xs text-red-500 mt-1">{error}</span>}
571
- </div>
572
- )}
573
- </motion.div>
574
-
575
- {/* Update Details Card */}
576
- {updateInfo && updateInfo.hasUpdate && (
577
- <motion.div
578
- className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
579
- initial={{ opacity: 0, y: 20 }}
580
- animate={{ opacity: 1, y: 0 }}
581
- transition={{ duration: 0.3, delay: 0.2 }}
582
  >
583
- <div className="flex items-center justify-between mb-6">
584
- <div className="flex items-center gap-3">
585
- <div className="i-ph:arrow-circle-up text-purple-500 w-5 h-5" />
586
- <span className="text-sm font-medium text-bolt-elements-textPrimary">
587
- Version {updateInfo.latestVersion}
588
- </span>
589
- </div>
590
- <span className="text-xs px-3 py-1 rounded-full bg-purple-500/10 text-purple-500">
591
- {updateInfo.downloadSize}
592
- </span>
593
- </div>
594
-
595
- {/* Update Options */}
596
- <div className="flex flex-col gap-4">
597
- <div className="flex items-center gap-3">
598
- <button
599
- onClick={initiateUpdate}
600
- disabled={isUpdating || updateFailed}
601
- className={classNames(
602
- 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
603
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
604
- 'hover:bg-purple-500/10 hover:text-purple-500',
605
- 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
606
- 'text-bolt-elements-textPrimary',
607
- 'transition-all duration-200',
608
- )}
609
- >
610
- <div className={classNames('i-ph:arrow-circle-up w-4 h-4', isUpdating ? 'animate-spin' : '')} />
611
- {isUpdating ? 'Updating...' : 'Auto Update'}
612
- </button>
613
- <button
614
- onClick={() => setShowManualInstructions(!showManualInstructions)}
615
- className={classNames(
616
- 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm',
617
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
618
- 'hover:bg-purple-500/10 hover:text-purple-500',
619
- 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
620
- 'text-bolt-elements-textPrimary',
621
- 'transition-all duration-200',
622
- )}
623
- >
624
- <div className="i-ph:book-open w-4 h-4" />
625
- {showManualInstructions ? 'Hide Instructions' : 'Manual Update'}
626
- </button>
627
  </div>
 
 
 
 
 
628
 
629
- {/* Manual Update Instructions */}
630
- <AnimatePresence>
631
- {showManualInstructions && (
632
- <motion.div
633
- initial={{ opacity: 0, height: 0 }}
634
- animate={{ opacity: 1, height: 'auto' }}
635
- exit={{ opacity: 0, height: 0 }}
636
- className="space-y-6 text-bolt-elements-textSecondary"
637
- >
638
- <div className="p-4 rounded-lg bg-purple-500/5 dark:bg-purple-500/10 border border-purple-500/20">
639
- <p className="font-medium text-purple-500">
640
- Update available from {isLatestBranch ? 'main' : 'stable'} branch!
641
- </p>
642
- <div className="mt-2 space-y-1">
643
- <p>
644
- Current: {updateInfo.currentVersion} ({updateInfo.currentCommit})
645
- </p>
646
- <p>
647
- Latest: {updateInfo.latestVersion} ({updateInfo.latestCommit})
648
- </p>
649
- </div>
650
- </div>
651
 
652
- <div>
653
- <h4 className="text-base font-medium text-bolt-elements-textPrimary mb-3">To update:</h4>
654
- <ol className="space-y-4">
655
- <li className="flex items-start gap-3">
656
- <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
657
- 1
658
- </div>
659
- <div>
660
- <p className="font-medium text-bolt-elements-textPrimary">Pull the latest changes:</p>
661
- <code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
662
- git pull upstream {isLatestBranch ? 'main' : 'stable'}
663
- </code>
664
- </div>
665
- </li>
666
- <li className="flex items-start gap-3">
667
- <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
668
- 2
669
- </div>
670
- <div>
671
- <p className="font-medium text-bolt-elements-textPrimary">Install dependencies:</p>
672
- <code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
673
- pnpm install
674
- </code>
675
- </div>
676
- </li>
677
- <li className="flex items-start gap-3">
678
- <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
679
- 3
680
- </div>
681
- <div>
682
- <p className="font-medium text-bolt-elements-textPrimary">Build the application:</p>
683
- <code className="mt-2 block p-3 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] font-mono text-sm">
684
- pnpm build
685
- </code>
686
- </div>
687
- </li>
688
- <li className="flex items-start gap-3">
689
- <div className="flex-shrink-0 w-6 h-6 rounded-full bg-purple-500/10 text-purple-500 flex items-center justify-center">
690
- 4
691
- </div>
692
- <p className="font-medium text-bolt-elements-textPrimary">Restart the application</p>
693
- </li>
694
- </ol>
695
- </div>
696
- </motion.div>
697
- )}
698
- </AnimatePresence>
699
 
700
- {/* Changelog */}
701
- {updateInfo.changelog && updateInfo.changelog.length > 0 && (
 
 
 
 
702
  <div className="mt-4">
703
- <button
704
- onClick={() => setShowChangelog(!showChangelog)}
705
- className={classNames(
706
- 'flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm',
707
- 'bg-[#F5F5F5] dark:bg-[#1A1A1A]',
708
- 'hover:bg-purple-500/10 hover:text-purple-500',
709
- 'dark:hover:bg-purple-500/20 dark:hover:text-purple-500',
710
- 'text-bolt-elements-textSecondary',
711
- 'transition-colors duration-200',
712
- )}
713
- >
714
- <div className={`i-ph:${showChangelog ? 'caret-up' : 'caret-down'} w-4 h-4`} />
715
- {showChangelog ? 'Hide Changelog' : 'View Changelog'}
716
- </button>
717
-
718
- <AnimatePresence>
719
- {showChangelog && (
720
- <motion.div
721
- initial={{ opacity: 0, height: 0 }}
722
- animate={{ opacity: 1, height: 'auto' }}
723
- exit={{ opacity: 0, height: 0 }}
724
- className="mt-4 rounded-lg bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
725
- >
726
- <div className="max-h-[400px] overflow-y-auto">
727
- {categorizeChangelog(updateInfo.changelog).map(([category, messages]) => (
728
- <div key={category} className="border-b last:border-b-0 border-bolt-elements-borderColor">
729
- <div className="p-3 bg-[#EAEAEA] dark:bg-[#2A2A2A]">
730
- <h5 className="text-sm font-medium text-bolt-elements-textPrimary">
731
- {category}
732
- <span className="ml-2 text-xs text-bolt-elements-textSecondary">
733
- ({messages.length})
734
- </span>
735
- </h5>
736
- </div>
737
- <div className="divide-y divide-bolt-elements-borderColor">
738
- {messages.map((message, index) => {
739
- const { title, description, prNumber } = parseCommitMessage(message);
740
- return (
741
- <div key={index} className="p-3 hover:bg-bolt-elements-bg-depth-4 transition-colors">
742
- <div className="flex items-start gap-3">
743
- <div className="mt-1.5 w-1.5 h-1.5 rounded-full bg-bolt-elements-textSecondary" />
744
- <div className="space-y-1 flex-1">
745
- <p className="text-sm font-medium text-bolt-elements-textPrimary">
746
- {title}
747
- {prNumber && (
748
- <span className="ml-2 text-xs text-bolt-elements-textSecondary">
749
- #{prNumber}
750
- </span>
751
- )}
752
- </p>
753
- {description && (
754
- <p className="text-xs text-bolt-elements-textSecondary">{description}</p>
755
- )}
756
- </div>
757
- </div>
758
- </div>
759
- );
760
- })}
761
- </div>
762
- </div>
763
- ))}
764
- </div>
765
- </motion.div>
766
- )}
767
- </AnimatePresence>
768
- </div>
769
- )}
770
- </div>
771
- </motion.div>
772
- )}
773
-
774
- {/* Update Progress */}
775
- {isUpdating && updateProgress && (
776
- <motion.div
777
- className="p-6 rounded-xl bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A]"
778
- initial={{ opacity: 0, y: 20 }}
779
- animate={{ opacity: 1, y: 0 }}
780
- transition={{ duration: 0.3 }}
781
- >
782
- <div className="space-y-4">
783
- <div className="flex items-center justify-between">
784
- <div>
785
- <span className="text-sm font-medium text-bolt-elements-textPrimary">
786
- {updateProgress.stage.charAt(0).toUpperCase() + updateProgress.stage.slice(1)}
787
- </span>
788
- <p className="text-xs text-bolt-elements-textSecondary">{updateProgress.message}</p>
789
- </div>
790
- {updateProgress.progress !== undefined && (
791
- <span className="text-sm text-bolt-elements-textSecondary">{Math.round(updateProgress.progress)}%</span>
792
- )}
793
- </div>
794
-
795
- {/* Show detailed information when available */}
796
- {updateProgress.details && (
797
- <div className="mt-4 space-y-4">
798
- {updateProgress.details.commitMessages && updateProgress.details.commitMessages.length > 0 && (
799
- <div>
800
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Commits to be applied:</h4>
801
- <div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[200px] overflow-y-auto text-sm">
802
- {updateProgress.details.commitMessages.map((msg, i) => (
803
- <div key={i} className="text-bolt-elements-textSecondary py-1">
804
- {msg}
805
- </div>
806
- ))}
807
- </div>
808
- </div>
809
- )}
810
-
811
- {updateProgress.details.changedFiles && updateProgress.details.changedFiles.length > 0 && (
812
- <div>
813
- <h4 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Changed Files:</h4>
814
- <div className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[200px] overflow-y-auto text-sm">
815
- {updateProgress.details.changedFiles.map((file, i) => (
816
- <div key={i} className="text-bolt-elements-textSecondary py-1">
817
- {file}
818
- </div>
819
- ))}
820
- </div>
821
- </div>
822
- )}
823
-
824
- {(updateProgress.details.additions !== undefined || updateProgress.details.deletions !== undefined) && (
825
- <div className="flex gap-4">
826
- {updateProgress.details.additions !== undefined && (
827
- <div className="text-green-500">
828
- <span className="text-sm">+{updateProgress.details.additions} additions</span>
829
- </div>
830
- )}
831
- {updateProgress.details.deletions !== undefined && (
832
- <div className="text-red-500">
833
- <span className="text-sm">-{updateProgress.details.deletions} deletions</span>
834
- </div>
835
- )}
836
- </div>
837
  )}
838
  </div>
839
  )}
840
-
841
- {updateProgress.progress !== undefined && (
842
- <div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
843
- <div
844
- className="h-full bg-purple-500 transition-all duration-300"
845
- style={{ width: `${updateProgress.progress}%` }}
846
- />
847
- </div>
848
- )}
849
-
850
- {/* Show restart button when update is complete */}
851
- {updateProgress.stage === 'complete' && !updateProgress.error && (
852
- <div className="mt-4 flex justify-end">
853
- <button
854
- onClick={handleRestart}
855
- className="px-4 py-2 bg-purple-500 text-white rounded-lg hover:bg-purple-600 transition-colors"
856
- >
857
- Restart Application
858
- </button>
859
- </div>
860
- )}
861
- </div>
862
- </motion.div>
863
- )}
864
-
865
- {/* Update Confirmation Dialog */}
866
- <DialogRoot open={showUpdateDialog} onOpenChange={setShowUpdateDialog}>
867
- <Dialog
868
- onClose={() => {
869
- setShowUpdateDialog(false);
870
- setHasUserRespondedToUpdate(true);
871
- logStore.logSystem('Update cancelled by user');
872
- }}
873
- >
874
- <div className="p-6 w-[500px]">
875
- <DialogTitle>Update Available</DialogTitle>
876
- <DialogDescription className="mt-2">
877
- A new version is available. Would you like to update now?
878
- </DialogDescription>
879
-
880
- <div className="mt-3">
881
- <h3 className="text-sm font-medium text-bolt-elements-textPrimary mb-2">Update Information:</h3>
882
- <div
883
- className="bg-[#F5F5F5] dark:bg-[#1A1A1A] rounded-lg p-3 max-h-[300px] overflow-y-auto"
884
- style={{
885
- scrollbarWidth: 'thin',
886
- scrollbarColor: 'rgba(155, 155, 155, 0.5) transparent',
887
- }}
888
- >
889
- <div className="text-sm text-bolt-elements-textSecondary space-y-1.5">
890
- {updateChangelog.map((log, index) => (
891
- <div key={index} className="break-words leading-relaxed">
892
- {log.startsWith('Compare changes:') ? (
893
- <a
894
- href={log.split(': ')[1]}
895
- target="_blank"
896
- rel="noopener noreferrer"
897
- className="text-purple-500 hover:text-purple-600 dark:text-purple-400 dark:hover:text-purple-300"
898
- >
899
- View changes on GitHub
900
- </a>
901
- ) : (
902
- log
903
- )}
904
- </div>
905
- ))}
906
- </div>
907
- </div>
908
- </div>
909
-
910
- <div className="mt-4 flex justify-end gap-3">
911
- <DialogButton
912
- type="secondary"
913
- onClick={() => {
914
- setShowUpdateDialog(false);
915
- setHasUserRespondedToUpdate(true);
916
- logStore.logSystem('Update cancelled by user');
917
- }}
918
- >
919
- Cancel
920
- </DialogButton>
921
- <DialogButton
922
- type="primary"
923
- onClick={async () => {
924
- setShowUpdateDialog(false);
925
- setHasUserRespondedToUpdate(true);
926
- await initiateUpdate();
927
- }}
928
- >
929
- Update Now
930
- </DialogButton>
931
- </div>
932
  </div>
933
  </Dialog>
934
  </DialogRoot>
 
1
  import React, { useState, useEffect } from 'react';
2
+ import { motion } from 'framer-motion';
3
  import { useSettings } from '~/lib/hooks/useSettings';
4
  import { logStore } from '~/lib/stores/logs';
 
5
  import { toast } from 'react-toastify';
6
  import { Dialog, DialogRoot, DialogTitle, DialogDescription, DialogButton } from '~/components/ui/Dialog';
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  interface UpdateProgress {
9
  stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
10
  message: string;
 
15
  additions?: number;
16
  deletions?: number;
17
  commitMessages?: string[];
18
+ totalSize?: string;
19
+ currentCommit?: string;
20
+ remoteCommit?: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  };
22
  }
23
 
 
27
  checkInterval: number;
28
  }
29
 
30
+ const ProgressBar = ({ progress }: { progress: number }) => (
31
+ <div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
32
+ <motion.div
33
+ className="h-full bg-blue-500"
34
+ initial={{ width: 0 }}
35
+ animate={{ width: `${progress}%` }}
36
+ transition={{ duration: 0.3 }}
37
+ />
38
+ </div>
39
+ );
40
+
41
+ const UpdateProgressDisplay = ({ progress }: { progress: UpdateProgress }) => (
42
+ <div className="mt-4 space-y-2">
43
+ <div className="flex justify-between items-center">
44
+ <span className="text-sm font-medium">{progress.message}</span>
45
+ <span className="text-sm text-gray-500">{progress.progress}%</span>
46
+ </div>
47
+ <ProgressBar progress={progress.progress || 0} />
48
+ {progress.details && (
49
+ <div className="mt-2 text-sm text-gray-600">
50
+ {progress.details.changedFiles && progress.details.changedFiles.length > 0 && (
51
+ <div className="mt-2">
52
+ <div className="font-medium">Changed Files:</div>
53
+ <ul className="list-disc list-inside">
54
+ {progress.details.changedFiles.map((file, index) => (
55
+ <li key={index} className="ml-2">
56
+ {file}
57
+ </li>
58
+ ))}
59
+ </ul>
60
+ </div>
61
+ )}
62
+ {progress.details.totalSize && <div className="mt-1">Total size: {progress.details.totalSize}</div>}
63
+ {progress.details.additions !== undefined && progress.details.deletions !== undefined && (
64
+ <div className="mt-1">
65
+ Changes: <span className="text-green-600">+{progress.details.additions}</span>{' '}
66
+ <span className="text-red-600">-{progress.details.deletions}</span>
67
+ </div>
68
+ )}
69
+ {progress.details.currentCommit && progress.details.remoteCommit && (
70
+ <div className="mt-1">
71
+ Updating from {progress.details.currentCommit} to {progress.details.remoteCommit}
72
+ </div>
73
+ )}
74
+ </div>
75
+ )}
76
+ </div>
77
+ );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  const UpdateTab = () => {
80
  const { isLatestBranch } = useSettings();
 
81
  const [isChecking, setIsChecking] = useState(false);
 
82
  const [error, setError] = useState<string | null>(null);
83
+ const [updateSettings] = useState<UpdateSettings>(() => {
 
 
 
 
84
  const stored = localStorage.getItem('update_settings');
85
  return stored
86
  ? JSON.parse(stored)
 
90
  checkInterval: 24,
91
  };
92
  });
 
93
  const [showUpdateDialog, setShowUpdateDialog] = useState(false);
 
94
  const [updateProgress, setUpdateProgress] = useState<UpdateProgress | null>(null);
95
 
96
  useEffect(() => {
 
101
  console.log('Starting update check...');
102
  setIsChecking(true);
103
  setError(null);
104
+ setUpdateProgress(null);
105
 
106
  try {
 
 
107
  const branchToCheck = isLatestBranch ? 'main' : 'stable';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ // Start the update check with streaming progress
110
  const response = await fetch('/api/update', {
111
  method: 'POST',
112
  headers: {
113
  'Content-Type': 'application/json',
114
  },
115
+ body: JSON.stringify({ branch: branchToCheck }),
 
 
116
  });
117
 
118
  if (!response.ok) {
119
+ throw new Error(`Update check failed: ${response.statusText}`);
 
120
  }
121
 
 
122
  const reader = response.body?.getReader();
123
 
124
  if (!reader) {
125
+ throw new Error('No response stream available');
126
  }
127
 
128
+ // Read the stream
 
129
  while (true) {
130
  const { done, value } = await reader.read();
131
 
 
133
  break;
134
  }
135
 
136
+ // Convert the chunk to text and parse the JSON
137
+ const chunk = new TextDecoder().decode(value);
138
+ const lines = chunk.split('\n').filter(Boolean);
139
 
140
+ for (const line of lines) {
141
  try {
142
+ const progress = JSON.parse(line) as UpdateProgress;
143
  setUpdateProgress(progress);
144
 
145
  if (progress.error) {
146
+ setError(progress.error);
147
  }
148
 
149
+ // If we're done, update the UI accordingly
150
  if (progress.stage === 'complete') {
151
+ setIsChecking(false);
 
 
 
 
 
152
 
153
+ if (!progress.error) {
154
+ // Update was successful
155
+ toast.success('Update check completed');
156
 
157
+ if (progress.details?.changedFiles?.length) {
158
+ setShowUpdateDialog(true);
159
+ }
160
+ }
161
+ }
162
  } catch (e) {
163
+ console.error('Error parsing progress update:', e);
164
  }
165
  }
166
  }
167
+ } catch (error) {
168
+ setError(error instanceof Error ? error.message : 'Unknown error occurred');
169
+ logStore.logWarning('Update Check Failed', {
170
+ type: 'update',
171
+ message: error instanceof Error ? error.message : 'Unknown error occurred',
172
+ });
 
173
  } finally {
174
+ setIsChecking(false);
 
 
 
 
 
 
 
 
 
 
 
175
  }
176
  };
177
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  return (
179
+ <div className="space-y-4 p-4">
180
+ <div className="flex items-center justify-between">
181
+ <h2 className="text-xl font-semibold">Updates</h2>
182
+ <button
183
+ onClick={() => {
184
+ setError(null);
185
+ checkForUpdates();
186
+ }}
187
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors disabled:opacity-50"
188
+ disabled={isChecking}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  >
190
+ {isChecking ? (
191
+ <div className="flex items-center">
192
+ <motion.div
193
+ animate={{ rotate: 360 }}
194
+ transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
195
+ className="w-4 h-4 border-2 border-white border-t-transparent rounded-full mr-2"
196
+ />
197
+ Checking...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  </div>
199
+ ) : (
200
+ 'Check for Updates'
201
+ )}
202
+ </button>
203
+ </div>
204
 
205
+ {/* Show progress information */}
206
+ {updateProgress && <UpdateProgressDisplay progress={updateProgress} />}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
+ {error && <div className="mt-4 p-4 bg-red-100 text-red-700 rounded">{error}</div>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
209
 
210
+ {/* Update dialog */}
211
+ <DialogRoot open={showUpdateDialog} onOpenChange={setShowUpdateDialog}>
212
+ <Dialog>
213
+ <DialogTitle>Update Available</DialogTitle>
214
+ <DialogDescription>
215
+ {updateProgress?.details?.changedFiles && (
216
  <div className="mt-4">
217
+ <p className="font-medium">Changes:</p>
218
+ <ul className="list-disc list-inside mt-2">
219
+ {updateProgress.details.changedFiles.map((file, index) => (
220
+ <li key={index} className="text-sm">
221
+ {file}
222
+ </li>
223
+ ))}
224
+ </ul>
225
+ {updateProgress.details.totalSize && (
226
+ <p className="mt-2 text-sm">Total size: {updateProgress.details.totalSize}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  )}
228
  </div>
229
  )}
230
+ </DialogDescription>
231
+ <div className="flex justify-end gap-2 mt-4">
232
+ <DialogButton type="secondary" onClick={() => setShowUpdateDialog(false)}>
233
+ Cancel
234
+ </DialogButton>
235
+ <DialogButton
236
+ type="primary"
237
+ onClick={() => {
238
+ setShowUpdateDialog(false);
239
+
240
+ // Handle update initiation here
241
+ }}
242
+ >
243
+ Update Now
244
+ </DialogButton>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  </div>
246
  </Dialog>
247
  </DialogRoot>
app/routes/api.update.ts CHANGED
@@ -48,11 +48,23 @@ export const action: ActionFunction = async ({ request }) => {
48
  };
49
 
50
  try {
 
 
 
 
 
 
 
51
  // Check if remote exists
52
  let defaultBranch = branch || 'main'; // Make branch mutable
53
 
54
  try {
55
  await execAsync('git remote get-url origin');
 
 
 
 
 
56
  } catch {
57
  throw new Error(
58
  '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 }) => {
61
 
62
  // Get default branch if not specified
63
  if (!branch) {
 
 
 
 
 
 
64
  try {
65
  const { stdout } = await execAsync('git remote show origin | grep "HEAD branch" | cut -d" " -f5');
66
  defaultBranch = stdout.trim() || 'main';
 
 
 
 
 
67
  } catch {
68
  defaultBranch = 'main'; // Fallback to main if we can't detect
 
 
 
 
 
69
  }
70
  }
71
 
@@ -73,20 +101,36 @@ export const action: ActionFunction = async ({ request }) => {
73
  sendProgress({
74
  stage: 'fetch',
75
  message: 'Fetching latest changes...',
76
- progress: 0,
77
  });
78
 
79
  // Fetch all remotes
80
  await execAsync('git fetch --all');
 
 
 
 
 
81
 
82
  // Check if remote branch exists
83
  try {
84
  await execAsync(`git rev-parse --verify origin/${defaultBranch}`);
 
 
 
 
 
85
  } catch {
86
  throw new Error(`Remote branch 'origin/${defaultBranch}' not found. Please push your changes first.`);
87
  }
88
 
89
  // Get current commit hash and remote commit hash
 
 
 
 
 
 
90
  const { stdout: currentCommit } = await execAsync('git rev-parse HEAD');
91
  const { stdout: remoteCommit } = await execAsync(`git rev-parse origin/${defaultBranch}`);
92
 
@@ -96,10 +140,20 @@ export const action: ActionFunction = async ({ request }) => {
96
  stage: 'complete',
97
  message: 'No updates available. You are on the latest version.',
98
  progress: 100,
 
 
 
 
99
  });
100
  return;
101
  }
102
 
 
 
 
 
 
 
103
  // Initialize variables
104
  let changedFiles: string[] = [];
105
  let commitMessages: string[] = [];
@@ -131,10 +185,20 @@ export const action: ActionFunction = async ({ request }) => {
131
  stage: 'complete',
132
  message: `No file changes detected between your version and origin/${defaultBranch}. You might be on a different branch.`,
133
  progress: 100,
 
 
 
 
134
  });
135
  return;
136
  }
137
 
 
 
 
 
 
 
138
  // Get size information for each changed file
139
  for (const line of files) {
140
  const [status, file] = line.split('\t');
 
48
  };
49
 
50
  try {
51
+ // Initial check stage
52
+ sendProgress({
53
+ stage: 'fetch',
54
+ message: 'Checking repository status...',
55
+ progress: 0,
56
+ });
57
+
58
  // Check if remote exists
59
  let defaultBranch = branch || 'main'; // Make branch mutable
60
 
61
  try {
62
  await execAsync('git remote get-url origin');
63
+ sendProgress({
64
+ stage: 'fetch',
65
+ message: 'Repository remote verified',
66
+ progress: 10,
67
+ });
68
  } catch {
69
  throw new Error(
70
  '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',
 
73
 
74
  // Get default branch if not specified
75
  if (!branch) {
76
+ sendProgress({
77
+ stage: 'fetch',
78
+ message: 'Detecting default branch...',
79
+ progress: 20,
80
+ });
81
+
82
  try {
83
  const { stdout } = await execAsync('git remote show origin | grep "HEAD branch" | cut -d" " -f5');
84
  defaultBranch = stdout.trim() || 'main';
85
+ sendProgress({
86
+ stage: 'fetch',
87
+ message: `Using branch: ${defaultBranch}`,
88
+ progress: 30,
89
+ });
90
  } catch {
91
  defaultBranch = 'main'; // Fallback to main if we can't detect
92
+ sendProgress({
93
+ stage: 'fetch',
94
+ message: 'Using default branch: main',
95
+ progress: 30,
96
+ });
97
  }
98
  }
99
 
 
101
  sendProgress({
102
  stage: 'fetch',
103
  message: 'Fetching latest changes...',
104
+ progress: 40,
105
  });
106
 
107
  // Fetch all remotes
108
  await execAsync('git fetch --all');
109
+ sendProgress({
110
+ stage: 'fetch',
111
+ message: 'Remote changes fetched',
112
+ progress: 50,
113
+ });
114
 
115
  // Check if remote branch exists
116
  try {
117
  await execAsync(`git rev-parse --verify origin/${defaultBranch}`);
118
+ sendProgress({
119
+ stage: 'fetch',
120
+ message: 'Remote branch verified',
121
+ progress: 60,
122
+ });
123
  } catch {
124
  throw new Error(`Remote branch 'origin/${defaultBranch}' not found. Please push your changes first.`);
125
  }
126
 
127
  // Get current commit hash and remote commit hash
128
+ sendProgress({
129
+ stage: 'fetch',
130
+ message: 'Comparing versions...',
131
+ progress: 70,
132
+ });
133
+
134
  const { stdout: currentCommit } = await execAsync('git rev-parse HEAD');
135
  const { stdout: remoteCommit } = await execAsync(`git rev-parse origin/${defaultBranch}`);
136
 
 
140
  stage: 'complete',
141
  message: 'No updates available. You are on the latest version.',
142
  progress: 100,
143
+ details: {
144
+ currentCommit: currentCommit.trim().substring(0, 7),
145
+ remoteCommit: remoteCommit.trim().substring(0, 7),
146
+ },
147
  });
148
  return;
149
  }
150
 
151
+ sendProgress({
152
+ stage: 'fetch',
153
+ message: 'Analyzing changes...',
154
+ progress: 80,
155
+ });
156
+
157
  // Initialize variables
158
  let changedFiles: string[] = [];
159
  let commitMessages: string[] = [];
 
185
  stage: 'complete',
186
  message: `No file changes detected between your version and origin/${defaultBranch}. You might be on a different branch.`,
187
  progress: 100,
188
+ details: {
189
+ currentCommit: currentCommit.trim().substring(0, 7),
190
+ remoteCommit: remoteCommit.trim().substring(0, 7),
191
+ },
192
  });
193
  return;
194
  }
195
 
196
+ sendProgress({
197
+ stage: 'fetch',
198
+ message: `Found ${files.length} changed files, calculating sizes...`,
199
+ progress: 90,
200
+ });
201
+
202
  // Get size information for each changed file
203
  for (const line of files) {
204
  const [status, file] = line.split('\t');