This commit is contained in:
2026-06-25 12:05:53 -05:00
commit b58e50e423
141 changed files with 28190 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
// 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}"`);
}