214 lines
6.3 KiB
TypeScript
214 lines
6.3 KiB
TypeScript
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 class PackageManagerUtil {
|
|
private shell = new plugins.smartshell.Smartshell({
|
|
executor: 'bash',
|
|
});
|
|
|
|
/**
|
|
* Check if a package manager is available on the system
|
|
*/
|
|
public async isAvailable(pm: TPackageManager): Promise<boolean> {
|
|
try {
|
|
const result = await this.shell.execSilent(`which ${pm} >/dev/null 2>&1 && echo "found"`);
|
|
return result.exitCode === 0 && result.stdout.includes('found');
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all globally installed @git.zone packages for a package manager
|
|
*/
|
|
public async getInstalledPackages(pm: TPackageManager): Promise<IInstalledPackage[]> {
|
|
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<string | null> {
|
|
// 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<boolean> {
|
|
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;
|
|
}
|
|
}
|