From aecabbeb4aa3c0974523c3b70e8920ece8297600 Mon Sep 17 00:00:00 2001 From: Haritabh <41149926+haritabh-z01@users.noreply.github.com> Date: Sat, 28 Jun 2025 09:43:46 +0530 Subject: [PATCH] chore: git publish + auto update handling (#31) * chore: git publish + auto update handling --- apps/desktop/forge.config.ts | 24 ++ apps/desktop/package.json | 4 +- apps/desktop/src/components/update-dialog.tsx | 238 ++++++++++++++++ apps/desktop/src/main/logger.ts | 1 + apps/desktop/src/main/main.ts | 30 +- apps/desktop/src/main/menu.ts | 13 +- apps/desktop/src/main/preload.ts | 1 + .../desktop/src/main/services/auto-updater.ts | 222 +++++++++++++++ apps/desktop/src/trpc/router.ts | 4 + apps/desktop/src/trpc/routers/updater.ts | 165 +++++++++++ apps/desktop/src/types/electron-api.ts | 1 + pnpm-lock.yaml | 257 ++++++++++++++++++ 12 files changed, 957 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/components/update-dialog.tsx create mode 100644 apps/desktop/src/main/services/auto-updater.ts create mode 100644 apps/desktop/src/trpc/routers/updater.ts diff --git a/apps/desktop/forge.config.ts b/apps/desktop/forge.config.ts index d2762e1..d6a083c 100644 --- a/apps/desktop/forge.config.ts +++ b/apps/desktop/forge.config.ts @@ -6,6 +6,7 @@ import { MakerRpm } from '@electron-forge/maker-rpm'; import { VitePlugin } from '@electron-forge/plugin-vite'; import { FusesPlugin } from '@electron-forge/plugin-fuses'; import { FuseV1Options, FuseVersion } from '@electron/fuses'; +import { PublisherGithub } from '@electron-forge/publisher-github'; import { readdirSync, rmdirSync, statSync, existsSync, mkdirSync, cpSync } from 'node:fs'; import { join, normalize } from 'node:path'; // Use flora-colossus for finding all dependencies of EXTERNAL_DEPENDENCIES @@ -192,6 +193,19 @@ const config: ForgeConfig = { NSMicrophoneUsageDescription: 'This app needs access to your microphone to record audio for transcription.', }, + // Code signing configuration for macOS (configure when ready to sign) + // osxSign: { + // identity: process.env.APPLE_SIGNING_IDENTITY, + // 'hardened-runtime': true, + // entitlements: './entitlements.plist', + // 'entitlements-inherit': './entitlements.plist', + // }, + // Notarization for macOS (configure when ready for distribution) + // osxNotarize: { + // appleId: process.env.APPLE_ID, + // appleIdPassword: process.env.APPLE_APP_PASSWORD, + // teamId: process.env.APPLE_TEAM_ID, + // }, //! issues with monorepo setup and module resolutions //! when forge walks paths via flora-colossus prune: false, @@ -305,6 +319,16 @@ const config: ForgeConfig = { [FuseV1Options.OnlyLoadAppFromAsar]: true, }), ], + publishers: [ + new PublisherGithub({ + repository: { + owner: 'amicalhq', + name: 'amical', + }, + prerelease: true, + draft: true, // Create draft releases first for review + }), + ], }; export default config; diff --git a/apps/desktop/package.json b/apps/desktop/package.json index a94bac7..704a493 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@amical/desktop", - "version": "1.0.0", + "version": "0.0.1", "description": "Amical Desktop app", "main": ".vite/build/main.js", "productName": "Amical", @@ -31,6 +31,7 @@ "@electron-forge/plugin-auto-unpack-natives": "^7.8.1", "@electron-forge/plugin-fuses": "^7.8.1", "@electron-forge/plugin-vite": "^7.8.1", + "@electron-forge/publisher-github": "^7.8.1", "@electron/fuses": "^1.8.0", "@rollup/plugin-commonjs": "^28.0.6", "@tailwindcss/vite": "^4.1.6", @@ -105,6 +106,7 @@ "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", diff --git a/apps/desktop/src/components/update-dialog.tsx b/apps/desktop/src/components/update-dialog.tsx new file mode 100644 index 0000000..ee9c116 --- /dev/null +++ b/apps/desktop/src/components/update-dialog.tsx @@ -0,0 +1,238 @@ +import React, { useState, useEffect } from 'react'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from './ui/alert-dialog'; +import { Progress } from './ui/progress'; +import { Button } from './ui/button'; +import { Download, RefreshCw, CheckCircle } from 'lucide-react'; +import { api } from '@/trpc/react'; +import { toast } from 'sonner'; + +interface UpdateDialogProps { + isOpen: boolean; + onClose: () => void; + updateInfo?: { + version: string; + releaseNotes?: string; + }; +} + +export function UpdateDialog({ isOpen, onClose, updateInfo }: UpdateDialogProps) { + const [isDownloading, setIsDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = useState(0); + + // tRPC queries for update status + const isCheckingQuery = api.updater.isCheckingForUpdate.useQuery(undefined, { + enabled: isOpen, + refetchInterval: isOpen ? 1000 : false, // Poll every second when dialog is open + }); + const isUpdateAvailableQuery = api.updater.isUpdateAvailable.useQuery(undefined, { + enabled: isOpen, + refetchInterval: isOpen ? 1000 : false, + }); + + const utils = api.useUtils(); + + // tRPC mutations + const checkForUpdatesMutation = api.updater.checkForUpdates.useMutation({ + onSuccess: () => { + toast.success('Update check completed'); + utils.updater.isUpdateAvailable.invalidate(); + utils.updater.isCheckingForUpdate.invalidate(); + }, + onError: (error) => { + console.error('Error checking for updates:', error); + toast.error('Failed to check for updates'); + } + }); + + const downloadUpdateMutation = api.updater.downloadUpdate.useMutation({ + onSuccess: () => { + toast.success('Update download started'); + }, + onError: (error) => { + console.error('Error downloading update:', error); + toast.error('Failed to download update'); + setIsDownloading(false); + } + }); + + const quitAndInstallMutation = api.updater.quitAndInstall.useMutation({ + onError: (error) => { + console.error('Error installing update:', error); + toast.error('Failed to install update'); + } + }); + + // Get status from queries + const isCheckingForUpdates = isCheckingQuery.data || false; + const updateAvailable = isUpdateAvailableQuery.data || false; + + // Subscribe to download progress via tRPC + api.updater.onDownloadProgress.useSubscription(undefined, { + enabled: isOpen && isDownloading, + onData: (progress) => { + setDownloadProgress(Math.round(progress.percent || 0)); + }, + onError: (error) => { + console.error('Download progress subscription error:', error); + } + }); + + const handleCheckForUpdates = async () => { + checkForUpdatesMutation.mutate({ userInitiated: true }); + }; + + const handleDownloadUpdate = async () => { + setIsDownloading(true); + setDownloadProgress(0); + downloadUpdateMutation.mutate(); + }; + + const handleInstallUpdate = async () => { + quitAndInstallMutation.mutate(); + }; + + if (!updateAvailable && !isCheckingForUpdates && !isDownloading) { + return ( + + + + + + Check for Updates + + + Click below to check for the latest version of Amical. + + + + Cancel + + Check for Updates + + + + + ); + } + + if (isCheckingForUpdates) { + return ( + + + + + + Checking for Updates... + + + Please wait while we check for the latest version. + + + + Cancel + + + + ); + } + + if (isDownloading) { + return ( + {}}> + + + + + Downloading Update... + + + {updateInfo?.version && ( + <>Downloading version {updateInfo.version}. Please wait...> + )} + + + + + + {downloadProgress}% complete + + + + + Downloading... + + + + + ); + } + + if (downloadProgress === 100 && !isDownloading) { + return ( + {}}> + + + + + Update Ready + + + {updateInfo?.version && ( + <> + Version {updateInfo.version} has been downloaded and is ready to install. + The app will restart to complete the installation. + > + )} + + + + Install Later + + Restart & Install + + + + + ); + } + + return ( + + + + + + Update Available + + + {updateInfo?.version && ( + <> + A new version ({updateInfo.version}) is available for download. + {updateInfo.releaseNotes && ( + + {updateInfo.releaseNotes} + + )} + > + )} + + + + Later + + Download Now + + + + + ); +} \ No newline at end of file diff --git a/apps/desktop/src/main/logger.ts b/apps/desktop/src/main/logger.ts index 9c7e2b8..c45b09a 100644 --- a/apps/desktop/src/main/logger.ts +++ b/apps/desktop/src/main/logger.ts @@ -111,6 +111,7 @@ export const logger = { swift: createLoggerForScope('swift'), ui: createLoggerForScope('ui'), db: createLoggerForScope('db'), + updater: createLoggerForScope('updater'), }; // Log startup information diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index e04a520..308f45c 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -29,6 +29,7 @@ import { ContextualTranscriptionManager } from '../modules/transcription/context import { SettingsService } from '../modules/settings'; import { createIPCHandler } from 'electron-trpc-experimental/main'; import { router } from '../trpc/router'; +import { AutoUpdaterService } from './services/auto-updater'; // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { @@ -52,6 +53,7 @@ let activeSpaceChangeSubscriptionId: number | null = null; // For display change // New chunk-based transcription variables let contextualTranscriptionManager: ContextualTranscriptionManager | null = null; const activeTranscriptionSessions: Map = new Map(); +let autoUpdaterService: AutoUpdaterService | null = null; // Store is imported from '../lib/store' and is database-backed @@ -117,6 +119,9 @@ const createOrShowMainWindow = () => { } mainWindow.on('closed', () => { mainWindow = null; + if (autoUpdaterService) { + autoUpdaterService.setMainWindow(null); + } }); // Update tRPC handler to include the main window @@ -124,6 +129,11 @@ const createOrShowMainWindow = () => { router, windows: [mainWindow, floatingButtonWindow].filter(Boolean) as BrowserWindow[], }); + + // Set main window reference for auto-updater + if (autoUpdaterService) { + autoUpdaterService.setMainWindow(mainWindow); + } }; const createFloatingButtonWindow = () => { @@ -215,6 +225,19 @@ app.on('ready', async () => { // Initialize Contextual Transcription Manager contextualTranscriptionManager = new ContextualTranscriptionManager(modelManagerService); + // Initialize Auto-Updater Service + autoUpdaterService = new AutoUpdaterService(); + + // Make auto-updater service available globally for tRPC + (globalThis as any).autoUpdaterService = autoUpdaterService; + + // Check for updates on startup (after a brief delay) + setTimeout(() => { + if (autoUpdaterService) { + autoUpdaterService.checkForUpdatesAndNotify(); + } + }, 5000); // Wait 5 seconds after startup + // Initialize AI service with the appropriate client based on configuration try { const transcriptionClient = createTranscriptionClient(); @@ -456,6 +479,7 @@ app.on('ready', async () => { await swiftIOBridgeClientInstance!.call('restoreSystemAudio', {}); }); + // Initialize the SwiftIOBridgeClient swiftIOBridgeClientInstance = new SwiftIOBridge(); @@ -512,7 +536,11 @@ app.on('ready', async () => { // Handle unexpected close, maybe attempt restart }); - setupApplicationMenu(createOrShowMainWindow); + setupApplicationMenu(createOrShowMainWindow, () => { + if (autoUpdaterService) { + autoUpdaterService.checkForUpdates(true); + } + }); if (process.platform === 'darwin') { try { diff --git a/apps/desktop/src/main/menu.ts b/apps/desktop/src/main/menu.ts index d42212f..7de16de 100644 --- a/apps/desktop/src/main/menu.ts +++ b/apps/desktop/src/main/menu.ts @@ -3,7 +3,10 @@ import { app, Menu, MenuItemConstructorOptions, BrowserWindow } from 'electron'; // Forward declaration or import of the function type if it's complex // For simplicity, we assume createOrShowSettingsWindow is a () => void function -export const setupApplicationMenu = (createOrShowSettingsWindow: () => void) => { +export const setupApplicationMenu = ( + createOrShowSettingsWindow: () => void, + checkForUpdates?: () => void +) => { const menuTemplate: MenuItemConstructorOptions[] = [ // { role: 'appMenu' } for macOS ...(process.platform === 'darwin' @@ -13,6 +16,10 @@ export const setupApplicationMenu = (createOrShowSettingsWindow: () => void) => submenu: [ { role: 'about' as const }, { type: 'separator' as const }, + ...(checkForUpdates ? [{ + label: 'Check for Updates...', + click: () => checkForUpdates(), + } as MenuItemConstructorOptions, { type: 'separator' as const }] : []), { label: 'Settings', accelerator: 'CmdOrCtrl+,', @@ -109,6 +116,10 @@ export const setupApplicationMenu = (createOrShowSettingsWindow: () => void) => { role: 'help' as const, submenu: [ + ...(checkForUpdates ? [{ + label: 'Check for Updates...', + click: () => checkForUpdates(), + } as MenuItemConstructorOptions, { type: 'separator' as const }] : []), { label: 'Learn More', click: async () => { diff --git a/apps/desktop/src/main/preload.ts b/apps/desktop/src/main/preload.ts index b0d6901..e956943 100644 --- a/apps/desktop/src/main/preload.ts +++ b/apps/desktop/src/main/preload.ts @@ -97,6 +97,7 @@ const api: ElectronAPI = { searchTranscriptions: (searchTerm: string, limit?: number) => ipcRenderer.invoke('search-transcriptions', searchTerm, limit), + // Vocabulary Database API on: (channel: string, callback: (...args: any[]) => void) => { const handler = (_event: IpcRendererEvent, ...args: any[]) => callback(...args); diff --git a/apps/desktop/src/main/services/auto-updater.ts b/apps/desktop/src/main/services/auto-updater.ts new file mode 100644 index 0000000..8cd7697 --- /dev/null +++ b/apps/desktop/src/main/services/auto-updater.ts @@ -0,0 +1,222 @@ +import { autoUpdater } from 'electron-updater'; +import { app, dialog, BrowserWindow } from 'electron'; +import { EventEmitter } from 'events'; +import { logger } from '../logger'; + +export class AutoUpdaterService extends EventEmitter { + private checkingForUpdate = false; + private updateAvailable = false; + private mainWindow: BrowserWindow | null = null; + + constructor() { + 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'); + } + } + + setMainWindow(window: BrowserWindow | null) { + this.mainWindow = window; + } + + 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 + if (this.mainWindow && !this.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) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) { + return; + } + + const result = await dialog.showMessageBox(this.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) { + if (!this.mainWindow || this.mainWindow.isDestroyed()) { + return; + } + + const result = await dialog.showMessageBox(this.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'); + } + } + + 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 && this.mainWindow && !this.mainWindow.isDestroyed()) { + dialog.showMessageBox(this.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 && this.mainWindow && !this.mainWindow.isDestroyed()) { + dialog.showErrorBox('Update Check Failed', 'Failed to check for updates. Please try again later.'); + } + } + } + + 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) + }); + } + } + + isCheckingForUpdate(): boolean { + return this.checkingForUpdate; + } + + isUpdateAvailable(): boolean { + return this.updateAvailable; + } + + 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; + } + } + + 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(); + } +} \ No newline at end of file diff --git a/apps/desktop/src/trpc/router.ts b/apps/desktop/src/trpc/router.ts index c74ebc2..b60319e 100644 --- a/apps/desktop/src/trpc/router.ts +++ b/apps/desktop/src/trpc/router.ts @@ -5,6 +5,7 @@ import { vocabularyRouter } from './routers/vocabulary'; import { transcriptionsRouter } from './routers/transcriptions'; import { modelsRouter } from './routers/models'; import { settingsRouter } from './routers/settings'; +import { updaterRouter } from './routers/updater'; const t = initTRPC.create({ isServer: true, @@ -47,6 +48,9 @@ export const router = t.router({ // Settings router settings: settingsRouter, + + // Auto-updater router + updater: updaterRouter, }); export type AppRouter = typeof router; diff --git a/apps/desktop/src/trpc/routers/updater.ts b/apps/desktop/src/trpc/routers/updater.ts new file mode 100644 index 0000000..eed841d --- /dev/null +++ b/apps/desktop/src/trpc/routers/updater.ts @@ -0,0 +1,165 @@ +import { initTRPC } from '@trpc/server'; +import superjson from 'superjson'; +import { z } from 'zod'; + +// Download progress type from electron-updater +interface DownloadProgress { + bytesPerSecond: number; + percent: number; + transferred: number; + total: number; +} + +const t = initTRPC.create({ + isServer: true, + transformer: superjson, +}); + +// We'll need to access the auto-updater service from the main process +declare global { + var autoUpdaterService: any; + var logger: any; +} + +export const updaterRouter = t.router({ + // Check for updates (manual trigger) + checkForUpdates: t.procedure + .input(z.object({ userInitiated: z.boolean().optional().default(false) }).optional()) + .mutation(async ({ input }) => { + try { + if (!globalThis.autoUpdaterService) { + throw new Error('Auto-updater service not available'); + } + + const userInitiated = input?.userInitiated ?? false; + await globalThis.autoUpdaterService.checkForUpdates(userInitiated); + globalThis.logger?.updater.info('Update check initiated via tRPC', { userInitiated }); + + return { success: true }; + } catch (error) { + globalThis.logger?.updater.error('Error checking for updates via tRPC', { + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + }), + + // Check for updates and notify (background check) + checkForUpdatesAndNotify: t.procedure.mutation(async () => { + try { + if (!globalThis.autoUpdaterService) { + throw new Error('Auto-updater service not available'); + } + + await globalThis.autoUpdaterService.checkForUpdatesAndNotify(); + globalThis.logger?.updater.info('Background update check initiated via tRPC'); + + return { success: true }; + } catch (error) { + globalThis.logger?.updater.error('Error in background update check via tRPC', { + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + }), + + // Download available update + downloadUpdate: t.procedure.mutation(async () => { + try { + if (!globalThis.autoUpdaterService) { + throw new Error('Auto-updater service not available'); + } + + await globalThis.autoUpdaterService.downloadUpdate(); + globalThis.logger?.updater.info('Update download initiated via tRPC'); + + return { success: true }; + } catch (error) { + globalThis.logger?.updater.error('Error downloading update via tRPC', { + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + }), + + // Quit and install update + quitAndInstall: t.procedure.mutation(async () => { + try { + if (!globalThis.autoUpdaterService) { + throw new Error('Auto-updater service not available'); + } + + globalThis.logger?.updater.info('Quit and install initiated via tRPC'); + globalThis.autoUpdaterService.quitAndInstall(); + + return { success: true }; + } catch (error) { + globalThis.logger?.updater.error('Error quitting and installing via tRPC', { + error: error instanceof Error ? error.message : String(error) + }); + throw error; + } + }), + + // Get current update checking status + isCheckingForUpdate: t.procedure.query(async () => { + try { + if (!globalThis.autoUpdaterService) { + return false; + } + + return globalThis.autoUpdaterService.isCheckingForUpdate(); + } catch (error) { + globalThis.logger?.updater.error('Error getting update checking status via tRPC', { + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + }), + + // Get current update available status + isUpdateAvailable: t.procedure.query(async () => { + try { + if (!globalThis.autoUpdaterService) { + return false; + } + + return globalThis.autoUpdaterService.isUpdateAvailable(); + } catch (error) { + globalThis.logger?.updater.error('Error getting update available status via tRPC', { + error: error instanceof Error ? error.message : String(error) + }); + return false; + } + }), + + // Subscribe to download progress updates + onDownloadProgress: t.procedure.subscription(async function* () { + if (!globalThis.autoUpdaterService) { + throw new Error('Auto-updater service not initialized'); + } + + const eventQueue: Array = []; + + const handleDownloadProgress = (progressObj: DownloadProgress) => { + eventQueue.push(progressObj); + }; + + globalThis.autoUpdaterService.on('download-progress', handleDownloadProgress); + + try { + while (true) { + await new Promise(resolve => setTimeout(resolve, 100)); + + while (eventQueue.length > 0) { + const progress = eventQueue.shift(); + if (progress) { + yield progress; + } + } + } + } finally { + globalThis.autoUpdaterService?.off('download-progress', handleDownloadProgress); + } + }), +}); \ No newline at end of file diff --git a/apps/desktop/src/types/electron-api.ts b/apps/desktop/src/types/electron-api.ts index 01c221f..59ca57a 100644 --- a/apps/desktop/src/types/electron-api.ts +++ b/apps/desktop/src/types/electron-api.ts @@ -63,6 +63,7 @@ export interface ElectronAPI { limit?: number ) => Promise; + on: (channel: string, callback: (...args: any[]) => void) => void; off: (channel: string, callback: (...args: any[]) => void) => void; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 604b3e3..16416df 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,6 +188,9 @@ 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) @@ -285,6 +288,9 @@ importers: '@electron-forge/plugin-vite': specifier: ^7.8.1 version: 7.8.1 + '@electron-forge/publisher-github': + specifier: ^7.8.1 + version: 7.8.1(encoding@0.1.13) '@electron/fuses': specifier: ^1.8.0 version: 1.8.0 @@ -903,6 +909,10 @@ packages: resolution: {integrity: sha512-z2C+C4pcFxyCXIFwXGDcxhU8qtVUPZa3sPL6tH5RuMxJi77768chLw2quDWk2/dfupcSELXcOMYCs7aLysCzeQ==} engines: {node: '>= 16.4.0'} + '@electron-forge/publisher-github@7.8.1': + resolution: {integrity: sha512-hjRSJ3/JwKHgUNuvJo4vTPBJQQyvF72QOudHr+WSXMSAPasTwfDhvGaTS54mQqcKlOQ53cwHQjWO0xVEwQYQ0g==} + engines: {node: '>= 16.4.0'} + '@electron-forge/shared-types@7.8.1': resolution: {integrity: sha512-guLyGjIISKQQRWHX+ugmcjIOjn2q/BEzCo3ioJXFowxiFwmZw/oCZ2KlPig/t6dMqgUrHTH5W/F0WKu0EY4M+Q==} engines: {node: '>= 16.4.0'} @@ -1946,6 +1956,61 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This functionality has been moved to @npmcli/fs + '@octokit/auth-token@2.5.0': + resolution: {integrity: sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==} + + '@octokit/core@3.6.0': + resolution: {integrity: sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==} + + '@octokit/endpoint@6.0.12': + resolution: {integrity: sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==} + + '@octokit/graphql@4.8.0': + resolution: {integrity: sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==} + + '@octokit/openapi-types@12.11.0': + resolution: {integrity: sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==} + + '@octokit/openapi-types@24.2.0': + resolution: {integrity: sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==} + + '@octokit/plugin-paginate-rest@2.21.3': + resolution: {integrity: sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==} + peerDependencies: + '@octokit/core': '>=2' + + '@octokit/plugin-request-log@1.0.4': + resolution: {integrity: sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==} + peerDependencies: + '@octokit/core': '>=3' + + '@octokit/plugin-rest-endpoint-methods@5.16.2': + resolution: {integrity: sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==} + peerDependencies: + '@octokit/core': '>=3' + + '@octokit/plugin-retry@3.0.9': + resolution: {integrity: sha512-r+fArdP5+TG6l1Rv/C9hVoty6tldw6cE2pRHNGmFPdyfrc696R6JjrQ3d7HdVqGwuzfyrcaLAKD7K8TX8aehUQ==} + + '@octokit/request-error@2.1.0': + resolution: {integrity: sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==} + + '@octokit/request-error@5.1.1': + resolution: {integrity: sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==} + engines: {node: '>= 18'} + + '@octokit/request@5.6.3': + resolution: {integrity: sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==} + + '@octokit/rest@18.12.0': + resolution: {integrity: sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==} + + '@octokit/types@13.10.0': + resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + + '@octokit/types@6.41.0': + resolution: {integrity: sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==} + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -3611,6 +3676,9 @@ packages: batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + before-after-hook@2.2.3: + resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} + better-sqlite3@11.10.0: resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} @@ -3634,6 +3702,9 @@ packages: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + bowser@2.11.0: resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} @@ -3665,6 +3736,10 @@ 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'} @@ -4181,6 +4256,9 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + deprecation@2.3.1: + resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -4385,6 +4463,9 @@ 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'} @@ -5513,6 +5594,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} @@ -5680,6 +5765,9 @@ 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'} @@ -5778,6 +5866,9 @@ 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. @@ -5785,6 +5876,10 @@ 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==} @@ -7106,6 +7201,9 @@ 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==} @@ -7569,6 +7667,9 @@ 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==} @@ -7841,6 +7942,9 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@6.0.1: + resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} @@ -8972,6 +9076,25 @@ snapshots: - bluebird - supports-color + '@electron-forge/publisher-github@7.8.1(encoding@0.1.13)': + dependencies: + '@electron-forge/publisher-base': 7.8.1 + '@electron-forge/shared-types': 7.8.1 + '@octokit/core': 3.6.0(encoding@0.1.13) + '@octokit/plugin-retry': 3.0.9 + '@octokit/request-error': 5.1.1 + '@octokit/rest': 18.12.0(encoding@0.1.13) + '@octokit/types': 6.41.0 + chalk: 4.1.2 + debug: 4.4.1 + fs-extra: 10.1.0 + log-symbols: 4.1.0 + mime-types: 2.1.35 + transitivePeerDependencies: + - bluebird + - encoding + - supports-color + '@electron-forge/shared-types@7.8.1': dependencies: '@electron-forge/tracer': 7.8.1 @@ -9893,6 +10016,100 @@ snapshots: mkdirp: 1.0.4 rimraf: 3.0.2 + '@octokit/auth-token@2.5.0': + dependencies: + '@octokit/types': 6.41.0 + + '@octokit/core@3.6.0(encoding@0.1.13)': + dependencies: + '@octokit/auth-token': 2.5.0 + '@octokit/graphql': 4.8.0(encoding@0.1.13) + '@octokit/request': 5.6.3(encoding@0.1.13) + '@octokit/request-error': 2.1.0 + '@octokit/types': 6.41.0 + before-after-hook: 2.2.3 + universal-user-agent: 6.0.1 + transitivePeerDependencies: + - encoding + + '@octokit/endpoint@6.0.12': + dependencies: + '@octokit/types': 6.41.0 + is-plain-object: 5.0.0 + universal-user-agent: 6.0.1 + + '@octokit/graphql@4.8.0(encoding@0.1.13)': + dependencies: + '@octokit/request': 5.6.3(encoding@0.1.13) + '@octokit/types': 6.41.0 + universal-user-agent: 6.0.1 + transitivePeerDependencies: + - encoding + + '@octokit/openapi-types@12.11.0': {} + + '@octokit/openapi-types@24.2.0': {} + + '@octokit/plugin-paginate-rest@2.21.3(@octokit/core@3.6.0(encoding@0.1.13))': + dependencies: + '@octokit/core': 3.6.0(encoding@0.1.13) + '@octokit/types': 6.41.0 + + '@octokit/plugin-request-log@1.0.4(@octokit/core@3.6.0(encoding@0.1.13))': + dependencies: + '@octokit/core': 3.6.0(encoding@0.1.13) + + '@octokit/plugin-rest-endpoint-methods@5.16.2(@octokit/core@3.6.0(encoding@0.1.13))': + dependencies: + '@octokit/core': 3.6.0(encoding@0.1.13) + '@octokit/types': 6.41.0 + deprecation: 2.3.1 + + '@octokit/plugin-retry@3.0.9': + dependencies: + '@octokit/types': 6.41.0 + bottleneck: 2.19.5 + + '@octokit/request-error@2.1.0': + dependencies: + '@octokit/types': 6.41.0 + deprecation: 2.3.1 + once: 1.4.0 + + '@octokit/request-error@5.1.1': + dependencies: + '@octokit/types': 13.10.0 + deprecation: 2.3.1 + once: 1.4.0 + + '@octokit/request@5.6.3(encoding@0.1.13)': + dependencies: + '@octokit/endpoint': 6.0.12 + '@octokit/request-error': 2.1.0 + '@octokit/types': 6.41.0 + is-plain-object: 5.0.0 + node-fetch: 2.7.0(encoding@0.1.13) + universal-user-agent: 6.0.1 + transitivePeerDependencies: + - encoding + + '@octokit/rest@18.12.0(encoding@0.1.13)': + dependencies: + '@octokit/core': 3.6.0(encoding@0.1.13) + '@octokit/plugin-paginate-rest': 2.21.3(@octokit/core@3.6.0(encoding@0.1.13)) + '@octokit/plugin-request-log': 1.0.4(@octokit/core@3.6.0(encoding@0.1.13)) + '@octokit/plugin-rest-endpoint-methods': 5.16.2(@octokit/core@3.6.0(encoding@0.1.13)) + transitivePeerDependencies: + - encoding + + '@octokit/types@13.10.0': + dependencies: + '@octokit/openapi-types': 24.2.0 + + '@octokit/types@6.41.0': + dependencies: + '@octokit/openapi-types': 12.11.0 + '@opentelemetry/api@1.9.0': {} '@orama/orama@3.1.7': {} @@ -11720,6 +11937,8 @@ snapshots: batch@0.6.1: {} + before-after-hook@2.2.3: {} + better-sqlite3@11.10.0: dependencies: bindings: 1.5.0 @@ -11761,6 +11980,8 @@ snapshots: boolean@3.2.0: optional: true + bottleneck@2.19.5: {} + bowser@2.11.0: {} bplist-creator@0.0.8: @@ -11797,6 +12018,13 @@ 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 @@ -12314,6 +12542,8 @@ snapshots: depd@2.0.0: {} + deprecation@2.3.1: {} + dequal@2.0.3: {} destroy@1.2.0: {} @@ -12475,6 +12705,19 @@ 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 @@ -14059,6 +14302,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-plain-object@5.0.0: {} + is-promise@2.2.2: {} is-property@1.0.2: @@ -14220,6 +14465,8 @@ snapshots: kind-of@6.0.3: {} + lazy-val@1.0.5: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -14318,10 +14565,14 @@ 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: {} @@ -16130,6 +16381,8 @@ snapshots: safer-buffer@2.1.2: {} + sax@1.4.1: {} + scheduler@0.26.0: {} scroll-into-view-if-needed@3.1.0: @@ -16747,6 +17000,8 @@ snapshots: tiny-invariant@1.3.3: {} + tiny-typed-emitter@2.1.0: {} + tinycolor2@1.6.0: {} tinygradient@1.1.5: @@ -17052,6 +17307,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@6.0.1: {} + universalify@0.1.2: {} universalify@2.0.1: {}
+ {downloadProgress}% complete +