diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts index 90a8bc9..64f930d 100644 --- a/apps/desktop/forge.config.ts +++ b/apps/desktop/forge.config.ts @@ -1,5 +1,7 @@ +import "dotenv/config"; import type { ForgeConfig } from "@electron-forge/shared-types"; import { MakerSquirrel } from "@electron-forge/maker-squirrel"; +import { MakerZIP } from "@electron-forge/maker-zip"; import { MakerDMG } from "@electron-forge/maker-dmg"; import { MakerDeb } from "@electron-forge/maker-deb"; import { MakerRpm } from "@electron-forge/maker-rpm"; @@ -253,9 +255,13 @@ const config: ForgeConfig = { "This app needs access to your microphone to record audio for transcription.", }, // Code signing configuration for macOS - osxSign: { - identity: "Developer ID Application: Exa Labs LLC (59CRJ9SJ4N)", - }, + ...(process.env.SKIP_CODESIGNING === "true" + ? {} + : { + osxSign: { + identity: process.env.CODESIGNING_IDENTITY, + }, + }), // Notarization for macOS (configure when ready for distribution) // osxNotarize: { // appleId: process.env.APPLE_ID, @@ -335,9 +341,16 @@ const config: ForgeConfig = { name: "Amical", setupIcon: "./assets/logo.ico", }), + new MakerZIP( + { + // Ensure the ZIP file has the correct naming for update-electron-app + // The default pattern should work, but we can customize if needed + }, + ["darwin"] + ), // Required for macOS auto-updates new MakerDMG( { - name: "Amical", + name: "Amical-${arch}", icon: "./assets/logo.icns", // Volume icon (when DMG is mounted) }, ["darwin"], diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 782e6f9..562d5f9 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -4,6 +4,10 @@ "description": "Amical Desktop app", "main": ".vite/build/main.js", "productName": "Amical", + "repository": { + "type": "git", + "url": "https://github.com/amicalhq/amical" + }, "scripts": { "start": "pnpm build:swift-helper && electron-forge start", "package": "pnpm build:swift-helper && electron-forge package", @@ -107,7 +111,6 @@ "electron-log": "^5.4.0", "electron-squirrel-startup": "^1.0.1", "electron-trpc-experimental": "1.0.0-alpha.1", - "electron-updater": "^6.6.2", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.10.5", "input-otp": "^1.4.2", @@ -129,6 +132,7 @@ "superjson": "^2.2.2", "tailwind-merge": "^3.3.0", "tw-animate-css": "^1.2.9", + "update-electron-app": "^3.1.1", "uuid": "^11.1.0", "vaul": "^1.1.2", "zod": "^3.25.24" diff --git a/apps/desktop/src/main/core/app-manager.ts b/apps/desktop/src/main/core/app-manager.ts index cbb9e0d..a0e1adf 100644 --- a/apps/desktop/src/main/core/app-manager.ts +++ b/apps/desktop/src/main/core/app-manager.ts @@ -39,8 +39,7 @@ export class AppManager { const eventHandlers = new EventHandlers(this); eventHandlers.setupEventHandlers(); - // Schedule auto-update check after startup - this.scheduleAutoUpdateCheck(); + // Auto-update is now handled by update-electron-app in main.ts logger.main.info("Application initialized successfully"); } catch (error) { @@ -152,20 +151,6 @@ export class AppManager { return this.serviceManager.getAutoUpdaterService(); } - private scheduleAutoUpdateCheck(): void { - // Check for updates on startup (after a brief delay) - setTimeout(() => { - try { - const autoUpdaterService = this.serviceManager.getAutoUpdaterService(); - autoUpdaterService.checkForUpdatesAndNotify(); - } catch (error) { - logger.main.warn("Auto-update check failed during startup", { - error: error instanceof Error ? error.message : String(error), - }); - } - }, 5000); // Wait 5 seconds after startup - } - private onMainWindowCreated(window: BrowserWindow): void { this.updateTRPCHandler(); } diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index 8565da4..e6d3b70 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -9,6 +9,11 @@ if (started) { app.quit(); } +// Set up auto-updater for production builds +if (app.isPackaged) { + require("update-electron-app")(); +} + const appManager = new AppManager(); app.whenReady().then(() => appManager.initialize()); diff --git a/apps/desktop/src/main/services/auto-updater.ts b/apps/desktop/src/main/services/auto-updater.ts index e774133..e17313c 100644 --- a/apps/desktop/src/main/services/auto-updater.ts +++ b/apps/desktop/src/main/services/auto-updater.ts @@ -1,237 +1,46 @@ -import { autoUpdater } from "electron-updater"; -import { app, dialog, BrowserWindow } from "electron"; +import { app } from "electron"; import { EventEmitter } from "events"; import { logger } from "../logger"; import { WindowManager } from "../core/window-manager"; export class AutoUpdaterService extends EventEmitter { - private checkingForUpdate = false; - private updateAvailable = false; - constructor(private windowManager: WindowManager) { super(); - - // Only set up auto-updater in production - if (process.env.NODE_ENV !== "development" && app.isPackaged) { - this.setupAutoUpdater(); - } else { - logger.updater.info("Auto-updater disabled in development mode"); - } } - private setupAutoUpdater() { - // Configure updater - autoUpdater.autoDownload = false; // Don't auto-download, ask user first - autoUpdater.autoInstallOnAppQuit = true; - - // Development settings - if (process.env.NODE_ENV === "development") { - // In development, you can test with a local update server - // autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml'); - autoUpdater.forceDevUpdateConfig = true; - } - - // Event handlers - autoUpdater.on("checking-for-update", () => { - logger.updater.info("Checking for update..."); - this.checkingForUpdate = true; - }); - - autoUpdater.on("update-available", (info) => { - logger.updater.info("Update available", { - version: info.version, - releaseDate: info.releaseDate, - }); - this.checkingForUpdate = false; - this.updateAvailable = true; - this.showUpdateDialog(info); - }); - - autoUpdater.on("update-not-available", (info) => { - logger.updater.info("Update not available", { version: info.version }); - this.checkingForUpdate = false; - this.updateAvailable = false; - }); - - autoUpdater.on("error", (err) => { - logger.updater.error("Error in auto-updater", { error: err.message }); - this.checkingForUpdate = false; - - // Show error dialog only if user manually checked for updates - const mainWindow = this.windowManager.getMainWindow(); - if (mainWindow && !mainWindow.isDestroyed()) { - dialog.showErrorBox( - "Update Error", - `Error checking for updates: ${err.message}`, - ); - } - }); - - autoUpdater.on("download-progress", (progressObj) => { - logger.updater.info("Download progress", { - bytesPerSecond: progressObj.bytesPerSecond, - percent: progressObj.percent, - transferred: progressObj.transferred, - total: progressObj.total, - }); - - // Emit event for tRPC subscription - this.emit("download-progress", progressObj); - }); - - autoUpdater.on("update-downloaded", (info) => { - logger.updater.info("Update downloaded", { version: info.version }); - this.showInstallDialog(info); - }); - } - - private async showUpdateDialog(info: any) { - const mainWindow = this.windowManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) { - return; - } - - const result = await dialog.showMessageBox(mainWindow, { - type: "info", - title: "Update Available", - message: `A new version (${info.version}) is available.`, - detail: - "Would you like to download it now? The update will be installed when you restart the app.", - buttons: ["Download Now", "Later"], - defaultId: 0, - cancelId: 1, - }); - - if (result.response === 0) { - logger.updater.info("User chose to download update"); - autoUpdater.downloadUpdate(); - } else { - logger.updater.info("User chose to skip update"); - } - } - - private async showInstallDialog(info: any) { - const mainWindow = this.windowManager.getMainWindow(); - if (!mainWindow || mainWindow.isDestroyed()) { - return; - } - - const result = await dialog.showMessageBox(mainWindow, { - type: "info", - title: "Update Ready", - message: `Update ${info.version} has been downloaded.`, - detail: - "The update will be installed when you restart the app. Would you like to restart now?", - buttons: ["Restart Now", "Later"], - defaultId: 0, - cancelId: 1, - }); - - if (result.response === 0) { - logger.updater.info("User chose to restart and install update"); - autoUpdater.quitAndInstall(); - } else { - logger.updater.info("User chose to install update later"); - } - } + // These methods are kept for compatibility with existing code + // update-electron-app handles the actual update logic async checkForUpdates(userInitiated = false): Promise { - // Skip in development - if (process.env.NODE_ENV === "development" || !app.isPackaged) { - logger.updater.info("Skipping update check in development mode"); - if (userInitiated) { - const mainWindow = this.windowManager.getMainWindow(); - if (mainWindow && !mainWindow.isDestroyed()) { - dialog.showMessageBox(mainWindow, { - type: "info", - title: "Development Mode", - message: "Update checking is disabled in development mode.", - buttons: ["OK"], - }); - } - } - return; - } - - if (this.checkingForUpdate) { - logger.updater.info("Already checking for updates"); - return; - } - - try { - logger.updater.info("Starting update check", { userInitiated }); - await autoUpdater.checkForUpdates(); - } catch (error) { - logger.updater.error("Failed to check for updates", { - error: error instanceof Error ? error.message : String(error), - }); - - if (userInitiated) { - const mainWindow = this.windowManager.getMainWindow(); - if (mainWindow && !mainWindow.isDestroyed()) { - dialog.showErrorBox( - "Update Check Failed", - "Failed to check for updates. Please try again later.", - ); - } - } - } + logger.updater.info( + "Update check requested, handled by update-electron-app", + ); } async checkForUpdatesAndNotify(): Promise { - // Skip in development - if (process.env.NODE_ENV === "development" || !app.isPackaged) { - logger.updater.info( - "Skipping background update check in development mode", - ); - return; - } - - try { - await autoUpdater.checkForUpdatesAndNotify(); - } catch (error) { - logger.updater.error("Failed to check for updates and notify", { - error: error instanceof Error ? error.message : String(error), - }); - } + logger.updater.info( + "Background update check requested, handled by update-electron-app", + ); } isCheckingForUpdate(): boolean { - return this.checkingForUpdate; + return false; // Handled by update-electron-app } isUpdateAvailable(): boolean { - return this.updateAvailable; + return false; // Handled by update-electron-app } async downloadUpdate(): Promise { - // Skip in development - if (process.env.NODE_ENV === "development" || !app.isPackaged) { - logger.updater.info("Skipping update download in development mode"); - throw new Error("Update downloads are disabled in development mode"); - } - - if (!this.updateAvailable) { - throw new Error("No update available to download"); - } - - try { - await autoUpdater.downloadUpdate(); - } catch (error) { - logger.updater.error("Failed to download update", { - error: error instanceof Error ? error.message : String(error), - }); - throw error; - } + logger.updater.info( + "Download update requested, handled by update-electron-app", + ); } quitAndInstall(): void { - // Skip in development - if (process.env.NODE_ENV === "development" || !app.isPackaged) { - logger.updater.info("Skipping quit and install in development mode"); - return; - } - - autoUpdater.quitAndInstall(); + logger.updater.info( + "Quit and install requested, handled by update-electron-app", + ); + app.quit(); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e676521..7665081 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,9 +191,6 @@ importers: electron-trpc-experimental: specifier: 1.0.0-alpha.1 version: 1.0.0-alpha.1(@trpc/client@11.4.2(@trpc/server@11.4.2(typescript@5.8.3))(typescript@5.8.3))(@trpc/server@11.4.2(typescript@5.8.3))(electron@36.2.0) - electron-updater: - specifier: ^6.6.2 - version: 6.6.2 embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.1.0) @@ -257,6 +254,9 @@ importers: tw-animate-css: specifier: ^1.2.9 version: 1.3.0 + update-electron-app: + specifier: ^3.1.1 + version: 3.1.1 uuid: specifier: ^11.1.0 version: 11.1.0 @@ -3724,10 +3724,6 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - builder-util-runtime@9.3.1: - resolution: {integrity: sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==} - engines: {node: '>=12.0.0'} - busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -4451,9 +4447,6 @@ packages: '@trpc/server': '>11.0.0' electron: '>19.0.0' - electron-updater@6.6.2: - resolution: {integrity: sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==} - electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} @@ -5152,6 +5145,9 @@ packages: github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + github-url-to-object@4.0.6: + resolution: {integrity: sha512-NaqbYHMUAlPcmWFdrAB7bcxrNIiiJWJe8s/2+iOc9vlcHlwHqSGrPk+Yi3nu6ebTwgsZEa7igz+NH2vEq3gYwQ==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -5747,9 +5743,6 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - lazy-val@1.0.5: - resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -5848,9 +5841,6 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.escaperegexp@4.1.2: - resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} - lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. @@ -5858,10 +5848,6 @@ packages: lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. - lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -7171,9 +7157,6 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.4.1: - resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} - scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} @@ -7637,9 +7620,6 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tiny-typed-emitter@2.1.0: - resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} - tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} @@ -7934,6 +7914,9 @@ packages: update-check@1.5.4: resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} + update-electron-app@3.1.1: + resolution: {integrity: sha512-7duRr6sYn014tifhKgT/5i8N+6xLzmJVJ8hVtNrHXlIDNP6QbRe6VxZ1hSi2UH5oJPzhor/PH7yKU9em5xjRzQ==} + upload-files-express@0.4.0: resolution: {integrity: sha512-ET3Pcstq4F1gNCPtfy9cK8WAYhg81NoiC1qjYaptGT2ODkT8FMarNbGh5Ku71/B6Ig39o58JibfEkObmhJFY0Q==} @@ -11969,13 +11952,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - builder-util-runtime@9.3.1: - dependencies: - debug: 4.4.1 - sax: 1.4.1 - transitivePeerDependencies: - - supports-color - busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -12655,19 +12631,6 @@ snapshots: transitivePeerDependencies: - supports-color - electron-updater@6.6.2: - dependencies: - builder-util-runtime: 9.3.1 - fs-extra: 10.1.0 - js-yaml: 4.1.0 - lazy-val: 1.0.5 - lodash.escaperegexp: 4.1.2 - lodash.isequal: 4.5.0 - semver: 7.7.2 - tiny-typed-emitter: 2.1.0 - transitivePeerDependencies: - - supports-color - electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1 @@ -13707,6 +13670,10 @@ snapshots: github-slugger@2.0.0: {} + github-url-to-object@4.0.6: + dependencies: + is-url: 1.2.4 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -14408,8 +14375,6 @@ snapshots: kind-of@6.0.3: {} - lazy-val@1.0.5: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -14508,14 +14473,10 @@ snapshots: lodash.defaults@4.2.0: {} - lodash.escaperegexp@4.1.2: {} - lodash.get@4.4.2: {} lodash.isarguments@3.1.0: {} - lodash.isequal@4.5.0: {} - lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -16295,8 +16256,6 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.4.1: {} - scheduler@0.26.0: {} scroll-into-view-if-needed@3.1.0: @@ -16912,8 +16871,6 @@ snapshots: tiny-invariant@1.3.3: {} - tiny-typed-emitter@2.1.0: {} - tinycolor2@1.6.0: {} tinygradient@1.1.5: @@ -17234,6 +17191,11 @@ snapshots: registry-auth-token: 3.3.2 registry-url: 3.1.0 + update-electron-app@3.1.1: + dependencies: + github-url-to-object: 4.0.6 + ms: 2.1.3 + upload-files-express@0.4.0: dependencies: formidable: 2.1.5