Skip to content

Commit aecabbe

Browse files
authored
chore: git publish + auto update handling (#31)
* chore: git publish + auto update handling
1 parent d05bf80 commit aecabbe

12 files changed

Lines changed: 957 additions & 3 deletions

File tree

apps/desktop/forge.config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MakerRpm } from '@electron-forge/maker-rpm';
66
import { VitePlugin } from '@electron-forge/plugin-vite';
77
import { FusesPlugin } from '@electron-forge/plugin-fuses';
88
import { FuseV1Options, FuseVersion } from '@electron/fuses';
9+
import { PublisherGithub } from '@electron-forge/publisher-github';
910
import { readdirSync, rmdirSync, statSync, existsSync, mkdirSync, cpSync } from 'node:fs';
1011
import { join, normalize } from 'node:path';
1112
// Use flora-colossus for finding all dependencies of EXTERNAL_DEPENDENCIES
@@ -192,6 +193,19 @@ const config: ForgeConfig = {
192193
NSMicrophoneUsageDescription:
193194
'This app needs access to your microphone to record audio for transcription.',
194195
},
196+
// Code signing configuration for macOS (configure when ready to sign)
197+
// osxSign: {
198+
// identity: process.env.APPLE_SIGNING_IDENTITY,
199+
// 'hardened-runtime': true,
200+
// entitlements: './entitlements.plist',
201+
// 'entitlements-inherit': './entitlements.plist',
202+
// },
203+
// Notarization for macOS (configure when ready for distribution)
204+
// osxNotarize: {
205+
// appleId: process.env.APPLE_ID,
206+
// appleIdPassword: process.env.APPLE_APP_PASSWORD,
207+
// teamId: process.env.APPLE_TEAM_ID,
208+
// },
195209
//! issues with monorepo setup and module resolutions
196210
//! when forge walks paths via flora-colossus
197211
prune: false,
@@ -305,6 +319,16 @@ const config: ForgeConfig = {
305319
[FuseV1Options.OnlyLoadAppFromAsar]: true,
306320
}),
307321
],
322+
publishers: [
323+
new PublisherGithub({
324+
repository: {
325+
owner: 'amicalhq',
326+
name: 'amical',
327+
},
328+
prerelease: true,
329+
draft: true, // Create draft releases first for review
330+
}),
331+
],
308332
};
309333

310334
export default config;

