import * as plugins from './mod.plugins.js'; export type TPackageManager = 'npm' | 'yarn' | 'pnpm'; export interface IInstalledPackage { name: string; version: string; packageManager: TPackageManager; } export interface IPackageUpdateInfo { name: string; currentVersion: string; latestVersion: string; packageManager: TPackageManager; needsUpdate: boolean; } export interface IPackageManagerInfo { name: TPackageManager; available: boolean; detectionMethod?: 'which' | 'version-command'; path?: string; currentVersion?: string; latestVersion?: string | null; needsUpdate?: boolean; } export class PackageManagerUtil { private shell = new plugins.smartshell.Smartshell({ executor: 'bash', }); /** * Check if a package manager is available on the system * Uses multiple detection methods for robustness across different shell contexts */ public async isAvailable(pm: TPackageManager, verbose = false): Promise { const info = await this.detectPackageManager(pm, verbose); return info.available; } /** * Detect a package manager and return detailed info */ public async detectPackageManager(pm: TPackageManager, verbose = false): Promise { const info: IPackageManagerInfo = { name: pm, available: false }; // Primary method: try 'which' command try { const whichResult = await this.shell.execSilent(`which ${pm} 2>/dev/null`); if (whichResult.exitCode === 0 && whichResult.stdout.trim()) { info.available = true; info.detectionMethod = 'which'; info.path = whichResult.stdout.trim(); if (verbose) { console.log(` Checking ${pm}... found via 'which' at ${info.path}`); } return info; } } catch { // Continue to fallback } // Fallback method: try running pm --version directly // This can find PMs that are available but not in PATH for 'which' try { const versionResult = await this.shell.execSilent(`${pm} --version 2>/dev/null`); if (versionResult.exitCode === 0 && versionResult.stdout.trim()) { info.available = true; info.detectionMethod = 'version-command'; if (verbose) { console.log(` Checking ${pm}... found via '--version' (which failed)`); } return info; } } catch { // Not available } if (verbose) { console.log(` Checking ${pm}... not found`); } return info; } /** * Get the current and latest version of a package manager */ public async getPackageManagerVersion(pm: TPackageManager): Promise<{ current: string; latest: string | null }> { let current = 'unknown'; let latest: string | null = null; // Get current version try { const result = await this.shell.execSilent(`${pm} --version 2>/dev/null`); if (result.exitCode === 0 && result.stdout.trim()) { // Parse version from output - handle different formats const output = result.stdout.trim(); // npm: "10.2.0", pnpm: "8.15.0", yarn: "1.22.19" // Some may include prefix like "v1.22.19" const versionMatch = output.match(/(\d+\.\d+\.\d+)/); if (versionMatch) { current = versionMatch[1]; } } } catch { // Keep as unknown } // Get latest version from npm registry try { const result = await this.shell.execSilent(`npm view ${pm} version 2>/dev/null`); if (result.exitCode === 0 && result.stdout.trim()) { latest = result.stdout.trim(); } } catch { // Keep as null } return { current, latest }; } /** * Get all globally installed @git.zone packages for a package manager */ public async getInstalledPackages(pm: TPackageManager): Promise { const packages: IInstalledPackage[] = []; try { let result; switch (pm) { case 'npm': result = await this.shell.execSilent('npm list -g --depth=0 --json 2>/dev/null || true'); break; case 'yarn': result = await this.shell.execSilent('yarn global list --depth=0 --json 2>/dev/null || true'); break; case 'pnpm': result = await this.shell.execSilent('pnpm list -g --depth=0 --json 2>/dev/null || true'); break; } const output = result.stdout.trim(); if (!output) { return packages; } if (pm === 'npm') { try { const data = JSON.parse(output); const deps = data.dependencies || {}; for (const [name, info] of Object.entries(deps)) { if (name.startsWith('@git.zone/')) { packages.push({ name, version: (info as any).version || 'unknown', packageManager: pm, }); } } } catch { // JSON parse failed } } else if (pm === 'pnpm') { // pnpm returns an array of objects try { const data = JSON.parse(output); // Handle array format from pnpm const dataArray = Array.isArray(data) ? data : [data]; for (const item of dataArray) { const deps = item.dependencies || {}; for (const [name, info] of Object.entries(deps)) { if (name.startsWith('@git.zone/')) { packages.push({ name, version: (info as any).version || 'unknown', packageManager: pm, }); } } } } catch { // JSON parse failed } } else if (pm === 'yarn') { // Yarn global list --json outputs multiple JSON lines const lines = output.split('\n').filter(l => l.trim()); for (const line of lines) { try { const data = JSON.parse(line); if (data.type === 'tree' && data.data && data.data.trees) { for (const tree of data.data.trees) { const name = tree.name?.split('@')[0] || ''; if (name.startsWith('@git.zone/')) { const version = tree.name?.split('@').pop() || 'unknown'; packages.push({ name, version, packageManager: pm, }); } } } } catch { // Skip invalid JSON lines } } } } catch { // Command failed, return empty array } return packages; } /** * Get the latest version of a package from npm registry * Tries private registry (verdaccio.lossless.digital) first via API, then falls back to public npm */ public async getLatestVersion(packageName: string): Promise { // URL-encode the package name for scoped packages (@scope/name -> @scope%2fname) const encodedName = packageName.replace('/', '%2f'); // Try private registry first via direct API call (npm view doesn't work reliably) try { const result = await this.shell.execSilent( `curl -sf "https://verdaccio.lossless.digital/${encodedName}" 2>/dev/null` ); if (result.exitCode === 0 && result.stdout.trim()) { const data = JSON.parse(result.stdout.trim()); if (data['dist-tags']?.latest) { return data['dist-tags'].latest; } } } catch { // Continue to public registry } // Fall back to public npm try { const result = await this.shell.execSilent(`npm view ${packageName} version 2>/dev/null`); if (result.exitCode === 0 && result.stdout.trim()) { return result.stdout.trim(); } } catch { // Command failed } return null; } /** * Execute an update for a package using the specified package manager */ public async executeUpdate(pm: TPackageManager, packageName: string): Promise { let command: string; switch (pm) { case 'npm': command = `npm install -g ${packageName}@latest`; break; case 'yarn': command = `yarn global add ${packageName}@latest`; break; case 'pnpm': command = `pnpm add -g ${packageName}@latest`; break; } console.log(` Updating ${packageName} via ${pm}...`); try { const result = await this.shell.exec(command); return result.exitCode === 0; } catch { return false; } } /** * Compare two semver versions * Returns true if latest > current */ public isNewerVersion(current: string, latest: string): boolean { const cleanVersion = (v: string) => v.replace(/^[^\d]*/, ''); const currentClean = cleanVersion(current); const latestClean = cleanVersion(latest); const currentParts = currentClean.split('.').map(n => parseInt(n, 10) || 0); const latestParts = latestClean.split('.').map(n => parseInt(n, 10) || 0); for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) { const curr = currentParts[i] || 0; const lat = latestParts[i] || 0; if (lat > curr) return true; if (lat < curr) return false; } return false; } }