diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts index 16613858f..44a3e9dc2 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.test.ts @@ -79,6 +79,8 @@ vi.mock('~build/git', () => ({ })) describe('setupAutoUpdater', () => { + const expectedChannelByArch = process.arch === 'arm64' ? 'latest-arm64' : 'latest-x64' + const laneReleaseTagMap = { stable: 'v0.9.9', beta: 'v0.9.10-beta.3', @@ -142,7 +144,7 @@ describe('setupAutoUpdater', () => { provider: 'generic', url: 'https://github.com/moeru-ai/airi/releases/download/v0.9.0-beta.6', }) - expect(updaterState.instance.channel).toBe('latest-arm64') + expect(updaterState.instance.channel).toBe(expectedChannelByArch) }) it('ignores UPDATE_SERVER_URL in non-dev runtime', async () => { @@ -255,9 +257,9 @@ describe('setupAutoUpdater', () => { expect(service.state.diagnostics).toEqual(expect.objectContaining({ platform: process.platform, arch: process.arch, - channel: 'latest-arm64', + channel: expectedChannelByArch, executablePath: expect.any(String), - logFilePath: '/tmp/airi/logs', + logFilePath: expect.stringMatching(/stage-tamagotchi-updater[\\/]updater-log\.txt$/), isOverrideActive: false, })) expect(service.state.diagnostics).not.toHaveProperty('updaterCacheDir') diff --git a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts index 34fab7465..c82139e7a 100644 --- a/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts +++ b/apps/stage-tamagotchi/src/main/services/electron/auto-updater.ts @@ -3,8 +3,11 @@ import type { AutoUpdaterState } from '@proj-airi/electron-eventa/electron-updat import type { BrowserWindow } from 'electron' import type { UpdateInfo } from 'electron-updater' +import fs from 'node:fs' import process from 'node:process' +import { join } from 'node:path' + import electronUpdater from 'electron-updater' import semver from 'semver' @@ -34,6 +37,32 @@ const GITHUB_RELEASES_ATOM_URL = 'https://github.com/moeru-ai/airi/releases.atom const GITHUB_RELEASE_DOWNLOAD_BASE_URL = 'https://github.com/moeru-ai/airi/releases/download' const UPDATE_CHANNEL_ENV_KEY = 'AIRI_UPDATE_CHANNEL' +function getCacheRoot() { + switch (process.platform) { + case 'win32': + return process.env.LOCALAPPDATA || join(process.env.USERPROFILE || '', 'AppData', 'Local') + case 'darwin': + return join(process.env.HOME || '', 'Library', 'Caches') + default: + return process.env.XDG_CACHE_HOME || join(process.env.HOME || '', '.cache') + } +} + +const UPDATER_DEBUG_CACHE_DIR = join(getCacheRoot(), 'stage-tamagotchi-updater') +const UPDATER_LOG_FILE = join(UPDATER_DEBUG_CACHE_DIR, 'updater-log.txt') +const OFFICIAL_UPDATER_CACHE_DIR = join(getCacheRoot(), 'ai.moeru.airi-updater') + +async function logToFile(level: 'INFO' | 'WARN' | 'ERROR' | 'DEBUG', message: string) { + await fs.promises.mkdir(UPDATER_DEBUG_CACHE_DIR, { recursive: true }).catch(() => {}) + await fs.promises.appendFile(UPDATER_LOG_FILE, `${new Date().toISOString()} [${level}] ${message}\n`).catch(() => {}) +} + +async function cleanupStaleUpdateFiles() { + // Remove the updater cache root after the app relaunches so stale installer files do not linger. + await fs.promises.rm(OFFICIAL_UPDATER_CACHE_DIR, { recursive: true, force: true }).catch(() => {}) + await logToFile('INFO', `Updater cache cleanup attempted: ${OFFICIAL_UPDATER_CACHE_DIR}`) +} + export type UpdateLane = 'stable' | 'alpha' | 'beta' | 'nightly' | 'canary' interface GitHubReleaseRecord { tag_name?: string @@ -208,14 +237,27 @@ export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater autoUpdater.allowPrerelease = isPrereleaseBuild autoUpdater.autoDownload = false + void cleanupStaleUpdateFiles() if (activeFeedUrlOverride) autoUpdater.channel = releaseChannelName autoUpdater.forceDevUpdateConfig = !!feedUrlOverride && !app.isPackaged autoUpdater.logger = { - info: (message: string) => log.log(message), - warn: (message: string) => log.warn(message), - error: (message: string) => log.error(message), - debug: (message: string) => log.debug(message), + info: (message: string) => { + log.log(message) + void logToFile('INFO', message) + }, + warn: (message: string) => { + log.warn(message) + void logToFile('WARN', message) + }, + error: (message: string) => { + log.error(message) + void logToFile('ERROR', message) + }, + debug: (message: string) => { + log.debug(message) + void logToFile('DEBUG', message) + }, } if (activeFeedUrlOverride) @@ -227,7 +269,7 @@ export function setupAutoUpdater(options: AutoUpdaterOptions = {}): AutoUpdater platform: process.platform, arch: process.arch, channel: autoUpdater.channel || releaseChannelName, - logFilePath: app.getPath('logs'), + logFilePath: UPDATER_LOG_FILE, executablePath: process.execPath, isOverrideActive: !!activeFeedUrlOverride, ...(activeFeedUrlOverride ? { feedUrl: activeFeedUrlOverride } : {}), diff --git a/apps/stage-tamagotchi/src/renderer/pages/about.vue b/apps/stage-tamagotchi/src/renderer/pages/about.vue index 0be5cb97c..4d5e5b9b6 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/about.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/about.vue @@ -3,6 +3,8 @@ import type { BugReportDialogSubmitPayload } from '@proj-airi/stage-ui/component import type { ElectronUpdaterChannel } from '../../shared/eventa' +import semver from 'semver' + import { useElectronAutoUpdater, useElectronEventaInvoke } from '@proj-airi/electron-vueuse' import { AboutContent, BugReportDialog, createBugReportPageContext, MarkdownRenderer } from '@proj-airi/stage-ui/components' import { useBreakpoints } from '@proj-airi/stage-ui/composables' @@ -88,6 +90,22 @@ const restartButtonLabel = computed(() => { : t('tamagotchi.stage.about.update.actions.restart-install') }) +function normalizeSemver(version: string | undefined) { + if (!version) + return undefined + + return semver.valid(version) ?? semver.valid(version.startsWith('v') ? version.slice(1) : version) +} + +const isDowngradeUpdate = computed(() => { + const currentVersion = normalizeSemver(buildInfo.value.version) + const targetVersion = normalizeSemver(updateState.value.info?.version) + if (!currentVersion || !targetVersion) + return false + + return semver.lt(targetVersion, currentVersion) +}) + const getUpdaterPreferences = useElectronEventaInvoke(electronGetUpdaterPreferences) const setUpdaterPreferences = useElectronEventaInvoke(electronSetUpdaterPreferences) @@ -244,6 +262,12 @@ onMounted(() => {
v{{ updateState.info?.version }}
+
+ Selected channel offers an older build than your current version. Installing this update will downgrade AIRI. +