fix: unify update-check API flow across runtimes (#672)
* fix: align update flow across web, desktop, and vscode Unify runtime update handling in shared UI update state Adjust desktop and VS Code bridge integration for consistent behavior Improve server package-manager update plumbing for safer checks * fix: sync update checks between vscode and server Align VS Code bridge behavior with server package-manager logic Reduce mismatches in update detection across runtimes * fix: stabilize update checks across ui and server
This commit is contained in:
committed by
GitHub
parent
d81b34575a
commit
7f37256e96
@@ -144,23 +144,39 @@ export const MainLayout: React.FC = () => {
|
||||
}
|
||||
}, [isRightSidebarOpen, isMobile]);
|
||||
|
||||
// Trigger initial update check shortly after mount, then every hour.
|
||||
// Trigger initial update check shortly after mount, then repeat using server-suggested cadence.
|
||||
const checkForUpdates = useUpdateStore((state) => state.checkForUpdates);
|
||||
React.useEffect(() => {
|
||||
const initialDelayMs = 3000;
|
||||
const periodicIntervalMs = 60 * 60 * 1000;
|
||||
const defaultIntervalMs = 60 * 60 * 1000;
|
||||
const minIntervalMs = 5 * 60 * 1000;
|
||||
const maxIntervalMs = 24 * 60 * 60 * 1000;
|
||||
let disposed = false;
|
||||
let timer: number | null = null;
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
checkForUpdates();
|
||||
}, initialDelayMs);
|
||||
const clampIntervalMs = (seconds: number): number => {
|
||||
const ms = Math.round(seconds * 1000);
|
||||
return Math.max(minIntervalMs, Math.min(maxIntervalMs, ms));
|
||||
};
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
checkForUpdates();
|
||||
}, periodicIntervalMs);
|
||||
const scheduleNext = (delayMs: number) => {
|
||||
if (disposed) return;
|
||||
timer = window.setTimeout(async () => {
|
||||
const suggestedSec = await checkForUpdates();
|
||||
const nextDelay = typeof suggestedSec === 'number' && Number.isFinite(suggestedSec)
|
||||
? clampIntervalMs(suggestedSec)
|
||||
: defaultIntervalMs;
|
||||
scheduleNext(nextDelay);
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
scheduleNext(initialDelayMs);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timer);
|
||||
window.clearInterval(interval);
|
||||
disposed = true;
|
||||
if (timer !== null) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [checkForUpdates]);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ export type UpdateInfo = {
|
||||
currentVersion: string;
|
||||
body?: string;
|
||||
date?: string;
|
||||
nextSuggestedCheckInSec?: number;
|
||||
// Web-specific fields
|
||||
packageManager?: string;
|
||||
updateCommand?: string;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { create } from 'zustand';
|
||||
import type { UpdateInfo, UpdateProgress } from '@/lib/desktop';
|
||||
import { getDeviceInfo } from '@/lib/device';
|
||||
import {
|
||||
checkForDesktopUpdates,
|
||||
downloadDesktopUpdate,
|
||||
restartToApplyUpdate,
|
||||
isDesktopLocalOriginActive,
|
||||
isTauriShell,
|
||||
isVSCodeRuntime,
|
||||
isWebRuntime,
|
||||
} from '@/lib/desktop';
|
||||
|
||||
@@ -19,19 +21,77 @@ export type UpdateState = {
|
||||
error: string | null;
|
||||
runtimeType: 'desktop' | 'web' | 'vscode' | null;
|
||||
lastChecked: number | null;
|
||||
nextCheckInSec: number | null;
|
||||
};
|
||||
|
||||
interface UpdateStore extends UpdateState {
|
||||
checkForUpdates: () => Promise<void>;
|
||||
checkForUpdates: () => Promise<number | null>;
|
||||
downloadUpdate: () => Promise<void>;
|
||||
restartToUpdate: () => Promise<void>;
|
||||
dismiss: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
async function checkForWebUpdates(): Promise<UpdateInfo | null> {
|
||||
type ClientRuntime = 'desktop' | 'web' | 'vscode';
|
||||
|
||||
function detectDeviceClass(): 'mobile' | 'tablet' | 'desktop' | 'unknown' {
|
||||
if (typeof window === 'undefined') return 'unknown';
|
||||
try {
|
||||
const response = await fetch('/api/openchamber/update-check', {
|
||||
const { deviceType } = getDeviceInfo();
|
||||
return deviceType;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function detectArch(): 'arm64' | 'x64' | 'unknown' {
|
||||
const nav = typeof navigator !== 'undefined' ? (navigator as Navigator & { userAgentData?: { architecture?: string } }).userAgentData : undefined;
|
||||
const fromUAData = nav?.architecture?.toLowerCase?.();
|
||||
if (fromUAData === 'arm' || fromUAData === 'arm64' || fromUAData === 'aarch64') return 'arm64';
|
||||
if (fromUAData === 'x86' || fromUAData === 'x64' || fromUAData === 'amd64') return 'x64';
|
||||
|
||||
const ua = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : '';
|
||||
if (ua.includes('aarch64') || ua.includes('arm64') || ua.includes('armv')) return 'arm64';
|
||||
if (ua.includes('x86_64') || ua.includes('x64') || ua.includes('amd64') || ua.includes('win64')) return 'x64';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function detectPlatform(): 'macos' | 'windows' | 'linux' | 'web' {
|
||||
if (typeof navigator === 'undefined') return 'web';
|
||||
const platform = (navigator.platform || '').toLowerCase();
|
||||
if (platform.includes('mac')) return 'macos';
|
||||
if (platform.includes('win')) return 'windows';
|
||||
if (platform.includes('linux')) return 'linux';
|
||||
return 'web';
|
||||
}
|
||||
|
||||
function mapRuntimeParams(runtime: ClientRuntime): URLSearchParams {
|
||||
const params = new URLSearchParams({ reportUsage: 'true' });
|
||||
params.set('deviceClass', detectDeviceClass());
|
||||
params.set('arch', detectArch());
|
||||
params.set('platform', detectPlatform());
|
||||
if (runtime === 'desktop') {
|
||||
params.set('appType', 'desktop-tauri');
|
||||
params.set('instanceMode', isDesktopLocalOriginActive() ? 'local' : 'remote');
|
||||
return params;
|
||||
}
|
||||
|
||||
if (runtime === 'vscode') {
|
||||
params.set('appType', 'vscode');
|
||||
params.set('instanceMode', 'local');
|
||||
return params;
|
||||
}
|
||||
|
||||
params.set('appType', 'web');
|
||||
params.set('instanceMode', 'unknown');
|
||||
return params;
|
||||
}
|
||||
|
||||
async function checkForWebUpdates(runtime: ClientRuntime, currentVersion?: string): Promise<UpdateInfo | null> {
|
||||
try {
|
||||
const params = mapRuntimeParams(runtime);
|
||||
if (currentVersion) params.set('currentVersion', currentVersion);
|
||||
const response = await fetch(`/api/openchamber/update-check?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
@@ -46,11 +106,15 @@ async function checkForWebUpdates(): Promise<UpdateInfo | null> {
|
||||
version: data.version,
|
||||
currentVersion: data.currentVersion ?? 'unknown',
|
||||
body: data.body,
|
||||
nextSuggestedCheckInSec:
|
||||
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
||||
? data.nextSuggestedCheckInSec
|
||||
: undefined,
|
||||
packageManager: data.packageManager,
|
||||
updateCommand: data.updateCommand,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to check for web updates:', error);
|
||||
console.warn('Failed to check for updates:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +125,7 @@ function detectRuntimeType(): 'desktop' | 'web' | 'vscode' | null {
|
||||
// When viewing a remote host inside the desktop shell, treat update as web update.
|
||||
return isDesktopLocalOriginActive() ? 'desktop' : 'web';
|
||||
}
|
||||
if (isVSCodeRuntime()) return 'vscode';
|
||||
if (isWebRuntime()) return 'web';
|
||||
return null;
|
||||
}
|
||||
@@ -75,6 +140,7 @@ const initialState: UpdateState = {
|
||||
error: null,
|
||||
runtimeType: null,
|
||||
lastChecked: null,
|
||||
nextCheckInSec: null,
|
||||
};
|
||||
|
||||
export const useUpdateStore = create<UpdateStore>()((set, get) => ({
|
||||
@@ -82,30 +148,40 @@ export const useUpdateStore = create<UpdateStore>()((set, get) => ({
|
||||
|
||||
checkForUpdates: async () => {
|
||||
const runtime = detectRuntimeType();
|
||||
if (!runtime) return;
|
||||
if (!runtime) return null;
|
||||
|
||||
set({ checking: true, error: null, runtimeType: runtime });
|
||||
|
||||
try {
|
||||
let info: UpdateInfo | null = null;
|
||||
let suggestedSec: number | null = null;
|
||||
|
||||
if (runtime === 'desktop') {
|
||||
info = await checkForDesktopUpdates();
|
||||
const sidecarInfo = await checkForWebUpdates('desktop', info?.currentVersion);
|
||||
suggestedSec = sidecarInfo?.nextSuggestedCheckInSec ?? null;
|
||||
} else if (runtime === 'web') {
|
||||
info = await checkForWebUpdates();
|
||||
info = await checkForWebUpdates('web');
|
||||
suggestedSec = info?.nextSuggestedCheckInSec ?? null;
|
||||
} else if (runtime === 'vscode') {
|
||||
const vscodeInfo = await checkForWebUpdates('vscode');
|
||||
suggestedSec = vscodeInfo?.nextSuggestedCheckInSec ?? null;
|
||||
}
|
||||
|
||||
set({
|
||||
checking: false,
|
||||
available: info?.available ?? false,
|
||||
info,
|
||||
available: runtime === 'vscode' ? false : (info?.available ?? false),
|
||||
info: runtime === 'vscode' ? null : info,
|
||||
lastChecked: Date.now(),
|
||||
nextCheckInSec: suggestedSec,
|
||||
});
|
||||
return suggestedSec;
|
||||
} catch (error) {
|
||||
set({
|
||||
checking: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to check for updates',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as vscode from 'vscode';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { spawn, execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { type OpenCodeManager } from './opencode';
|
||||
@@ -112,6 +113,37 @@ const execFileAsync = promisify(execFile);
|
||||
const gpgconfCandidates = ['gpgconf', '/opt/homebrew/bin/gpgconf', '/usr/local/bin/gpgconf'];
|
||||
|
||||
const OPENCHAMBER_SHARED_SETTINGS_PATH = path.join(os.homedir(), '.config', 'openchamber', 'settings.json');
|
||||
const VSCODE_INSTALL_ID_KEY = 'openchamber.installId.vscode';
|
||||
const UPDATE_CHECK_URL = process.env.OPENCHAMBER_UPDATE_API_URL || 'https://api.openchamber.dev/v1/update/check';
|
||||
|
||||
const mapNodePlatformToApiPlatform = (value: string): 'macos' | 'windows' | 'linux' | 'web' => {
|
||||
if (value === 'darwin') return 'macos';
|
||||
if (value === 'win32') return 'windows';
|
||||
if (value === 'linux') return 'linux';
|
||||
return 'web';
|
||||
};
|
||||
|
||||
const mapNodeArchToApiArch = (value: string): 'arm64' | 'x64' | 'unknown' => {
|
||||
if (value === 'arm64' || value === 'aarch64') return 'arm64';
|
||||
if (value === 'x64' || value === 'amd64') return 'x64';
|
||||
return 'unknown';
|
||||
};
|
||||
|
||||
const getOrCreateVSCodeInstallId = async (ctx?: BridgeContext): Promise<string> => {
|
||||
const state = ctx?.context?.globalState;
|
||||
if (state) {
|
||||
const existing = state.get<string>(VSCODE_INSTALL_ID_KEY);
|
||||
if (typeof existing === 'string' && existing.trim().length > 0) {
|
||||
return existing.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const generated = randomUUID();
|
||||
if (state) {
|
||||
await state.update(VSCODE_INSTALL_ID_KEY, generated);
|
||||
}
|
||||
return generated;
|
||||
};
|
||||
|
||||
const guessMimeTypeFromExtension = (ext: string) => {
|
||||
switch (ext) {
|
||||
@@ -2890,6 +2922,56 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:openchamber:update-check': {
|
||||
try {
|
||||
const body = (payload && typeof payload === 'object' ? payload : {}) as Record<string, unknown>;
|
||||
const currentVersion = typeof body.currentVersion === 'string' && body.currentVersion.trim().length > 0
|
||||
? body.currentVersion.trim()
|
||||
: 'unknown';
|
||||
const instanceMode = typeof body.instanceMode === 'string' && body.instanceMode.trim().length > 0
|
||||
? body.instanceMode.trim()
|
||||
: 'local';
|
||||
const deviceClass = typeof body.deviceClass === 'string' && body.deviceClass.trim().length > 0
|
||||
? body.deviceClass.trim()
|
||||
: 'desktop';
|
||||
const reportUsage = body.reportUsage !== false;
|
||||
|
||||
const installId = await getOrCreateVSCodeInstallId(ctx);
|
||||
const requestBody = {
|
||||
appType: 'vscode',
|
||||
deviceClass,
|
||||
platform: mapNodePlatformToApiPlatform(os.platform()),
|
||||
arch: mapNodeArchToApiArch(os.arch()),
|
||||
channel: 'stable',
|
||||
currentVersion,
|
||||
installId,
|
||||
instanceMode,
|
||||
reportUsage,
|
||||
};
|
||||
|
||||
const response = await fetch(UPDATE_CHECK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => 'update check failed');
|
||||
return { id, type, success: false, error: text || `Update check failed with ${response.status}` };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return { id, type, success: true, data };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { id, type, success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
case 'editor:openFile': {
|
||||
const { path: filePath, line, column } = payload as { path: string; line?: number; column?: number };
|
||||
try {
|
||||
|
||||
@@ -746,6 +746,26 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/api/openchamber/update-check')) {
|
||||
try {
|
||||
const currentVersion = url.searchParams.get('currentVersion') || undefined;
|
||||
const instanceMode = url.searchParams.get('instanceMode') || 'local';
|
||||
const deviceClass = url.searchParams.get('deviceClass') || 'desktop';
|
||||
const reportUsageRaw = (url.searchParams.get('reportUsage') || 'true').toLowerCase();
|
||||
const reportUsage = !(reportUsageRaw === 'false' || reportUsageRaw === '0' || reportUsageRaw === 'no');
|
||||
const data = await sendBridgeMessage('api:openchamber:update-check', {
|
||||
currentVersion,
|
||||
instanceMode,
|
||||
deviceClass,
|
||||
reportUsage,
|
||||
});
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return new Response(JSON.stringify({ available: false, error: message }), { status: 502, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/auth/session') {
|
||||
// VS Code host is trusted; mirror web server shape to keep UI logic happy
|
||||
const body = {
|
||||
|
||||
@@ -7556,10 +7556,49 @@ async function main(options = {}) {
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/openchamber/update-check', async (_req, res) => {
|
||||
app.get('/api/openchamber/update-check', async (req, res) => {
|
||||
try {
|
||||
const { checkForUpdates } = await import('./lib/package-manager.js');
|
||||
const updateInfo = await checkForUpdates();
|
||||
const parseString = (value) => (typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined);
|
||||
const parseReportUsage = (value) => {
|
||||
if (typeof value !== 'string') return true;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
|
||||
return true;
|
||||
};
|
||||
const inferDeviceClass = (ua) => {
|
||||
const value = (ua || '').toLowerCase();
|
||||
if (!value) return 'unknown';
|
||||
if (value.includes('ipad') || value.includes('tablet')) return 'tablet';
|
||||
if (value.includes('mobi') || value.includes('android') || value.includes('iphone')) return 'mobile';
|
||||
return 'desktop';
|
||||
};
|
||||
const inferArch = (ua) => {
|
||||
const value = (ua || '').toLowerCase();
|
||||
if (!value) return 'unknown';
|
||||
if (value.includes('aarch64') || value.includes('arm64') || value.includes(' arm;') || value.includes('armv')) return 'arm64';
|
||||
if (value.includes('x86_64') || value.includes('x64') || value.includes('amd64') || value.includes('win64') || value.includes('x86-64')) return 'x64';
|
||||
return 'unknown';
|
||||
};
|
||||
const inferPlatform = (ua) => {
|
||||
const value = (ua || '').toLowerCase();
|
||||
if (!value) return undefined;
|
||||
if (value.includes('mac os') || value.includes('macintosh') || value.includes('darwin')) return 'macos';
|
||||
if (value.includes('windows') || value.includes('win32') || value.includes('win64')) return 'windows';
|
||||
if (value.includes('linux') || value.includes('x11')) return 'linux';
|
||||
return 'web';
|
||||
};
|
||||
const userAgent = typeof req.headers['user-agent'] === 'string' ? req.headers['user-agent'] : '';
|
||||
|
||||
const updateInfo = await checkForUpdates({
|
||||
appType: parseString(req.query.appType),
|
||||
deviceClass: parseString(req.query.deviceClass) || inferDeviceClass(userAgent),
|
||||
platform: parseString(req.query.platform) || inferPlatform(userAgent),
|
||||
arch: parseString(req.query.arch) || inferArch(userAgent),
|
||||
instanceMode: parseString(req.query.instanceMode),
|
||||
currentVersion: parseString(req.query.currentVersion),
|
||||
reportUsage: parseReportUsage(parseString(req.query.reportUsage)),
|
||||
});
|
||||
res.json(updateInfo);
|
||||
} catch (error) {
|
||||
console.error('Failed to check for updates:', error);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { spawnSync } from 'child_process';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@@ -9,6 +11,116 @@ const __dirname = path.dirname(__filename);
|
||||
const PACKAGE_NAME = '@openchamber/web';
|
||||
const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}`;
|
||||
const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md';
|
||||
const UPDATE_CHECK_URL = process.env.OPENCHAMBER_UPDATE_API_URL || 'https://api.openchamber.dev/v1/update/check';
|
||||
|
||||
function getOpenChamberConfigDir() {
|
||||
if (process.platform === 'win32') {
|
||||
const appData = process.env.APPDATA;
|
||||
if (appData) return path.join(appData, 'openchamber');
|
||||
}
|
||||
|
||||
return path.join(os.homedir(), '.config', 'openchamber');
|
||||
}
|
||||
|
||||
function sanitizeInstallScope(scope) {
|
||||
if (scope === 'desktop-tauri' || scope === 'vscode' || scope === 'web') return scope;
|
||||
return 'web';
|
||||
}
|
||||
|
||||
function getOrCreateInstallId(scope = 'web') {
|
||||
const configDir = getOpenChamberConfigDir();
|
||||
const normalizedScope = sanitizeInstallScope(scope);
|
||||
const idPath = path.join(configDir, `install-id-${normalizedScope}`);
|
||||
|
||||
try {
|
||||
const existing = fs.readFileSync(idPath, 'utf8').trim();
|
||||
if (existing) return existing;
|
||||
} catch {
|
||||
// Generate new id.
|
||||
}
|
||||
|
||||
const installId = crypto.randomUUID();
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
fs.writeFileSync(idPath, `${installId}\n`, { encoding: 'utf8', mode: 0o600 });
|
||||
return installId;
|
||||
}
|
||||
|
||||
function mapPlatform(value) {
|
||||
if (value === 'darwin') return 'macos';
|
||||
if (value === 'win32') return 'windows';
|
||||
if (value === 'linux') return 'linux';
|
||||
return 'web';
|
||||
}
|
||||
|
||||
function mapArch(value) {
|
||||
if (value === 'arm64' || value === 'aarch64') return 'arm64';
|
||||
if (value === 'x64' || value === 'amd64') return 'x64';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function normalizeAppType(value) {
|
||||
if (value === 'web' || value === 'desktop-tauri' || value === 'vscode') return value;
|
||||
return 'web';
|
||||
}
|
||||
|
||||
function normalizeDeviceClass(value) {
|
||||
if (value === 'mobile' || value === 'tablet' || value === 'desktop' || value === 'unknown') return value;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function normalizePlatform(value) {
|
||||
if (value === 'macos' || value === 'windows' || value === 'linux' || value === 'web') return value;
|
||||
return mapPlatform(process.platform);
|
||||
}
|
||||
|
||||
function normalizeArch(value) {
|
||||
if (value === 'arm64' || value === 'x64' || value === 'unknown') return value;
|
||||
return mapArch(process.arch);
|
||||
}
|
||||
|
||||
async function checkForUpdatesFromApi(currentVersion, options = {}) {
|
||||
try {
|
||||
const appType = normalizeAppType(options.appType);
|
||||
const payload = {
|
||||
appType,
|
||||
deviceClass: normalizeDeviceClass(options.deviceClass),
|
||||
platform: normalizePlatform(options.platform),
|
||||
arch: normalizeArch(options.arch),
|
||||
channel: 'stable',
|
||||
currentVersion,
|
||||
installId: getOrCreateInstallId(appType),
|
||||
instanceMode: options.instanceMode || 'unknown',
|
||||
reportUsage: options.reportUsage !== false,
|
||||
};
|
||||
|
||||
const response = await fetch(UPDATE_CHECK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
if (typeof data?.latestVersion !== 'string') return null;
|
||||
|
||||
return {
|
||||
available: Boolean(data.updateAvailable),
|
||||
version: data.latestVersion,
|
||||
currentVersion,
|
||||
body: typeof data.releaseNotes === 'string' ? data.releaseNotes : undefined,
|
||||
nextSuggestedCheckInSec:
|
||||
typeof data.nextSuggestedCheckInSec === 'number' && Number.isFinite(data.nextSuggestedCheckInSec)
|
||||
? data.nextSuggestedCheckInSec
|
||||
: undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect which package manager was used to install this package.
|
||||
@@ -304,11 +416,21 @@ export async function fetchChangelogNotes(fromVersion, toVersion) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for updates and return update info
|
||||
*/
|
||||
export async function checkForUpdates() {
|
||||
const currentVersion = getCurrentVersion();
|
||||
export async function checkForUpdates(options = {}) {
|
||||
const currentVersion = options.currentVersion || getCurrentVersion();
|
||||
const pm = detectPackageManager();
|
||||
|
||||
if (currentVersion !== 'unknown') {
|
||||
const remote = await checkForUpdatesFromApi(currentVersion, options);
|
||||
if (remote) {
|
||||
return {
|
||||
...remote,
|
||||
packageManager: pm,
|
||||
updateCommand: 'openchamber update',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestVersion();
|
||||
|
||||
if (!latestVersion || currentVersion === 'unknown') {
|
||||
@@ -323,8 +445,6 @@ export async function checkForUpdates() {
|
||||
const latestNum = parseVersion(latestVersion);
|
||||
const available = latestNum > currentNum;
|
||||
|
||||
const pm = detectPackageManager();
|
||||
|
||||
let changelog;
|
||||
if (available) {
|
||||
changelog = await fetchChangelogNotes(currentVersion, latestVersion);
|
||||
|
||||
Reference in New Issue
Block a user