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("'", "'\\''")}'`;
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
import * as plugins from "./mod.plugins.js";
|
||||
import { commitinfo } from "../00_commitinfo_data.js";
|
||||
import type { ICliMode } from "../helpers.climode.js";
|
||||
import { getCliMode, printJson } from "../helpers.climode.js";
|
||||
import {
|
||||
PackageManagerUtil,
|
||||
type IInstalledPackage,
|
||||
type IPackageUpdateInfo,
|
||||
} from "./classes.packagemanager.js";
|
||||
|
||||
export const GITZONE_PACKAGES = [
|
||||
"@git.zone/cli",
|
||||
"@git.zone/tsdoc",
|
||||
"@git.zone/tsbuild",
|
||||
"@git.zone/tstest",
|
||||
"@git.zone/tspublish",
|
||||
"@git.zone/tsbundle",
|
||||
"@git.zone/tsdocker",
|
||||
"@git.zone/tsview",
|
||||
"@git.zone/tswatch",
|
||||
"@git.zone/tsrust",
|
||||
];
|
||||
|
||||
export const run = async (argvArg: any = {}): Promise<void> => {
|
||||
const mode = await getCliMode(argvArg);
|
||||
const command = argvArg._?.[1] || "help";
|
||||
|
||||
if (mode.help || command === "help") {
|
||||
showHelp(mode);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
case "update":
|
||||
await runUpdate(argvArg, mode);
|
||||
break;
|
||||
case "install":
|
||||
await runInstall(argvArg, mode);
|
||||
break;
|
||||
default:
|
||||
showHelp(mode);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
const verbose = Boolean(argvArg.v || argvArg.verbose);
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
|
||||
console.log("Scanning for installed @git.zone packages...\n");
|
||||
|
||||
const pnpmInfo = await pmUtil.getPnpmVersionInfo();
|
||||
if (!pnpmInfo.available) {
|
||||
console.log("pnpm is required for gitzone tools update, but it was not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Package manager:\n");
|
||||
console.log(" Name Current Latest Status");
|
||||
console.log(" ----------------------------------------------");
|
||||
const latestPnpm = (pnpmInfo.latestVersion || "unknown").padEnd(12);
|
||||
const pnpmStatus = pnpmInfo.latestVersion === null
|
||||
? "? Version unknown"
|
||||
: pnpmInfo.needsUpdate
|
||||
? "Update available"
|
||||
: "Up to date";
|
||||
console.log(` ${"pnpm".padEnd(9)}${pnpmInfo.currentVersion.padEnd(12)}${latestPnpm}${pnpmStatus}`);
|
||||
console.log("");
|
||||
|
||||
if (verbose) {
|
||||
console.log("Using pnpm as the supported global package manager.\n");
|
||||
}
|
||||
|
||||
const selfUpdated = await handleSelfUpdate(pmUtil, mode);
|
||||
if (selfUpdated) {
|
||||
return;
|
||||
}
|
||||
|
||||
const installedPackages = await pmUtil.getInstalledPackages();
|
||||
const packageInfos = await getPackageUpdateInfos(pmUtil, installedPackages);
|
||||
|
||||
if (packageInfos.length === 0) {
|
||||
console.log("No managed @git.zone packages found installed globally.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Installed @git.zone packages:\n");
|
||||
console.log(" Package Current Latest Status");
|
||||
console.log(" ------------------------------------------------------------");
|
||||
for (const packageInfo of packageInfos) {
|
||||
const status = packageInfo.latestVersion === "unknown"
|
||||
? "? Version unknown"
|
||||
: packageInfo.needsUpdate
|
||||
? "Update available"
|
||||
: "Up to date";
|
||||
console.log(
|
||||
` ${packageInfo.name.padEnd(28)}${packageInfo.currentVersion.padEnd(12)}${packageInfo.latestVersion.padEnd(12)}${status}`,
|
||||
);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
await printMissingPackages(pmUtil, installedPackages);
|
||||
|
||||
const packagesToUpdate = packageInfos.filter((packageInfo) => packageInfo.needsUpdate);
|
||||
if (packagesToUpdate.length === 0) {
|
||||
console.log("All managed packages are up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${packagesToUpdate.length} package(s) with available updates.\n`);
|
||||
|
||||
if (!mode.yes && !mode.interactive) {
|
||||
console.log("Run gitzone tools update -y to update without prompts.");
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldUpdate = mode.yes;
|
||||
if (!shouldUpdate) {
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const answer = await interactInstance.askQuestion({
|
||||
type: "confirm",
|
||||
name: "confirmUpdate",
|
||||
message: "Do you want to update these packages?",
|
||||
default: true,
|
||||
});
|
||||
shouldUpdate = answer.value === true;
|
||||
}
|
||||
|
||||
if (!shouldUpdate) {
|
||||
console.log("Update cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
await installPackages(pmUtil, packagesToUpdate.map((packageInfo) => packageInfo.name), "updated");
|
||||
}
|
||||
|
||||
async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
const verbose = Boolean(argvArg.v || argvArg.verbose);
|
||||
const pmUtil = new PackageManagerUtil();
|
||||
|
||||
console.log("Scanning for missing @git.zone packages...\n");
|
||||
|
||||
const pnpmAvailable = await pmUtil.detectPnpm();
|
||||
if (!pnpmAvailable) {
|
||||
console.log("pnpm is required for gitzone tools install, but it was not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
console.log("Using pnpm as the supported global package manager.\n");
|
||||
}
|
||||
|
||||
const installedPackages = await pmUtil.getInstalledPackages();
|
||||
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||
|
||||
if (missingPackages.length === 0) {
|
||||
console.log("All managed @git.zone packages are already installed.");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${missingPackages.length} missing package(s).\n`);
|
||||
|
||||
if (!mode.yes && !mode.interactive) {
|
||||
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||
console.log("Run gitzone tools install -y to install all missing packages without prompts.");
|
||||
return;
|
||||
}
|
||||
|
||||
let selectedPackages = missingPackages;
|
||||
if (!mode.yes) {
|
||||
const choicesWithVersions: Array<{ name: string; value: string }> = [];
|
||||
for (const packageName of missingPackages) {
|
||||
const latest = await pmUtil.getLatestVersion(packageName);
|
||||
choicesWithVersions.push({
|
||||
name: `${packageName}${latest ? `@${latest}` : ""}`,
|
||||
value: packageName,
|
||||
});
|
||||
}
|
||||
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const answer = await interactInstance.askQuestion({
|
||||
type: "checkbox",
|
||||
name: "packages",
|
||||
message: "Select packages to install:",
|
||||
default: missingPackages,
|
||||
choices: choicesWithVersions,
|
||||
});
|
||||
|
||||
selectedPackages = answer.value as string[];
|
||||
if (selectedPackages.length === 0) {
|
||||
console.log("No packages selected. Nothing to install.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await installPackages(pmUtil, selectedPackages, "installed");
|
||||
}
|
||||
|
||||
async function handleSelfUpdate(
|
||||
pmUtil: PackageManagerUtil,
|
||||
mode: ICliMode,
|
||||
): Promise<boolean> {
|
||||
console.log("Checking for gitzone self-update...\n");
|
||||
const currentVersion = commitinfo.version;
|
||||
const latestVersion = await pmUtil.getLatestVersion("@git.zone/cli");
|
||||
|
||||
if (!latestVersion || !pmUtil.isNewerVersion(currentVersion, latestVersion)) {
|
||||
console.log(` @git.zone/cli ${currentVersion} Up to date\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(` @git.zone/cli ${currentVersion} -> ${latestVersion} Update available\n`);
|
||||
|
||||
if (!mode.yes && !mode.interactive) {
|
||||
console.log("Run gitzone tools update -y to update gitzone first.");
|
||||
return true;
|
||||
}
|
||||
|
||||
let shouldUpdate = mode.yes;
|
||||
if (!shouldUpdate) {
|
||||
const interactInstance = new plugins.smartinteract.SmartInteract();
|
||||
const answer = await interactInstance.askQuestion({
|
||||
type: "confirm",
|
||||
name: "confirmSelfUpdate",
|
||||
message: "Do you want to update gitzone itself first?",
|
||||
default: true,
|
||||
});
|
||||
shouldUpdate = answer.value === true;
|
||||
}
|
||||
|
||||
if (!shouldUpdate) {
|
||||
console.log("Skipping gitzone self-update.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const success = await pmUtil.installLatest("@git.zone/cli");
|
||||
if (!success) {
|
||||
console.log("\ngitzone self-update failed. Continuing with the current version.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log("\ngitzone has been updated. Re-run gitzone tools update to check remaining packages.");
|
||||
return true;
|
||||
}
|
||||
|
||||
async function getPackageUpdateInfos(
|
||||
pmUtil: PackageManagerUtil,
|
||||
installedPackages: IInstalledPackage[],
|
||||
): Promise<IPackageUpdateInfo[]> {
|
||||
const packageInfos: IPackageUpdateInfo[] = [];
|
||||
for (const installedPackage of installedPackages) {
|
||||
if (!GITZONE_PACKAGES.includes(installedPackage.name)) {
|
||||
continue;
|
||||
}
|
||||
const latestVersion = await pmUtil.getLatestVersion(installedPackage.name);
|
||||
packageInfos.push({
|
||||
name: installedPackage.name,
|
||||
currentVersion: installedPackage.version,
|
||||
latestVersion: latestVersion || "unknown",
|
||||
needsUpdate: latestVersion
|
||||
? pmUtil.isNewerVersion(installedPackage.version, latestVersion)
|
||||
: false,
|
||||
});
|
||||
}
|
||||
return packageInfos;
|
||||
}
|
||||
|
||||
async function printMissingPackages(
|
||||
pmUtil: PackageManagerUtil,
|
||||
installedPackages: IInstalledPackage[],
|
||||
): Promise<void> {
|
||||
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||
if (missingPackages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Not installed (managed @git.zone packages):\n");
|
||||
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||
console.log("Run gitzone tools install to install missing packages.\n");
|
||||
}
|
||||
|
||||
async function printPackageListWithLatest(
|
||||
pmUtil: PackageManagerUtil,
|
||||
packageNames: string[],
|
||||
): Promise<void> {
|
||||
console.log(" Package Latest");
|
||||
console.log(" ----------------------------------------");
|
||||
for (const packageName of packageNames) {
|
||||
const latest = await pmUtil.getLatestVersion(packageName);
|
||||
console.log(` ${packageName.padEnd(28)} ${latest || "unknown"}`);
|
||||
}
|
||||
console.log("");
|
||||
}
|
||||
|
||||
async function installPackages(
|
||||
pmUtil: PackageManagerUtil,
|
||||
packageNames: string[],
|
||||
action: "installed" | "updated",
|
||||
): Promise<void> {
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const success = await pmUtil.installLatest(packageName);
|
||||
if (success) {
|
||||
console.log(` ${packageName} ${action} successfully`);
|
||||
successCount++;
|
||||
} else {
|
||||
console.log(` ${packageName} failed`);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
if (failCount === 0) {
|
||||
console.log(`All ${successCount} package(s) ${action} successfully.`);
|
||||
} else {
|
||||
console.log(`${successCount} package(s) ${action}, ${failCount} failed.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function showHelp(mode?: ICliMode): void {
|
||||
if (mode?.json) {
|
||||
printJson({
|
||||
name: "gitzone tools",
|
||||
usage: "gitzone tools <command> [options]",
|
||||
commands: [
|
||||
{ name: "update", description: "Check and update globally installed @git.zone packages" },
|
||||
{ name: "install", description: "Install missing managed @git.zone packages" },
|
||||
],
|
||||
flags: [
|
||||
{ flag: "-y, --yes", description: "Run without confirmation prompts" },
|
||||
{ flag: "-v, --verbose", description: "Show package manager diagnostics" },
|
||||
],
|
||||
packageManager: "pnpm",
|
||||
managedPackages: GITZONE_PACKAGES,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("");
|
||||
console.log("Usage: gitzone tools <command> [options]");
|
||||
console.log("");
|
||||
console.log("Commands:");
|
||||
console.log(" update Check and update globally installed @git.zone packages");
|
||||
console.log(" install Install missing managed @git.zone packages");
|
||||
console.log("");
|
||||
console.log("Options:");
|
||||
console.log(" -y, --yes Run without confirmation prompts");
|
||||
console.log(" -v, --verbose Show package manager diagnostics");
|
||||
console.log("");
|
||||
console.log("Examples:");
|
||||
console.log(" gitzone tools update");
|
||||
console.log(" gitzone tools update -y");
|
||||
console.log(" gitzone tools install");
|
||||
console.log("");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "../plugins.js";
|
||||
Reference in New Issue
Block a user