diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index b798a8027031..a586fd7110f9 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useRef } from 'react'; import { Switch } from '../../ui/switch'; import UpdateSection from './UpdateSection'; +import { UPDATES_ENABLED } from '../../../updates'; interface AppSettingsSectionProps { scrollToSection?: string; @@ -11,7 +12,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti const [dockIconEnabled, setDockIconEnabled] = useState(true); const [isMacOS, setIsMacOS] = useState(false); const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false); - const [updatesEnabled, setUpdatesEnabled] = useState(false); const updateSectionRef = useRef(null); // Check if running on macOS @@ -19,25 +19,6 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti setIsMacOS(window.electron.platform === 'darwin'); }, []); - // Load updater state - useEffect(() => { - window.electron.getUpdaterEnabled().then((enabled) => { - setUpdatesEnabled(enabled); - }); - - // Listen for updater state changes - const handleUpdaterStateChange = (enabled: boolean) => { - setUpdatesEnabled(enabled); - }; - - window.electron.onUpdaterStateChanged(handleUpdaterStateChange); - - // Cleanup listener on unmount - return () => { - window.electron.removeUpdaterStateListener(handleUpdaterStateChange); - }; - }, []); - // Handle scrolling to update section useEffect(() => { if (scrollToSection === 'update' && updateSectionRef.current) { @@ -144,7 +125,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti {/* Update Section */} - {updatesEnabled && ( + {UPDATES_ENABLED && (
diff --git a/ui/desktop/src/components/settings/app/UpdateSection.tsx b/ui/desktop/src/components/settings/app/UpdateSection.tsx index 3d0639a334d7..974a274f9520 100644 --- a/ui/desktop/src/components/settings/app/UpdateSection.tsx +++ b/ui/desktop/src/components/settings/app/UpdateSection.tsx @@ -147,8 +147,6 @@ export default function UpdateSection() { }; const installUpdate = () => { - setUpdateStatus('installing'); - // This will quit the app and install the update window.electron.installUpdate(); }; @@ -158,8 +156,6 @@ export default function UpdateSection() { return 'Checking for updates...'; case 'downloading': return `Downloading update... ${Math.round(progress)}%`; - case 'installing': - return 'Installing update...'; case 'ready': return 'Update downloaded and ready to install!'; case 'success': @@ -180,7 +176,6 @@ export default function UpdateSection() { switch (updateStatus) { case 'checking': case 'downloading': - case 'installing': return ; case 'success': return ; @@ -257,9 +252,10 @@ export default function UpdateSection() { {/* Update information */} {updateInfo.isUpdateAvailable && (
-

Update will be downloaded and automatically extracted to your Downloads folder.

+

Update will be downloaded to your Downloads folder.

- After download, move the Goose app to /Applications to complete the update. + After download, extract Goose-{updateInfo.latestVersion}.zip and move the Goose app + to /Applications to complete the update.

)} diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index 9a50cf281609..630f208b7cc6 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -44,22 +44,10 @@ import { } from './utils/autoUpdater'; import { UPDATES_ENABLED } from './updates'; -// Updater toggle functions (moved here to keep updates.ts minimal for release replacement) -let updatesEnabled = UPDATES_ENABLED; - -function toggleUpdates(): boolean { - updatesEnabled = !updatesEnabled; - return updatesEnabled; -} - -function getUpdatesEnabled(): boolean { - // Only return the toggle state, ignore ENABLE_DEV_UPDATES for UI visibility - return updatesEnabled; -} - +// Updater functions (moved here to keep updates.ts minimal for release replacement) function shouldSetupUpdater(): boolean { - // Setup updater if either the toggle is enabled OR dev updates are enabled - return updatesEnabled || process.env.ENABLE_DEV_UPDATES === 'true'; + // Setup updater if either the flag is enabled OR dev updates are enabled + return UPDATES_ENABLED || process.env.ENABLE_DEV_UPDATES === 'true'; } // Define temp directory for pasted images @@ -1222,38 +1210,6 @@ app.whenReady().then(async () => { // Register the default global hotkey registerGlobalHotkey('CommandOrControl+Alt+Shift+G'); - // Register hidden key combination to toggle updater (Cmd+Shift+U+P+D+A+T+E) - globalShortcut.register('CommandOrControl+Shift+U', () => { - // This is a multi-key sequence, we'll use a simpler approach - // Register a hidden key combination: Cmd/Ctrl + Alt + Shift + U for "Update toggle" - const newState = toggleUpdates(); - log.info( - `Updater toggled via keyboard shortcut. New state: ${newState ? 'ENABLED' : 'DISABLED'}` - ); - - // Show a notification to the user - new Notification({ - title: 'Goose Updater', - body: `Updates ${newState ? 'enabled' : 'disabled'}`, - }).show(); - - // If we're enabling updates and haven't set up the auto-updater yet, set it up now - if (newState) { - try { - setupAutoUpdater(tray || undefined); - log.info('Auto-updater setup completed after keyboard toggle'); - } catch (error) { - log.error('Error setting up auto-updater after keyboard toggle:', error); - } - } - - // Notify all windows about the updater state change - const windows = BrowserWindow.getAllWindows(); - windows.forEach((win) => { - win.webContents.send('updater-state-changed', newState); - }); - }); - session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['Origin'] = 'http://localhost:5173'; callback({ cancel: false, requestHeaders: details.requestHeaders }); @@ -1664,11 +1620,6 @@ app.whenReady().then(async () => { ipcMain.on('get-app-version', (event) => { event.returnValue = app.getVersion(); }); - - // Handler for getting updater state - ipcMain.handle('get-updater-enabled', () => { - return getUpdatesEnabled(); - }); }); /** diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index 731a9d001dbb..d79f68a5d6fc 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -89,10 +89,6 @@ type ElectronAPI = { restartApp: () => void; onUpdaterEvent: (callback: (event: UpdaterEvent) => void) => void; getUpdateState: () => Promise<{ updateAvailable: boolean; latestVersion?: string } | null>; - // Updater state functions - getUpdaterEnabled: () => Promise; - onUpdaterStateChanged: (callback: (enabled: boolean) => void) => void; - removeUpdaterStateListener: (callback: (enabled: boolean) => void) => void; }; type AppConfigAPI = { @@ -100,12 +96,6 @@ type AppConfigAPI = { getAll: () => Record; }; -// Store callback wrappers for proper cleanup -const updaterStateCallbacks = new Map< - (enabled: boolean) => void, - (event: Electron.IpcRendererEvent, enabled: boolean) => void ->(); - const electronAPI: ElectronAPI = { platform: process.platform, reactReady: () => ipcRenderer.send('react-ready'), @@ -193,22 +183,6 @@ const electronAPI: ElectronAPI = { getUpdateState: (): Promise<{ updateAvailable: boolean; latestVersion?: string } | null> => { return ipcRenderer.invoke('get-update-state'); }, - // Updater state functions - getUpdaterEnabled: (): Promise => { - return ipcRenderer.invoke('get-updater-enabled'); - }, - onUpdaterStateChanged: (callback: (enabled: boolean) => void): void => { - const wrapper = (_event: Electron.IpcRendererEvent, enabled: boolean) => callback(enabled); - updaterStateCallbacks.set(callback, wrapper); - ipcRenderer.on('updater-state-changed', wrapper); - }, - removeUpdaterStateListener: (callback: (enabled: boolean) => void): void => { - const wrapper = updaterStateCallbacks.get(callback); - if (wrapper) { - ipcRenderer.off('updater-state-changed', wrapper); - updaterStateCallbacks.delete(callback); - } - }, }; const appConfigAPI: AppConfigAPI = { diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts index 1ba31dab9e75..da6e19cfee1f 100644 --- a/ui/desktop/src/updates.ts +++ b/ui/desktop/src/updates.ts @@ -1 +1 @@ -export const UPDATES_ENABLED = false; +export const UPDATES_ENABLED = true; diff --git a/ui/desktop/src/utils/autoUpdater.ts b/ui/desktop/src/utils/autoUpdater.ts index 524c112e74aa..711e076d5356 100644 --- a/ui/desktop/src/utils/autoUpdater.ts +++ b/ui/desktop/src/utils/autoUpdater.ts @@ -201,16 +201,11 @@ export function registerUpdateIpcHandlers() { } // Show dialog to inform user about manual installation - const isExtracted = !!githubUpdateInfo.extractedPath; const dialogResult = (await dialog.showMessageBox({ type: 'info', - title: 'Update Ready', - message: isExtracted - ? 'The update has been downloaded and extracted to your Downloads folder.' - : 'The update has been downloaded to your Downloads folder.', - detail: isExtracted - ? `Please move the Goose app from ${path.basename(updatePath)} to your Applications folder to complete the update.` - : `Please extract ${path.basename(updatePath)} and move the Goose app to your Applications folder to complete the update.`, + title: 'Update Downloaded', + message: 'The update has been downloaded to your Downloads folder.', + detail: `Please extract ${path.basename(updatePath)} and move the Goose app to your Applications folder to complete the update.`, buttons: ['Open Downloads', 'Cancel'], defaultId: 0, cancelId: 1, diff --git a/ui/desktop/src/utils/githubUpdater.ts b/ui/desktop/src/utils/githubUpdater.ts index b4bb2d7f5557..f9cdebaa1068 100644 --- a/ui/desktop/src/utils/githubUpdater.ts +++ b/ui/desktop/src/utils/githubUpdater.ts @@ -1,10 +1,8 @@ import { app } from 'electron'; import { compareVersions } from 'compare-versions'; import * as fs from 'fs/promises'; -import { createWriteStream } from 'fs'; import * as path from 'path'; import * as os from 'os'; -import * as yauzl from 'yauzl'; import log from './logger'; interface GitHubRelease { @@ -191,70 +189,8 @@ export class GitHubUpdater { log.info(`GitHubUpdater: Update downloaded to ${downloadPath}`); - // Auto-unzip the downloaded file using yauzl (secure ZIP library) - try { - const tempExtractDir = path.join(downloadsDir, `temp-extract-${Date.now()}`); - - // Create temp extraction directory - await fs.mkdir(tempExtractDir, { recursive: true }); - - log.info(`GitHubUpdater: Extracting ${fileName} to temp directory using yauzl`); - - // Use yauzl to extract the ZIP file securely - await extractZipFile(downloadPath, tempExtractDir); - - // Check if Goose.app exists in the extracted content - const appPath = path.join(tempExtractDir, 'Goose.app'); - try { - await fs.access(appPath); - log.info(`GitHubUpdater: Found Goose.app at ${appPath}`); - } catch (error) { - log.error('GitHubUpdater: Goose.app not found in extracted content'); - throw new Error('Goose.app not found in extracted content'); - } - - // Move Goose.app to Downloads folder - const finalAppPath = path.join(downloadsDir, 'Goose.app'); - - // Remove existing Goose.app if it exists - try { - await fs.rm(finalAppPath, { recursive: true, force: true }); - } catch (e) { - // File might not exist, that's fine - } - - // Move the app to Downloads - log.info(`GitHubUpdater: Moving Goose.app to Downloads folder`); - await fs.rename(appPath, finalAppPath); - - // Verify the move was successful - try { - await fs.access(finalAppPath); - log.info(`GitHubUpdater: Successfully moved Goose.app to Downloads`); - } catch (error) { - log.error('GitHubUpdater: Failed to move Goose.app'); - throw new Error('Failed to move Goose.app to Downloads'); - } - - // Clean up temp directory and zip file - try { - await fs.rm(tempExtractDir, { recursive: true, force: true }); - await fs.unlink(downloadPath); - log.info(`GitHubUpdater: Cleaned up temporary files`); - } catch (cleanupError) { - log.warn(`GitHubUpdater: Failed to clean up temporary files: ${cleanupError}`); - } - - return { success: true, downloadPath: finalAppPath, extractedPath: downloadsDir }; - } catch (unzipError) { - log.error('GitHubUpdater: Error extracting update:', unzipError); - // Still return success for download, but note the extraction error - return { - success: true, - downloadPath, - error: `Downloaded successfully but extraction failed: ${unzipError instanceof Error ? unzipError.message : 'Unknown error'}`, - }; - } + // Return success - user will handle extraction manually + return { success: true, downloadPath, extractedPath: downloadsDir }; } catch (error) { log.error('GitHubUpdater: Error downloading update:', error); return { @@ -265,108 +201,5 @@ export class GitHubUpdater { } } -/** - * Securely extract a ZIP file using yauzl with security checks - * @param zipPath Path to the ZIP file - * @param extractDir Directory to extract to - */ -async function extractZipFile(zipPath: string, extractDir: string): Promise { - return new Promise((resolve, reject) => { - yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { - if (err) { - reject(err); - return; - } - - if (!zipfile) { - reject(new Error('Failed to open ZIP file')); - return; - } - - zipfile.readEntry(); - - zipfile.on('entry', async (entry: yauzl.Entry) => { - try { - // Security check: prevent directory traversal attacks - if (entry.fileName.includes('..') || path.isAbsolute(entry.fileName)) { - log.warn(`GitHubUpdater: Skipping potentially dangerous path: ${entry.fileName}`); - zipfile.readEntry(); - return; - } - - const fullPath = path.join(extractDir, entry.fileName); - - // Ensure the resolved path is still within the extraction directory - const resolvedPath = path.resolve(fullPath); - const resolvedExtractDir = path.resolve(extractDir); - if (!resolvedPath.startsWith(resolvedExtractDir + path.sep)) { - log.warn(`GitHubUpdater: Path traversal attempt detected: ${entry.fileName}`); - zipfile.readEntry(); - return; - } - - // Handle directories - if (entry.fileName.endsWith('/')) { - await fs.mkdir(fullPath, { recursive: true }); - zipfile.readEntry(); - return; - } - - // Handle files - zipfile.openReadStream(entry, async (err, readStream) => { - if (err) { - reject(err); - return; - } - - if (!readStream) { - reject(new Error('Failed to open read stream')); - return; - } - - try { - // Ensure parent directory exists - await fs.mkdir(path.dirname(fullPath), { recursive: true }); - - // Create write stream - const writeStream = createWriteStream(fullPath); - - readStream.on('end', () => { - writeStream.end(); - zipfile.readEntry(); - }); - - readStream.on('error', (streamErr) => { - writeStream.destroy(); - reject(streamErr); - }); - - writeStream.on('error', (writeErr: Error) => { - reject(writeErr); - }); - - // Pipe the data - readStream.pipe(writeStream); - } catch (fileErr) { - reject(fileErr); - } - }); - } catch (entryErr) { - reject(entryErr); - } - }); - - zipfile.on('end', () => { - log.info('GitHubUpdater: ZIP extraction completed successfully'); - resolve(); - }); - - zipfile.on('error', (zipErr) => { - reject(zipErr); - }); - }); - }); -} - // Create singleton instance export const githubUpdater = new GitHubUpdater();