feat(cli): add toolchain management command
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
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<boolean> {
|
||||
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<IPackageManagerInfo> {
|
||||
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<IInstalledPackage[]> {
|
||||
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<string | null> {
|
||||
for (const registry of registries) {
|
||||
const latest = await this.getLatestVersionFromRegistry(registry, packageName);
|
||||
if (latest) {
|
||||
return latest;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async installLatest(packageName: string): Promise<boolean> {
|
||||
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<string> {
|
||||
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<string | null> {
|
||||
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("'", "'\\''")}'`;
|
||||
}
|
||||
Reference in New Issue
Block a user