diff --git a/changelog.md b/changelog.md index d787cd6..32afbf2 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-02-09 - 3.5.0 - feat(install) +add interactive install command and module to detect and install missing @git.zone packages + +- Add ts/mod_install/index.ts: implements interactive/non-interactive flow to detect package managers, collect installed @git.zone packages, prompt user (via smartinteract) and install selected packages via PackageManagerUtil. +- Add ts/mod_install/mod.plugins.ts: re-export smartinteract and smartshell for the installer. +- Update ts/tools.cli.ts: register new 'install' command and add help text for install flags. +- Update ts/mod_update/index.ts: export GITZONE_PACKAGES and print a summary of managed packages that are not installed with latest versions and a suggestion to run 'gtools install'. + ## 2026-02-09 - 3.4.1 - fix(tools) no changes to commit diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index a6b79e3..a5ad5e5 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tools', - version: '3.4.1', + version: '3.5.0', description: 'A CLI tool placeholder for development utilities.' } diff --git a/ts/mod_install/index.ts b/ts/mod_install/index.ts new file mode 100644 index 0000000..94a8e58 --- /dev/null +++ b/ts/mod_install/index.ts @@ -0,0 +1,151 @@ +import * as plugins from './mod.plugins.js'; +import { PackageManagerUtil, type TPackageManager, type IPackageManagerInfo, type IInstalledPackage } from '../mod_update/classes.packagemanager.js'; +import { GITZONE_PACKAGES } from '../mod_update/index.js'; + +export interface IInstallOptions { + yes?: boolean; + verbose?: boolean; +} + +export const run = async (options: IInstallOptions = {}): Promise => { + const pmUtil = new PackageManagerUtil(); + const verbose = options.verbose === true; + + console.log('Scanning for missing @git.zone packages...\n'); + + // 1. Detect available package managers + const detectedPMs: IPackageManagerInfo[] = []; + for (const pm of ['npm', 'yarn', 'pnpm'] as TPackageManager[]) { + const info = await pmUtil.detectPackageManager(pm, verbose); + if (info.available) { + detectedPMs.push(info); + } + } + + if (detectedPMs.length === 0) { + console.log('No package managers found (npm, yarn, pnpm).'); + return; + } + + if (verbose) { + console.log(`Detected package managers: ${detectedPMs.map(p => p.name).join(', ')}\n`); + } + + // 2. Collect all globally installed @git.zone packages across all PMs + const installedByPm = new Map(); + const allInstalledNames = new Set(); + + for (const pmInfo of detectedPMs) { + const installed = await pmUtil.getInstalledPackages(pmInfo.name); + installedByPm.set(pmInfo.name, installed); + for (const pkg of installed) { + if (GITZONE_PACKAGES.includes(pkg.name)) { + allInstalledNames.add(pkg.name); + } + } + } + + // 3. Determine which managed packages are not installed + const notInstalled = GITZONE_PACKAGES.filter(name => !allInstalledNames.has(name)); + + if (notInstalled.length === 0) { + console.log('All managed @git.zone packages are already installed.'); + return; + } + + console.log(`Found ${notInstalled.length} package(s) not installed.\n`); + + // 4. Determine the best default PM (the one with most @git.zone packages) + let bestPm = detectedPMs[0].name; + let bestCount = 0; + for (const pmInfo of detectedPMs) { + const pkgs = installedByPm.get(pmInfo.name) || []; + const gitzoneCount = pkgs.filter(p => GITZONE_PACKAGES.includes(p.name)).length; + if (gitzoneCount > bestCount) { + bestCount = gitzoneCount; + bestPm = pmInfo.name; + } + } + + let selectedPm: TPackageManager; + let selectedPackages: string[]; + + if (options.yes === true) { + // Non-interactive: use best PM, install all missing + selectedPm = bestPm; + selectedPackages = notInstalled; + console.log(`Using ${selectedPm} (auto-detected).\n`); + } else { + // 5. Ask which PM to use + const smartinteractInstance = new plugins.smartinteract.SmartInteract(); + + if (detectedPMs.length === 1) { + selectedPm = detectedPMs[0].name; + console.log(`Using ${selectedPm} (only available PM).\n`); + } else { + const pmAnswer = await smartinteractInstance.askQuestion({ + name: 'packageManager', + type: 'list', + message: 'Which package manager should be used for installation?', + default: bestPm, + choices: detectedPMs.map(pm => ({ + name: `${pm.name}${pm.name === bestPm ? ' (recommended — most @git.zone packages)' : ''}`, + value: pm.name, + })), + }); + selectedPm = pmAnswer.value as TPackageManager; + } + + // 6. Ask which packages to install + // Fetch latest versions for display + const choicesWithVersions: Array<{ name: string; value: string }> = []; + for (const pkgName of notInstalled) { + const latest = await pmUtil.getLatestVersion(pkgName); + const versionLabel = latest ? `@${latest}` : ''; + choicesWithVersions.push({ + name: `${pkgName}${versionLabel}`, + value: pkgName, + }); + } + + const pkgAnswer = await smartinteractInstance.askQuestion({ + name: 'packages', + type: 'checkbox', + message: 'Select packages to install:', + default: notInstalled, // all pre-checked + choices: choicesWithVersions, + }); + + selectedPackages = pkgAnswer.value as string[]; + + if (selectedPackages.length === 0) { + console.log('No packages selected. Nothing to install.'); + return; + } + } + + // 7. Install selected packages + console.log(`Installing ${selectedPackages.length} package(s) via ${selectedPm}...\n`); + + let successCount = 0; + let failCount = 0; + + for (const pkgName of selectedPackages) { + const success = await pmUtil.executeUpdate(selectedPm, pkgName); + if (success) { + console.log(` ✓ ${pkgName} installed successfully`); + successCount++; + } else { + console.log(` ✗ ${pkgName} installation failed`); + failCount++; + } + } + + // 8. Summary + console.log(''); + if (failCount === 0) { + console.log(`All ${successCount} package(s) installed successfully!`); + } else { + console.log(`Installed ${successCount} package(s), ${failCount} failed.`); + } +}; diff --git a/ts/mod_install/mod.plugins.ts b/ts/mod_install/mod.plugins.ts new file mode 100644 index 0000000..a17be6f --- /dev/null +++ b/ts/mod_install/mod.plugins.ts @@ -0,0 +1,4 @@ +import * as smartinteract from '@push.rocks/smartinteract'; +import * as smartshell from '@push.rocks/smartshell'; + +export { smartinteract, smartshell }; diff --git a/ts/mod_update/index.ts b/ts/mod_update/index.ts index 7d2d561..4fd860f 100644 --- a/ts/mod_update/index.ts +++ b/ts/mod_update/index.ts @@ -5,7 +5,7 @@ import { commitinfo } from '../00_commitinfo_data.js'; // Curated list of known @git.zone CLI tools to track for updates. // This list is intentionally hardcoded to only track official tools. // Add new entries here when new @git.zone packages are published. -const GITZONE_PACKAGES = [ +export const GITZONE_PACKAGES = [ '@git.zone/cli', '@git.zone/tsdoc', '@git.zone/tsbuild', @@ -171,6 +171,24 @@ export const run = async (options: IUpdateOptions = {}): Promise => { console.log(''); + // Show managed packages that are not installed anywhere + const installedNames = new Set(allPackages.map(p => p.name)); + const notInstalled = GITZONE_PACKAGES.filter(name => !installedNames.has(name)); + + if (notInstalled.length > 0) { + console.log('Not installed (managed @git.zone packages):\n'); + console.log(' Package Latest'); + console.log(' ─────────────────────────────────────────'); + for (const pkgName of notInstalled) { + const latest = await pmUtil.getLatestVersion(pkgName); + const name = pkgName.padEnd(28); + const version = latest || 'unknown'; + console.log(` ${name} ${version}`); + } + console.log(''); + console.log(' Run "gtools install" to install missing packages.\n'); + } + // Filter packages that need updates const packagesToUpdate = allPackages.filter(p => p.needsUpdate); diff --git a/ts/tools.cli.ts b/ts/tools.cli.ts index dc7f13f..51569a1 100644 --- a/ts/tools.cli.ts +++ b/ts/tools.cli.ts @@ -1,5 +1,6 @@ import * as plugins from './tools.plugins.js'; import * as modUpdate from './mod_update/index.js'; +import * as modInstall from './mod_install/index.js'; import { commitinfo } from './00_commitinfo_data.js'; export const run = async () => { @@ -8,9 +9,12 @@ export const run = async () => { toolsCli.standardCommand().subscribe(async (argvArg) => { console.log('@git.zone/tools - CLI utility for managing @git.zone packages\n'); console.log('Commands:'); - console.log(' update Check and update globally installed @git.zone packages'); - console.log(' update -y Update without confirmation prompt'); - console.log(' update --verbose Show detection diagnostics'); + console.log(' update Check and update globally installed @git.zone packages'); + console.log(' update -y Update without confirmation prompt'); + console.log(' update --verbose Show detection diagnostics'); + console.log(' install Interactively install missing @git.zone packages'); + console.log(' install -y Install all missing packages without prompts'); + console.log(' install --verbose Show detection diagnostics'); console.log(''); console.log('Use "gtools --help" for more information about a command.'); }); @@ -21,6 +25,12 @@ export const run = async () => { await modUpdate.run({ yes: yesFlag, verbose: verboseFlag }); }); + toolsCli.addCommand('install').subscribe(async (argvArg) => { + const yesFlag = argvArg.y === true || argvArg.yes === true; + const verboseFlag = argvArg.v === true || argvArg.verbose === true; + await modInstall.run({ yes: yesFlag, verbose: verboseFlag }); + }); + toolsCli.addVersion(commitinfo.version); toolsCli.startParse(); };