codeburn/app/electron/preload.ts
Resham Joshi fce75d5420
feat(app,windows): one-click updates for the desktop app and the Windows tray (#1685)
* feat(desktop): one-click updates through electron-updater on the update-feeds release

* feat(tray): signed one-click updates through tauri-plugin-updater; feed publishing and release docs

* ci(tray): build without the updater key until it is configured

* chore(windows): set the tray updater public key

* ci(windows): skip updater signing in the release build smoke

* ci(windows): build without updater artifacts when no signing key is available

* ci(windows): turn off updater artifacts through TAURI_CONFIG, not CLI flags pwsh mangles

* ci(windows): run tauri build under bash so --no-sign reaches the CLI (pwsh drops the -- separator)
2026-10-08 05:06:55 +02:00

146 lines
11 KiB
TypeScript

import { contextBridge, ipcRenderer } from 'electron'
// Handlers resolve with { ok, value } | { ok, error } so the structured error
// `kind` survives the contextBridge boundary. `import type` is erased at build,
// so this shares main.ts's declaration without pulling its runtime in.
import type { Envelope } from './main'
type DateRange = { from: string; to: string }
type PriceRates = { input?: number; output?: number; cacheRead?: number; cacheCreation?: number }
async function invoke<T>(channel: string, ...args: unknown[]): Promise<T> {
const res = (await ipcRenderer.invoke(channel, ...args)) as Envelope<T>
if (res.ok) return res.value
// Reject with a plain object so `kind` is preserved (Error subclasses lose
// custom fields when cloned across worlds).
return Promise.reject(res.error)
}
// Shape matches CodeburnBridge (app/renderer/lib/types.ts); typing is enforced
// renderer-side where `window.codeburn` is declared as CodeburnBridge.
const bridge = {
// The Electron app's own UI language tag (app.getLocale()), for the renderer's
// 'system' locale choice. Seeded into the environment by main before the
// window loads, so it is sync-safe at preload time.
appLocale: process.env.__CODEBURN_APP_LOCALE__ ?? '',
getLanguage: () => invoke('codeburn:getLanguage'),
setLanguage: (language: string | null) => invoke('codeburn:setLanguage', language),
getCursorSync: () => invoke('codeburn:getCursorSync'),
setCursorSync: (enabled: boolean) => invoke('codeburn:setCursorSync', enabled),
getQuota: (force?: boolean, disabled?: string[]) => invoke('codeburn:getQuota', force, disabled),
getOverview: (period: string, provider: string, range?: DateRange, configSource?: string | null, background?: boolean, scope?: string) => invoke('codeburn:getOverview', period, provider, range, configSource, background, scope),
getTimeline: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getTimeline', period, provider, range),
getPlans: (period: string, background?: boolean) => invoke('codeburn:getPlans', period, background),
getActReport: () => invoke('codeburn:getActReport'),
getModels: (period: string, provider: string, byTask: boolean, range?: DateRange, background?: boolean) => invoke('codeburn:getModels', period, provider, byTask, range, background),
getSessions: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getSessions', period, provider, range, background),
getSessionsContributions: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getSessionsContributions', period, provider, range, background),
getSessionWhy: (id: string) => invoke('codeburn:getSessionWhy', id),
getCompareModels: (period: string, provider: string, background?: boolean) => invoke('codeburn:getCompareModels', period, provider, background),
getCompare: (period: string, provider: string, modelA: string, modelB: string) => invoke('codeburn:getCompare', period, provider, modelA, modelB),
getPeriodCompare: (rangeA: DateRange, rangeB: DateRange, provider: string, background?: boolean) => invoke('codeburn:getPeriodCompare', rangeA, rangeB, provider, background),
getPeriodCompareSessions: (rangeA: DateRange, rangeB: DateRange, provider: string, dimension: string, key: string) => invoke('codeburn:getPeriodCompareSessions', rangeA, rangeB, provider, dimension, key),
getCompareCohortModels: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getCompareCohortModels', period, provider, range, background),
getCompareCohort: (period: string, provider: string, modelA: string, modelB: string, range?: DateRange, projects?: string[], category?: string, background?: boolean) =>
invoke('codeburn:getCompareCohort', period, provider, modelA, modelB, range, projects, category, background),
getYield: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getYield', period, provider, range, background),
getSpendFlow: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getSpendFlow', period, provider, range, background),
getBranchSpend: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getBranchSpend', period, provider, range, background),
getOptimizeReport: (period: string, provider: string, range?: DateRange, background?: boolean) => invoke('codeburn:getOptimizeReport', period, provider, range, background),
// The once-a-day optimize scan, cached on disk per query scope. `maxAgeMs` 0
// forces a recompute (Optimize page, manual refresh).
getOptimizeSnapshot: (period: string, provider: string, range?: DateRange, configSource?: string | null, scope?: string, maxAgeMs?: number) =>
invoke('codeburn:getOptimizeSnapshot', period, provider, range, configSource, scope, maxAgeMs),
// Power source for the battery-aware live cadence: one read plus a push.
powerStatus: () => invoke('codeburn:powerStatus'),
onPowerStatus: (cb: (onBattery: boolean) => void) => {
const listener = (_e: unknown, onBattery: boolean) => cb(onBattery)
ipcRenderer.on('codeburn:power', listener)
return () => { ipcRenderer.removeListener('codeburn:power', listener) }
},
getDevices: (period: string) => invoke('codeburn:getDevices', period),
getDevicesScan: () => invoke('codeburn:getDevicesScan'),
getShareStatus: () => invoke('codeburn:getShareStatus'),
getIdentity: () => invoke('codeburn:getIdentity'),
getAliases: () => invoke('codeburn:getAliases'),
getProxyPaths: () => invoke('codeburn:getProxyPaths'),
getAudit: (period: string, provider: string, range?: DateRange) => invoke('codeburn:getAudit', period, provider, range),
getPriceOverrides: () => invoke('codeburn:getPriceOverrides'),
getProjectFilter: () => invoke('codeburn:getProjectFilter'),
setProjectFilter: (filter: { project: string[]; exclude: string[] }) => invoke('codeburn:setProjectFilter', filter),
getUnfilteredProjects: () => invoke('codeburn:getUnfilteredProjects'),
setTransientProject: (projectPath: string | null) => invoke('codeburn:setTransientProject', projectPath),
setPriceOverride: (model: string, rates: PriceRates) => invoke('codeburn:setPriceOverride', model, rates),
removePriceOverride: (model: string) => invoke('codeburn:removePriceOverride', model),
setCurrency: (code: string) => invoke('codeburn:setCurrency', code),
resetCurrency: () => invoke('codeburn:resetCurrency'),
addAlias: (from: string, to: string) => invoke('codeburn:addAlias', from, to),
removeAlias: (from: string) => invoke('codeburn:removeAlias', from),
removeDevice: (name: string) => invoke('codeburn:removeDevice', name),
setPlan: (id: string, provider: string) => invoke('codeburn:setPlan', id, provider),
resetPlan: (provider: string) => invoke('codeburn:resetPlan', provider),
exportData: (format: string, provider: string, outPath: string) => invoke('codeburn:exportData', format, provider, outPath),
chooseDirectory: () => invoke('codeburn:chooseDirectory'),
cliStatus: () => invoke('codeburn:cliStatus'),
telemetryStatus: () => invoke('codeburn:telemetryStatus'),
setTelemetryEnabled: (enabled: boolean) => invoke('codeburn:telemetrySetEnabled', enabled),
completeOnboarding: (enabled: boolean) => invoke('codeburn:telemetryOnboarded', enabled),
telemetryTrack: (name: string, props?: Record<string, unknown>) => invoke('codeburn:telemetryTrack', name, props),
openExternal: (url: string) => ipcRenderer.invoke('open-external', url),
// Cold-start scan progress (main → renderer). Returns an unsubscribe fn.
onProgress: (cb: (event: unknown) => void) => {
const listener = (_e: unknown, event: unknown) => cb(event)
ipcRenderer.on('codeburn:progress', listener)
return () => { ipcRenderer.removeListener('codeburn:progress', listener) }
},
// Update availability: a one-shot read plus a push for the launch + 24h checks.
getUpdateStatus: () => invoke('codeburn:getUpdateStatus'),
onUpdateStatus: (cb: (status: unknown) => void) => {
const listener = (_e: unknown, status: unknown) => cb(status)
ipcRenderer.on('codeburn:update', listener)
return () => { ipcRenderer.removeListener('codeburn:update', listener) }
},
downloadUpdate: () => invoke('codeburn:downloadUpdate'),
installUpdate: () => invoke('codeburn:installUpdate'),
// The bundled tray app and its Capacity Dock (Windows). Every setter answers with the
// whole status, so the sidebar renders what took rather than what it asked for.
companionStatus: () => invoke('codeburn:companionStatus'),
// The Plugins card's discrete actions, mirroring the macOS card.
companionInstall: () => invoke('codeburn:companionInstall'),
companionOpen: () => invoke('codeburn:companionOpen'),
companionQuit: () => invoke('codeburn:companionQuit'),
companionUninstall: () => invoke('codeburn:companionUninstall'),
companionSetDock: (enabled: boolean) => invoke('codeburn:companionSetDock', enabled),
// The tray app's own settings, in the two files it reads them from.
trayPrefs: () => invoke('codeburn:trayPrefs'),
setTrayAppPref: (patch: Record<string, unknown>) => invoke('codeburn:setTrayAppPref', patch),
setTrayDockPref: (patch: Record<string, unknown>) => invoke('codeburn:setTrayDockPref', patch),
setLaunchAtLogin: (enabled: boolean) => invoke('codeburn:setLaunchAtLogin', enabled),
// The macOS menubar app's card on the Plugins page.
macMenubarStatus: () => invoke('codeburn:macMenubarStatus'),
macMenubarInstall: () => invoke('codeburn:macMenubarInstall'),
macMenubarOpen: () => invoke('codeburn:macMenubarOpen'),
macMenubarSetDock: (enabled: boolean) => invoke('codeburn:macMenubarSetDock', enabled),
macMenubarSettings: () => invoke('codeburn:macMenubarSettings'),
macMenubarQuit: () => invoke('codeburn:macMenubarQuit'),
macMenubarUninstall: () => invoke('codeburn:macMenubarUninstall'),
onMacMenubarProgress: (cb: (phase: string) => void) => {
const listener = (_e: unknown, phase: string) => cb(phase)
ipcRenderer.on('codeburn:macMenubarProgress', listener)
return () => { ipcRenderer.removeListener('codeburn:macMenubarProgress', listener) }
},
// Plugin management
pluginList: () => invoke('codeburn:pluginList'),
pluginInfo: (name: string) => invoke('codeburn:pluginInfo', name),
pluginAdd: (source: string) => invoke('codeburn:pluginAdd', source),
pluginRemove: (name: string) => invoke('codeburn:pluginRemove', name),
pluginVerify: (name: string) => invoke('codeburn:pluginVerify', name),
// Sync auto
syncAutoStatus: () => invoke('codeburn:syncAutoStatus'),
syncAutoEnable: (cadence: string, attribution: boolean, accept: boolean) => invoke('codeburn:syncAutoEnable', cadence, attribution, accept),
syncAutoDisable: () => invoke('codeburn:syncAutoDisable'),
platform: process.platform,
arch: process.arch,
}
contextBridge.exposeInMainWorld('codeburn', bridge)