feat(updater): electron-updater wiring + IPC bridge

electron-updater integration so users get fixes automatically:
  - new dependency electron-updater@^6.6.2 in package.json
  - electron-builder.config.js publishes to GitHub releases
    (owner: concreteinfo, repo: markdownconverter)
  - new src/main/auto-updater.js wraps autoUpdater: wires the six
    lifecycle events to a 'updates:status' IPC channel, exposes
    check() and install() helpers, dev-gates via isDev(), and
    swallows the 'missing app-update.yml' error that dev runs hit
  - main.js: ensureAutoUpdater() forwards status to the renderer
    via mainWindow.webContents.send; ipcMain handlers for
    'updates:check' (invoke) and 'updates:install' (invoke)
  - preload.js: 'updates:check' / 'updates:install' added to
    ALLOWED_SEND_CHANNELS; 'updates:status' added to receive
    channels; electronAPI.updates.{check, install, onStatus}
    exposed

7 new tests cover event registration, dev-gate behaviour,
'not-available' fallback for missing app-update.yml, typed event
payloads, error forwarding, and install() quitAndInstall.

Amit Haridas
This commit is contained in:
Amit Haridas
2026-09-30 20:30:19 +05:30
parent 0d3df32140
commit 47315add80
6 changed files with 240 additions and 0 deletions
+24
View File
@@ -11,6 +11,7 @@ const VideoOperations = require('./main/VideoOperations');
const { collectFilesByExtension } = require('./main/collectFilesByExtension');
const { listWorkspaceFiles } = require('./quick-switcher/workspace-file-lister');
const { completeStream } = require('./main/AiProviders');
const { setupAutoUpdater } = require('./main/auto-updater');
const { runPDFBatchOperation } = require('./main/PDFBatchOperations');
const GitOperations = require('./main/GitOperations');
const PandocArgs = require('./main/PandocArgs');
@@ -4948,6 +4949,29 @@ ipcMain.on('ai-assist-stream:cancel', (_event, { requestId } = {}) => {
if (entry) entry.abort();
});
// Auto-update wiring (v4.13.0). Surfaces status to the renderer and
// exposes check-now / install-now IPC channels. Lazy-instantiated so a
// missing app-update.yml in dev doesn't blow up startup.
let autoUpdaterCtl = null;
function ensureAutoUpdater() {
if (autoUpdaterCtl) return autoUpdaterCtl;
autoUpdaterCtl = setupAutoUpdater({
send: (channel, payload) => {
if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send(channel, payload);
}
},
isDev: () => process.env.NODE_ENV === 'development' || !!process.env.ELECTRON_DEV,
});
return autoUpdaterCtl;
}
ipcMain.handle('updates:check', async () => {
return ensureAutoUpdater().check();
});
ipcMain.handle('updates:install', () => {
ensureAutoUpdater().install();
});
// Plugins (loaded in the renderer) report the export formats they've
// registered; rebuild the Export menu so they show up as entries.
// createMenu() is idempotent and already re-invoked elsewhere (e.g. after
+75
View File
@@ -0,0 +1,75 @@
/**
* Auto-updater wiring for electron-updater.
*
* Wraps electron-updater's autoUpdater so the main process has one place
* to:
* - start a check on app launch
* - forward events to the renderer over 'updates:status' IPC
* - accept "install now" / "check now" requests from the renderer
*
* Safe to call in dev: when no app-update.yml is packaged (which is the
* case for `npm start` / electron .), electron-updater surfaces an
* "app-update.yml not found" error which we swallow and log. The renderer
* still gets a `not-available` event so the UI can show "you're up to
* date" instead of a spinning indicator that never resolves.
*
* @module auto-updater
*/
/**
* @param {object} deps
* @param {(channel:string, payload?:any) => void} deps.send - forwards
* status events to the BrowserWindow's webContents
* @param {() => boolean} [deps.isDev] - dev gate (skip check in dev)
*/
function setupAutoUpdater({ send, isDev }) {
// Lazy-load — electron-updater touches `app` at import time, which fails
// in test environments where the app isn't bootstrapped.
const { autoUpdater } = require('electron-updater');
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = true;
// Forward every event to the renderer with a typed payload.
autoUpdater.on('checking-for-update', () => send('updates:status', { state: 'checking' }));
autoUpdater.on('update-available', (info) =>
send('updates:status', { state: 'available', version: info?.version })
);
autoUpdater.on('update-not-available', (info) =>
send('updates:status', { state: 'not-available', version: info?.version })
);
autoUpdater.on('download-progress', (p) =>
send('updates:status', { state: 'downloading', percent: Math.round(p.percent || 0) })
);
autoUpdater.on('update-downloaded', (info) =>
send('updates:status', { state: 'downloaded', version: info?.version })
);
autoUpdater.on('error', (err) =>
send('updates:status', {
state: 'error',
message: err && err.message ? err.message : 'Update check failed.',
})
);
function check() {
if (typeof isDev === 'function' && isDev()) return Promise.resolve(null);
return autoUpdater.checkForUpdates().catch((err) => {
// Dev runs surface "app-update.yml not found" — treat that as benign.
const msg = err && err.message ? err.message : String(err);
if (msg.includes('app-update.yml') || msg.includes('Cannot find')) {
send('updates:status', { state: 'not-available' });
return null;
}
send('updates:status', { state: 'error', message: msg });
return null;
});
}
function install() {
autoUpdater.quitAndInstall();
}
return { check, install };
}
module.exports = { setupAutoUpdater };
+16
View File
@@ -48,6 +48,10 @@ const ALLOWED_SEND_CHANNELS = [
'ai-assist-stream:start',
'ai-assist-stream:cancel',
// v4.13.0 — auto-update (electron-updater)
'updates:check',
'updates:install',
// Batch conversion
'batch-convert',
'select-folder',
@@ -338,6 +342,9 @@ const ALLOWED_RECEIVE_CHANNELS = [
// v4.13.0 — quick-switcher overlay trigger
'show-quick-switcher',
// v4.13.0 — auto-update status events from main
'updates:status',
// Plugin export formats
'run-plugin-export-format',
];
@@ -581,6 +588,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
onError: (cb) => ipcRenderer.on('ai-assist-stream:error', (_e, p) => cb(p)),
},
// v4.13.0 — auto-update controls. Check-now asks main to poll
// GitHub releases; install-now triggers quitAndInstall. Status events
// arrive via the generic on('updates:status', cb).
updates: {
check: () => ipcRenderer.invoke('updates:check'),
install: () => ipcRenderer.invoke('updates:install'),
onStatus: (cb) => ipcRenderer.on('updates:status', (_e, p) => cb(p)),
},
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
});