diff --git a/.github/workflows/desktop-artifacts.yml b/.github/workflows/desktop-artifacts.yml new file mode 100644 index 00000000..290b6cc1 --- /dev/null +++ b/.github/workflows/desktop-artifacts.yml @@ -0,0 +1,66 @@ +name: Desktop Artifacts + +on: + pull_request: + branches: [main] + paths: + - 'desktop/**' + - 'server/**' + - 'web/**' + - 'shared/**' + - 'docs/DESKTOP-RELEASE.md' + - 'scripts/desktop-after-pack.mjs' + - 'scripts/prepare-desktop-release.mjs' + - 'package.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' + - '.github/workflows/desktop-artifacts.yml' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: '22' + +jobs: + mac-unsigned: + name: Unsigned macOS Artifact + runs-on: macos-15 + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace + run: pnpm build + + - name: Prepare desktop runtime payload + run: pnpm --filter @veritas-kanban/desktop package:prepare + + - name: Build unsigned macOS DMG and update metadata + working-directory: desktop + env: + CSC_IDENTITY_AUTO_DISCOVERY: 'false' + run: pnpm exec electron-builder --mac dmg zip --publish never --config.mac.identity=null --config.mac.notarize=false + + - name: Upload desktop artifacts + uses: actions/upload-artifact@v5 + with: + name: veritas-kanban-mac-unsigned + path: | + desktop/release/*.dmg + desktop/release/*.zip + desktop/release/*.yml + desktop/release/*.blockmap + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml new file mode 100644 index 00000000..92a36672 --- /dev/null +++ b/.github/workflows/desktop-release.yml @@ -0,0 +1,78 @@ +name: Desktop Release + +on: + workflow_dispatch: + inputs: + channel: + description: Update channel to publish. + required: true + default: stable + type: choice + options: + - stable + - beta + - dev + release: + types: [published] + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + NODE_VERSION: '22' + VERITAS_UPDATE_CHANNEL: ${{ github.event.inputs.channel || 'stable' }} + +jobs: + mac-signed: + name: Signed and Notarized macOS Artifact + runs-on: macos-15 + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + + - name: Verify signing secrets are configured + env: + CSC_LINK: ${{ secrets.MACOS_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + missing=0 + for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do + if [ -z "${!name}" ]; then + echo "::error::$name is required for signed/notarized desktop releases" + missing=1 + fi + done + exit "$missing" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace + run: pnpm build + + - name: Prepare desktop runtime payload + run: pnpm --filter @veritas-kanban/desktop package:prepare + + - name: Build, sign, notarize, and publish macOS artifacts + working-directory: desktop + env: + GH_TOKEN: ${{ github.token }} + CSC_LINK: ${{ secrets.MACOS_CSC_LINK }} + CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: pnpm exec electron-builder --mac dmg zip --publish always diff --git a/.gitignore b/.gitignore index 3e4c0ea0..1b336992 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,8 @@ tasks/archive-attachments/ .veritas-kanban/* !.veritas-kanban/.gitkeep .veritas-desktop-dev/ +desktop/.desktop-release/ +desktop/release/ # Historical broken config data (should never have been tracked) .veritas-kanban.broken/ diff --git a/desktop/README.md b/desktop/README.md index 066b31be..cb3ea54c 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -78,6 +78,15 @@ with generic copy while preserving the durable target for click-through. Window size, position, and maximized state are persisted per profile/workspace in `config/window-state.json`. +## Release Packaging + +Unsigned PR artifacts, signed/notarized release artifacts, update metadata, and +macOS smoke steps are documented in +[`docs/DESKTOP-RELEASE.md`](../docs/DESKTOP-RELEASE.md). Use the root +`desktop:package:mac:unsigned` script for local unsigned DMG/ZIP validation and +`desktop:release:mac` only when Apple signing/notarization credentials are +configured. + ## Production Scaffold `pnpm desktop:build` compiles the Electron main, preload, and fallback renderer. diff --git a/desktop/package.json b/desktop/package.json index 7a383c7c..28314bc0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -11,17 +11,23 @@ "dev": "electron-vite dev", "dev:fresh": "VERITAS_DESKTOP_PROFILE=fresh electron-vite dev", "build": "electron-vite build", + "package:prepare": "node ../scripts/prepare-desktop-release.mjs", + "package:mac:dir": "pnpm package:prepare && electron-builder --mac dir --publish never --config.mac.identity=null --config.mac.notarize=false", + "package:mac:unsigned": "pnpm package:prepare && electron-builder --mac dmg zip --publish never --config.mac.identity=null --config.mac.notarize=false", + "release:mac": "pnpm package:prepare && electron-builder --mac dmg zip --publish always", "typecheck": "tsc --noEmit", "lint": "eslint src electron.vite.config.ts --ext .ts", "test": "vitest run --config vitest.config.ts", "clean": "rm -rf dist out .veritas-desktop-dev" }, "dependencies": { - "@veritas-kanban/shared": "workspace:*" + "@veritas-kanban/shared": "workspace:*", + "electron-updater": "^6.8.3" }, "devDependencies": { "@types/node": "^25.7.0", "electron": "^39.2.6", + "electron-builder": "^26.8.1", "electron-vite": "^5.0.0", "typescript": "^6.0.3", "vite": "^7.2.7", @@ -30,11 +36,73 @@ "build": { "appId": "io.digitalmeld.veritas-kanban", "productName": "Veritas Kanban", + "artifactName": "${productName}-${version}-${os}-${arch}.${ext}", + "asar": true, + "afterPack": "../scripts/desktop-after-pack.mjs", "directories": { - "buildResources": "resources" + "buildResources": "resources", + "output": "release" }, + "files": [ + "out/**/*", + "resources/**/*", + "package.json", + "node_modules/**/*" + ], + "extraResources": [ + { + "from": ".desktop-release/server", + "to": "server" + }, + { + "from": ".desktop-release/web", + "to": "web" + } + ], + "protocols": [ + { + "name": "Veritas Kanban", + "schemes": [ + "veritas" + ], + "role": "Viewer" + } + ], + "generateUpdatesFilesForAllChannels": true, "mac": { - "category": "public.app-category.productivity" - } + "category": "public.app-category.productivity", + "target": [ + "dmg", + "zip" + ], + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.inherit.plist", + "notarize": true + }, + "dmg": { + "sign": false, + "contents": [ + { + "x": 130, + "y": 220 + }, + { + "x": 410, + "y": 220, + "type": "link", + "path": "/Applications" + } + ] + }, + "publish": [ + { + "provider": "github", + "owner": "BradGroux", + "repo": "veritas-kanban", + "releaseType": "draft" + } + ] } } diff --git a/desktop/resources/entitlements.mac.inherit.plist b/desktop/resources/entitlements.mac.inherit.plist new file mode 100644 index 00000000..6dcade08 --- /dev/null +++ b/desktop/resources/entitlements.mac.inherit.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.inherit + + + diff --git a/desktop/resources/entitlements.mac.plist b/desktop/resources/entitlements.mac.plist new file mode 100644 index 00000000..05d489e9 --- /dev/null +++ b/desktop/resources/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/desktop/src/main/__tests__/commands.test.ts b/desktop/src/main/__tests__/commands.test.ts index ad88988b..3d2af955 100644 --- a/desktop/src/main/__tests__/commands.test.ts +++ b/desktop/src/main/__tests__/commands.test.ts @@ -8,7 +8,10 @@ import { } from '../commands.js'; import type { DesktopRuntime } from '../runtime.js'; import type { DesktopStatusSnapshot } from '../types.js'; -import { DESKTOP_COMMAND_NAMES } from '../../shared/desktop-bridge-contracts.js'; +import { + DESKTOP_COMMAND_NAMES, + type DesktopUpdateStatus, +} from '../../shared/desktop-bridge-contracts.js'; function status(): DesktopStatusSnapshot { return { @@ -37,6 +40,15 @@ function status(): DesktopStatusSnapshot { }; } +function updateStatus(state: DesktopUpdateStatus['state'] = 'idle'): DesktopUpdateStatus { + return { + state, + currentVersion: '4.3.2', + channel: 'stable', + checkedAt: '2026-05-31T00:00:00.000Z', + }; +} + function dispatcher() { const runtime = { snapshot: vi.fn(status), @@ -46,7 +58,9 @@ function dispatcher() { openPath: vi.fn(async () => ''), } as unknown as Shell; const sendRendererCommand = vi.fn(); - const sendUpdateStatus = vi.fn(); + const checkForUpdates = vi.fn(async () => updateStatus('idle')); + const downloadUpdate = vi.fn(async () => updateStatus('ready')); + const installUpdate = vi.fn(() => updateStatus('ready')); const showTestNotification = vi.fn(); const copyRedactedDiagnostics = vi.fn(); @@ -54,7 +68,9 @@ function dispatcher() { runtime, shell, sendRendererCommand, - sendUpdateStatus, + checkForUpdates, + downloadUpdate, + installUpdate, showTestNotification, copyRedactedDiagnostics, dispatcher: new DesktopCommandDispatcher({ @@ -62,7 +78,9 @@ function dispatcher() { shell, quit: vi.fn(), sendRendererCommand, - sendUpdateStatus, + checkForUpdates, + downloadUpdate, + installUpdate, showTestNotification, copyRedactedDiagnostics, }), @@ -107,6 +125,8 @@ describe('desktop command registry', () => { }); await harness.dispatcher.dispatch(createDesktopCommandRequest('open-logs', 'menu')); await harness.dispatcher.dispatch(createDesktopCommandRequest('check-for-updates', 'menu')); + await harness.dispatcher.dispatch(createDesktopCommandRequest('download-update', 'menu')); + await harness.dispatcher.dispatch(createDesktopCommandRequest('install-update', 'menu')); await harness.dispatcher.dispatch(createDesktopCommandRequest('test-notification', 'menu')); await harness.dispatcher.dispatch( createDesktopCommandRequest('copy-redacted-diagnostics', 'menu') @@ -114,11 +134,9 @@ describe('desktop command registry', () => { expect(harness.runtime.restartLocalServer).toHaveBeenCalledTimes(1); expect(harness.shell.openPath).toHaveBeenCalledWith('/tmp/veritas/logs'); - expect(harness.sendUpdateStatus).toHaveBeenCalledWith( - expect.objectContaining({ - state: 'unsupported', - }) - ); + expect(harness.checkForUpdates).toHaveBeenCalledTimes(1); + expect(harness.downloadUpdate).toHaveBeenCalledTimes(1); + expect(harness.installUpdate).toHaveBeenCalledTimes(1); expect(harness.showTestNotification).toHaveBeenCalledTimes(1); expect(harness.copyRedactedDiagnostics).toHaveBeenCalledWith(status()); }); diff --git a/desktop/src/main/__tests__/lifecycle.test.ts b/desktop/src/main/__tests__/lifecycle.test.ts index 23048743..34ad59fd 100644 --- a/desktop/src/main/__tests__/lifecycle.test.ts +++ b/desktop/src/main/__tests__/lifecycle.test.ts @@ -47,11 +47,30 @@ describe('desktop lifecycle config', () => { expect(env.VERITAS_ADMIN_KEY).toBe('desktop-keychain-admin-key'); expect(env.VERITAS_JWT_SECRET).toBe('desktop-keychain-jwt-secret'); expect(env.VERITAS_STORAGE).toBe('sqlite'); + expect(env.VERITAS_DESKTOP_RUNTIME).toBe('0'); + expect(env.DATA_DIR).toBe('/tmp/veritas-desktop/data'); expect(env.VERITAS_DATA_DIR).toBe('/tmp/veritas-desktop/data'); expect(env.VERITAS_AUTH_ENABLED).toBe('false'); expect(env.CORS_ORIGINS).toContain('http://127.0.0.1:39124'); }); + it('runs packaged server with the Electron binary in Node mode from resources', () => { + const configs = createManagedProcessConfigs({ + ...options(), + isPackaged: true, + resourcesPath: '/Applications/Veritas Kanban.app/Contents/Resources', + }); + + expect(configs).toHaveLength(1); + expect(configs[0]?.args).toEqual([ + '/Applications/Veritas Kanban.app/Contents/Resources/server/dist/index.js', + ]); + expect(configs[0]?.cwd).toBe('/Applications/Veritas Kanban.app/Contents/Resources/server'); + expect(configs[0]?.env.ELECTRON_RUN_AS_NODE).toBe('1'); + expect(configs[0]?.env.VERITAS_DESKTOP_RUNTIME).toBe('1'); + expect(configs[0]?.env.VERITAS_AUTH_ENABLED).toBe('true'); + }); + it('points web dev proxies at the selected server port', () => { const env = buildWebEnvironment(options()); diff --git a/desktop/src/main/__tests__/menu.test.ts b/desktop/src/main/__tests__/menu.test.ts index d885ed5e..2863ebaf 100644 --- a/desktop/src/main/__tests__/menu.test.ts +++ b/desktop/src/main/__tests__/menu.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest'; import { createDesktopMenuTemplate } from '../menu.js'; import type { DesktopStatusSnapshot } from '../types.js'; +import type { DesktopUpdateStatus } from '../../shared/desktop-bridge-contracts.js'; function status(state: DesktopStatusSnapshot['server']['state'] = 'ready'): DesktopStatusSnapshot { return { @@ -30,6 +31,15 @@ function status(state: DesktopStatusSnapshot['server']['state'] = 'ready'): Desk }; } +function updateStatus(state: DesktopUpdateStatus['state']): DesktopUpdateStatus { + return { + state, + currentVersion: '4.3.2', + channel: 'stable', + checkedAt: '2026-05-31T00:00:00.000Z', + }; +} + describe('desktop native menu', () => { it('exposes common actions with keyboard shortcuts', () => { const dispatch = vi.fn(); @@ -65,4 +75,21 @@ describe('desktop native menu', () => { expect(externalTest?.enabled).toBe(false); }); + + it('keeps update install actions tied to updater state', () => { + const appMenu = createDesktopMenuTemplate({ + status: status(), + updateStatus: updateStatus('available'), + dispatch: vi.fn(), + }).find((item) => item.label === 'Veritas Kanban'); + const downloadUpdate = Array.isArray(appMenu?.submenu) + ? appMenu.submenu.find((item) => item.label === 'Download Update') + : null; + const installUpdate = Array.isArray(appMenu?.submenu) + ? appMenu.submenu.find((item) => item.label === 'Install Update') + : null; + + expect(downloadUpdate?.enabled).toBe(true); + expect(installUpdate?.enabled).toBe(false); + }); }); diff --git a/desktop/src/main/__tests__/process-supervisor.test.ts b/desktop/src/main/__tests__/process-supervisor.test.ts new file mode 100644 index 00000000..d7be949d --- /dev/null +++ b/desktop/src/main/__tests__/process-supervisor.test.ts @@ -0,0 +1,73 @@ +import { mkdtemp } from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { ProcessSupervisor } from '../process-supervisor.js'; +import type { DesktopProcessState, ManagedProcessConfig } from '../types.js'; + +async function createConfig(args: string[]): Promise { + const tempDir = await mkdtemp(path.join(os.tmpdir(), 'veritas-process-supervisor-')); + + return { + name: 'server', + command: process.execPath, + args, + cwd: process.cwd(), + env: process.env, + logFile: path.join(tempDir, 'server.log'), + }; +} + +function waitForState( + supervisor: ProcessSupervisor, + state: DesktopProcessState +): Promise> { + const snapshot = supervisor.snapshot(); + if (snapshot.state === state) { + return Promise.resolve(snapshot); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + supervisor.off('state', onState); + reject(new Error(`Timed out waiting for ${state}`)); + }, 2_000); + + const onState = (next: ReturnType) => { + if (next.state === state) { + clearTimeout(timeout); + supervisor.off('state', onState); + resolve(next); + } + }; + + supervisor.on('state', onState); + }); +} + +describe('ProcessSupervisor', () => { + it('fails when a process exits before readiness, even with code 0', async () => { + const supervisor = new ProcessSupervisor(await createConfig(['-e', 'process.exit(0)'])); + + const failed = waitForState(supervisor, 'failed'); + await supervisor.start(); + + const snapshot = await failed; + expect(snapshot.lastError).toContain('server exited before becoming ready with code 0'); + }); + + it('treats a clean exit after readiness as stopped', async () => { + const supervisor = new ProcessSupervisor( + await createConfig(['-e', 'setTimeout(() => process.exit(0), 50)']) + ); + + const stopped = waitForState(supervisor, 'stopped'); + await supervisor.start(); + supervisor.markReady(); + + const snapshot = await stopped; + expect(snapshot.lastError).toBeNull(); + }); +}); diff --git a/desktop/src/main/__tests__/updates.test.ts b/desktop/src/main/__tests__/updates.test.ts new file mode 100644 index 00000000..b048bea4 --- /dev/null +++ b/desktop/src/main/__tests__/updates.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + DesktopUpdateService, + resolveDesktopUpdateChannel, + type DesktopUpdateAdapter, + type DesktopUpdateAdapterConfigureOptions, +} from '../updates.js'; + +type UpdateListener = (...args: unknown[]) => void; + +class FakeUpdateAdapter implements DesktopUpdateAdapter { + configure = vi.fn((options: DesktopUpdateAdapterConfigureOptions) => { + this.config = options; + }); + checkForUpdates = vi.fn(async () => null); + downloadUpdate = vi.fn(async () => []); + quitAndInstall = vi.fn(); + active = true; + config: DesktopUpdateAdapterConfigureOptions | null = null; + private readonly listeners = new Map(); + + on(event: string, listener: UpdateListener): void { + const existing = this.listeners.get(event) ?? []; + this.listeners.set(event, [...existing, listener]); + } + + emit(event: string, ...args: unknown[]): void { + for (const listener of this.listeners.get(event) ?? []) { + listener(...args); + } + } + + isUpdaterActive(): boolean { + return this.active; + } +} + +function service(adapter = new FakeUpdateAdapter()) { + const emitStatus = vi.fn(); + return { + adapter, + emitStatus, + service: new DesktopUpdateService({ + adapter, + packaged: true, + currentVersion: '4.3.2', + channel: 'stable', + now: () => new Date('2026-05-31T00:00:00.000Z'), + emitStatus, + }), + }; +} + +describe('desktop update service', () => { + it('configures updater for manual download and stable channel release checks', () => { + const harness = service(); + + expect(harness.adapter.configure).toHaveBeenCalledWith({ + allowPrerelease: false, + autoDownload: false, + autoInstallOnAppQuit: false, + channel: 'stable', + forceDevUpdateConfig: false, + }); + expect(harness.service.snapshot()).toMatchObject({ + state: 'idle', + currentVersion: '4.3.2', + channel: 'stable', + }); + }); + + it('emits available, downloading, and ready states from updater events', async () => { + const harness = service(); + + await harness.service.checkForUpdates(); + harness.adapter.emit('update-available', { version: '4.3.3' }); + harness.adapter.emit('download-progress', { percent: 55.2 }); + harness.adapter.emit('update-downloaded', { version: '4.3.3' }); + + expect(harness.emitStatus).toHaveBeenCalledWith( + expect.objectContaining({ state: 'available', availableVersion: '4.3.3' }) + ); + expect(harness.emitStatus).toHaveBeenCalledWith( + expect.objectContaining({ state: 'downloading', detail: '55% downloaded.' }) + ); + expect(harness.service.snapshot()).toMatchObject({ + state: 'ready', + availableVersion: '4.3.3', + }); + }); + + it('keeps dev builds unsupported unless force dev update config is enabled', async () => { + const adapter = new FakeUpdateAdapter(); + const updateService = new DesktopUpdateService({ + adapter, + packaged: false, + currentVersion: '4.3.2', + channel: 'dev', + }); + + await expect(updateService.checkForUpdates()).resolves.toMatchObject({ + state: 'unsupported', + }); + expect(adapter.checkForUpdates).not.toHaveBeenCalled(); + }); + + it('redacts sensitive update errors before publishing status', async () => { + const harness = service(); + harness.adapter.checkForUpdates.mockRejectedValueOnce( + new Error('download failed token=abc123 path=/Users/bradgroux/private') + ); + + await harness.service.checkForUpdates(); + + expect(harness.service.snapshot()).toMatchObject({ + state: 'failed', + detail: 'download failed token=[redacted] path=/Users/[redacted]/private', + }); + }); + + it('resolves stable, beta, and dev channels conservatively', () => { + expect(resolveDesktopUpdateChannel(undefined, '4.3.2', true)).toBe('stable'); + expect(resolveDesktopUpdateChannel(undefined, '5.0.0-beta.1', true)).toBe('beta'); + expect(resolveDesktopUpdateChannel('dev', '4.3.2', true)).toBe('dev'); + expect(resolveDesktopUpdateChannel(undefined, '4.3.2', false)).toBe('dev'); + }); +}); diff --git a/desktop/src/main/bridge.ts b/desktop/src/main/bridge.ts index 6e9375bf..ecfae22a 100644 --- a/desktop/src/main/bridge.ts +++ b/desktop/src/main/bridge.ts @@ -4,6 +4,7 @@ import { DESKTOP_APP_ID, DESKTOP_APP_NAME } from './app-metadata.js'; import type { DesktopCommandDispatcher } from './commands.js'; import type { DesktopAppInfo } from './types.js'; import type { DesktopRuntime } from './runtime.js'; +import type { DesktopUpdateService } from './updates.js'; import { createDesktopSetupDiagnostics, createDesktopSupportSnapshot, @@ -36,7 +37,8 @@ export function createDesktopBridgeHandlers( runtime: DesktopRuntime, shell: Shell, packaged: boolean, - commandDispatcher?: DesktopCommandDispatcher + commandDispatcher?: DesktopCommandDispatcher, + updateService?: DesktopUpdateService ): DesktopBridgeHandlerMap { const appInfo = (): DesktopAppInfo => ({ name: DESKTOP_APP_NAME, @@ -65,13 +67,14 @@ export function createDesktopBridgeHandlers( return runtime.restartLocalServer(); }, getSupportSnapshot: () => createDesktopSupportSnapshot(runtime.snapshot()), - getUpdateStatus: () => ({ - state: 'unsupported', - currentVersion: appInfo().version, - channel: packaged ? 'stable' : 'dev', - checkedAt: new Date().toISOString(), - detail: 'Updater implementation is tracked in the desktop release pipeline issue.', - }), + getUpdateStatus: () => + updateService?.snapshot() ?? { + state: 'unsupported', + currentVersion: appInfo().version, + channel: packaged ? 'stable' : 'dev', + checkedAt: new Date().toISOString(), + detail: 'Updater service is not initialized.', + }, dispatchCommand: (request) => { const command = validateDesktopCommandDispatchRequest(request); return commandDispatcher @@ -128,9 +131,16 @@ export function registerDesktopBridge( runtime: DesktopRuntime, shell: Shell, packaged: boolean, - commandDispatcher?: DesktopCommandDispatcher + commandDispatcher?: DesktopCommandDispatcher, + updateService?: DesktopUpdateService ): void { - const handlers = createDesktopBridgeHandlers(runtime, shell, packaged, commandDispatcher); + const handlers = createDesktopBridgeHandlers( + runtime, + shell, + packaged, + commandDispatcher, + updateService + ); for (const method of DESKTOP_BRIDGE_METHOD_NAMES) { const definition = DESKTOP_BRIDGE_METHODS[method]; diff --git a/desktop/src/main/commands.ts b/desktop/src/main/commands.ts index 9df7bba6..5f91e783 100644 --- a/desktop/src/main/commands.ts +++ b/desktop/src/main/commands.ts @@ -19,6 +19,8 @@ export type DesktopCommandNativeAction = | 'show-diagnostics' | 'create-debug-bundle' | 'check-updates' + | 'download-update' + | 'install-update' | 'test-notification' | 'test-external-delivery' | 'copy-diagnostics' @@ -74,6 +76,10 @@ function commandLabel(name: DesktopCommandName): string { return 'Create Debug Bundle'; case 'check-for-updates': return 'Check for Updates'; + case 'download-update': + return 'Download Update'; + case 'install-update': + return 'Install Update'; case 'test-notification': return 'Test Local Notification'; case 'test-squad-webhook': @@ -116,6 +122,10 @@ function commandNativeAction(name: DesktopCommandName): DesktopCommandNativeActi return 'create-debug-bundle'; case 'check-for-updates': return 'check-updates'; + case 'download-update': + return 'download-update'; + case 'install-update': + return 'install-update'; case 'test-notification': return 'test-notification'; case 'test-squad-webhook': @@ -138,7 +148,9 @@ export interface DesktopCommandDispatcherOptions { shell: Shell; quit(): void; sendRendererCommand(command: DesktopCommandDispatchRequest): void; - sendUpdateStatus(status: DesktopUpdateStatus): void; + checkForUpdates(): Promise; + downloadUpdate(): Promise; + installUpdate(): DesktopUpdateStatus; showTestNotification(): void; copyRedactedDiagnostics(status: DesktopStatusSnapshot): void; } @@ -166,13 +178,13 @@ export class DesktopCommandDispatcher { this.options.sendRendererCommand(request); return accepted(request, 'renderer'); case 'check-updates': - this.options.sendUpdateStatus({ - state: 'unsupported', - currentVersion: process.env.npm_package_version || '0.0.0', - channel: 'dev', - checkedAt: new Date().toISOString(), - detail: 'Updater implementation is tracked in the desktop release pipeline issue.', - }); + await this.options.checkForUpdates(); + return accepted(request, 'desktop'); + case 'download-update': + await this.options.downloadUpdate(); + return accepted(request, 'desktop'); + case 'install-update': + this.options.installUpdate(); return accepted(request, 'desktop'); case 'test-notification': this.options.showTestNotification(); diff --git a/desktop/src/main/index.ts b/desktop/src/main/index.ts index 4d1c0b58..cf8e0de3 100644 --- a/desktop/src/main/index.ts +++ b/desktop/src/main/index.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow, clipboard, ipcMain, Notification, safeStorage, shell } from 'electron'; import path from 'node:path'; import { mkdirSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { DESKTOP_APP_ID, DESKTOP_APP_NAME, DESKTOP_MIN_WINDOW } from './app-metadata.js'; import { registerDesktopBridge } from './bridge.js'; @@ -13,9 +14,15 @@ import { findAvailablePort } from './ports.js'; import { DesktopRuntime } from './runtime.js'; import { DesktopSecretStore } from './secrets.js'; import { statusPageUrl } from './status-page.js'; +import { + DesktopUpdateService, + ElectronAutoUpdaterAdapter, + resolveDesktopUpdateChannel, +} from './updates.js'; import { DESKTOP_BRIDGE_EVENTS, redactDesktopBridgeValue, + type DesktopUpdateStatus, } from '../shared/desktop-bridge-contracts.js'; import { applyDesktopWindowState, @@ -25,9 +32,13 @@ import { type DesktopWindowState, } from './window-state.js'; +const require = createRequire(import.meta.url); +const { autoUpdater } = require('electron-updater') as typeof import('electron-updater'); + let mainWindow: BrowserWindow | null = null; let runtime: DesktopRuntime | null = null; let commandDispatcher: DesktopCommandDispatcher | null = null; +let updateService: DesktopUpdateService | null = null; let windowStatePaths: ReturnType | null = null; let quitting = false; let shutdownStarted = false; @@ -133,6 +144,32 @@ function flushPendingDeepLinks(): void { } } +function refreshDesktopMenu(): void { + if (!runtime || !commandDispatcher) { + return; + } + + configureDesktopMenu({ + status: runtime.snapshot(), + updateStatus: updateService?.snapshot(), + dispatch: (command) => { + if (commandDispatcher) { + dispatchDesktopMenuCommand(commandDispatcher, command); + } + }, + }); +} + +function updateServiceFallback(packaged: boolean): DesktopUpdateStatus { + return { + state: 'unsupported', + currentVersion: app.getVersion(), + channel: packaged ? 'stable' : 'dev', + checkedAt: new Date().toISOString(), + detail: 'Updater service is not initialized.', + }; +} + async function boot(): Promise { app.setName(DESKTOP_APP_NAME); app.setAppUserModelId(DESKTOP_APP_ID); @@ -182,6 +219,7 @@ async function boot(): Promise { runtime = new DesktopRuntime({ repoRoot, + resourcesPath: process.resourcesPath, paths, serverPort, webPort, @@ -198,6 +236,21 @@ async function boot(): Promise { mainWindow?.webContents.send(DESKTOP_BRIDGE_EVENTS.notificationAction.channel, request); } ); + updateService = new DesktopUpdateService({ + adapter: new ElectronAutoUpdaterAdapter(autoUpdater), + packaged, + currentVersion: app.getVersion(), + channel: resolveDesktopUpdateChannel( + process.env.VERITAS_UPDATE_CHANNEL, + app.getVersion(), + packaged + ), + forceDevUpdateConfig: process.env.VERITAS_DESKTOP_UPDATER_FORCE_DEV === 'true', + emitStatus: (status) => { + mainWindow?.webContents.send(DESKTOP_BRIDGE_EVENTS.updateStatus.channel, status); + refreshDesktopMenu(); + }, + }); commandDispatcher = new DesktopCommandDispatcher({ runtime, @@ -206,9 +259,11 @@ async function boot(): Promise { sendRendererCommand: (command) => { mainWindow?.webContents.send(DESKTOP_BRIDGE_EVENTS.menuCommand.channel, command); }, - sendUpdateStatus: (status) => { - mainWindow?.webContents.send(DESKTOP_BRIDGE_EVENTS.updateStatus.channel, status); - }, + checkForUpdates: () => + updateService?.checkForUpdates() ?? Promise.resolve(updateServiceFallback(packaged)), + downloadUpdate: () => + updateService?.downloadUpdate() ?? Promise.resolve(updateServiceFallback(packaged)), + installUpdate: () => updateService?.installUpdate() ?? updateServiceFallback(packaged), showTestNotification: () => { notifications.show({ id: `setup-test-${Date.now()}`, @@ -224,25 +279,11 @@ async function boot(): Promise { }, }); - registerDesktopBridge(ipcMain, runtime, shell, packaged, commandDispatcher); - configureDesktopMenu({ - status: runtime.snapshot(), - dispatch: (command) => { - if (commandDispatcher) { - dispatchDesktopMenuCommand(commandDispatcher, command); - } - }, - }); + registerDesktopBridge(ipcMain, runtime, shell, packaged, commandDispatcher, updateService); + refreshDesktopMenu(); runtime.on('status', (status) => { mainWindow?.webContents.send(DESKTOP_BRIDGE_EVENTS.serverStatus.channel, status); - configureDesktopMenu({ - status, - dispatch: (command) => { - if (commandDispatcher) { - dispatchDesktopMenuCommand(commandDispatcher, command); - } - }, - }); + refreshDesktopMenu(); }); try { diff --git a/desktop/src/main/lifecycle.ts b/desktop/src/main/lifecycle.ts index 724a2239..b31dcaa7 100644 --- a/desktop/src/main/lifecycle.ts +++ b/desktop/src/main/lifecycle.ts @@ -4,6 +4,7 @@ import type { DesktopPaths, DesktopRuntimeSecrets, ManagedProcessConfig } from ' export interface DesktopLifecycleOptions { repoRoot: string; + resourcesPath?: string; paths: DesktopPaths; serverPort: number; webPort: number; @@ -22,13 +23,16 @@ export function buildServerEnvironment(options: DesktopLifecycleOptions): NodeJS return { ...process.env, NODE_ENV: options.isPackaged ? 'production' : 'development', + ...(options.isPackaged ? { ELECTRON_RUN_AS_NODE: '1' } : {}), HOST: '127.0.0.1', PORT: String(options.serverPort), VERITAS_ADMIN_KEY: options.secrets.adminKey, VERITAS_JWT_SECRET: options.secrets.jwtSecret, VERITAS_AUTH_ENABLED: options.isPackaged ? 'true' : 'false', VERITAS_AUTH_LOCALHOST_BYPASS: 'false', + VERITAS_DESKTOP_RUNTIME: options.isPackaged ? '1' : '0', VERITAS_STORAGE: 'sqlite', + DATA_DIR: options.paths.dataDir, VERITAS_DATA_DIR: options.paths.dataDir, VERITAS_DISABLE_WATCHERS: '1', CORS_ORIGINS: `${serverOrigin},${webOrigin},http://localhost:${options.webPort}`, @@ -49,15 +53,18 @@ export function buildWebEnvironment(options: DesktopLifecycleOptions): NodeJS.Pr export function createManagedProcessConfigs( options: DesktopLifecycleOptions ): ManagedProcessConfig[] { + const packagedServerRoot = options.resourcesPath + ? path.join(options.resourcesPath, 'server') + : path.join(options.repoRoot, 'server'); + const packagedServerEntry = + process.env.VERITAS_DESKTOP_SERVER_ENTRY || path.join(packagedServerRoot, 'dist', 'index.js'); + const serverConfig: ManagedProcessConfig = options.isPackaged ? { name: 'server', command: process.execPath, - args: [ - process.env.VERITAS_DESKTOP_SERVER_ENTRY || - path.join(options.repoRoot, 'server/dist/index.js'), - ], - cwd: options.repoRoot, + args: [packagedServerEntry], + cwd: packagedServerRoot, env: buildServerEnvironment(options), logFile: path.join(options.paths.logsDir, 'server.log'), readyUrl: `http://127.0.0.1:${options.serverPort}/api/health`, diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 8e8fd1ca..d0ce82cb 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -6,11 +6,15 @@ import { type DesktopCommandDispatcher, } from './commands.js'; import type { DesktopStatusSnapshot } from './types.js'; -import type { DesktopCommandName } from '../shared/desktop-bridge-contracts.js'; +import type { + DesktopCommandName, + DesktopUpdateStatus, +} from '../shared/desktop-bridge-contracts.js'; export interface ConfigureDesktopMenuOptions { dispatch(command: DesktopCommandName): void; status: DesktopStatusSnapshot; + updateStatus?: DesktopUpdateStatus; } export function configureDesktopMenu(options: ConfigureDesktopMenuOptions): void { @@ -25,7 +29,7 @@ export function createDesktopMenuTemplate( return { label: definition.label, accelerator: definition.accelerator, - enabled: isCommandEnabled(name, options.status), + enabled: isCommandEnabled(name, options.status, options.updateStatus), click: () => options.dispatch(name), }; }; @@ -38,6 +42,8 @@ export function createDesktopMenuTemplate( command('communication-health'), { type: 'separator' }, command('check-for-updates'), + command('download-update'), + command('install-update'), { type: 'separator' }, command('quit'), ], @@ -78,7 +84,11 @@ export function dispatchDesktopMenuCommand( void dispatcher.dispatch(createDesktopCommandRequest(command, 'menu')); } -function isCommandEnabled(command: DesktopCommandName, status: DesktopStatusSnapshot): boolean { +function isCommandEnabled( + command: DesktopCommandName, + status: DesktopStatusSnapshot, + updateStatus?: DesktopUpdateStatus +): boolean { if (command === 'restart-local-server') { return status.mode === 'local-dev' || status.mode === 'local-production'; } @@ -88,5 +98,11 @@ function isCommandEnabled(command: DesktopCommandName, status: DesktopStatusSnap if (command === 'test-squad-webhook') { return status.server.state === 'ready'; } + if (command === 'download-update') { + return updateStatus?.state === 'available'; + } + if (command === 'install-update') { + return updateStatus?.state === 'ready'; + } return true; } diff --git a/desktop/src/main/process-supervisor.ts b/desktop/src/main/process-supervisor.ts index 2a995c3b..302a27eb 100644 --- a/desktop/src/main/process-supervisor.ts +++ b/desktop/src/main/process-supervisor.ts @@ -62,13 +62,17 @@ export class ProcessSupervisor extends EventEmitter { child.once('exit', (code, signal) => { this.exitedAt = new Date().toISOString(); this.log(`[desktop] exited code=${code ?? 'null'} signal=${signal ?? 'null'}\n`); + const wasReady = this.state === 'ready'; this.child = null; this.closeLogStream(); - if (this.stopping || code === 0) { + if (this.stopping || (code === 0 && wasReady)) { this.setState('stopped'); return; } - this.lastError = `${this.config.name} exited unexpectedly with code ${code ?? 'null'} signal ${signal ?? 'null'}`; + const exitDetail = `code ${code ?? 'null'} signal ${signal ?? 'null'}`; + this.lastError = wasReady + ? `${this.config.name} exited unexpectedly with ${exitDetail}` + : `${this.config.name} exited before becoming ready with ${exitDetail}`; this.setState('failed'); }); } diff --git a/desktop/src/main/runtime.ts b/desktop/src/main/runtime.ts index be9e6a29..7c289124 100644 --- a/desktop/src/main/runtime.ts +++ b/desktop/src/main/runtime.ts @@ -9,6 +9,7 @@ import type { DesktopPaths, DesktopRuntimeSecrets, DesktopStatusSnapshot } from export interface DesktopRuntimeOptions { repoRoot: string; + resourcesPath?: string; paths: DesktopPaths; serverPort: number; webPort: number; @@ -75,12 +76,12 @@ export class DesktopRuntime extends EventEmitter { await this.ensureDirectories(); await this.writeRuntimeState(); await this.server.start(); - await this.waitForReady(this.serverOrigin + '/api/health', 'server'); + await this.waitForReady(this.serverOrigin + '/api/health', 'server', this.server); this.server.markReady(); if (this.web) { await this.web.start(); - await this.waitForReady(this.rendererOrigin, 'web'); + await this.waitForReady(this.rendererOrigin, 'web', this.web); this.web.markReady(); } @@ -89,7 +90,7 @@ export class DesktopRuntime extends EventEmitter { async restartLocalServer(): Promise { await this.server.restart(); - await this.waitForReady(this.serverOrigin + '/api/health', 'server'); + await this.waitForReady(this.serverOrigin + '/api/health', 'server', this.server); this.server.markReady(); this.emitStatus(); return this.snapshot(); @@ -126,11 +127,23 @@ export class DesktopRuntime extends EventEmitter { ); } - private async waitForReady(url: string, label: string): Promise { + private async waitForReady( + url: string, + label: string, + supervisor: ProcessSupervisor + ): Promise { const deadline = Date.now() + 45_000; let lastError: string | null = null; while (Date.now() < deadline) { + const processSnapshot = supervisor.snapshot(); + if (processSnapshot.state === 'failed' || processSnapshot.state === 'stopped') { + const detail = processSnapshot.lastError ? `: ${processSnapshot.lastError}` : ''; + this.lastError = `${label} stopped before becoming ready${detail}`; + this.emitStatus(); + throw new Error(this.lastError); + } + try { const response = await fetch(url); if (response.ok) { diff --git a/desktop/src/main/updates.ts b/desktop/src/main/updates.ts new file mode 100644 index 00000000..8312a0d9 --- /dev/null +++ b/desktop/src/main/updates.ts @@ -0,0 +1,256 @@ +import type { AppUpdater } from 'electron-updater'; + +import { + redactSensitiveString, + type DesktopUpdateStatus, +} from '../shared/desktop-bridge-contracts.js'; + +type DesktopUpdateEvent = + | 'checking-for-update' + | 'update-not-available' + | 'update-available' + | 'download-progress' + | 'update-downloaded' + | 'error'; + +export interface DesktopUpdateAdapterConfigureOptions { + allowPrerelease: boolean; + autoDownload: boolean; + autoInstallOnAppQuit: boolean; + channel: DesktopUpdateStatus['channel']; + forceDevUpdateConfig: boolean; +} + +export interface DesktopUpdateAdapter { + configure(options: DesktopUpdateAdapterConfigureOptions): void; + on(event: DesktopUpdateEvent, listener: (...args: unknown[]) => void): void; + checkForUpdates(): Promise; + downloadUpdate(): Promise; + quitAndInstall(): void; + isUpdaterActive(): boolean; +} + +export interface DesktopUpdateServiceOptions { + adapter: DesktopUpdateAdapter; + channel: DesktopUpdateStatus['channel']; + currentVersion: string; + emitStatus?: (status: DesktopUpdateStatus) => void; + forceDevUpdateConfig?: boolean; + packaged: boolean; + now?: () => Date; +} + +export class ElectronAutoUpdaterAdapter implements DesktopUpdateAdapter { + constructor(private readonly updater: AppUpdater) {} + + configure(options: DesktopUpdateAdapterConfigureOptions): void { + this.updater.autoDownload = options.autoDownload; + this.updater.autoInstallOnAppQuit = options.autoInstallOnAppQuit; + this.updater.allowPrerelease = options.allowPrerelease; + this.updater.channel = options.channel === 'stable' ? null : options.channel; + this.updater.forceDevUpdateConfig = options.forceDevUpdateConfig; + } + + on(event: DesktopUpdateEvent, listener: (...args: unknown[]) => void): void { + this.updater.on(event, listener as never); + } + + checkForUpdates(): Promise { + return this.updater.checkForUpdates(); + } + + downloadUpdate(): Promise { + return this.updater.downloadUpdate(); + } + + quitAndInstall(): void { + this.updater.quitAndInstall(); + } + + isUpdaterActive(): boolean { + return this.updater.isUpdaterActive(); + } +} + +export class DesktopUpdateService { + private readonly enabled: boolean; + private status: DesktopUpdateStatus; + + constructor(private readonly options: DesktopUpdateServiceOptions) { + this.enabled = options.packaged || options.forceDevUpdateConfig === true; + this.status = this.enabled + ? this.createStatus('idle', 'Update checks are ready.') + : this.createStatus('unsupported', 'Updater checks run only from packaged builds.'); + + options.adapter.configure({ + allowPrerelease: options.channel !== 'stable', + autoDownload: false, + autoInstallOnAppQuit: false, + channel: options.channel, + forceDevUpdateConfig: options.forceDevUpdateConfig === true, + }); + this.bindEvents(); + } + + snapshot(): DesktopUpdateStatus { + return this.status; + } + + async checkForUpdates(): Promise { + if (!this.canRunUpdater()) { + return this.status; + } + + this.setStatus('checking', 'Checking for updates.'); + try { + await this.options.adapter.checkForUpdates(); + if (this.status.state === 'checking') { + this.setStatus('idle', 'No update metadata was returned.'); + } + } catch (error) { + this.setStatus('failed', redactUpdateError(error)); + } + return this.status; + } + + async downloadUpdate(): Promise { + if (!this.canRunUpdater()) { + return this.status; + } + + this.setStatus('downloading', 'Downloading update.'); + try { + await this.options.adapter.downloadUpdate(); + if (this.status.state === 'downloading') { + this.setStatus('ready', 'Update downloaded and ready to install.'); + } + } catch (error) { + this.setStatus('failed', redactUpdateError(error)); + } + return this.status; + } + + installUpdate(): DesktopUpdateStatus { + if (!this.canRunUpdater()) { + return this.status; + } + + if (this.status.state !== 'ready') { + this.setStatus('failed', 'No downloaded update is ready to install.'); + return this.status; + } + + this.options.adapter.quitAndInstall(); + this.setStatus('ready', 'Installing update.'); + return this.status; + } + + private bindEvents(): void { + this.options.adapter.on('checking-for-update', () => { + this.setStatus('checking', 'Checking for updates.'); + }); + this.options.adapter.on('update-not-available', () => { + this.setStatus('idle', 'Already running the latest version.'); + }); + this.options.adapter.on('update-available', (info) => { + this.setStatus( + 'available', + `Update ${readUpdateVersion(info) ?? 'available'} is available.`, + readUpdateVersion(info) + ); + }); + this.options.adapter.on('download-progress', (info) => { + const percent = readDownloadPercent(info); + this.setStatus( + 'downloading', + percent === null ? 'Downloading update.' : `${percent}% downloaded.` + ); + }); + this.options.adapter.on('update-downloaded', (info) => { + this.setStatus( + 'ready', + `Update ${readUpdateVersion(info) ?? 'downloaded'} is ready to install.`, + readUpdateVersion(info) + ); + }); + this.options.adapter.on('error', (error) => { + this.setStatus('failed', redactUpdateError(error)); + }); + } + + private canRunUpdater(): boolean { + if (!this.enabled) { + this.setStatus('unsupported', 'Updater checks run only from packaged builds.'); + return false; + } + + if (!this.options.adapter.isUpdaterActive()) { + this.setStatus('unsupported', 'Updater provider is not active for this build.'); + return false; + } + + return true; + } + + private setStatus( + state: DesktopUpdateStatus['state'], + detail: string, + availableVersion?: string + ): void { + this.status = this.createStatus(state, detail, availableVersion); + this.options.emitStatus?.(this.status); + } + + private createStatus( + state: DesktopUpdateStatus['state'], + detail: string, + availableVersion?: string + ): DesktopUpdateStatus { + return { + state, + currentVersion: this.options.currentVersion, + availableVersion, + channel: this.options.channel, + checkedAt: (this.options.now ?? (() => new Date()))().toISOString(), + detail, + }; + } +} + +export function resolveDesktopUpdateChannel( + requested: string | undefined, + currentVersion: string, + packaged: boolean +): DesktopUpdateStatus['channel'] { + const normalized = requested?.trim().toLowerCase(); + if (normalized === 'stable' || normalized === 'beta' || normalized === 'dev') { + return normalized; + } + if (/\b(alpha|beta|rc|next|canary|dev)\b/i.test(currentVersion)) { + return 'beta'; + } + return packaged ? 'stable' : 'dev'; +} + +function readUpdateVersion(value: unknown): string | undefined { + if (!isRecord(value)) { + return undefined; + } + const version = value.version; + return typeof version === 'string' && version.trim() ? version.trim() : undefined; +} + +function readDownloadPercent(value: unknown): number | null { + if (!isRecord(value) || typeof value.percent !== 'number' || !Number.isFinite(value.percent)) { + return null; + } + return Math.max(0, Math.min(100, Math.round(value.percent))); +} + +function redactUpdateError(error: unknown): string { + return redactSensitiveString(error instanceof Error ? error.message : String(error)); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/desktop/src/shared/desktop-bridge-contracts.ts b/desktop/src/shared/desktop-bridge-contracts.ts index 63c096df..0583f40f 100644 --- a/desktop/src/shared/desktop-bridge-contracts.ts +++ b/desktop/src/shared/desktop-bridge-contracts.ts @@ -95,6 +95,8 @@ export const DESKTOP_COMMAND_NAMES = [ 'show-diagnostics', 'create-debug-bundle', 'check-for-updates', + 'download-update', + 'install-update', 'test-notification', 'test-squad-webhook', 'copy-redacted-diagnostics', @@ -161,6 +163,7 @@ export interface DesktopDiagnosticsBundleResult { export interface DesktopUpdateStatus { state: 'unsupported' | 'idle' | 'checking' | 'available' | 'downloading' | 'ready' | 'failed'; currentVersion: string; + availableVersion?: string; channel: 'dev' | 'beta' | 'stable'; checkedAt: string; detail?: string; diff --git a/docs/DESKTOP-RELEASE.md b/docs/DESKTOP-RELEASE.md new file mode 100644 index 00000000..db7a5386 --- /dev/null +++ b/docs/DESKTOP-RELEASE.md @@ -0,0 +1,117 @@ +# Veritas Kanban Desktop Release + +This guide covers the v5 macOS desktop packaging path: unsigned PR artifacts, +signed/notarized release artifacts, update metadata, and smoke testing. + +## Local Commands + +Run these from the repository root: + +```bash +pnpm desktop:package:mac:dir +pnpm desktop:package:mac:unsigned +pnpm desktop:release:mac +``` + +`desktop:package:mac:dir` creates an unpacked local app for fast inspection. +`desktop:package:mac:unsigned` creates unsigned DMG/ZIP artifacts and update +metadata for PR validation. `desktop:release:mac` expects signing and +notarization credentials and publishes update metadata through electron-builder. + +The package step builds the workspace, stages the production server runtime in +`desktop/.desktop-release/server`, stages the built web app in +`desktop/.desktop-release/web`, and writes artifacts to `desktop/release/`. +Both staging and release directories are ignored by git. + +## GitHub Workflows + +`Desktop Artifacts` runs on desktop/server/web/shared changes and on manual +dispatch. It builds unsigned macOS artifacts on `macos-15`, uploads the DMG, +ZIP, blockmap, and update YAML files, and does not require Apple credentials. + +`Desktop Release` runs on manual dispatch or a published GitHub release. It +requires the signing secrets below, builds signed/notarized macOS artifacts, +and publishes update metadata with the GitHub provider. + +## Required Release Secrets + +Configure these repository secrets before running `Desktop Release`: + +- `MACOS_CSC_LINK`: base64 encoded `.p12` Developer ID Application certificate + or a secure URL accepted by electron-builder `CSC_LINK`. +- `MACOS_CSC_KEY_PASSWORD`: password for the `.p12` signing identity. +- `APPLE_ID`: Apple Developer account email for notarization. +- `APPLE_APP_SPECIFIC_PASSWORD`: app-specific password for notarization. +- `APPLE_TEAM_ID`: Apple Developer team ID. + +The workflow maps those secrets to electron-builder's `CSC_LINK`, +`CSC_KEY_PASSWORD`, `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and +`APPLE_TEAM_ID` environment variables. + +## Update Channels + +The app uses `electron-updater` with manual download/install behavior: + +- `stable`: default packaged release channel. +- `beta`: prerelease/test channel. +- `dev`: explicit development channel for controlled test metadata. + +Set `VERITAS_UPDATE_CHANNEL=stable|beta|dev` in release workflows or local +packaged test runs. Dev-mode update checks remain unsupported unless +`VERITAS_DESKTOP_UPDATER_FORCE_DEV=true` is set with a valid dev update config. + +The native desktop bridge exposes update status events for checking, +available, downloading, ready, failed, and unsupported states. The menu enables +download only when an update is available and install only when an update has +downloaded. + +## Release Checklist + +- Bump all workspace package versions together. +- Update `CHANGELOG.md`. +- Run `pnpm typecheck`, `pnpm lint:budget`, `pnpm build`, and + `pnpm test:unit`. +- Run `pnpm desktop:package:mac:unsigned` and inspect artifact names. +- Run `Desktop Artifacts` and download the uploaded DMG/ZIP/update metadata. +- Run `Desktop Release` only after Apple signing secrets are configured. +- Confirm notarization succeeds and the DMG installs without Gatekeeper + warnings on a clean Mac. +- Confirm a first run creates the profile/workspace app data directories. +- Confirm update check, download, install, failed-download, and rollback paths + on the selected channel. + +## Smoke Tests + +Unsigned PR artifact: + +1. Download `veritas-kanban-mac-unsigned` from the workflow run. +2. Mount the DMG and drag Veritas Kanban into `/Applications`. +3. For unsigned local artifacts only, use right-click Open or remove quarantine + with `xattr -dr com.apple.quarantine "/Applications/Veritas Kanban.app"`. +4. Launch the app and confirm the desktop status page reaches the local app. +5. Confirm app data appears under + `~/Library/Application Support/@veritas-kanban/desktop/profiles/default/workspaces/local/`. + +Signed release artifact: + +1. Install the DMG on a clean Mac. +2. Launch normally. There should be no Gatekeeper warning. +3. Confirm local server health through the desktop UI and logs. +4. Check for updates from the native menu. +5. Publish a higher test-channel build, then confirm available, downloading, + ready, and install states. + +Rollback: + +1. Quit Veritas Kanban. +2. Install the previous signed DMG. +3. Launch and confirm the existing profile/workspace data is preserved. +4. If an update artifact is bad, remove or supersede the affected GitHub + release assets and publish corrected update metadata. + +## Future Targets + +Linux and Windows packages are intentionally not v5 Mac GA blockers. The +current packaging config keeps artifact naming and update-channel conventions +portable, but Windows signing, Linux package formats, auto-launch behavior, and +OS-specific smoke tests should be handled in follow-up issues. diff --git a/package.json b/package.json index dcdd09db..c62ebc5a 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "desktop:dev": "pnpm --filter @veritas-kanban/desktop dev", "desktop:dev:fresh": "pnpm --filter @veritas-kanban/desktop dev:fresh", "desktop:build": "pnpm --filter @veritas-kanban/desktop build", + "desktop:package:mac:dir": "pnpm build && pnpm --filter @veritas-kanban/desktop package:mac:dir", + "desktop:package:mac:unsigned": "pnpm build && pnpm --filter @veritas-kanban/desktop package:mac:unsigned", + "desktop:release:mac": "pnpm build && pnpm --filter @veritas-kanban/desktop release:mac", "desktop:test": "pnpm --filter @veritas-kanban/desktop test", "dev:clean": "bash scripts/dev-clean.sh", "dev:watchdog": "bash scripts/dev-watchdog.sh", @@ -65,19 +68,6 @@ "prettier --write" ] }, - "pnpm": { - "overrides": { - "@xmldom/xmldom": ">=0.8.13", - "fast-uri": ">=3.1.2", - "hono": ">=4.12.18", - "ip-address": ">=10.1.1", - "postcss": ">=8.5.10", - "qs": "^6.14.2", - "minimatch": ">=10.2.3", - "path-to-regexp": ">=8.4.0", - "tmp": ">=0.2.6" - } - }, "repository": { "type": "git", "url": "https://github.com/BradGroux/veritas-kanban.git" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76fd53cc..f4cd8e27 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,17 +4,6 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false -overrides: - '@xmldom/xmldom': '>=0.8.13' - fast-uri: '>=3.1.2' - hono: '>=4.12.18' - ip-address: '>=10.1.1' - postcss: '>=8.5.10' - qs: ^6.14.2 - minimatch: '>=10.2.3' - path-to-regexp: '>=8.4.0' - tmp: '>=0.2.6' - importers: .: devDependencies: @@ -85,6 +74,9 @@ importers: '@veritas-kanban/shared': specifier: workspace:* version: link:../shared + electron-updater: + specifier: ^6.8.3 + version: 6.8.3 devDependencies: '@types/node': specifier: ^25.7.0 @@ -92,6 +84,9 @@ importers: electron: specifier: ^39.2.6 version: 39.8.10 + electron-builder: + specifier: ^26.8.1 + version: 26.8.1(electron-builder-squirrel-windows@26.8.1) electron-vite: specifier: ^5.0.0 version: 5.0.0(vite@7.3.3(@types/node@25.7.0)(jiti@2.7.0)(lightningcss@1.32.0)(sugarss@5.0.1(postcss@8.5.14))(tsx@4.21.0)(yaml@2.9.0)) @@ -114,7 +109,7 @@ importers: specifier: workspace:* version: link:../shared hono: - specifier: '>=4.12.18' + specifier: ^4.12.18 version: 4.12.18 zod: specifier: ^4.4.3 @@ -417,7 +412,7 @@ importers: specifier: ^29.1.1 version: 29.1.1(@noble/hashes@1.8.0) postcss: - specifier: '>=8.5.10' + specifier: ^8.5.14 version: 8.5.14 postcss-preset-mantine: specifier: ^1.18.0 @@ -439,6 +434,12 @@ importers: version: 4.1.6(@types/node@25.7.0)(@vitest/coverage-v8@4.1.6)(jsdom@29.1.1(@noble/hashes@1.8.0))(msw@2.14.3(@types/node@25.7.0)(typescript@6.0.3))(vite@8.0.12(@types/node@25.7.0)(esbuild@0.27.7)(jiti@2.7.0)(sugarss@5.0.1(postcss@8.5.14))(tsx@4.21.0)(yaml@2.9.0)) packages: + 7zip-bin@5.2.0: + resolution: + { + integrity: sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==, + } + '@adobe/css-tools@4.4.4': resolution: { @@ -827,6 +828,13 @@ packages: } engines: { node: '>=20.19.0' } + '@develar/schema-utils@2.6.5': + resolution: + { + integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==, + } + engines: { node: '>= 8.9.0' } + '@dnd-kit/accessibility@3.1.1': resolution: { @@ -877,6 +885,21 @@ packages: peerDependencies: '@noble/ciphers': ^1.0.0 + '@electron/asar@3.4.1': + resolution: + { + integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==, + } + engines: { node: '>=10.12.0' } + hasBin: true + + '@electron/fuses@1.8.0': + resolution: + { + integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==, + } + hasBin: true + '@electron/get@2.0.3': resolution: { @@ -884,6 +907,51 @@ packages: } engines: { node: '>=12' } + '@electron/get@3.1.0': + resolution: + { + integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==, + } + engines: { node: '>=14' } + + '@electron/notarize@2.5.0': + resolution: + { + integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==, + } + engines: { node: '>= 10.0.0' } + + '@electron/osx-sign@1.3.3': + resolution: + { + integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==, + } + engines: { node: '>=12.0.0' } + hasBin: true + + '@electron/rebuild@4.0.4': + resolution: + { + integrity: sha512-Rzc39XPdk/+/wBG8MfwAHohXflep0ITUfulb6Rgz3R0NeSB1noE+E9/M/cb8ftCAiyDD9PPhLuuWgE1GaInbKg==, + } + engines: { node: '>=22.12.0' } + hasBin: true + + '@electron/universal@2.0.3': + resolution: + { + integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==, + } + engines: { node: '>=16.4' } + + '@electron/windows-sign@1.2.2': + resolution: + { + integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==, + } + engines: { node: '>=14.14' } + hasBin: true + '@emnapi/core@1.10.0': resolution: { @@ -1509,7 +1577,7 @@ packages: } engines: { node: '>=18.14.1' } peerDependencies: - hono: '>=4.12.18' + hono: ^4 '@humanfs/core@0.19.1': resolution: @@ -1589,6 +1657,13 @@ packages: '@types/node': optional: true + '@isaacs/fs-minipass@4.0.1': + resolution: + { + integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==, + } + engines: { node: '>=18.0.0' } + '@jridgewell/gen-mapping@0.3.13': resolution: { @@ -1638,6 +1713,20 @@ packages: integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==, } + '@malept/cross-spawn-promise@2.0.0': + resolution: + { + integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==, + } + engines: { node: '>= 12.13.0' } + + '@malept/flatpak-bundler@0.4.0': + resolution: + { + integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==, + } + engines: { node: '>= 10.0.0' } + '@mantine/core@9.2.2': resolution: { @@ -2736,6 +2825,12 @@ packages: integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==, } + '@types/fs-extra@9.0.13': + resolution: + { + integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==, + } + '@types/hast@3.0.4': resolution: { @@ -2820,6 +2915,12 @@ packages: integrity: sha512-z+pdZyxE+RTQE9AcboAZCb4otwcrvgHD+GlBpPgn0emDVt0ohrTMhAwlr2Wd9nZ+nihhYFxO2pThz3C5qSu2Eg==, } + '@types/plist@3.0.5': + resolution: + { + integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==, + } + '@types/qs@6.15.0': resolution: { @@ -2943,6 +3044,12 @@ packages: integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==, } + '@types/verror@1.10.11': + resolution: + { + integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==, + } + '@types/ws@8.18.1': resolution: { @@ -3122,6 +3229,13 @@ packages: integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==, } + '@xmldom/xmldom@0.8.13': + resolution: + { + integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==, + } + engines: { node: '>=10.0.0' } + '@xmldom/xmldom@0.9.10': resolution: { @@ -3129,6 +3243,13 @@ packages: } engines: { node: '>=14.6' } + abbrev@4.0.0: + resolution: + { + integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==, + } + engines: { node: ^20.17.0 || >=22.9.0 } + accepts@2.0.0: resolution: { @@ -3170,6 +3291,14 @@ packages: ajv: optional: true + ajv-keywords@3.5.2: + resolution: + { + integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==, + } + peerDependencies: + ajv: ^6.9.1 + ajv@6.15.0: resolution: { @@ -3230,6 +3359,22 @@ packages: } engines: { node: '>=12' } + app-builder-bin@5.0.0-alpha.12: + resolution: + { + integrity: sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==, + } + + app-builder-lib@26.8.1: + resolution: + { + integrity: sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw==, + } + engines: { node: '>=14.0.0' } + peerDependencies: + dmg-builder: 26.8.1 + electron-builder-squirrel-windows: 26.8.1 + append-field@1.0.0: resolution: { @@ -3337,6 +3482,13 @@ packages: integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==, } + assert-plus@1.0.0: + resolution: + { + integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==, + } + engines: { node: '>=0.8' } + assertion-error@2.0.1: resolution: { @@ -3357,6 +3509,20 @@ packages: integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==, } + astral-regex@2.0.0: + resolution: + { + integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==, + } + engines: { node: '>=8' } + + async-exit-hook@2.0.1: + resolution: + { + integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==, + } + engines: { node: '>=0.12.0' } + async-function@1.0.0: resolution: { @@ -3376,6 +3542,13 @@ packages: integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, } + at-least-node@1.0.0: + resolution: + { + integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==, + } + engines: { node: '>= 4.0.0' } + atomic-sleep@1.0.0: resolution: { @@ -3391,7 +3564,7 @@ packages: engines: { node: ^10 || ^12 || >=14 } hasBin: true peerDependencies: - postcss: '>=8.5.10' + postcss: ^8.1.0 available-typed-arrays@1.0.7: resolution: @@ -3406,6 +3579,12 @@ packages: integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==, } + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + balanced-match@4.0.4: resolution: { @@ -3479,6 +3658,18 @@ packages: } deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + brace-expansion@1.1.15: + resolution: + { + integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==, + } + + brace-expansion@2.1.1: + resolution: + { + integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==, + } + brace-expansion@5.0.5: resolution: { @@ -3539,6 +3730,19 @@ packages: } engines: { node: '>=0.2.0' } + builder-util-runtime@9.5.1: + resolution: + { + integrity: sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==, + } + engines: { node: '>=12.0.0' } + + builder-util@26.8.1: + resolution: + { + integrity: sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw==, + } + bundle-name@4.1.0: resolution: { @@ -3691,6 +3895,33 @@ packages: integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==, } + chownr@3.0.0: + resolution: + { + integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==, + } + engines: { node: '>=18' } + + chromium-pickle-js@0.2.0: + resolution: + { + integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==, + } + + ci-info@4.3.1: + resolution: + { + integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==, + } + engines: { node: '>=8' } + + ci-info@4.4.0: + resolution: + { + integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==, + } + engines: { node: '>=8' } + class-variance-authority@0.7.1: resolution: { @@ -3711,6 +3942,13 @@ packages: } engines: { node: '>=6' } + cli-truncate@2.1.0: + resolution: + { + integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==, + } + engines: { node: '>=8' } + cli-truncate@5.2.0: resolution: { @@ -3797,6 +4035,13 @@ packages: } engines: { node: '>=20' } + commander@5.1.0: + resolution: + { + integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==, + } + engines: { node: '>= 6' } + commander@6.2.0: resolution: { @@ -3811,6 +4056,13 @@ packages: } engines: { node: ^12.20.0 || >=14 } + compare-version@0.1.2: + resolution: + { + integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==, + } + engines: { node: '>=0.10.0' } + component-emitter@1.3.1: resolution: { @@ -3838,6 +4090,12 @@ packages: } engines: { node: '>= 0.8.0' } + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } + concat-stream@2.0.0: resolution: { @@ -3920,6 +4178,12 @@ packages: integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==, } + core-util-is@1.0.2: + resolution: + { + integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==, + } + core-util-is@1.0.3: resolution: { @@ -3960,6 +4224,18 @@ packages: } engines: { node: '>= 10' } + crc@3.8.0: + resolution: + { + integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==, + } + + cross-dirname@0.1.0: + resolution: + { + integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==, + } + cross-spawn@7.0.6: resolution: { @@ -4297,6 +4573,27 @@ packages: integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==, } + dir-compare@4.2.0: + resolution: + { + integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==, + } + + dmg-builder@26.8.1: + resolution: + { + integrity: sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==, + } + + dmg-license@1.0.11: + resolution: + { + integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==, + } + engines: { node: '>=8' } + os: [darwin] + hasBin: true + doctrine@2.1.0: resolution: { @@ -4360,6 +4657,20 @@ packages: integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==, } + dotenv-expand@11.0.7: + resolution: + { + integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==, + } + engines: { node: '>=12' } + + dotenv@16.6.1: + resolution: + { + integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==, + } + engines: { node: '>=12' } + dotenv@17.4.2: resolution: { @@ -4405,12 +4716,46 @@ packages: integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, } + ejs@3.1.10: + resolution: + { + integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==, + } + engines: { node: '>=0.10.0' } + hasBin: true + + electron-builder-squirrel-windows@26.8.1: + resolution: + { + integrity: sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA==, + } + + electron-builder@26.8.1: + resolution: + { + integrity: sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw==, + } + engines: { node: '>=14.0.0' } + hasBin: true + + electron-publish@26.8.1: + resolution: + { + integrity: sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w==, + } + electron-to-chromium@1.5.351: resolution: { integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==, } + electron-updater@6.8.3: + resolution: + { + integrity: sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ==, + } + electron-vite@5.0.0: resolution: { @@ -4425,6 +4770,13 @@ packages: '@swc/core': optional: true + electron-winstaller@5.4.0: + resolution: + { + integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==, + } + engines: { node: '>=8.0.0' } + electron@39.8.10: resolution: { @@ -4500,6 +4852,12 @@ packages: } engines: { node: '>=18' } + err-code@2.0.3: + resolution: + { + integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==, + } + error-ex@1.3.4: resolution: { @@ -4792,6 +5150,12 @@ packages: } engines: { node: '>=12.0.0' } + exponential-backoff@3.1.3: + resolution: + { + integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==, + } + express-rate-limit@8.5.0: resolution: { @@ -4838,6 +5202,13 @@ packages: engines: { node: '>= 10.17.0' } hasBin: true + extsprintf@1.4.1: + resolution: + { + integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==, + } + engines: { '0': node >=0.6.0 } + fast-copy@4.0.3: resolution: { @@ -4964,6 +5335,12 @@ packages: } engines: { node: '>=22' } + filelist@1.0.6: + resolution: + { + integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==, + } + fill-range@7.1.1: resolution: { @@ -5052,6 +5429,13 @@ packages: integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==, } + fs-extra@10.1.0: + resolution: + { + integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==, + } + engines: { node: '>=12' } + fs-extra@11.3.4: resolution: { @@ -5059,6 +5443,13 @@ packages: } engines: { node: '>=14.14' } + fs-extra@7.0.1: + resolution: + { + integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==, + } + engines: { node: '>=6 <7 || >=8' } + fs-extra@8.1.0: resolution: { @@ -5066,6 +5457,13 @@ packages: } engines: { node: '>=6 <7 || >=8' } + fs-extra@9.1.0: + resolution: + { + integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==, + } + engines: { node: '>=10' } + fs.realpath@1.0.0: resolution: { @@ -5418,6 +5816,13 @@ packages: } engines: { node: '>=16.9.0' } + hosted-git-info@4.1.0: + resolution: + { + integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==, + } + engines: { node: '>=10' } + html-encoding-sniffer@6.0.0: resolution: { @@ -5456,6 +5861,13 @@ packages: } engines: { node: '>= 0.8' } + http-proxy-agent@7.0.2: + resolution: + { + integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==, + } + engines: { node: '>= 14' } + http2-wrapper@1.0.3: resolution: { @@ -5492,6 +5904,21 @@ packages: engines: { node: '>=18' } hasBin: true + iconv-corefoundation@1.1.7: + resolution: + { + integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==, + } + engines: { node: ^8.11.2 || >=10 } + os: [darwin] + + iconv-lite@0.6.3: + resolution: + { + integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, + } + engines: { node: '>=0.10.0' } + iconv-lite@0.7.2: resolution: { @@ -5591,6 +6018,13 @@ packages: } engines: { node: '>=12' } + ip-address@10.1.0: + resolution: + { + integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==, + } + engines: { node: '>= 12' } + ip-address@10.2.0: resolution: { @@ -5954,6 +6388,20 @@ packages: integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==, } + isbinaryfile@4.0.10: + resolution: + { + integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==, + } + engines: { node: '>= 8.0.0' } + + isbinaryfile@5.0.7: + resolution: + { + integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==, + } + engines: { node: '>= 18.0.0' } + isexe@2.0.0: resolution: { @@ -5967,6 +6415,13 @@ packages: } engines: { node: '>=18' } + isexe@4.0.0: + resolution: + { + integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==, + } + engines: { node: '>=20' } + istanbul-lib-coverage@3.2.2: resolution: { @@ -5995,6 +6450,14 @@ packages: } engines: { node: '>= 0.4' } + jake@10.9.4: + resolution: + { + integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==, + } + engines: { node: '>=10' } + hasBin: true + jiti@2.7.0: resolution: { @@ -6195,6 +6658,12 @@ packages: integrity: sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw==, } + lazy-val@1.0.5: + resolution: + { + integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==, + } + lazystream@1.0.1: resolution: { @@ -6483,6 +6952,12 @@ packages: integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==, } + lodash@4.18.1: + resolution: + { + integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==, + } + log-symbols@6.0.0: resolution: { @@ -6542,6 +7017,13 @@ packages: integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, } + lru-cache@6.0.0: + resolution: + { + integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==, + } + engines: { node: '>=10' } + lucide-react@1.14.0: resolution: { @@ -6994,12 +7476,46 @@ packages: } engines: { node: 18 || 20 || >=22 } + minimatch@3.1.5: + resolution: + { + integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, + } + + minimatch@5.1.9: + resolution: + { + integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==, + } + engines: { node: '>=10' } + + minimatch@9.0.9: + resolution: + { + integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==, + } + engines: { node: '>=16 || 14 >=14.17' } + minimist@1.2.8: resolution: { integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, } + minipass@7.1.3: + resolution: + { + integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==, + } + engines: { node: '>=16 || 14 >=14.17' } + + minizlib@3.1.0: + resolution: + { + integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==, + } + engines: { node: '>= 18' } + mkdirp@0.5.6: resolution: { @@ -7082,6 +7598,19 @@ packages: } engines: { node: '>= 0.6' } + node-abi@4.31.0: + resolution: + { + integrity: sha512-Erq5w/t3syw3s4sDsUaX4QttIdBPsGKTT1DTRsCkTonGggczhlDKm/wDX3o+HPJpQ41EjXCbcmXf0tgr5YZJXw==, + } + engines: { node: '>=22.12.0' } + + node-addon-api@1.7.2: + resolution: + { + integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==, + } + node-addon-api@8.7.0: resolution: { @@ -7089,6 +7618,12 @@ packages: } engines: { node: ^18 || ^20 || >= 21 } + node-api-version@0.2.1: + resolution: + { + integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==, + } + node-domexception@1.0.0: resolution: { @@ -7118,12 +7653,28 @@ packages: } hasBin: true + node-gyp@12.3.0: + resolution: + { + integrity: sha512-QNcUWM+HgJplcPzBvFBZ9VXacyGZ4+VTOb80PwWR+TlVzoHbRKULNEzpRsnaoxG3Wzr7Qh7BYxGDU3CbKib2Yg==, + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + node-releases@2.0.38: resolution: { integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==, } + nopt@9.0.0: + resolution: + { + integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==, + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + normalize-path@3.0.0: resolution: { @@ -7414,6 +7965,12 @@ packages: integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==, } + path-to-regexp@6.3.0: + resolution: + { + integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==, + } + path-to-regexp@8.4.2: resolution: { @@ -7426,6 +7983,13 @@ packages: integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==, } + pe-library@0.4.1: + resolution: + { + integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==, + } + engines: { node: '>=12', npm: '>=6' } + pend@1.2.0: resolution: { @@ -7501,6 +8065,20 @@ packages: engines: { node: '>=18' } hasBin: true + plist@3.1.0: + resolution: + { + integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==, + } + engines: { node: '>=10.4.0' } + + plist@3.1.1: + resolution: + { + integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==, + } + engines: { node: '>=10.4.0' } + possible-typed-array-names@1.1.0: resolution: { @@ -7515,7 +8093,7 @@ packages: } engines: { node: ^12 || ^14 || >= 16 } peerDependencies: - postcss: '>=8.5.10' + postcss: ^8.4.21 postcss-mixins@12.1.2: resolution: @@ -7524,7 +8102,7 @@ packages: } engines: { node: ^20.0 || ^22.0 || >=24.0 } peerDependencies: - postcss: '>=8.5.10' + postcss: ^8.2.14 postcss-nested@7.0.2: resolution: @@ -7533,7 +8111,7 @@ packages: } engines: { node: '>=18.0' } peerDependencies: - postcss: '>=8.5.10' + postcss: ^8.2.14 postcss-preset-mantine@1.18.0: resolution: @@ -7541,7 +8119,7 @@ packages: integrity: sha512-sP6/s1oC7cOtBdl4mw/IRKmKvYTuzpRrH/vT6v9enMU/EQEQ31eQnHcWtFghOXLH87AAthjL/Q75rLmin1oZoA==, } peerDependencies: - postcss: '>=8.5.10' + postcss: '>=8.0.0' postcss-selector-parser@7.1.1: resolution: @@ -7557,7 +8135,7 @@ packages: } engines: { node: '>=14.0' } peerDependencies: - postcss: '>=8.5.10' + postcss: ^8.2.1 postcss-value-parser@4.2.0: resolution: @@ -7572,6 +8150,14 @@ packages: } engines: { node: ^10 || ^12 || >=14 } + postject@1.0.0-alpha.6: + resolution: + { + integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==, + } + engines: { node: '>=14.0.0' } + hasBin: true + powershell-utils@0.1.0: resolution: { @@ -7608,6 +8194,13 @@ packages: } engines: { node: '>=18' } + proc-log@6.1.0: + resolution: + { + integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==, + } + engines: { node: ^20.17.0 || >=22.9.0 } + process-nextick-args@2.0.1: resolution: { @@ -7627,6 +8220,13 @@ packages: } engines: { node: '>=0.4.0' } + promise-retry@2.0.1: + resolution: + { + integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==, + } + engines: { node: '>=10' } + prompts@2.4.2: resolution: { @@ -7640,6 +8240,12 @@ packages: integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, } + proper-lockfile@4.1.2: + resolution: + { + integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==, + } + property-information@7.1.0: resolution: { @@ -7848,6 +8454,13 @@ packages: } engines: { node: '>=0.10.0' } + read-binary-file-arch@1.0.6: + resolution: + { + integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==, + } + hasBin: true + readable-stream@2.3.8: resolution: { @@ -7971,6 +8584,13 @@ packages: } engines: { node: '>=0.10.0' } + resedit@1.7.2: + resolution: + { + integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==, + } + engines: { node: '>=12', npm: '>=6' } + reselect@5.1.1: resolution: { @@ -8023,6 +8643,13 @@ packages: } engines: { node: '>=18' } + retry@0.12.0: + resolution: + { + integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==, + } + engines: { node: '>= 4' } + rettime@0.11.11: resolution: { @@ -8042,6 +8669,14 @@ packages: integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, } + rimraf@2.6.3: + resolution: + { + integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==, + } + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@2.7.1: resolution: { @@ -8157,6 +8792,13 @@ packages: integrity: sha512-2HW7v2ol/uAM7sX4hbD8Z59OGWmAPrvjL8E71UWlBcj6m+kcF6ilQBLny+cIgY214QJeJT5tQuxKKqX0SQqjGQ==, } + sax@1.6.0: + resolution: + { + integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==, + } + engines: { node: '>=11.0.0' } + saxes@5.0.1: resolution: { @@ -8196,6 +8838,13 @@ packages: integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==, } + semver@5.7.2: + resolution: + { + integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==, + } + hasBin: true + semver@6.3.1: resolution: { @@ -8360,12 +9009,26 @@ packages: integrity: sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q==, } + simple-update-notifier@2.0.0: + resolution: + { + integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==, + } + engines: { node: '>=10' } + sisteransi@1.0.5: resolution: { integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, } + slice-ansi@3.0.0: + resolution: + { + integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==, + } + engines: { node: '>=8' } + slice-ansi@7.1.2: resolution: { @@ -8380,6 +9043,13 @@ packages: } engines: { node: '>=20' } + smart-buffer@4.2.0: + resolution: + { + integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==, + } + engines: { node: '>= 6.0.0', npm: '>= 3.0.0' } + sonic-boom@4.2.1: resolution: { @@ -8393,6 +9063,12 @@ packages: } engines: { node: '>=0.10.0' } + source-map-support@0.5.21: + resolution: + { + integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==, + } + source-map@0.6.1: resolution: { @@ -8431,6 +9107,13 @@ packages: integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==, } + stat-mode@1.0.0: + resolution: + { + integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==, + } + engines: { node: '>= 6' } + statuses@2.0.2: resolution: { @@ -8647,7 +9330,7 @@ packages: } engines: { node: '>=18.0' } peerDependencies: - postcss: '>=8.5.10' + postcss: ^8.3.3 sumchecker@3.0.1: resolution: @@ -8774,6 +9457,26 @@ packages: } engines: { node: '>=6' } + tar@7.5.15: + resolution: + { + integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==, + } + engines: { node: '>=18' } + + temp-file@3.4.0: + resolution: + { + integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==, + } + + temp@0.9.4: + resolution: + { + integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==, + } + engines: { node: '>=6.0.0' } + thread-stream@4.0.0: resolution: { @@ -8781,12 +9484,24 @@ packages: } engines: { node: '>=20' } + tiny-async-pool@1.3.0: + resolution: + { + integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==, + } + 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==, + } + tinybench@2.9.0: resolution: { @@ -8827,6 +9542,12 @@ packages: } hasBin: true + tmp-promise@3.0.3: + resolution: + { + integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==, + } + tmp@0.2.7: resolution: { @@ -9051,6 +9772,13 @@ packages: integrity: sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ==, } + undici@6.26.0: + resolution: + { + integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==, + } + engines: { node: '>=18.17' } + undici@7.25.0: resolution: { @@ -9241,6 +9969,13 @@ packages: } engines: { node: '>= 0.8' } + verror@1.10.1: + resolution: + { + integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==, + } + engines: { node: '>=0.6.0' } + vfile-message@4.0.3: resolution: { @@ -9471,6 +10206,22 @@ packages: engines: { node: ^16.13.0 || >=18.0.0 } hasBin: true + which@5.0.0: + resolution: + { + integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==, + } + engines: { node: ^18.17.0 || >=20.5.0 } + hasBin: true + + which@6.0.1: + resolution: + { + integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==, + } + engines: { node: ^20.17.0 || >=22.9.0 } + hasBin: true + why-is-node-running@2.3.0: resolution: { @@ -9549,6 +10300,13 @@ packages: } engines: { node: '>=4.0' } + xmlbuilder@15.1.1: + resolution: + { + integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==, + } + engines: { node: '>=8.0' } + xmlchars@2.2.0: resolution: { @@ -9568,6 +10326,19 @@ packages: integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, } + yallist@4.0.0: + resolution: + { + integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, + } + + yallist@5.0.0: + resolution: + { + integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==, + } + engines: { node: '>=18' } + yaml@2.0.0-1: resolution: { @@ -9681,6 +10452,8 @@ packages: } snapshots: + 7zip-bin@5.2.0: {} + '@adobe/css-tools@4.4.4': {} '@apidevtools/json-schema-ref-parser@9.1.2': @@ -9961,6 +10734,11 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} + '@develar/schema-utils@2.6.5': + dependencies: + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + '@dnd-kit/accessibility@3.1.1(react@19.2.6)': dependencies: react: 19.2.6 @@ -10003,6 +10781,18 @@ snapshots: dependencies: '@noble/ciphers': 1.3.0 + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + '@electron/get@2.0.3': dependencies: debug: 4.4.3 @@ -10017,6 +10807,73 @@ snapshots: transitivePeerDependencies: - supports-color + '@electron/get@3.1.0': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@4.0.4': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + node-abi: 4.31.0 + node-api-version: 0.2.1 + node-gyp: 12.3.0 + read-binary-file-arch: 1.0.6 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.3': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + dir-compare: 4.2.0 + fs-extra: 11.3.4 + minimatch: 9.0.9 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.3 + fs-extra: 11.3.4 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + '@emnapi/core@1.10.0': dependencies: '@emnapi/wasi-threads': 1.2.1 @@ -10200,7 +11057,7 @@ snapshots: dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 - minimatch: 10.2.5 + minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -10225,7 +11082,7 @@ snapshots: ignore: 5.3.2 import-fresh: 3.3.1 js-yaml: 4.1.1 - minimatch: 10.2.5 + minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color @@ -10329,6 +11186,10 @@ snapshots: optionalDependencies: '@types/node': 25.7.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -10358,6 +11219,19 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + '@mantine/core@9.2.2(@mantine/hooks@9.2.2(react@19.2.6))(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@floating-ui/react': 0.27.19(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -10962,6 +11836,10 @@ snapshots: '@types/express-serve-static-core': 5.1.1 '@types/serve-static': 2.2.0 + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 25.7.0 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -11005,6 +11883,12 @@ snapshots: dependencies: undici-types: 7.21.0 + '@types/plist@3.0.5': + dependencies: + '@types/node': 25.7.0 + xmlbuilder: 15.1.1 + optional: true + '@types/qs@6.15.0': {} '@types/range-parser@1.2.7': {} @@ -11077,6 +11961,9 @@ snapshots: '@types/validate-npm-package-name@4.0.2': {} + '@types/verror@1.10.11': + optional: true + '@types/ws@8.18.1': dependencies: '@types/node': 25.7.0 @@ -11256,7 +12143,12 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@xmldom/xmldom@0.9.10': {} + '@xmldom/xmldom@0.8.13': {} + + '@xmldom/xmldom@0.9.10': + optional: true + + abbrev@4.0.0: {} accepts@2.0.0: dependencies: @@ -11275,6 +12167,10 @@ snapshots: optionalDependencies: ajv: 8.18.0 + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -11312,6 +12208,51 @@ snapshots: ansi-styles@6.2.3: {} + app-builder-bin@5.0.0-alpha.12: {} + + app-builder-lib@26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1): + dependencies: + '@develar/schema-utils': 2.6.5 + '@electron/asar': 3.4.1 + '@electron/fuses': 1.8.0 + '@electron/get': 3.1.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.3 + '@electron/rebuild': 4.0.4 + '@electron/universal': 2.0.3 + '@malept/flatpak-bundler': 0.4.0 + '@types/fs-extra': 9.0.13 + async-exit-hook: 2.0.1 + builder-util: 26.8.1 + builder-util-runtime: 9.5.1 + chromium-pickle-js: 0.2.0 + ci-info: 4.3.1 + debug: 4.4.3 + dmg-builder: 26.8.1(electron-builder-squirrel-windows@26.8.1) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.8.1(dmg-builder@26.8.1) + electron-publish: 26.8.1 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + isbinaryfile: 5.0.7 + jiti: 2.7.0 + js-yaml: 4.1.1 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.5 + plist: 3.1.0 + proper-lockfile: 4.1.2 + resedit: 1.7.2 + semver: 7.7.4 + tar: 7.5.15 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + append-field@1.0.0: {} archiver-utils@2.1.0: @@ -11421,6 +12362,9 @@ snapshots: asap@2.0.6: {} + assert-plus@1.0.0: + optional: true + assertion-error@2.0.1: {} ast-types@0.16.1: @@ -11433,12 +12377,19 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + astral-regex@2.0.0: + optional: true + + async-exit-hook@2.0.1: {} + async-function@1.0.0: {} async@3.2.6: {} asynckit@0.4.0: {} + at-least-node@1.0.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.5.0(postcss@8.5.14): @@ -11456,6 +12407,8 @@ snapshots: bail@2.0.2: {} + balanced-match@1.0.2: {} + balanced-match@4.0.4: {} base64-js@1.5.1: {} @@ -11503,6 +12456,15 @@ snapshots: boolean@3.2.0: optional: true + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.5: dependencies: balanced-match: 4.0.4 @@ -11534,6 +12496,34 @@ snapshots: buffers@0.1.1: {} + builder-util-runtime@9.5.1: + dependencies: + debug: 4.4.3 + sax: 1.6.0 + transitivePeerDependencies: + - supports-color + + builder-util@26.8.1: + dependencies: + 7zip-bin: 5.2.0 + '@types/debug': 4.1.13 + app-builder-bin: 5.0.0-alpha.12 + builder-util-runtime: 9.5.1 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + js-yaml: 4.1.1 + sanitize-filename: 1.6.4 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -11608,6 +12598,14 @@ snapshots: character-reference-invalid@2.0.1: {} + chownr@3.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@4.3.1: {} + + ci-info@4.4.0: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -11618,6 +12616,12 @@ snapshots: cli-spinners@2.9.2: {} + cli-truncate@2.1.0: + dependencies: + slice-ansi: 3.0.0 + string-width: 4.2.3 + optional: true + cli-truncate@5.2.0: dependencies: slice-ansi: 8.0.0 @@ -11657,11 +12661,15 @@ snapshots: commander@14.0.3: {} + commander@5.1.0: {} + commander@6.2.0: {} commander@9.5.0: optional: true + compare-version@0.1.2: {} + component-emitter@1.3.1: {} compress-commons@4.1.2: @@ -11687,6 +12695,8 @@ snapshots: transitivePeerDependencies: - supports-color + concat-map@0.0.1: {} + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -11726,6 +12736,9 @@ snapshots: cookiejar@2.1.4: {} + core-util-is@1.0.2: + optional: true + core-util-is@1.0.3: {} cors@2.8.6: @@ -11749,6 +12762,14 @@ snapshots: crc-32: 1.2.2 readable-stream: 3.6.2 + crc@3.8.0: + dependencies: + buffer: 5.7.1 + optional: true + + cross-dirname@0.1.0: + optional: true + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -11910,6 +12931,36 @@ snapshots: dingbat-to-unicode@1.0.1: {} + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@26.8.1(electron-builder-squirrel-windows@26.8.1): + dependencies: + app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1) + builder-util: 26.8.1 + fs-extra: 10.1.0 + iconv-lite: 0.6.3 + js-yaml: 4.1.1 + optionalDependencies: + dmg-license: 1.0.11 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + dmg-license@1.0.11: + dependencies: + '@types/plist': 3.0.5 + '@types/verror': 1.10.11 + ajv: 6.15.0 + crc: 3.8.0 + iconv-corefoundation: 1.1.7 + plist: 3.1.1 + smart-buffer: 4.2.0 + verror: 1.10.1 + optional: true + doctrine@2.1.0: dependencies: esutils: 2.0.3 @@ -11949,6 +13000,12 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + dotenv@17.4.2: {} duck@0.1.12: @@ -11978,8 +13035,63 @@ snapshots: ee-first@1.1.1: {} + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@26.8.1(dmg-builder@26.8.1): + dependencies: + app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1) + builder-util: 26.8.1 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@26.8.1(electron-builder-squirrel-windows@26.8.1): + dependencies: + app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1) + builder-util: 26.8.1 + builder-util-runtime: 9.5.1 + chalk: 4.1.2 + ci-info: 4.4.0 + dmg-builder: 26.8.1(electron-builder-squirrel-windows@26.8.1) + fs-extra: 10.1.0 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-publish@26.8.1: + dependencies: + '@types/fs-extra': 9.0.13 + builder-util: 26.8.1 + builder-util-runtime: 9.5.1 + chalk: 4.1.2 + form-data: 4.0.5 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + electron-to-chromium@1.5.351: {} + electron-updater@6.8.3: + dependencies: + builder-util-runtime: 9.5.1 + fs-extra: 10.1.0 + js-yaml: 4.1.1 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.7.4 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + electron-vite@5.0.0(vite@7.3.3(@types/node@25.7.0)(jiti@2.7.0)(lightningcss@1.32.0)(sugarss@5.0.1(postcss@8.5.14))(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@babel/core': 7.29.0 @@ -11992,6 +13104,18 @@ snapshots: transitivePeerDependencies: - supports-color + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.3 + fs-extra: 7.0.1 + lodash: 4.18.1 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2 + transitivePeerDependencies: + - supports-color + electron@39.8.10: dependencies: '@electron/get': 2.0.3 @@ -12025,6 +13149,8 @@ snapshots: environment@1.1.0: {} + err-code@2.0.3: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -12226,7 +13352,7 @@ snapshots: estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 - minimatch: 10.2.5 + minimatch: 3.1.5 object.entries: 1.1.9 object.fromentries: 2.0.8 object.values: 1.2.1 @@ -12280,7 +13406,7 @@ snapshots: is-glob: 4.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 10.2.5 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: @@ -12365,10 +13491,12 @@ snapshots: expect-type@1.3.0: {} + exponential-backoff@3.1.3: {} + express-rate-limit@8.5.0(express@5.2.1): dependencies: express: 5.2.1 - ip-address: 10.2.0 + ip-address: 10.1.0 express-rate-limit@8.5.1(express@5.2.1): dependencies: @@ -12424,6 +13552,9 @@ snapshots: transitivePeerDependencies: - supports-color + extsprintf@1.4.1: + optional: true + fast-copy@4.0.3: {} fast-csv@4.3.6: @@ -12495,6 +13626,10 @@ snapshots: transitivePeerDependencies: - supports-color + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -12552,18 +13687,37 @@ snapshots: fs-constants@1.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 jsonfile: 6.2.1 universalify: 2.0.1 + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs.realpath@1.0.0: {} fsevents@2.3.2: @@ -12660,7 +13814,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 10.2.5 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -12669,7 +13823,7 @@ snapshots: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 - minimatch: 10.2.5 + minimatch: 3.1.5 once: 1.4.0 path-is-absolute: 1.0.1 @@ -12793,6 +13947,10 @@ snapshots: hono@4.12.18: {} + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + html-encoding-sniffer@6.0.0(@noble/hashes@1.8.0): dependencies: '@exodus/bytes': 1.15.0(@noble/hashes@1.8.0) @@ -12820,6 +13978,13 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http2-wrapper@1.0.3: dependencies: quick-lru: 5.1.1 @@ -12838,6 +14003,16 @@ snapshots: husky@9.1.7: {} + iconv-corefoundation@1.1.7: + dependencies: + cli-truncate: 2.1.0 + node-addon-api: 1.7.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -12880,6 +14055,8 @@ snapshots: internmap@2.0.3: {} + ip-address@10.1.0: {} + ip-address@10.2.0: {} ipaddr.js@1.9.1: {} @@ -13053,10 +14230,16 @@ snapshots: isarray@2.0.5: {} + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + isexe@2.0.0: {} isexe@3.1.5: {} + isexe@4.0.0: {} + istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -13079,6 +14262,12 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + jiti@2.7.0: {} jose@6.2.2: {} @@ -13207,6 +14396,8 @@ snapshots: dependencies: dayjs: 1.11.20 + lazy-val@1.0.5: {} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -13336,6 +14527,8 @@ snapshots: lodash.uniq@4.5.0: {} + lodash@4.18.1: {} + log-symbols@6.0.0: dependencies: chalk: 5.6.2 @@ -13375,6 +14568,10 @@ snapshots: dependencies: yallist: 3.1.1 + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + lucide-react@1.14.0(react@19.2.6): dependencies: react: 19.2.6 @@ -13397,7 +14594,7 @@ snapshots: mammoth@1.12.0: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.8.13 argparse: 1.0.10 base64-js: 1.5.1 bluebird: 3.4.7 @@ -13808,8 +15005,26 @@ snapshots: dependencies: brace-expansion: 5.0.5 + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.1 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + minimist@1.2.8: {} + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -13829,7 +15044,7 @@ snapshots: headers-polyfill: 5.0.1 is-node-process: 1.2.0 outvariant: 1.4.3 - path-to-regexp: 8.4.2 + path-to-regexp: 6.3.0 picocolors: 1.1.1 rettime: 0.11.11 statuses: 2.0.2 @@ -13862,8 +15077,19 @@ snapshots: negotiator@1.0.0: {} + node-abi@4.31.0: + dependencies: + semver: 7.8.0 + + node-addon-api@1.7.2: + optional: true + node-addon-api@8.7.0: {} + node-api-version@0.2.1: + dependencies: + semver: 7.8.0 + node-domexception@1.0.0: {} node-exports-info@1.6.0: @@ -13881,8 +15107,25 @@ snapshots: node-gyp-build@4.8.4: {} + node-gyp@12.3.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.8.0 + tar: 7.5.15 + tinyglobby: 0.2.16 + undici: 6.26.0 + which: 6.0.1 + node-releases@2.0.38: {} + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + normalize-path@3.0.0: {} normalize-url@6.1.0: {} @@ -14053,10 +15296,14 @@ snapshots: path-parse@1.0.7: {} + path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} pathe@2.0.3: {} + pe-library@0.4.1: {} + pend@1.2.0: {} picocolors@1.1.1: {} @@ -14111,6 +15358,19 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.13 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + plist@3.1.1: + dependencies: + '@xmldom/xmldom': 0.9.10 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + optional: true + possible-typed-array-names@1.1.0: {} postcss-js@4.1.0(postcss@8.5.14): @@ -14154,6 +15414,11 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + powershell-utils@0.1.0: {} prelude-ls@1.2.1: {} @@ -14170,12 +15435,19 @@ snapshots: dependencies: parse-ms: 4.0.0 + proc-log@6.1.0: {} + process-nextick-args@2.0.1: {} process-warning@5.0.0: {} progress@2.0.3: {} + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -14187,6 +15459,12 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + property-information@7.1.0: {} proxy-addr@2.0.7: @@ -14326,6 +15604,12 @@ snapshots: react@19.2.6: {} + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -14344,7 +15628,7 @@ snapshots: readdir-glob@1.1.3: dependencies: - minimatch: 10.2.5 + minimatch: 5.1.9 real-require@0.2.0: {} @@ -14453,6 +15737,10 @@ snapshots: require-from-string@2.0.2: {} + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + reselect@5.1.1: {} resize-observer-polyfill@1.5.1: {} @@ -14481,12 +15769,18 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retry@0.12.0: {} + rettime@0.11.11: {} reusify@1.1.0: {} rfdc@1.4.1: {} + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + rimraf@2.7.1: dependencies: glob: 7.2.3 @@ -14614,6 +15908,8 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.5.14 + sax@1.6.0: {} + saxes@5.0.1: dependencies: xmlchars: 2.2.0 @@ -14634,6 +15930,8 @@ snapshots: semver-compare@1.0.0: optional: true + semver@5.7.2: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -14793,8 +16091,19 @@ snapshots: transitivePeerDependencies: - supports-color + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.0 + sisteransi@1.0.5: {} + slice-ansi@3.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + optional: true + slice-ansi@7.1.2: dependencies: ansi-styles: 6.2.3 @@ -14805,12 +16114,20 @@ snapshots: ansi-styles: 6.2.3 is-fullwidth-code-point: 5.1.0 + smart-buffer@4.2.0: + optional: true + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} space-separated-tokens@2.0.2: {} @@ -14824,6 +16141,8 @@ snapshots: stackback@0.0.2: {} + stat-mode@1.0.0: {} + statuses@2.0.2: {} std-env@4.1.0: {} @@ -15049,12 +16368,36 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + tar@7.5.15: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + thread-stream@4.0.0: dependencies: real-require: 0.2.0 + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + tiny-invariant@1.3.3: {} + tiny-typed-emitter@2.1.0: {} + tinybench@2.9.0: {} tinyexec@1.1.2: {} @@ -15072,6 +16415,10 @@ snapshots: dependencies: tldts-core: 7.0.30 + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.7 + tmp@0.2.7: {} to-regex-range@5.0.1: @@ -15206,6 +16553,8 @@ snapshots: undici-types@7.21.0: {} + undici@6.26.0: {} + undici@7.25.0: {} unicorn-magic@0.3.0: {} @@ -15312,6 +16661,13 @@ snapshots: vary@1.1.2: {} + verror@1.10.1: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.4.1 + optional: true + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -15497,6 +16853,14 @@ snapshots: dependencies: isexe: 3.1.5 + which@5.0.0: + dependencies: + isexe: 3.1.5 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 @@ -15535,12 +16899,18 @@ snapshots: xmlbuilder@10.1.1: {} + xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} y18n@5.0.8: {} yallist@3.1.1: {} + yallist@4.0.0: {} + + yallist@5.0.0: {} + yaml@2.0.0-1: {} yaml@2.9.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cf85cbe8..8760d8ac 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,14 @@ packages: - 'cli' - 'mcp' - 'desktop' + +overrides: + '@xmldom/xmldom': 0.8.13 + fast-uri: '>=3.1.2' + hono: '>=4.12.18' + ip-address: '>=10.1.1' + postcss: '>=8.5.10' + qs: ^6.14.2 + minimatch: '>=10.2.3' + path-to-regexp: '>=8.4.0' + tmp: '>=0.2.6' diff --git a/scripts/desktop-after-pack.mjs b/scripts/desktop-after-pack.mjs new file mode 100644 index 00000000..f6a6f63d --- /dev/null +++ b/scripts/desktop-after-pack.mjs @@ -0,0 +1,53 @@ +import { cp, rm, stat } from 'node:fs/promises'; +import path from 'node:path'; + +async function assertDirectory(label, targetPath) { + const stats = await stat(targetPath); + if (!stats.isDirectory()) { + throw new Error(`${label} is not a directory: ${targetPath}`); + } +} + +export default async function desktopAfterPack(context) { + if (context.electronPlatformName !== 'darwin') { + return; + } + + const desktopDir = context.packager.projectDir; + const stagingDir = path.join(desktopDir, '.desktop-release'); + const resourcesDir = path.join( + context.appOutDir, + `${context.packager.appInfo.productFilename}.app`, + 'Contents', + 'Resources' + ); + + const stagedServer = path.join(stagingDir, 'server'); + const stagedWeb = path.join(stagingDir, 'web'); + const packagedServer = path.join(resourcesDir, 'server'); + const packagedWeb = path.join(resourcesDir, 'web'); + + await Promise.all([ + assertDirectory('Desktop server staging', stagedServer), + assertDirectory('Desktop web staging', stagedWeb), + assertDirectory('Packaged resources', resourcesDir), + ]); + + await Promise.all([ + rm(packagedServer, { recursive: true, force: true }), + rm(packagedWeb, { recursive: true, force: true }), + ]); + + await Promise.all([ + cp(stagedServer, packagedServer, { + recursive: true, + force: true, + verbatimSymlinks: true, + }), + cp(stagedWeb, packagedWeb, { + recursive: true, + force: true, + verbatimSymlinks: true, + }), + ]); +} diff --git a/scripts/prepare-desktop-release.mjs b/scripts/prepare-desktop-release.mjs new file mode 100644 index 00000000..8aad40ef --- /dev/null +++ b/scripts/prepare-desktop-release.mjs @@ -0,0 +1,82 @@ +#!/usr/bin/env node +import { constants } from 'node:fs'; +import { access, cp, mkdir, rm } from 'node:fs/promises'; +import { spawnSync } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const desktopDir = path.join(rootDir, 'desktop'); +const stagingDir = path.join(desktopDir, '.desktop-release'); +const serverStage = path.join(stagingDir, 'server'); +const webStage = path.join(stagingDir, 'web'); + +function pnpmCommand() { + return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; +} + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: rootDir, + encoding: 'utf8', + stdio: 'inherit', + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + throw new Error(`${command} ${args.join(' ')} failed with exit code ${result.status}`); + } +} + +async function assertExists(label, targetPath) { + try { + await access(targetPath, constants.F_OK); + } catch { + throw new Error(`${label} is missing: ${path.relative(rootDir, targetPath)}`); + } +} + +async function pruneServerDeploy() { + await Promise.all([ + rm(path.join(serverStage, 'src'), { recursive: true, force: true }), + rm(path.join(serverStage, '.veritas-kanban'), { recursive: true, force: true }), + rm(path.join(serverStage, 'tsconfig.json'), { force: true }), + rm(path.join(serverStage, 'vitest.config.ts'), { force: true }), + ]); +} + +async function main() { + await assertExists('Server build output', path.join(rootDir, 'server/dist/index.js')); + await assertExists('Web build output', path.join(rootDir, 'web/dist/index.html')); + + await rm(stagingDir, { recursive: true, force: true }); + await mkdir(stagingDir, { recursive: true }); + + run(pnpmCommand(), [ + '--filter', + '@veritas-kanban/server', + 'deploy', + '--prod', + serverStage, + ]); + await pruneServerDeploy(); + + await mkdir(webStage, { recursive: true }); + await cp(path.join(rootDir, 'web/dist'), path.join(webStage, 'dist'), { + recursive: true, + }); + + await assertExists('Packaged server entry', path.join(serverStage, 'dist/index.js')); + await assertExists('Packaged server dependencies', path.join(serverStage, 'node_modules')); + await assertExists('Packaged web app', path.join(webStage, 'dist/index.html')); + + console.log(`Prepared desktop release staging at ${path.relative(rootDir, stagingDir)}`); +} + +main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exit(1); +}); diff --git a/server/src/index.ts b/server/src/index.ts index d93d1cb2..b33f1fff 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -16,6 +16,7 @@ import cors from 'cors'; import cookieParser from 'cookie-parser'; import { WebSocketServer, WebSocket } from 'ws'; import { createServer } from 'http'; +import { readFile } from 'fs/promises'; import os from 'os'; import path from 'path'; import { fileURLToPath } from 'url'; @@ -196,6 +197,7 @@ app.set('etag', 'weak'); // Set CSP_REPORT_URI to a URL to receive violation reports (e.g., // https://your-domain.com/csp-report or a service like report-uri.com). const isDev = process.env.NODE_ENV !== 'production'; +const isDesktopRuntime = process.env.VERITAS_DESKTOP_RUNTIME === '1'; const cspReportOnly = process.env.CSP_REPORT_ONLY === 'true'; const cspReportUri = process.env.CSP_REPORT_URI || null; @@ -245,7 +247,9 @@ app.use( frameSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], - upgradeInsecureRequests: isDev ? null : [], + // The packaged desktop app serves the SPA over loopback HTTP. Do not + // upgrade those local asset requests to HTTPS. + upgradeInsecureRequests: isDev || isDesktopRuntime ? null : [], // CSP violation reporting — only included when CSP_REPORT_URI is set. // Works with both enforced and report-only modes. @@ -486,6 +490,22 @@ app.use('/api', v1Router); if (process.env.NODE_ENV === 'production') { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const webDistPath = path.resolve(__dirname, '../../web/dist'); + const indexHtmlPath = path.join(webDistPath, 'index.html'); + const injectScriptNonce = (html: string, nonce: string | undefined): string => + nonce ? html.replace(/)/g, `