apps/desktop/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@amical/desktop",
3-
"version": "1.0.0",
3+
"version": "0.0.1",
44
"description": "Amical Desktop app",
55
"main": ".vite/build/main.js",
66
"productName": "Amical",
@@ -31,6 +31,7 @@
3131
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
3232
"@electron-forge/plugin-fuses": "^7.8.1",
3333
"@electron-forge/plugin-vite": "^7.8.1",
34+
"@electron-forge/publisher-github": "^7.8.1",
3435
"@electron/fuses": "^1.8.0",
3536
"@rollup/plugin-commonjs": "^28.0.6",
3637
"@tailwindcss/vite": "^4.1.6",
@@ -105,6 +106,7 @@
105106
"electron-log": "^5.4.0",
106107
"electron-squirrel-startup": "^1.0.1",
107108
"electron-trpc-experimental": "1.0.0-alpha.1",
109+
"electron-updater": "^6.6.2",
108110
"embla-carousel-react": "^8.6.0",
109111
"framer-motion": "^12.10.5",
110112
"input-otp": "^1.4.2",
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import React, { useState, useEffect } from 'react';
2+
import {
3+
AlertDialog,
4+
AlertDialogAction,
5+
AlertDialogCancel,
6+
AlertDialogContent,
7+
AlertDialogDescription,
8+
AlertDialogFooter,
9+
AlertDialogHeader,
10+
AlertDialogTitle,
11+
} from './ui/alert-dialog';
12+
import { Progress } from './ui/progress';
13+
import { Button } from './ui/button';
14+
import { Download, RefreshCw, CheckCircle } from 'lucide-react';
15+
import { api } from '@/trpc/react';
16+
import { toast } from 'sonner';
17+
18+
interface UpdateDialogProps {
19+
isOpen: boolean;
20+
onClose: () => void;
21+
updateInfo?: {
22+
version: string;
23+
releaseNotes?: string;
24+
};
25+
}
26+
27+
export function UpdateDialog({ isOpen, onClose, updateInfo }: UpdateDialogProps) {
28+
const [isDownloading, setIsDownloading] = useState(false);
29+
const [downloadProgress, setDownloadProgress] = useState(0);
30+
31+
// tRPC queries for update status
32+
const isCheckingQuery = api.updater.isCheckingForUpdate.useQuery(undefined, {
33+
enabled: isOpen,
34+
refetchInterval: isOpen ? 1000 : false, // Poll every second when dialog is open
35+
});
36+
const isUpdateAvailableQuery = api.updater.isUpdateAvailable.useQuery(undefined, {
37+
enabled: isOpen,
38+
refetchInterval: isOpen ? 1000 : false,
39+
});
40+
41+
const utils = api.useUtils();
42+
43+
// tRPC mutations
44+
const checkForUpdatesMutation = api.updater.checkForUpdates.useMutation({
45+
onSuccess: () => {
46+
toast.success('Update check completed');
47+
utils.updater.isUpdateAvailable.invalidate();
48+
utils.updater.isCheckingForUpdate.invalidate();
49+
},
50+
onError: (error) => {
51+
console.error('Error checking for updates:', error);
52+
toast.error('Failed to check for updates');
53+
}
54+
});
55+
56+
const downloadUpdateMutation = api.updater.downloadUpdate.useMutation({
57+
onSuccess: () => {
58+
toast.success('Update download started');
59+
},
60+
onError: (error) => {
61+
console.error('Error downloading update:', error);
62+
toast.error('Failed to download update');
63+
setIsDownloading(false);
64+
}
65+
});
66+
67+
const quitAndInstallMutation = api.updater.quitAndInstall.useMutation({
68+
onError: (error) => {
69+
console.error('Error installing update:', error);
70+
toast.error('Failed to install update');
71+
}
72+
});
73+
74+
// Get status from queries
75+
const isCheckingForUpdates = isCheckingQuery.data || false;
76+
const updateAvailable = isUpdateAvailableQuery.data || false;
77+
78+
// Subscribe to download progress via tRPC
79+
api.updater.onDownloadProgress.useSubscription(undefined, {
80+
enabled: isOpen && isDownloading,
81+
onData: (progress) => {
82+
setDownloadProgress(Math.round(progress.percent || 0));
83+
},
84+
onError: (error) => {
85+
console.error('Download progress subscription error:', error);
86+
}
87+
});
88+
89+
const handleCheckForUpdates = async () => {
90+
checkForUpdatesMutation.mutate({ userInitiated: true });
91+
};
92+
93+
const handleDownloadUpdate = async () => {
94+
setIsDownloading(true);
95+
setDownloadProgress(0);
96+
downloadUpdateMutation.mutate();
97+
};
98+
99+
const handleInstallUpdate = async () => {
100+
quitAndInstallMutation.mutate();
101+
};
102+
103+
if (!updateAvailable && !isCheckingForUpdates && !isDownloading) {
104+
return (
105+
<AlertDialog open={isOpen} onOpenChange={onClose}>
106+
<AlertDialogContent>
107+
<AlertDialogHeader>
108+
<AlertDialogTitle className="flex items-center gap-2">
109+
<RefreshCw className="h-5 w-5" />
110+
Check for Updates
111+
</AlertDialogTitle>
112+
<AlertDialogDescription>
113+
Click below to check for the latest version of Amical.
114+
</AlertDialogDescription>
115+
</AlertDialogHeader>
116+
<AlertDialogFooter>
117+
<AlertDialogCancel onClick={onClose}>Cancel</AlertDialogCancel>
118+
<AlertDialogAction onClick={handleCheckForUpdates}>
119+
Check for Updates
120+
</AlertDialogAction>
121+
</AlertDialogFooter>
122+
</AlertDialogContent>
123+
</AlertDialog>
124+
);
125+
}
126+
127+
if (isCheckingForUpdates) {
128+
return (
129+
<AlertDialog open={isOpen} onOpenChange={onClose}>
130+
<AlertDialogContent>
131+
<AlertDialogHeader>
132+
<AlertDialogTitle className="flex items-center gap-2">
133+
<RefreshCw className="h-5 w-5 animate-spin" />
134+
Checking for Updates...
135+
</AlertDialogTitle>
136+
<AlertDialogDescription>
137+
Please wait while we check for the latest version.
138+
</AlertDialogDescription>
139+
</AlertDialogHeader>
140+
<AlertDialogFooter>
141+
<AlertDialogCancel onClick={onClose}>Cancel</AlertDialogCancel>
142+
</AlertDialogFooter>
143+
</AlertDialogContent>
144+
</AlertDialog>
145+
);
146+
}
147+
148+
if (isDownloading) {
149+
return (
150+
<AlertDialog open={isOpen} onOpenChange={() => {}}>
151+
<AlertDialogContent>
152+
<AlertDialogHeader>
153+
<AlertDialogTitle className="flex items-center gap-2">
154+
<Download className="h-5 w-5" />
155+
Downloading Update...
156+
</AlertDialogTitle>
157+
<AlertDialogDescription>
158+
{updateInfo?.version && (
159+
<>Downloading version {updateInfo.version}. Please wait...</>
160+
)}
161+
</AlertDialogDescription>
162+
</AlertDialogHeader>
163+
<div className="py-4">
164+
<Progress value={downloadProgress} className="w-full" />
165+
<p className="text-sm text-muted-foreground mt-2 text-center">
166+
{downloadProgress}% complete
167+
</p>
168+
</div>
169+
<AlertDialogFooter>
170+
<Button variant="outline" disabled>
171+
Downloading...
172+
</Button>
173+
</AlertDialogFooter>
174+
</AlertDialogContent>
175+
</AlertDialog>
176+
);
177+
}
178+
179+
if (downloadProgress === 100 && !isDownloading) {
180+
return (
181+
<AlertDialog open={isOpen} onOpenChange={() => {}}>
182+
<AlertDialogContent>
183+
<AlertDialogHeader>
184+
<AlertDialogTitle className="flex items-center gap-2">
185+
<CheckCircle className="h-5 w-5 text-green-500" />
186+
Update Ready
187+
</AlertDialogTitle>
188+
<AlertDialogDescription>
189+
{updateInfo?.version && (
190+
<>
191+
Version {updateInfo.version} has been downloaded and is ready to install.
192+
The app will restart to complete the installation.
193+
</>
194+
)}
195+
</AlertDialogDescription>
196+
</AlertDialogHeader>
197+
<AlertDialogFooter>
198+
<AlertDialogCancel onClick={onClose}>Install Later</AlertDialogCancel>
199+
<AlertDialogAction onClick={handleInstallUpdate}>
200+
Restart & Install
201+
</AlertDialogAction>
202+
</AlertDialogFooter>
203+
</AlertDialogContent>
204+
</AlertDialog>
205+
);
206+
}
207+
208+
return (
209+
<AlertDialog open={isOpen} onOpenChange={onClose}>
210+
<AlertDialogContent>
211+
<AlertDialogHeader>
212+
<AlertDialogTitle className="flex items-center gap-2">
213+
<Download className="h-5 w-5" />
214+
Update Available
215+
</AlertDialogTitle>
216+
<AlertDialogDescription>
217+
{updateInfo?.version && (
218+
<>
219+
A new version ({updateInfo.version}) is available for download.
220+
{updateInfo.releaseNotes && (
221+
<div className="mt-2 p-2 bg-muted rounded text-sm">
222+
{updateInfo.releaseNotes}
223+
</div>
224+
)}
225+
</>
226+
)}
227+
</AlertDialogDescription>
228+
</AlertDialogHeader>
229+
<AlertDialogFooter>
230+
<AlertDialogCancel onClick={onClose}>Later</AlertDialogCancel>
231+
<AlertDialogAction onClick={handleDownloadUpdate}>
232+
Download Now
233+
</AlertDialogAction>
234+
</AlertDialogFooter>
235+
</AlertDialogContent>
236+
</AlertDialog>
237+
);
238+
}

