79 lines
3.7 KiB
JavaScript
79 lines
3.7 KiB
JavaScript
// Application version comparison + upstream version lookup for the Phase 5
|
|
// catalog. The comparison and Graph/winget mapping are pure and unit-tested; the
|
|
// network lookups (`fetchLatestVersion`) require live sources and are marked
|
|
// needs-live-verification.
|
|
|
|
// Compare two dotted version strings numerically with lexical fallback for
|
|
// non-numeric segments. Returns -1 (a<b), 0 (equal), or 1 (a>b).
|
|
export function compareVersions(a = '', b = '') {
|
|
const norm = (v) => String(v).trim().replace(/^v/i, '').split(/[.\-+]/).filter(Boolean);
|
|
const pa = norm(a);
|
|
const pb = norm(b);
|
|
const len = Math.max(pa.length, pb.length);
|
|
for (let i = 0; i < len; i++) {
|
|
const sa = pa[i] ?? '0';
|
|
const sb = pb[i] ?? '0';
|
|
const na = Number(sa);
|
|
const nb = Number(sb);
|
|
const bothNumeric = Number.isFinite(na) && Number.isFinite(nb) && /^\d+$/.test(sa) && /^\d+$/.test(sb);
|
|
let cmp;
|
|
if (bothNumeric) cmp = na === nb ? 0 : (na < nb ? -1 : 1);
|
|
else cmp = sa === sb ? 0 : (sa < sb ? -1 : 1);
|
|
if (cmp !== 0) return cmp;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Decide whether a catalog entry has a newer upstream version than what is
|
|
// currently published.
|
|
export function evaluateUpdateState({ currentVersion, latestKnownVersion } = {}) {
|
|
const current = currentVersion || '';
|
|
const latest = latestKnownVersion || '';
|
|
if (!latest) return { updateAvailable: false, current, latest, reason: 'no upstream version known' };
|
|
if (!current) return { updateAvailable: true, current, latest, reason: 'no current version recorded' };
|
|
const cmp = compareVersions(latest, current);
|
|
return {
|
|
updateAvailable: cmp > 0,
|
|
current,
|
|
latest,
|
|
reason: cmp > 0 ? 'newer upstream version available' : 'up to date'
|
|
};
|
|
}
|
|
|
|
// Pure mapping from a Graph mobileApp (win32LobApp) to catalog fields, used when
|
|
// adopting an existing tenant app into the catalog.
|
|
export function mapGraphAppToCatalog(graphApp = {}) {
|
|
return {
|
|
name: graphApp.displayName || graphApp.name || graphApp.id || 'Imported app',
|
|
vendor: graphApp.publisher || '',
|
|
publisher: graphApp.publisher || '',
|
|
currentVersion: graphApp.displayVersion || graphApp.version || '',
|
|
currentGraphAppId: graphApp.id || ''
|
|
};
|
|
}
|
|
|
|
// Look up the latest upstream version for a catalog entry. `manual` sources are
|
|
// operator-driven; `url` expects JSON with a `version` field; `winget` queries a
|
|
// community winget index (configurable ref). Network-dependent — verify live.
|
|
export async function fetchLatestVersion({ versionSource = 'manual', versionSourceRef = '' } = {}) {
|
|
if (versionSource === 'manual' || !versionSourceRef) {
|
|
return { version: '', source: 'manual', message: 'Manual source; set the latest version by hand.' };
|
|
}
|
|
if (versionSource === 'url') {
|
|
const res = await fetch(versionSourceRef, { headers: { Accept: 'application/json' } });
|
|
if (!res.ok) throw new Error(`Version URL returned ${res.status}`);
|
|
const data = await res.json().catch(() => ({}));
|
|
const version = data.version || data.latestVersion || data.tag_name || '';
|
|
return { version: String(version).replace(/^v/i, ''), source: 'url', message: version ? '' : 'No version field found in URL response.' };
|
|
}
|
|
if (versionSource === 'winget') {
|
|
// Community winget index (e.g. https://api.winget.run/v2/packages/<id>).
|
|
const res = await fetch(versionSourceRef, { headers: { Accept: 'application/json' } });
|
|
if (!res.ok) throw new Error(`winget source returned ${res.status}`);
|
|
const data = await res.json().catch(() => ({}));
|
|
const version = data?.Package?.Latest?.PackageVersion || data?.latestVersion || data?.version || '';
|
|
return { version: String(version), source: 'winget', message: version ? '' : 'No version found in winget response.' };
|
|
}
|
|
throw new Error(`Unknown version source "${versionSource}"`);
|
|
}
|