Skip to content

Commit 338523c

Browse files
committed
feat: implement Phase 5 auto-update system with npm and app update checking
1 parent f56d46a commit 338523c

4 files changed

Lines changed: 163 additions & 19 deletions

File tree

‎src/main/main.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { app, BrowserWindow, Menu } from 'electron'
22
import path from 'path'
33
import { fileURLToPath } from 'url'
44
import { setupIPCHandlers } from './ipc-handlers.js'
5-
import { checkUpdates } from './updater.js'
5+
import { setupUpdater } from './updater.js'
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url))
88
const isDev = process.env.NODE_ENV === 'development'
@@ -34,7 +34,7 @@ async function createWindow() {
3434
})
3535

3636
setupIPCHandlers()
37-
checkUpdates()
37+
setupUpdater()
3838
}
3939

4040
app.on('ready', createWindow)

‎src/main/preload.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ contextBridge.exposeInMainWorld('electron', {
1212
checkUpdates: () => ipcRenderer.invoke(IPC_CHANNELS.CHECK_UPDATES),
1313
onUpdatesAvailable: (callback: (updates: any) => void) =>
1414
ipcRenderer.on(IPC_CHANNELS.UPDATES_AVAILABLE, (_, data) => callback(data)),
15+
onUpdateProgress: (callback: (progress: any) => void) =>
16+
ipcRenderer.on('update-progress', (_, data) => callback(data)),
17+
onUpdateReady: (callback: (data: any) => void) =>
18+
ipcRenderer.on('update-ready', (_, data) => callback(data)),
19+
offUpdateProgress: () => ipcRenderer.removeAllListeners('update-progress'),
20+
offUpdateReady: () => ipcRenderer.removeAllListeners('update-ready'),
1521
})
1622

1723
declare global {
@@ -23,6 +29,10 @@ declare global {
2329
getDrives: () => Promise<any[]>
2430
checkUpdates: () => Promise<any>
2531
onUpdatesAvailable: (callback: (updates: any) => void) => void
32+
onUpdateProgress: (callback: (progress: any) => void) => void
33+
onUpdateReady: (callback: (data: any) => void) => void
34+
offUpdateProgress: () => void
35+
offUpdateReady: () => void
2636
}
2737
}
2838
}

‎src/main/updater.ts‎

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
import { autoUpdater } from 'electron-updater'
2-
import { BrowserWindow } from 'electron'
2+
import { BrowserWindow, ipcMain } from 'electron'
33
import { IPC_CHANNELS } from '../shared/constants.js'
4+
import { execSync } from 'child_process'
45