apps/desktop/src/main/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const logger = {
111111
swift: createLoggerForScope('swift'),
112112
ui: createLoggerForScope('ui'),
113113
db: createLoggerForScope('db'),
114+
updater: createLoggerForScope('updater'),
114115
};
115116

116117
// Log startup information

apps/desktop/src/main/main.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { ContextualTranscriptionManager } from '../modules/transcription/context
2929
import { SettingsService } from '../modules/settings';
3030
import { createIPCHandler } from 'electron-trpc-experimental/main';
3131
import { router } from '../trpc/router';
32+
import { AutoUpdaterService } from './services/auto-updater';
3233

3334
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
3435
if (started) {
@@ -52,6 +53,7 @@ let activeSpaceChangeSubscriptionId: number | null = null; // For display change
5253
// New chunk-based transcription variables
5354
let contextualTranscriptionManager: ContextualTranscriptionManager | null = null;
5455
const activeTranscriptionSessions: Map<string, TranscriptionSession> = new Map();
56+
let autoUpdaterService: AutoUpdaterService | null = null;
5557

5658
// Store is imported from '../lib/store' and is database-backed
5759

@@ -117,13 +119,21 @@ const createOrShowMainWindow = () => {
117119
}
118120
mainWindow.on('closed', () => {
119121
mainWindow = null;
122+
if (autoUpdaterService) {
123+
autoUpdaterService.setMainWindow(null);
124+
}
120125
});
121126

122127
// Update tRPC handler to include the main window
123128
createIPCHandler({
124129
router,
125130
windows: [mainWindow, floatingButtonWindow].filter(Boolean) as BrowserWindow[],
126131
});
132+
133+
// Set main window reference for auto-updater
134+
if (autoUpdaterService) {
135+
autoUpdaterService.setMainWindow(mainWindow);
136+
}
127137
};
128138

