/** * Pear OTA for peardock-client (Electron). * Uses pear-runtime-updater + Corestore + Hyperswarm directly — NOT pear-runtime — * so we avoid packaging bare-sidecar (~370MB) and stay fast to asar. */ 'use strict' const path = require('path') const fs = require('fs') const { app, BrowserWindow } = require('electron') /** * Load shared OTA config. Prefer lib/ota-config.cjs (packaged in asar); * fall back to inline helpers so a missing file never bricks the client. */ function loadOtaConfig() { const candidates = [ path.join(__dirname, '..', 'lib', 'ota-config.cjs'), path.join(__dirname, 'ota-config.cjs'), ] for (const file of candidates) { try { if (fs.existsSync(file)) return require(file) } catch { // try next } } // Inline fallback (mirrors lib/ota-config.cjs client path) return { artifactName(kind) { if (kind === 'server') { return process.platform === 'win32' ? 'peardock-server.exe' : 'peardock-server' } if (process.platform === 'darwin') return 'peardock.app' if (process.platform === 'win32') return 'peardock-client.exe' return 'peardock-client' }, buildUpdaterOptions(kind, opts = {}) { const pkg = opts.pkg || {} const upgrade = String( process.env.PEARDOCK_OTA_UPGRADE || process.env.PEARDOCK_OTA_CLIENT || pkg.upgradeClient || pkg.upgrade || '' ).trim() const valid = Boolean(upgrade && /^pear:\/\//i.test(upgrade)) const disabled = process.env.PEARDOCK_OTA_DISABLE === '1' || process.env.PEARDOCK_OTA_DISABLE === 'true' return { enabled: !disabled && valid, upgrade: upgrade || null, dir: opts.dir, version: String(opts.version || pkg.version || '0.0.0'), name: opts.name || this.artifactName(kind), app: opts.app != null ? opts.app : null, kind, } }, } } const otaConfig = loadOtaConfig() /** * @returns {Promise<{ enabled: boolean, upgrade: string|null, close: () => Promise }>} */ async function startClientOta(opts = {}) { const pkg = require('../package.json') const cfg = otaConfig.buildUpdaterOptions('client', { pkg, dir: path.join(app.getPath('userData'), 'pear-runtime-data'), app: getClientAppPath(), name: otaConfig.artifactName('client'), }) const broadcast = opts.broadcast || ((channel, data) => { for (const win of BrowserWindow.getAllWindows()) { if (!win.isDestroyed()) win.webContents.send(channel, data) } }) if (!cfg.enabled) { console.log('[peardock-ota] disabled or no upgrade link') return { enabled: false, upgrade: null, close: async () => {} } } if (!app.isPackaged && !process.env.PEARDOCK_OTA_FORCE) { console.log('[peardock-ota] skipped in unpackaged dev (PEARDOCK_OTA_FORCE=1 to test)') return { enabled: false, upgrade: cfg.upgrade, close: async () => {} } } fs.mkdirSync(cfg.dir, { recursive: true }) let PearRuntimeUpdater let Corestore let Hyperswarm try { PearRuntimeUpdater = require('pear-runtime-updater') Corestore = require('corestore') Hyperswarm = require('hyperswarm') } catch (err) { console.warn('[peardock-ota] deps missing:', err.message) return { enabled: false, upgrade: cfg.upgrade, close: async () => {} } } const store = new Corestore(path.join(cfg.dir, 'corestore')) await store.ready() const updater = new PearRuntimeUpdater({ dir: cfg.dir, version: cfg.version, upgrade: cfg.upgrade, name: cfg.name, app: cfg.app, updates: true, store, }) await updater.ready() const keyPair = await store.createKeyPair('peardock-client-ota') const swarm = new Hyperswarm({ keyPair }) swarm.on('connection', (c) => store.replicate(c)) swarm.join(updater.drive.core.discoveryKey, { client: true, server: false }) console.log( `[peardock-ota] listening version=${cfg.version} name=${cfg.name} app=${cfg.app}` ) updater.on('error', (err) => { console.warn('[peardock-ota] error', err?.message || err) broadcast('peardock:ota', { type: 'error', message: String(err?.message || err) }) }) updater.on('update-scheduled', (delay) => { broadcast('peardock:ota', { type: 'scheduled', delayMs: delay }) }) updater.on('updating', () => { console.log('[peardock-ota] downloading update…') broadcast('peardock:ota', { type: 'updating' }) }) updater.on('updated', async () => { console.log('[peardock-ota] update ready, applying…', updater.nextVersion) broadcast('peardock:ota', { type: 'updated', nextVersion: updater.nextVersion }) try { await updater.applyUpdate() broadcast('peardock:ota', { type: 'applied' }) if (process.platform === 'linux' && process.env.APPIMAGE) { app.relaunch({ execPath: process.env.APPIMAGE, args: [ '--appimage-extract-and-run', ...process.argv.slice(1).filter((a) => a !== '--appimage-extract-and-run'), ], }) } else { app.relaunch() } app.exit(0) } catch (err) { console.error('[peardock-ota] apply failed', err) broadcast('peardock:ota', { type: 'error', message: String(err.message || err) }) } }) return { enabled: true, upgrade: cfg.upgrade, close: async () => { try { await swarm.destroy() } catch { // ignore } try { await updater.close() } catch { // ignore } try { await store.close() } catch { // ignore } }, } } function getClientAppPath() { if (process.env.PEARDOCK_OTA_APP) return process.env.PEARDOCK_OTA_APP if (process.platform === 'linux' && process.env.APPIMAGE) return process.env.APPIMAGE if (process.platform === 'win32') return process.execPath if (process.platform === 'darwin' && app.isPackaged) { return path.join(process.resourcesPath, '..', '..') } return process.execPath } module.exports = { startClientOta, getClientAppPath }