import * as plugins from "./mod.plugins.js"; export interface IInstalledPackage { name: string; version: string; } export interface IPackageUpdateInfo { name: string; currentVersion: string; latestVersion: string; needsUpdate: boolean; } export interface IPackageManagerInfo { available: boolean; currentVersion: string; latestVersion: string | null; needsUpdate: boolean; } export class PackageManagerUtil { private shell = new plugins.smartshell.Smartshell({ executor: "bash", }); public async detectPnpm(): Promise { try { const result = await this.shell.execSilent("pnpm --version 2>/dev/null"); return result.exitCode === 0 && Boolean(result.stdout.trim()); } catch { return false; } } public async getPnpmVersionInfo(): Promise { const available = await this.detectPnpm(); if (!available) { return { available: false, currentVersion: "unknown", latestVersion: null, needsUpdate: false, }; } const currentVersion = await this.getCurrentPnpmVersion(); const latestVersion = await this.getLatestVersion("pnpm", ["https://registry.npmjs.org"]); return { available: true, currentVersion, latestVersion, needsUpdate: latestVersion ? this.isNewerVersion(currentVersion, latestVersion) : false, }; } public async getInstalledPackages(): Promise { const packages: IInstalledPackage[] = []; try { const result = await this.shell.execSilent("pnpm list -g --depth=0 --json 2>/dev/null || true"); const output = result.stdout.trim(); if (!output) { return packages; } const data = JSON.parse(output); const dataArray = Array.isArray(data) ? data : [data]; for (const item of dataArray) { const dependencies = item.dependencies || {}; for (const [name, info] of Object.entries(dependencies)) { if (!name.startsWith("@git.zone/")) { continue; } packages.push({ name, version: (info as any).version || "unknown", }); } } } catch { return packages; } return packages; } public async getLatestVersion( packageName: string, registries = ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"], ): Promise { for (const registry of registries) { const latest = await this.getLatestVersionFromRegistry(registry, packageName); if (latest) { return latest; } } return null; } public async installLatest(packageName: string): Promise { const packageSpecifier = `${packageName}@latest`; console.log(` Installing ${packageSpecifier} via pnpm...`); try { const result = await this.shell.exec(`pnpm add -g ${shellQuote(packageSpecifier)}`); return result.exitCode === 0; } catch { return false; } } public isNewerVersion(current: string, latest: string): boolean { const currentParts = normalizeSemver(current); const latestParts = normalizeSemver(latest); for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { const currentPart = currentParts[i] || 0; const latestPart = latestParts[i] || 0; if (latestPart > currentPart) return true; if (latestPart < currentPart) return false; } return false; } private async getCurrentPnpmVersion(): Promise { try { const result = await this.shell.execSilent("pnpm --version 2>/dev/null"); const versionMatch = result.stdout.trim().match(/(\d+\.\d+\.\d+)/); return versionMatch?.[1] || "unknown"; } catch { return "unknown"; } } private async getLatestVersionFromRegistry( registry: string, packageName: string, ): Promise { const encodedName = packageName.replace("/", "%2f"); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 8000); try { const response = await fetch(`${registry}/${encodedName}`, { signal: controller.signal, headers: { accept: "application/json", }, }); if (!response.ok) { return null; } const data = await response.json(); const latest = (data as any)["dist-tags"]?.latest; return typeof latest === "string" && latest.length > 0 ? latest : null; } catch { return null; } finally { clearTimeout(timeout); } } } function normalizeSemver(version: string): number[] { return version .replace(/^[^\d]*/, "") .split(".") .map((part) => parseInt(part, 10) || 0); } function shellQuote(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; }