129139
const createFloatingButtonWindow = () => {
@@ -215,6 +225,19 @@ app.on('ready', async () => {
215225
// Initialize Contextual Transcription Manager
216226
contextualTranscriptionManager = new ContextualTranscriptionManager(modelManagerService);
217227

228+
// Initialize Auto-Updater Service
229+
autoUpdaterService = new AutoUpdaterService();
230+
231+
// Make auto-updater service available globally for tRPC
232+
(globalThis as any).autoUpdaterService = autoUpdaterService;
233+
234+
// Check for updates on startup (after a brief delay)
235+
setTimeout(() => {
236+
if (autoUpdaterService) {
237+
autoUpdaterService.checkForUpdatesAndNotify();
238+
}
239+
}, 5000); // Wait 5 seconds after startup
240+
218241
// Initialize AI service with the appropriate client based on configuration
219242
try {
220243
const transcriptionClient = createTranscriptionClient();
@@ -456,6 +479,7 @@ app.on('ready', async () => {
456479
await swiftIOBridgeClientInstance!.call('restoreSystemAudio', {});
457480
});
458481

482+
459483
// Initialize the SwiftIOBridgeClient
460484
swiftIOBridgeClientInstance = new SwiftIOBridge();
461485

@@ -512,7 +536,11 @@ app.on('ready', async () => {
512536
// Handle unexpected close, maybe attempt restart
513537
});
514538

515-
setupApplicationMenu(createOrShowMainWindow);
539+
setupApplicationMenu(createOrShowMainWindow, () => {
540+
if (autoUpdaterService) {
541+
autoUpdaterService.checkForUpdates(true);
542+
}
543+
});
516544

517545
if (process.platform === 'darwin') {
518546
try {

0 commit comments

Comments
 (0)