diff --git a/electron-builder.yml b/electron-builder.yml index d78d8d3..c8ef09b 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -31,3 +31,7 @@ linux: appImage: artifactName: ${name}-${version}.${ext} npmRebuild: false +publish: + provider: github + owner: KrisAsvestas + repo: auroradockside diff --git a/package-lock.json b/package-lock.json index 24eb065..ba01d8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@electron-toolkit/utils": "^4.0.0", "@tanstack/react-query": "^5.101.4", "clsx": "^2.1.1", + "electron-updater": "^6.8.9", "lucide-react": "^1.28.0", "tailwind-merge": "^3.6.0", "zustand": "^5.0.14" @@ -3883,7 +3884,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -4307,7 +4307,6 @@ "version": "9.7.0", "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz", "integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4", @@ -5293,6 +5292,69 @@ "dev": true, "license": "ISC" }, + "node_modules/electron-updater": { + "version": "6.8.9", + "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz", + "integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==", + "license": "MIT", + "dependencies": { + "builder-util-runtime": "9.7.0", + "fs-extra": "^10.1.0", + "js-yaml": "^4.1.0", + "lazy-val": "^1.0.5", + "lodash.escaperegexp": "^4.1.2", + "lodash.isequal": "^4.5.0", + "semver": "~7.7.3", + "tiny-typed-emitter": "^2.1.0" + } + }, + "node_modules/electron-updater/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-updater/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/electron-updater/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/electron-updater/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/electron-vite": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/electron-vite/-/electron-vite-5.0.0.tgz", @@ -7484,7 +7546,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", - "dev": true, "funding": [ { "type": "github", @@ -7645,7 +7706,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "dev": true, "license": "MIT" }, "node_modules/levn": { @@ -7958,6 +8018,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9461,7 +9534,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -10164,6 +10236,12 @@ "semver": "bin/semver" } }, + "node_modules/tiny-typed-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", + "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", diff --git a/package.json b/package.json index 2019c63..9de70e6 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@electron-toolkit/utils": "^4.0.0", "@tanstack/react-query": "^5.101.4", "clsx": "^2.1.1", + "electron-updater": "^6.8.9", "lucide-react": "^1.28.0", "tailwind-merge": "^3.6.0", "zustand": "^5.0.14" diff --git a/src/main/index.ts b/src/main/index.ts index 825c91b..05be5f0 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -11,6 +11,7 @@ import { registerCreateIpc } from './ipc/create' import { registerWindowIpc } from './ipc/window' import { registerSecretsIpc } from './ipc/secrets' import { killAllRunningCommands, powerOffAllProjects } from './commandRunner' +import { startAutomaticUpdates } from './updater' function createWindow(): void { // Create the browser window. @@ -69,6 +70,7 @@ app.whenReady().then(() => { registerSecretsIpc() createWindow() + startAutomaticUpdates() app.on('activate', function () { // On macOS it's common to re-create a window in the app when the diff --git a/src/main/updater.test.ts b/src/main/updater.test.ts new file mode 100644 index 0000000..56008d3 --- /dev/null +++ b/src/main/updater.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from 'vitest' +import { compareVersions } from './updater' + +describe('update version comparison', () => { + it('detects newer module versions', () => { + expect(compareVersions('1.2.0', '1.1.0')).toBeGreaterThan(0) + expect(compareVersions('1.2.0', '1.2.0')).toBe(0) + expect(compareVersions('1.1.9', '1.2.0')).toBeLessThan(0) + }) +}) diff --git a/src/main/updater.ts b/src/main/updater.ts new file mode 100644 index 0000000..98f978d --- /dev/null +++ b/src/main/updater.ts @@ -0,0 +1,58 @@ +import { app } from 'electron' +import electronUpdater, { type AppUpdater } from 'electron-updater' +import { + getAvailableModulePackages, + getModuleRegistry, + installModulePackage +} from './moduleRegistry' + +function updater(): AppUpdater { + // electron-updater is CommonJS; destructuring its default import keeps the + // production bundle compatible with Electron's ESM interop. + return electronUpdater.autoUpdater +} + +export function compareVersions(left: string, right: string): number { + const a = left.split(/[.-]/).map((part) => Number(part) || 0) + const b = right.split(/[.-]/).map((part) => Number(part) || 0) + for (let index = 0; index < Math.max(a.length, b.length); index += 1) { + if ((a[index] ?? 0) !== (b[index] ?? 0)) return (a[index] ?? 0) - (b[index] ?? 0) + } + return 0 +} + +export async function updateInstalledModulesFromCatalog(): Promise { + const installed = new Map((await getModuleRegistry()).map((module) => [module.id, module])) + const available = await getAvailableModulePackages() + const updated: string[] = [] + + for (const candidate of available) { + const current = installed.get(candidate.manifest.id) + if (!current || compareVersions(candidate.manifest.version, current.version) <= 0) continue + await installModulePackage(candidate.sourcePath) + updated.push(`${candidate.manifest.name} ${candidate.manifest.version}`) + } + + return updated +} + +export function startAutomaticUpdates(): void { + void updateInstalledModulesFromCatalog() + .then((updated) => { + if (updated.length) console.info(`Updated Aurora modules: ${updated.join(', ')}`) + }) + .catch((error) => console.warn('Aurora module update check failed:', error)) + + // electron-updater needs a packaged application and release metadata. The + // extracted development build intentionally skips the remote app check. + if (!app.isPackaged || process.env.AURORA_DISABLE_AUTO_UPDATE === '1') return + + const autoUpdater = updater() + autoUpdater.autoDownload = true + autoUpdater.autoInstallOnAppQuit = true + autoUpdater.allowPrerelease = true + autoUpdater.logger = console + void autoUpdater.checkForUpdatesAndNotify().catch((error) => + console.warn('Aurora application update check failed:', error) + ) +}