Compare commits

...

4 Commits

Author SHA1 Message Date
jkunz 06f2de3230 v2.16.1
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 11:11:03 +00:00
jkunz cc3128f07b fix(cli): guard startup update check 2026-05-10 11:10:30 +00:00
jkunz 358d677e72 v2.16.0
Default (tags) / security (push) Failing after 1s
Default (tags) / test (push) Failing after 1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-05-10 11:05:17 +00:00
jkunz f421c5851d feat(cli): add toolchain management command 2026-05-10 11:04:57 +00:00
9 changed files with 605 additions and 8 deletions
+12
View File
@@ -3,6 +3,18 @@
## Pending ## Pending
## 2026-05-10 - 2.16.1
### Fixes
- Prevent startup update checks from crashing when installed package metadata is incomplete.
## 2026-05-10 - 2.16.0
### Features
- Add `gitzone tools` for managing the global `@git.zone` toolchain from the main CLI.
## 2026-05-10 - 2.15.0 ## 2026-05-10 - 2.15.0
### Features ### Features
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@git.zone/cli", "name": "@git.zone/cli",
"private": false, "private": false,
"version": "2.15.0", "version": "2.16.1",
"description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.", "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts", "typings": "dist_ts/index.d.ts",
+18
View File
@@ -57,6 +57,7 @@ gitzone release
| `format` | Plan or apply project formatting and standardization | | `format` | Plan or apply project formatting and standardization |
| `config` | Inspect, update, and migrate `.smartconfig.json` | | `config` | Inspect, update, and migrate `.smartconfig.json` |
| `services` | Manage local MongoDB, MinIO, and Elasticsearch containers | | `services` | Manage local MongoDB, MinIO, and Elasticsearch containers |
| `tools` | Manage the global `@git.zone` toolchain |
| `template` | Scaffold projects from built-in templates | | `template` | Scaffold projects from built-in templates |
| `meta` | Manage multi-repository workspaces | | `meta` | Manage multi-repository workspaces |
| `open` | Open repository assets like CI pages | | `open` | Open repository assets like CI pages |
@@ -67,6 +68,23 @@ gitzone release
Global flags include `--help`, `--json`, `--plain`, `--agent`, `--no-interactive`, and `--no-check-updates`. Global flags include `--help`, `--json`, `--plain`, `--agent`, `--no-interactive`, and `--no-check-updates`.
## Toolchain Management
`gitzone tools` replaces the former `gtools` command from `@git.zone/tools`. It manages globally installed `@git.zone` development tools through pnpm.
```bash
# Check installed @git.zone tools and update outdated packages
gitzone tools update
# Update without prompts
gitzone tools update -y
# Install missing managed @git.zone tools
gitzone tools install
```
`gitzone tools update` checks `@git.zone/cli` first. If the CLI itself needs an update, it updates `@git.zone/cli` and asks you to rerun the command before updating the rest of the toolchain.
## Commit Workflow ## Commit Workflow
`gitzone commit` creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images. `gitzone commit` creates one semantic source commit. It does not bump versions, create tags, publish packages, or push Docker images.
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', name: '@git.zone/cli',
version: '2.15.0', version: '2.16.1',
description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
} }
+24 -6
View File
@@ -2,6 +2,7 @@ import * as plugins from "./plugins.js";
import * as paths from "./paths.js"; import * as paths from "./paths.js";
import { GitzoneConfig } from "./classes.gitzoneconfig.js"; import { GitzoneConfig } from "./classes.gitzoneconfig.js";
import { getRawCliMode } from "./helpers.climode.js"; import { getRawCliMode } from "./helpers.climode.js";
import { commitinfo } from "./00_commitinfo_data.js";
const gitzoneSmartcli = new plugins.smartcli.Smartcli(); const gitzoneSmartcli = new plugins.smartcli.Smartcli();
@@ -11,20 +12,29 @@ export let run = async () => {
// get packageInfo // get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir); const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
const projectInfoVersion = (projectInfo.npm as any)?.version;
const packageVersion =
typeof projectInfoVersion === "string" && projectInfoVersion.length > 0
? projectInfoVersion
: commitinfo.version;
// check for updates // check for updates
if (rawCliMode.checkUpdates) { if (rawCliMode.checkUpdates) {
const smartupdateInstance = new plugins.smartupdate.SmartUpdate(); const smartupdateInstance = new plugins.smartupdate.SmartUpdate();
await smartupdateInstance.check( try {
"gitzone", await smartupdateInstance.check(
projectInfo.npm.version, "gitzone",
"http://gitzone.gitlab.io/gitzone/changelog.html", packageVersion,
); "http://gitzone.gitlab.io/gitzone/changelog.html",
);
} catch {
// Update checks must never block actual CLI commands.
}
} }
if (rawCliMode.output === "human") { if (rawCliMode.output === "human") {
console.log("---------------------------------------------"); console.log("---------------------------------------------");
} }
gitzoneSmartcli.addVersion(projectInfo.npm.version); gitzoneSmartcli.addVersion(packageVersion);
// ======> Standard task <====== // ======> Standard task <======
@@ -137,6 +147,14 @@ export let run = async () => {
modHelpers.run(argvArg); modHelpers.run(argvArg);
}); });
/**
* manage the global @git.zone toolchain
*/
gitzoneSmartcli.addCommand("tools").subscribe(async (argvArg) => {
const modTools = await import("./mod_tools/index.js");
await modTools.run(argvArg);
});
/** /**
* manage release configuration * manage release configuration
*/ */
+13
View File
@@ -23,6 +23,7 @@ const commandSummaries: ICommandHelpSummary[] = [
{ name: "format", description: "Plan or apply project formatting changes" }, { name: "format", description: "Plan or apply project formatting changes" },
{ name: "config", description: "Read and change .smartconfig.json settings" }, { name: "config", description: "Read and change .smartconfig.json settings" },
{ name: "services", description: "Manage or configure development services" }, { name: "services", description: "Manage or configure development services" },
{ name: "tools", description: "Manage the global @git.zone toolchain" },
{ name: "template", description: "Create a project from a template" }, { name: "template", description: "Create a project from a template" },
{ name: "open", description: "Open project assets and CI pages" }, { name: "open", description: "Open project assets and CI pages" },
{ name: "docker", description: "Run Docker-related maintenance tasks" }, { name: "docker", description: "Run Docker-related maintenance tasks" },
@@ -75,6 +76,7 @@ export let run = async (argvArg: any = {}) => {
{ name: "Configure release settings", value: "config" }, { name: "Configure release settings", value: "config" },
{ name: "Create from template", value: "template" }, { name: "Create from template", value: "template" },
{ name: "Manage dev services (MongoDB, S3)", value: "services" }, { name: "Manage dev services (MongoDB, S3)", value: "services" },
{ name: "Manage global @git.zone tools", value: "tools" },
{ name: "Open project assets", value: "open" }, { name: "Open project assets", value: "open" },
{ name: "Show help", value: "help" }, { name: "Show help", value: "help" },
], ],
@@ -113,6 +115,11 @@ export let run = async (argvArg: any = {}) => {
await modServices.run({ _: ["services"] }); await modServices.run({ _: ["services"] });
break; break;
} }
case "tools": {
const modTools = await import("../mod_tools/index.js");
await modTools.run({ _: ["tools"] });
break;
}
case "open": { case "open": {
const modOpen = await import("../mod_open/index.js"); const modOpen = await import("../mod_open/index.js");
await modOpen.run({ _: ["open"] }); await modOpen.run({ _: ["open"] });
@@ -196,6 +203,7 @@ export async function showHelp(
console.log(" gitzone release --plan"); console.log(" gitzone release --plan");
console.log(" gitzone format plan --json"); console.log(" gitzone format plan --json");
console.log(" gitzone services set mongodb,minio"); console.log(" gitzone services set mongodb,minio");
console.log(" gitzone tools update");
console.log(""); console.log("");
console.log("Run gitzone <command> --help for command-specific usage."); console.log("Run gitzone <command> --help for command-specific usage.");
console.log(""); console.log("");
@@ -231,6 +239,11 @@ async function showCommandHelp(
modServices.showHelp(mode); modServices.showHelp(mode);
return true; return true;
} }
case "tools": {
const modTools = await import("../mod_tools/index.js");
modTools.showHelp(mode);
return true;
}
default: default:
return false; return false;
} }
+176
View File
@@ -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("'", "'\\''")}'`;
}
+359
View File
@@ -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("");
}
+1
View File
@@ -0,0 +1 @@
export * from "../plugins.js";