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:
Bohdan Triapitsyn
2026-03-15 23:14:37 +02:00
committed by GitHub
parent d81b34575a
commit 7f37256e96
7 changed files with 381 additions and 27 deletions
+127 -7
View File
@@ -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);