Compare commits

...

6 Commits

Author SHA1 Message Date
jkunz 9823b580bb v2.19.8
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-06-06 07:38:49 +00:00
jkunz 2c34cca1a8 fix(tools): stream package update rows 2026-06-06 07:34:26 +00:00
jkunz 0d05df8591 v2.19.7
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-06-05 17:08:20 +00:00
jkunz ae91c8f23d fix(tools): use pnpm update for global self updates 2026-06-05 17:03:16 +00:00
jkunz 5279f5dcec v2.19.6
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-06-05 11:22:54 +00:00
jkunz 9f6012e79c fix(tools): respect pnpm maturity policy during updates 2026-06-05 07:20:18 +00:00
5 changed files with 372 additions and 30 deletions
+24
View File
@@ -1,5 +1,29 @@
# Changelog
## 2026-06-06 - 2.19.8
### Fixes
- stream `gitzone tools update` package rows while fetching versions
- Prints each installed package row as soon as its latest version lookup completes.
## 2026-06-05 - 2.19.7
### Fixes
- make global tool self-updates verify and use pnpm's update path
- Uses `pnpm update -g <package> --latest` before falling back to install for latest-version updates.
- Verifies installed package versions through `pnpm list -g` so pnpm v11 hash roots are handled correctly.
## 2026-06-05 - 2.19.6
### Fixes
- make `gitzone tools update` respect pnpm maturity policy and finish cleanup
- Selects mature pnpm and managed tool versions when `minimumReleaseAge` is active.
- Refreshes command shims after self-updates and successful pnpm-only updates.
- Avoids counting retained mixed legacy pnpm roots as perpetual cleanup actions.
## 2026-06-03 - 2.19.5
### Fixes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@git.zone/cli",
"private": false,
"version": "2.19.5",
"version": "2.19.8",
"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",
"typings": "dist_ts/index.d.ts",
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/cli',
version: '2.19.5',
version: '2.19.8',
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.'
}
+297 -7
View File
@@ -70,7 +70,7 @@ export class PackageManagerUtil {
}
const currentVersion = await this.getCurrentPnpmVersion();
const latestVersion = await this.getLatestVersion("pnpm", [
const latestVersion = await this.getLatestMatureVersion("pnpm", [
"https://registry.npmjs.org",
]);
@@ -371,6 +371,51 @@ export class PackageManagerUtil {
return null;
}
public async getLatestMatureVersion(
packageName: string,
registries = [
"https://verdaccio.lossless.digital",
"https://registry.npmjs.org",
],
): Promise<string | null> {
const minimumReleaseAgeMinutes = await this.getMinimumReleaseAgeMinutes();
for (const registry of registries) {
const metadata = await this.getPackageMetadataFromRegistry(
registry,
packageName,
);
if (!metadata) {
continue;
}
const latest = getLatestVersionFromMetadata(metadata);
if (!latest && !minimumReleaseAgeMinutes) {
continue;
}
if (!minimumReleaseAgeMinutes) {
return latest;
}
const minimumReleaseAgeExcluded = latest
? await this.isMinimumReleaseAgeExcluded(packageName, latest)
: false;
if (latest && minimumReleaseAgeExcluded) {
return latest;
}
const matureLatest = getLatestMatureVersionFromMetadata(
metadata,
minimumReleaseAgeMinutes,
);
if (matureLatest) {
return matureLatest;
}
}
return null;
}
public async installLatest(
packageName: string,
version = "latest",
@@ -380,14 +425,48 @@ export class PackageManagerUtil {
return false;
}
const packageSpecifier = `${packageName}@${version}`;
console.log(` Installing ${packageSpecifier} via pnpm...`);
let effectiveVersion = version;
if (version === "latest") {
const latestVersion = await this.getLatestMatureVersion(packageName);
console.log(` Updating ${packageName} via pnpm...`);
try {
const updateResult = await this.shell.exec(
`${pnpmCommand} update -g ${shellQuote(packageName)} --latest`,
);
const installedVersion = await this.getCurrentInstalledPackageVersion(
packageName,
);
if (
updateResult.exitCode === 0 &&
installedVersion &&
(!latestVersion || installedVersion === latestVersion)
) {
return true;
}
} catch {
// Missing globals need an add instead of update.
}
effectiveVersion = latestVersion || version;
}
const packageSpecifier = `${packageName}@${effectiveVersion}`;
console.log(` Installing ${packageSpecifier} via pnpm...`);
try {
const result = await this.shell.exec(
`${pnpmCommand} add -g ${shellQuote(packageSpecifier)}`,
);
return result.exitCode === 0;
if (result.exitCode !== 0) {
return false;
}
if (effectiveVersion === "latest") {
return true;
}
const installedVersion = await this.getCurrentInstalledPackageVersion(
packageName,
);
return installedVersion === effectiveVersion;
} catch {
return false;
}
@@ -484,6 +563,42 @@ export class PackageManagerUtil {
return this.shell.execSilent(`${pnpmCommand} ${commandArgs}`);
}
private async getMinimumReleaseAgeMinutes(): Promise<number> {
try {
const result = await this.execPnpmSilent(
"config get minimum-release-age 2>/dev/null",
);
const rawValue = result?.stdout.trim() || "";
const value = Number.parseInt(rawValue, 10);
if (Number.isFinite(value) && value >= 0) {
return value;
}
const currentVersion = await this.getCurrentPnpmVersion();
return getDefaultMinimumReleaseAgeMinutes(currentVersion);
} catch {
const currentVersion = await this.getCurrentPnpmVersion();
return getDefaultMinimumReleaseAgeMinutes(currentVersion);
}
}
private async isMinimumReleaseAgeExcluded(
packageName: string,
version: string,
): Promise<boolean> {
try {
const result = await this.execPnpmSilent(
"config get minimum-release-age-exclude 2>/dev/null",
);
const rawPatterns = parsePnpmConfigStringList(result?.stdout || "");
return rawPatterns.some((pattern) =>
packageSelectorMatches(packageName, version, pattern),
);
} catch {
return false;
}
}
private async getPnpmListProjects(): Promise<IPnpmListProject[]> {
try {
const result = await this.execPnpmSilent(
@@ -555,6 +670,15 @@ export class PackageManagerUtil {
return Array.from(packageMap.values());
}
private async getCurrentInstalledPackageVersion(
packageName: string,
): Promise<string | null> {
const installedPackage = (await this.getCurrentInstalledPackages()).find(
(packageInfo) => packageInfo.name === packageName,
);
return installedPackage?.version || null;
}
private async inspectGlobalRoot(
globalDir: string,
current: boolean,
@@ -710,6 +834,17 @@ export class PackageManagerUtil {
registry: string,
packageName: string,
): Promise<string | null> {
const data = await this.getPackageMetadataFromRegistry(
registry,
packageName,
);
return data ? getLatestVersionFromMetadata(data) : null;
}
private async getPackageMetadataFromRegistry(
registry: string,
packageName: string,
): Promise<any | null> {
const encodedName = packageName.replace("/", "%2f");
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
@@ -724,9 +859,7 @@ export class PackageManagerUtil {
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;
return await response.json();
} catch {
return null;
} finally {
@@ -767,6 +900,163 @@ function getDependencyPackagePath(info: any): string | undefined {
: undefined;
}
function getLatestVersionFromMetadata(metadata: any): string | null {
const latest = metadata?.["dist-tags"]?.latest;
return typeof latest === "string" && latest.length > 0 ? latest : null;
}
function getDefaultMinimumReleaseAgeMinutes(currentPnpmVersion: string): number {
const majorVersion = normalizeSemver(currentPnpmVersion)[0] || 0;
return majorVersion >= 11 ? 1440 : 0;
}
function getLatestMatureVersionFromMetadata(
metadata: any,
minimumReleaseAgeMinutes: number,
): string | null {
const versions = metadata?.versions;
const versionTimes = metadata?.time;
if (
!versions ||
typeof versions !== "object" ||
!versionTimes ||
typeof versionTimes !== "object"
) {
return null;
}
const cutoffTimestamp = Date.now() - minimumReleaseAgeMinutes * 60 * 1000;
return Object.keys(versions)
.filter((version) => /^\d+\.\d+\.\d+$/.test(version))
.filter((version) => {
const publishedAt = Date.parse(versionTimes[version]);
return Number.isFinite(publishedAt) && publishedAt <= cutoffTimestamp;
})
.sort(compareSemverDescending)[0] || null;
}
function compareSemverDescending(versionA: string, versionB: string): number {
const versionAParts = normalizeSemver(versionA);
const versionBParts = normalizeSemver(versionB);
for (
let i = 0;
i < Math.max(versionAParts.length, versionBParts.length);
i++
) {
const versionAPart = versionAParts[i] || 0;
const versionBPart = versionBParts[i] || 0;
if (versionAPart !== versionBPart) {
return versionBPart - versionAPart;
}
}
return 0;
}
function parsePnpmConfigStringList(rawValue: string): string[] {
const trimmedValue = rawValue.trim();
if (
!trimmedValue ||
trimmedValue === "undefined" ||
trimmedValue === "null"
) {
return [];
}
try {
const parsedValue = JSON.parse(trimmedValue);
if (Array.isArray(parsedValue)) {
return parsedValue
.filter((item) => typeof item === "string")
.map((item) => normalizePnpmConfigListItem(item))
.filter((item) => item.length > 0);
}
if (typeof parsedValue === "string") {
return [normalizePnpmConfigListItem(parsedValue)].filter(
(item) => item.length > 0,
);
}
} catch {
// pnpm commonly prints string arrays as comma-delimited text.
}
return trimmedValue
.split(/[\n,]/)
.map((item) => normalizePnpmConfigListItem(item))
.filter((item) => item.length > 0);
}
function normalizePnpmConfigListItem(rawItem: string): string {
return rawItem
.trim()
.replace(/^-\s*/, "")
.replace(/^\[/, "")
.replace(/\]$/, "")
.trim()
.replace(/^['"]/, "")
.replace(/['"]$/, "")
.trim();
}
function packageSelectorMatches(
packageName: string,
version: string,
pattern: string,
): boolean {
const selectorParts = splitPackageSelector(pattern);
if (!globPatternMatches(packageName, selectorParts.packagePattern)) {
return false;
}
if (!selectorParts.versionSelector) {
return true;
}
return selectorParts.versionSelector
.split("||")
.map((versionSelector) => versionSelector.trim())
.some((versionSelector) => {
const packageVersionPrefix = `${packageName}@`;
const normalizedVersionSelector = versionSelector.startsWith(
packageVersionPrefix,
)
? versionSelector.slice(packageVersionPrefix.length)
: versionSelector;
return normalizedVersionSelector === version;
});
}
function splitPackageSelector(pattern: string): {
packagePattern: string;
versionSelector: string | null;
} {
const versionSeparatorIndex = pattern.startsWith("@")
? pattern.indexOf("@", 1)
: pattern.indexOf("@");
if (versionSeparatorIndex <= 0) {
return {
packagePattern: pattern,
versionSelector: null,
};
}
return {
packagePattern: pattern.slice(0, versionSeparatorIndex),
versionSelector: pattern.slice(versionSeparatorIndex + 1).trim() || null,
};
}
function globPatternMatches(value: string, pattern: string): boolean {
const escapedPattern = pattern
.split("*")
.map((patternPart) => escapeRegExp(patternPart))
.join(".*");
return new RegExp(`^${escapedPattern}$`).test(value);
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function getPackagePath(globalDir: string, packageName: string): string {
return plugins.path.join(
globalDir,
+49 -21
View File
@@ -86,11 +86,17 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
}
const installedPackages = await pmUtil.getInstalledPackages();
const packageInfos = await getPackageUpdateInfos(pmUtil, installedPackages);
const managedInstalledPackages = installedPackages.filter((packageInfo) =>
GITZONE_PACKAGES.includes(packageInfo.name),
);
let packageInfos: IPackageUpdateInfo[] = [];
const legacyRoots = await pmUtil.getLegacyGlobalRoots();
const legacyCleanupNeeded = legacyRoots.length > 0;
const legacyCleanupRoots = legacyRoots.filter(
(legacyRoot) => legacyRoot.safeToDelete,
);
const legacyCleanupNeeded = legacyCleanupRoots.length > 0;
if (packageInfos.length === 0) {
if (managedInstalledPackages.length === 0) {
console.log("No managed @git.zone packages found installed globally.");
} else {
console.log("Installed @git.zone packages:\n");
@@ -100,11 +106,11 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
console.log(
" ------------------------------------------------------------",
);
for (const packageInfo of packageInfos) {
console.log(
` ${packageInfo.name.padEnd(28)}${packageInfo.currentVersion.padEnd(12)}${packageInfo.latestVersion.padEnd(12)}${getPackageStatus(packageInfo)}`,
);
}
packageInfos = await getPackageUpdateInfos(
pmUtil,
managedInstalledPackages,
true,
);
console.log("");
await printMissingPackages(pmUtil, installedPackages);
@@ -131,10 +137,10 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
console.log("");
} else if (legacyCleanupNeeded) {
console.log(
`Detected ${legacyRoots.length} legacy pnpm global root(s) for cleanup.`,
`Detected ${legacyCleanupRoots.length} legacy pnpm global root(s) for cleanup.`,
);
if (verbose) {
for (const legacyRoot of legacyRoots) {
for (const legacyRoot of legacyCleanupRoots) {
console.log(` ${legacyRoot.globalDir}`);
}
}
@@ -180,11 +186,12 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
return;
}
let pnpmUpdated = false;
if (pnpmNeedsUpdate && pnpmInfo.latestVersion) {
console.log(`Updating pnpm to ${pnpmInfo.latestVersion}...`);
const success = await pmUtil.updatePnpm(pnpmInfo.latestVersion);
pnpmUpdated = await pmUtil.updatePnpm(pnpmInfo.latestVersion);
console.log(
success
pnpmUpdated
? "pnpm updated successfully.\n"
: "pnpm update failed. Continuing with package updates.\n",
);
@@ -205,8 +212,11 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
)
: { successCount: 0, failCount: 0 };
if (packagesToUpdate.length > 0 || legacyCleanupNeeded) {
if (packagesToUpdate.length > 0 || legacyCleanupNeeded || pnpmUpdated) {
await syncCurrentGlobalShims(pmUtil);
}
if (packagesToUpdate.length > 0 || legacyCleanupNeeded) {
await cleanupLegacyInstalls(pmUtil);
}
}
@@ -256,7 +266,7 @@ async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
if (!mode.yes) {
const choicesWithVersions: Array<{ name: string; value: string }> = [];
for (const packageName of missingPackages) {
const latest = await pmUtil.getLatestVersion(packageName);
const latest = await pmUtil.getLatestMatureVersion(packageName);
choicesWithVersions.push({
name: `${packageName}${latest ? `@${latest}` : ""}`,
value: packageName,
@@ -288,7 +298,7 @@ async function handleSelfUpdate(
): Promise<boolean> {
console.log("Checking for gitzone self-update...\n");
const currentVersion = commitinfo.version;
const latestVersion = await pmUtil.getLatestVersion("@git.zone/cli");
const latestVersion = await pmUtil.getLatestMatureVersion("@git.zone/cli");
if (!latestVersion || !pmUtil.isNewerVersion(currentVersion, latestVersion)) {
console.log(` @git.zone/cli ${currentVersion} Up to date\n`);
@@ -329,8 +339,10 @@ async function handleSelfUpdate(
return false;
}
await syncCurrentGlobalShims(pmUtil);
await cleanupLegacyInstalls(pmUtil);
console.log(
"\ngitzone has been updated. Re-run gitzone tools update to check remaining packages.",
"\ngitzone has been updated and command shims have been refreshed. Re-run gitzone tools update to check remaining packages.",
);
return true;
}
@@ -338,14 +350,17 @@ async function handleSelfUpdate(
async function getPackageUpdateInfos(
pmUtil: PackageManagerUtil,
installedPackages: IInstalledPackage[],
logRows = false,
): 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({
const latestVersion = await pmUtil.getLatestMatureVersion(
installedPackage.name,
);
const packageInfo: IPackageUpdateInfo = {
name: installedPackage.name,
currentVersion: installedPackage.version,
latestVersion: latestVersion || "unknown",
@@ -354,7 +369,13 @@ async function getPackageUpdateInfos(
: false,
needsMigration: installedPackage.legacy === true,
globalDir: installedPackage.globalDir,
});
};
packageInfos.push(packageInfo);
if (logRows) {
console.log(
` ${packageInfo.name.padEnd(28)}${packageInfo.currentVersion.padEnd(12)}${packageInfo.latestVersion.padEnd(12)}${getPackageStatus(packageInfo)}`,
);
}
}
return packageInfos;
}
@@ -401,7 +422,7 @@ async function printPackageListWithLatest(
console.log(" Package Latest");
console.log(" ----------------------------------------");
for (const packageName of packageNames) {
const latest = await pmUtil.getLatestVersion(packageName);
const latest = await pmUtil.getLatestMatureVersion(packageName);
console.log(` ${packageName.padEnd(28)} ${latest || "unknown"}`);
}
console.log("");
@@ -420,7 +441,14 @@ async function installPackages(
typeof packageSpec === "string" ? packageSpec : packageSpec.name;
const packageVersion =
typeof packageSpec === "string" ? undefined : packageSpec.version;
const success = await pmUtil.installLatest(packageName, packageVersion);
const effectivePackageVersion =
packageVersion ||
(await pmUtil.getLatestMatureVersion(packageName)) ||
undefined;
const success = await pmUtil.installLatest(
packageName,
effectivePackageVersion,
);
if (success) {
console.log(` ${packageName} ${action} successfully`);
successCount++;