Files
cli/ts/mod_tools/index.ts
T

540 lines
15 KiB
TypeScript

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 pnpmNeedsUpdate = Boolean(
pnpmInfo.latestVersion && pnpmInfo.needsUpdate,
);
const selfUpdated = await handleSelfUpdate(pmUtil, mode);
if (selfUpdated) {
return;
}
const installedPackages = await pmUtil.getInstalledPackages();
const packageInfos = await getPackageUpdateInfos(pmUtil, installedPackages);
const legacyRoots = await pmUtil.getLegacyGlobalRoots();
const legacyCleanupNeeded = legacyRoots.length > 0;
if (packageInfos.length === 0) {
console.log("No managed @git.zone packages found installed globally.");
} else {
console.log("Installed @git.zone packages:\n");
console.log(
" Package Current Latest Status",
);
console.log(
" ------------------------------------------------------------",
);
for (const packageInfo of packageInfos) {
console.log(
` ${packageInfo.name.padEnd(28)}${packageInfo.currentVersion.padEnd(12)}${packageInfo.latestVersion.padEnd(12)}${getPackageStatus(packageInfo)}`,
);
}
console.log("");
await printMissingPackages(pmUtil, installedPackages);
}
const packagesToUpdate = packageInfos.filter(
(packageInfo) => packageInfo.needsUpdate || packageInfo.needsMigration,
);
const packagesToMigrate = packageInfos.filter(
(packageInfo) => packageInfo.needsMigration,
);
if (packagesToMigrate.length > 0) {
console.log(
`Detected ${packagesToMigrate.length} package(s) in legacy pnpm global roots.`,
);
if (verbose) {
for (const packageInfo of packagesToMigrate) {
console.log(
` ${packageInfo.name} -> ${packageInfo.globalDir || "unknown"}`,
);
}
}
console.log("");
} else if (legacyCleanupNeeded) {
console.log(
`Detected ${legacyRoots.length} legacy pnpm global root(s) for cleanup.`,
);
if (verbose) {
for (const legacyRoot of legacyRoots) {
console.log(` ${legacyRoot.globalDir}`);
}
}
console.log("");
}
if (
packagesToUpdate.length === 0 &&
!pnpmNeedsUpdate &&
!legacyCleanupNeeded
) {
console.log("All managed packages are up to date.");
return;
}
const actionCount =
packagesToUpdate.length +
(pnpmNeedsUpdate ? 1 : 0) +
(legacyCleanupNeeded ? 1 : 0);
console.log(`Found ${actionCount} update action(s).\n`);
if (!mode.yes && !mode.interactive) {
console.log(
"Run gitzone tools update -y to update, migrate, and cleanup 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, migrate, and cleanup these tools?",
default: true,
});
shouldUpdate = answer.value === true;
}
if (!shouldUpdate) {
console.log("Update cancelled.");
return;
}
if (pnpmNeedsUpdate && pnpmInfo.latestVersion) {
console.log(`Updating pnpm to ${pnpmInfo.latestVersion}...`);
const success = await pmUtil.updatePnpm(pnpmInfo.latestVersion);
console.log(
success
? "pnpm updated successfully.\n"
: "pnpm update failed. Continuing with package updates.\n",
);
}
const installResult =
packagesToUpdate.length > 0
? await installPackages(
pmUtil,
packagesToUpdate.map((packageInfo) => ({
name: packageInfo.name,
version:
packageInfo.latestVersion !== "unknown"
? packageInfo.latestVersion
: undefined,
})),
"updated",
)
: { successCount: 0, failCount: 0 };
if (packagesToUpdate.length > 0 || legacyCleanupNeeded) {
await syncCurrentGlobalShims(pmUtil);
await cleanupLegacyInstalls(pmUtil);
}
}
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,
needsMigration: installedPackage.legacy === true,
globalDir: installedPackage.globalDir,
});
}
return packageInfos;
}
function getPackageStatus(packageInfo: IPackageUpdateInfo): string {
if (packageInfo.latestVersion === "unknown") {
return "? Version unknown";
}
if (packageInfo.needsUpdate && packageInfo.needsMigration) {
return "Update + migrate";
}
if (packageInfo.needsUpdate) {
return "Update available";
}
if (packageInfo.needsMigration) {
return "Migrate global root";
}
return "Up to date";
}
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,
packageSpecs: Array<string | { name: string; version?: string }>,
action: "installed" | "updated",
): Promise<{ successCount: number; failCount: number }> {
let successCount = 0;
let failCount = 0;
for (const packageSpec of packageSpecs) {
const packageName =
typeof packageSpec === "string" ? packageSpec : packageSpec.name;
const packageVersion =
typeof packageSpec === "string" ? undefined : packageSpec.version;
const success = await pmUtil.installLatest(packageName, packageVersion);
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.`);
}
return { successCount, failCount };
}
async function cleanupLegacyInstalls(
pmUtil: PackageManagerUtil,
): Promise<void> {
const cleanupResults = await pmUtil.cleanupLegacyGlobalRoots();
if (cleanupResults.length === 0) {
return;
}
console.log("Legacy pnpm global roots:\n");
for (const cleanupResult of cleanupResults) {
if (cleanupResult.deleted) {
console.log(` ${cleanupResult.globalDir} deleted`);
} else {
console.log(
` ${cleanupResult.globalDir} kept (${cleanupResult.reason || "unknown reason"})`,
);
}
}
console.log("");
}
async function syncCurrentGlobalShims(
pmUtil: PackageManagerUtil,
): Promise<void> {
const shimResults = await pmUtil.syncCurrentGlobalShims();
const changedResults = shimResults.filter(
(shimResult) => shimResult.action !== "skipped",
);
const skippedResults = shimResults.filter(
(shimResult) => shimResult.action === "skipped",
);
if (changedResults.length === 0 && skippedResults.length === 0) {
return;
}
console.log("Command shims:\n");
for (const shimResult of changedResults) {
console.log(` ${shimResult.name} ${shimResult.action}`);
}
for (const shimResult of skippedResults) {
console.log(
` ${shimResult.name} skipped (${shimResult.reason || "unknown reason"})`,
);
}
console.log("");
}
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("");
}