fix: retarget updater to @mosaic/mosaic (#384)
This commit was merged in pull request #384.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Mosaic update checker — compares installed @mosaic/cli version against the
|
||||
* Mosaic update checker — compares the installed Mosaic package against the
|
||||
* Gitea npm registry and reports when an upgrade is available.
|
||||
*
|
||||
* Used by:
|
||||
@@ -23,8 +23,12 @@ import { join } from 'node:path';
|
||||
export interface UpdateCheckResult {
|
||||
/** Currently installed version (empty if not found) */
|
||||
current: string;
|
||||
/** Currently installed package name */
|
||||
currentPackage?: string;
|
||||
/** Latest published version (empty if check failed) */
|
||||
latest: string;
|
||||
/** Package that should be installed for the latest version */
|
||||
targetPackage?: string;
|
||||
/** True when a newer version is available */
|
||||
updateAvailable: boolean;
|
||||
/** ISO timestamp of this check */
|
||||
@@ -36,7 +40,9 @@ export interface UpdateCheckResult {
|
||||
// ─── Constants ──────────────────────────────────────────────────────────────
|
||||
|
||||
const REGISTRY = 'https://git.mosaicstack.dev/api/packages/mosaic/npm/';
|
||||
const CLI_PKG = '@mosaic/cli';
|
||||
const MODERN_PKG = '@mosaic/mosaic';
|
||||
const LEGACY_PKG = '@mosaic/cli';
|
||||
const INSTALLED_PACKAGE_ORDER = [MODERN_PKG, LEGACY_PKG] as const;
|
||||
const CACHE_DIR = join(homedir(), '.cache', 'mosaic');
|
||||
const CACHE_FILE = join(CACHE_DIR, 'update-check.json');
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
||||
@@ -125,17 +131,20 @@ export function semverLt(a: string, b: string): boolean {
|
||||
/** Cache stores only the latest registry version (the expensive network call).
|
||||
* The installed version is always checked fresh — it's a local `npm ls`. */
|
||||
interface RegistryCache {
|
||||
currentPackage?: string;
|
||||
latest: string;
|
||||
targetPackage?: string;
|
||||
checkedAt: string;
|
||||
registry: string;
|
||||
}
|
||||
|
||||
function readCache(): RegistryCache | null {
|
||||
function readCache(currentPackage: string): RegistryCache | null {
|
||||
try {
|
||||
if (!existsSync(CACHE_FILE)) return null;
|
||||
const raw = JSON.parse(readFileSync(CACHE_FILE, 'utf-8')) as RegistryCache;
|
||||
const age = Date.now() - new Date(raw.checkedAt).getTime();
|
||||
if (age > CACHE_TTL_MS) return null;
|
||||
if ((raw.currentPackage || '') !== currentPackage) return null;
|
||||
return raw;
|
||||
} catch {
|
||||
return null;
|
||||
@@ -154,10 +163,10 @@ function writeCache(entry: RegistryCache): void {
|
||||
// ─── Public API ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Get the currently installed version of @mosaic/cli.
|
||||
* Returns empty string if not installed.
|
||||
* Get the currently installed Mosaic package version.
|
||||
* Prefers the consolidated @mosaic/mosaic package over legacy @mosaic/cli.
|
||||
*/
|
||||
export function getInstalledVersion(): string {
|
||||
export function getInstalledVersion(): { name: string; version: string } {
|
||||
// Fast path: check via package.json require chain
|
||||
try {
|
||||
const raw = npmExec(`ls -g --depth=0 --json 2>/dev/null`, 3000);
|
||||
@@ -165,20 +174,41 @@ export function getInstalledVersion(): string {
|
||||
const data = JSON.parse(raw) as {
|
||||
dependencies?: Record<string, { version?: string }>;
|
||||
};
|
||||
return data?.dependencies?.[CLI_PKG]?.version ?? '';
|
||||
for (const pkg of INSTALLED_PACKAGE_ORDER) {
|
||||
const version = data?.dependencies?.[pkg]?.version;
|
||||
if (version) {
|
||||
return { name: pkg, version };
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
return '';
|
||||
return { name: '', version: '' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest published version from the Gitea npm registry.
|
||||
* For legacy @mosaic/cli installs, try the legacy package first and fall back
|
||||
* to @mosaic/mosaic to support the CLI -> mosaic package consolidation.
|
||||
* Returns empty string on failure.
|
||||
*/
|
||||
export function getLatestVersion(): string {
|
||||
return npmExec(`view ${CLI_PKG} version --registry=${REGISTRY}`);
|
||||
export function getLatestVersion(installedPackage = ''): { name: string; version: string } {
|
||||
const candidates =
|
||||
installedPackage === LEGACY_PKG ? [LEGACY_PKG, MODERN_PKG] : [MODERN_PKG, LEGACY_PKG];
|
||||
|
||||
for (const pkg of candidates) {
|
||||
const version = npmExec(`view ${pkg} version --registry=${REGISTRY}`);
|
||||
if (version) {
|
||||
return { name: pkg, version };
|
||||
}
|
||||
}
|
||||
|
||||
return { name: '', version: '' };
|
||||
}
|
||||
|
||||
export function getInstallCommand(result: Pick<UpdateCheckResult, 'targetPackage'>): string {
|
||||
return `npm i -g ${result.targetPackage || MODERN_PKG}@latest`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,31 +218,49 @@ export function getLatestVersion(): string {
|
||||
* Never throws.
|
||||
*/
|
||||
export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckResult {
|
||||
const current = getInstalledVersion();
|
||||
const currentInfo = getInstalledVersion();
|
||||
const current = currentInfo.version;
|
||||
|
||||
let latest: string;
|
||||
let latestInfo: { name: string; version: string };
|
||||
let checkedAt: string;
|
||||
|
||||
if (!options?.skipCache) {
|
||||
const cached = readCache();
|
||||
const cached = readCache(currentInfo.name);
|
||||
if (cached) {
|
||||
latest = cached.latest;
|
||||
latestInfo = {
|
||||
name: cached.targetPackage || MODERN_PKG,
|
||||
version: cached.latest,
|
||||
};
|
||||
checkedAt = cached.checkedAt;
|
||||
} else {
|
||||
latest = getLatestVersion();
|
||||
latestInfo = getLatestVersion(currentInfo.name);
|
||||
checkedAt = new Date().toISOString();
|
||||
writeCache({ latest, checkedAt, registry: REGISTRY });
|
||||
writeCache({
|
||||
currentPackage: currentInfo.name,
|
||||
latest: latestInfo.version,
|
||||
targetPackage: latestInfo.name,
|
||||
checkedAt,
|
||||
registry: REGISTRY,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
latest = getLatestVersion();
|
||||
latestInfo = getLatestVersion(currentInfo.name);
|
||||
checkedAt = new Date().toISOString();
|
||||
writeCache({ latest, checkedAt, registry: REGISTRY });
|
||||
writeCache({
|
||||
currentPackage: currentInfo.name,
|
||||
latest: latestInfo.version,
|
||||
targetPackage: latestInfo.name,
|
||||
checkedAt,
|
||||
registry: REGISTRY,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
current,
|
||||
latest,
|
||||
updateAvailable: !!(current && latest && semverLt(current, latest)),
|
||||
currentPackage: currentInfo.name,
|
||||
latest: latestInfo.version,
|
||||
targetPackage: latestInfo.name || MODERN_PKG,
|
||||
updateAvailable: !!(current && latestInfo.version && semverLt(current, latestInfo.version)),
|
||||
checkedAt,
|
||||
registry: REGISTRY,
|
||||
};
|
||||
@@ -224,14 +272,21 @@ export function checkForUpdate(options?: { skipCache?: boolean }): UpdateCheckRe
|
||||
export function formatUpdateNotice(result: UpdateCheckResult): string {
|
||||
if (!result.updateAvailable) return '';
|
||||
|
||||
const installCommand = getInstallCommand(result);
|
||||
const targetChanged =
|
||||
result.currentPackage && result.targetPackage && result.currentPackage !== result.targetPackage;
|
||||
|
||||
const lines = [
|
||||
'',
|
||||
'╭─────────────────────────────────────────────────╮',
|
||||
'│ │',
|
||||
`│ Update available: ${result.current} → ${result.latest}`.padEnd(50) + '│',
|
||||
...(targetChanged
|
||||
? [`│ Package target: ${result.currentPackage} → ${result.targetPackage}`.padEnd(50) + '│']
|
||||
: []),
|
||||
'│ │',
|
||||
'│ Run: bash tools/install.sh │',
|
||||
'│ Or: npm i -g @mosaic/cli@latest │',
|
||||
`│ Or: ${installCommand}`.padEnd(50) + '│',
|
||||
'│ │',
|
||||
'╰─────────────────────────────────────────────────╯',
|
||||
'',
|
||||
|
||||
Reference in New Issue
Block a user