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
@@ -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