Enable updater and remove unzipping and installing update text (#2918)

This commit is contained in:
Zane
2025-06-13 16:15:20 -07:00
committed by GitHub
parent 83cd9e1613
commit 92b3735953
7 changed files with 14 additions and 284 deletions
@@ -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<HTMLDivElement>(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
</div>
{/* Update Section */}
{updatesEnabled && (
{UPDATES_ENABLED && (
<div ref={updateSectionRef} className="mt-8 pt-8 border-t border-gray-200">
<UpdateSection />
</div>
@@ -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 <Loader2 className="w-4 h-4 animate-spin" />;
case 'success':
return <CheckCircle className="w-4 h-4 text-green-500" />;
@@ -257,9 +252,10 @@ export default function UpdateSection() {
{/* Update information */}
{updateInfo.isUpdateAvailable && (
<div className="text-xs text-textSubtle mt-4 space-y-1">
<p>Update will be downloaded and automatically extracted to your Downloads folder.</p>
<p>Update will be downloaded to your Downloads folder.</p>
<p className="text-xs text-amber-600">
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.
</p>
</div>
)}
+3 -52
View File
@@ -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();
});
});
/**
-26
View File
@@ -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<boolean>;
onUpdaterStateChanged: (callback: (enabled: boolean) => void) => void;
removeUpdaterStateListener: (callback: (enabled: boolean) => void) => void;
};
type AppConfigAPI = {
@@ -100,12 +96,6 @@ type AppConfigAPI = {
getAll: () => Record<string, unknown>;
};
// 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<boolean> => {
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 = {
+1 -1
View File
@@ -1 +1 @@
export const UPDATES_ENABLED = false;
export const UPDATES_ENABLED = true;
+3 -8
View File
@@ -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,
+2 -169
View File
@@ -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<void> {
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();