5-
export function checkUpdates() {
6+
export function setupUpdater() {
67
if (process.env.NODE_ENV === 'development') {
78
return
89
}
910

10-
autoUpdater.checkForUpdatesAndNotify()
11+
autoUpdater.allowDowngrade = false
12+
autoUpdater.allowPrerelease = false
13+
14+
autoUpdater.on('checking-for-update', () => {
15+
console.log('Checking for app updates...')
16+
})
17+
18+
autoUpdater.on('update-available', (info) => {
19+
console.log('Update available:', info.version)
1120

12-
autoUpdater.on('update-available', () => {
1321
const window = BrowserWindow.getAllWindows()[0]
1422
if (window) {
1523
window.webContents.send(IPC_CHANNELS.UPDATES_AVAILABLE, {
1624
type: 'app',
17-
message: 'New version available',
25+
version: info.version,
26+
message: `Version ${info.version} is available`,
1827
})
1928
}
2029
})
@@ -23,7 +32,80 @@ export function checkUpdates() {
2332
console.log('App is up to date')
2433
})
2534

35+
autoUpdater.on('error', (err) => {
36+
console.error('Update error:', err)
37+
})
38+
39+
autoUpdater.on('download-progress', (progress) => {
40+
const window = BrowserWindow.getAllWindows()[0]
41+
if (window) {
42+
window.webContents.send('update-progress', {
43+
percentage: Math.round(progress.percent),
44+
})
45+
}
46+
})
47+
2648
autoUpdater.on('update-downloaded', () => {
27-
autoUpdater.quitAndInstall()
49+
const window = BrowserWindow.getAllWindows()[0]
50+
if (window) {
51+
window.webContents.send('update-ready', {
52+
message: 'Update ready. Will install on next restart.',
53+
})
54+
}
2855
})
56+
57+
// Check for updates on startup
58+
autoUpdater.checkForUpdatesAndNotify()
59+
60+
// Check periodically (every 6 hours)
61+
setInterval(() => {
62+
autoUpdater.checkForUpdatesAndNotify()
63+
}, 6 * 60 * 60 * 1000)
2964
}
65+
66+
export function checkNpmUpdates(): {
67+
available: boolean
68+
count: number
69+
updates: string[]
70+
} {
71+
try {
72+
const output = execSync('npm outdated --json', {
73+
encoding: 'utf-8',
74+
timeout: 10000,
75+
stdio: ['pipe', 'pipe', 'pipe'],
76+
})
77+
78+
const outdated = JSON.parse(output)
79+
const packages = Object.keys(outdated)
80+
81+
return {
82+
available: packages.length > 0,
83+
count: packages.length,
84+
updates: packages,
85+
}
86+
} catch (err) {
87+
console.error('Failed to check npm updates:', err)
88+
return {
89+
available: false,
90+
count: 0,
91+
updates: [],
92+
}
93+
}
94+
}
95+
96+
export function installUpdates(): void {
97+
try {
98+
// Run npm install with timeout
99+
execSync('npm install', {
100+
cwd: process.resourcesPath,
101+
timeout: 60000,
102+
stdio: 'inherit',
103+
})
104+
} catch (err) {
105+
throw new Error(`Failed to install updates: ${err}`)
106+
}
107+
}
108+
109+
ipcMain.handle(IPC_CHANNELS.CHECK_UPDATES, async () => {
110+
return checkNpmUpdates()
111+
})

‎src/renderer/components/SettingsPanel.tsx‎

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { Moon, Sun, X, Check, AlertCircle } from 'lucide-react'
2-
import { useState } from 'react'
1+
import { Moon, Sun, X, Check, AlertCircle, Download } from 'lucide-react'
2+
import { useState, useEffect } from 'react'
33

44
interface SettingsPanelProps {
55
darkMode: boolean
@@ -13,21 +13,48 @@ export function SettingsPanel({
1313
onClose,
1414
}: SettingsPanelProps) {
1515
const [checkingUpdates, setCheckingUpdates] = useState(false)
16-
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'uptodate'>('idle')
16+
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'npm-available' | 'app-available' | 'uptodate'>('idle')
17+
const [npmUpdates, setNpmUpdates] = useState(0)
18+
const [appUpdateVersion, setAppUpdateVersion] = useState<string | null>(null)
19+
const [lastChecked, setLastChecked] = useState<Date | null>(null)
20+
21+
useEffect(() => {
22+
window.electron.onUpdatesAvailable((data: any) => {
23+
if (data.type === 'app') {
24+
setAppUpdateVersion(data.version)
25+
setUpdateStatus('app-available')
26+
}
27+
})
28+
29+
return () => {
30+
window.electron.offUpdateReady()
31+
}
32+
}, [])
1733

1834
async function handleCheckUpdates() {
1935
setCheckingUpdates(true)
2036
setUpdateStatus('checking')
37+
setLastChecked(new Date())
2138

2239
try {
2340
const result = await window.electron.checkUpdates()
24-
setUpdateStatus(result ? 'available' : 'uptodate')
41+
42+
if (result.available) {
43+
setNpmUpdates(result.count)
44+
setUpdateStatus('npm-available')
45+
} else {
46+
setNpmUpdates(0)
47+
setUpdateStatus('uptodate')
48+
}
2549

2650
setTimeout(() => {
2751
setCheckingUpdates(false)
28-
setTimeout(() => setUpdateStatus('idle'), 3000)
29-
}, 1500)
52+
if (updateStatus === 'uptodate') {
53+
setTimeout(() => setUpdateStatus('idle'), 2000)
54+
}
55+
}, 500)
3056
} catch (err) {
57+
console.error('Check updates error:', err)
3158
setCheckingUpdates(false)
3259
setUpdateStatus('idle')
3360
}
@@ -77,6 +104,23 @@ export function SettingsPanel({
77104
<h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wide mb-4">
78105
Updates
79106
</h3>
107+
108+
{appUpdateVersion && (
109+
<div className="mb-4 p-4 bg-amber-50 dark:bg-amber-900 rounded-lg border border-amber-200 dark:border-amber-800">
110+
<div className="flex items-start gap-3">
111+
<Download size={18} className="text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
112+
<div className="flex-1">
113+
<p className="font-semibold text-amber-900 dark:text-amber-100">
114+
App Update Available
115+
</p>
116+
<p className="text-sm text-amber-800 dark:text-amber-200 mt-1">
117+
Version {appUpdateVersion} is downloading. Will install on next restart.
118+
</p>
119+
</div>
120+
</div>
121+
</div>
122+
)}
123+
80124
<button
81125
onClick={handleCheckUpdates}
82126
disabled={checkingUpdates}
@@ -92,22 +136,30 @@ export function SettingsPanel({
92136
</div>
93137
)}
94138

95-
{updateStatus === 'available' && (
139+
{updateStatus === 'npm-available' && (
96140
<div className="mt-3 p-3 bg-amber-50 dark:bg-amber-900 rounded-lg flex items-center gap-2">
97141
<AlertCircle size={16} className="text-amber-600 dark:text-amber-400 flex-shrink-0" />
98-
<p className="text-sm text-amber-900 dark:text-amber-100">Update available</p>
142+
<p className="text-sm text-amber-900 dark:text-amber-100">
143+
{npmUpdates} npm package{npmUpdates !== 1 ? 's' : ''} available
144+
</p>
99145
</div>
100146
)}
101147

102148
{updateStatus === 'uptodate' && (
103149
<div className="mt-3 p-3 bg-green-50 dark:bg-green-900 rounded-lg flex items-center gap-2">
104150
<Check size={16} className="text-green-600 dark:text-green-400 flex-shrink-0" />
105-
<p className="text-sm text-green-900 dark:text-green-100">You are up to date</p>
151+
<p className="text-sm text-green-900 dark:text-green-100">All up to date</p>
106152
</div>
107153
)}
108154

109-
<p className="text-xs text-slate-500 dark:text-slate-500 mt-3">
110-
Automatically checks on startup. Updates install on next launch.
155+
{lastChecked && (
156+
<p className="text-xs text-slate-500 dark:text-slate-500 mt-3">
157+
Last checked: {lastChecked.toLocaleTimeString()}
158+
</p>
159+
)}
160+
161+
<p className="text-xs text-slate-500 dark:text-slate-500 mt-2">
162+
App updates check automatically every 6 hours.
111163
</p>
112164
</div>
113165

0 commit comments

Comments
 (0)