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 => { 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 { 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 { 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 { 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 { 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 { 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 { 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, 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 { 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 { 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 [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 [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(""); }