Compare commits

...

5 Commits

Author SHA1 Message Date
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
jkunz 382d51453a v2.19.5
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-03 11:48:45 +00:00
jkunz cc094d8801 fix(metadata): update project metadata for code.foss.global repository 2026-06-03 10:32:33 +00:00
jkunz 0b7cd9c635 fix(cli): improve changelog release handling and TypeScript compatibility 2026-06-03 09:53:36 +00:00
24 changed files with 1371 additions and 3233 deletions
+4 -4
View File
@@ -9,11 +9,11 @@
"schemaVersion": 2,
"projectType": "npm",
"module": {
"githost": "gitlab.com",
"gitscope": "gitzone/private",
"gitrepo": "gitzone",
"githost": "code.foss.global",
"gitscope": "git.zone",
"gitrepo": "cli",
"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.",
"npmPackagename": "@gitzone_private/gitzone",
"npmPackagename": "@git.zone/cli",
"license": "MIT",
"keywords": [
"CLI",
+20 -1
View File
@@ -1,7 +1,26 @@
# Changelog
## Pending
## 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
- improve changelog release handling and TypeScript compatibility (cli)
- Avoid mutating changelog files when only reading pending entries
- Remove the consumed Pending section when moving entries to a versioned release
- Use async project metadata creation and safer unknown error handling for updated dependencies
- Add changelog helper tests for missing, consumed, and recreated Pending sections
- update project metadata for code.foss.global repository (metadata)
- Updated repository, issue tracker, and homepage URLs to code.foss.global/git.zone/cli
- Aligned smartconfig module metadata with the @git.zone/cli package and git.zone scope
## 2026-05-24 - 2.19.4
+11 -11
View File
@@ -1,7 +1,7 @@
{
"name": "@git.zone/cli",
"private": false,
"version": "2.19.4",
"version": "2.19.6",
"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",
@@ -30,7 +30,7 @@
},
"repository": {
"type": "git",
"url": "https://gitlab.com/gitzone/private/gitzone.git"
"url": "https://code.foss.global/git.zone/cli.git"
},
"keywords": [
"CLI",
@@ -53,21 +53,21 @@
"author": "Task Venture Capital GmbH",
"license": "MIT",
"bugs": {
"url": "https://gitlab.com/gitzone/private/gitzone/issues"
"url": "https://code.foss.global/git.zone/cli/issues"
},
"homepage": "https://gitlab.com/gitzone/private/gitzone#readme",
"homepage": "https://code.foss.global/git.zone/cli#README",
"devDependencies": {
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbuild": "^4.4.2",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.3.2",
"@types/node": "^25.4.0"
"@git.zone/tstest": "^3.6.6",
"@types/node": "^25.9.1"
},
"dependencies": {
"@git.zone/tsdoc": "^2.0.6",
"@git.zone/tspublish": "^1.11.2",
"@push.rocks/commitinfo": "^1.0.12",
"@push.rocks/commitinfo": "^1.0.13",
"@push.rocks/early": "^4.0.4",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartdelay": "^3.0.5",
@@ -86,10 +86,10 @@
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartscaf": "^4.0.21",
"@push.rocks/smartscaf": "^4.0.22",
"@push.rocks/smartshell": "^3.5.0",
"@push.rocks/smartunique": "^3.0.9",
"@push.rocks/smartupdate": "^2.0.6",
"@push.rocks/smartupdate": "^2.0.7",
"prettier": "^3.8.1"
},
"files": [
+912 -3166
View File
File diff suppressed because it is too large Load Diff
+79
View File
@@ -0,0 +1,79 @@
import { rm, writeFile, readFile, mkdtemp } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { tap, expect } from '@git.zone/tstest/tapbundle';
import {
appendPendingChangelogEntry,
movePendingToVersion,
readPendingChangelog,
} from './ts/helpers.changelog.js';
const withChangelogFile = async (
initialContent: string,
testFunction: (filePath: string) => Promise<void>,
) => {
const testDir = await mkdtemp(join(tmpdir(), 'gitzone-changelog-'));
const changelogPath = join(testDir, 'changelog.md');
try {
await writeFile(changelogPath, initialContent);
await testFunction(changelogPath);
} finally {
await rm(testDir, { recursive: true, force: true });
}
};
tap.test('readPendingChangelog should not create a missing Pending section', async () => {
await withChangelogFile('# Changelog\n\n## 2026-01-01 - 1.0.0\n\n### Fixes\n\n- existing\n', async (filePath) => {
const pending = await readPendingChangelog(filePath, 'Pending');
const changelogContent = await readFile(filePath, 'utf8');
expect(pending.isEmpty).toBeTrue();
expect(pending.block).toEqual('');
expect(changelogContent).not.toInclude('## Pending');
});
});
tap.test('movePendingToVersion should fail when Pending is missing', async () => {
await withChangelogFile('# Changelog\n\n## 2026-01-01 - 1.0.0\n\n### Fixes\n\n- existing\n', async (filePath) => {
let errorMessage = '';
try {
await movePendingToVersion(filePath, 'Pending', '## {{date}} - {{version}}', '1.0.1', '2026-01-02');
} catch (error) {
errorMessage = error instanceof Error ? error.message : String(error);
}
expect(errorMessage).toEqual('No pending changelog entries. Nothing to release.');
const changelogContent = await readFile(filePath, 'utf8');
expect(changelogContent).not.toInclude('## Pending');
});
});
tap.test('movePendingToVersion should remove the consumed Pending section', async () => {
await withChangelogFile('# Changelog\n\n## Pending\n\n### Fixes\n\n- pending fix\n\n## 2026-01-01 - 1.0.0\n\n### Fixes\n\n- existing\n', async (filePath) => {
await movePendingToVersion(filePath, 'Pending', '## {{date}} - {{version}}', '1.0.1', '2026-01-02');
const changelogContent = await readFile(filePath, 'utf8');
expect(changelogContent).not.toInclude('## Pending');
expect(changelogContent).toInclude('## 2026-01-02 - 1.0.1\n\n### Fixes\n\n- pending fix');
expect(changelogContent).toInclude('## 2026-01-01 - 1.0.0\n\n### Fixes\n\n- existing');
});
});
tap.test('appendPendingChangelogEntry should recreate Pending when needed', async () => {
await withChangelogFile('# Changelog\n\n## 2026-01-01 - 1.0.0\n\n### Fixes\n\n- existing\n', async (filePath) => {
await appendPendingChangelogEntry(filePath, 'Pending', {
type: 'fix',
scope: 'changelog',
message: 'record pending changes',
});
const changelogContent = await readFile(filePath, 'utf8');
expect(changelogContent).toInclude('## Pending\n\n### Fixes\n\n- record pending changes (changelog)');
expect(changelogContent).toInclude('## 2026-01-01 - 1.0.0');
});
});
export default tap.start();
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/cli',
version: '2.19.4',
version: '2.19.6',
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.'
}
+1 -1
View File
@@ -35,7 +35,7 @@ export class GitzoneConfig {
return gitzoneConfig;
}
public data: IGitzoneConfigData;
public data!: IGitzoneConfigData;
public async readConfigFromCwd() {
const smartconfigInstance = new plugins.smartconfig.Smartconfig(paths.cwd);
+1 -1
View File
@@ -105,7 +105,7 @@ export let run = async () => {
const rawCliMode = await getRawCliMode();
// get packageInfo
const projectInfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
const projectInfo = await plugins.projectinfo.ProjectInfo.create(paths.packageDir);
const projectInfoVersion = (projectInfo.npm as any)?.version;
const packageVersion =
typeof projectInfoVersion === "string" && projectInfoVersion.length > 0
+22 -8
View File
@@ -19,6 +19,12 @@ export interface IPendingChangelog {
isEmpty: boolean;
}
interface IChangelogSection {
start: number;
bodyStart: number;
end: number;
}
const bucketForCommitType = (commitType: string): TChangelogBucket => {
switch (commitType) {
case "BREAKING CHANGE":
@@ -48,7 +54,7 @@ const writeChangelog = async (filePath: string, content: string): Promise<void>
const findPendingSection = (
content: string,
sectionName: string,
): { start: number; bodyStart: number; end: number } | null => {
): IChangelogSection | null => {
const headingRegex = new RegExp(`^##\\s+${sectionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
const match = headingRegex.exec(content);
if (!match || match.index === undefined) {
@@ -123,9 +129,11 @@ export const readPendingChangelog = async (
filePath: string,
sectionName = "Pending",
): Promise<IPendingChangelog> => {
const content = await ensurePendingSection(filePath, sectionName);
const pendingSection = findPendingSection(content, sectionName)!;
const block = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
const content = await readChangelog(filePath);
const pendingSection = findPendingSection(content, sectionName);
const block = pendingSection
? content.slice(pendingSection.bodyStart, pendingSection.end).trim()
: "";
return {
block,
isEmpty: block.length === 0,
@@ -149,8 +157,11 @@ export const movePendingToVersion = async (
version: string,
dateString: string,
): Promise<void> => {
let content = await ensurePendingSection(filePath, sectionName);
const pendingSection = findPendingSection(content, sectionName)!;
let content = await readChangelog(filePath);
const pendingSection = findPendingSection(content, sectionName);
if (!pendingSection) {
throw new Error("No pending changelog entries. Nothing to release.");
}
const pendingBlock = content.slice(pendingSection.bodyStart, pendingSection.end).trim();
if (!pendingBlock) {
throw new Error("No pending changelog entries. Nothing to release.");
@@ -159,7 +170,10 @@ export const movePendingToVersion = async (
const renderedHeading = versionHeading
.replaceAll("{{version}}", version)
.replaceAll("{{date}}", dateString);
const nextContent = content.slice(pendingSection.end).replace(/^\n+/, "");
content = `${content.slice(0, pendingSection.bodyStart)}\n\n${renderedHeading}\n\n${pendingBlock}\n\n${nextContent}`;
const beforePending = content.slice(0, pendingSection.start).trimEnd();
const afterPending = content.slice(pendingSection.end).replace(/^\n+/, "").trimEnd();
content = [beforePending, renderedHeading, pendingBlock, afterPending]
.filter((block) => block.length > 0)
.join("\n\n");
await writeChangelog(filePath, content);
};
+4 -2
View File
@@ -27,7 +27,8 @@ export async function detectCurrentBranch(): Promise<string> {
logger.log('info', `Detected current branch: ${branchName}`);
return branchName;
} catch (error) {
logger.log('warn', `Failed to detect branch: ${error.message}, falling back to "master"`);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log('warn', `Failed to detect branch: ${errorMessage}, falling back to "master"`);
return 'master';
}
}
@@ -225,6 +226,7 @@ export async function bumpProjectVersion(
return newVersion;
} catch (error) {
throw new Error(`Failed to bump project version: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to bump project version: ${errorMessage}`);
}
}
+2 -1
View File
@@ -42,9 +42,10 @@ export class DiffReporter {
change.content,
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log(
'error',
`Failed to generate diff for ${change.path}: ${error.message}`,
`Failed to generate diff for ${change.path}: ${errorMessage}`,
);
return null;
}
+2 -1
View File
@@ -93,7 +93,8 @@ export class CopyFormatter extends BaseFormatter {
}
}
} catch (error) {
logVerbose(`Failed to process pattern ${pattern.from}: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Failed to process pattern ${pattern.from}: ${errorMessage}`);
}
}
@@ -94,7 +94,8 @@ export class PackageJsonFormatter extends BaseFormatter {
packageJson.pnpm = packageJson.pnpm || {};
packageJson.pnpm.overrides = overrides;
} catch (error) {
logVerbose(`Could not read overrides.json: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Could not read overrides.json: ${errorMessage}`);
}
const newContent = JSON.stringify(packageJson, null, 2);
@@ -117,7 +117,8 @@ export class TemplatesFormatter extends BaseFormatter {
try {
renderedFiles = await this.renderTemplate(templateName);
} catch (error) {
logVerbose(`Failed to render template ${templateName}: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Failed to render template ${templateName}: ${errorMessage}`);
return changes;
}
@@ -46,7 +46,8 @@ export class TsconfigFormatter extends BaseFormatter {
];
}
} catch (error) {
logVerbose(`Could not get tspublish modules: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logVerbose(`Could not get tspublish modules: ${errorMessage}`);
}
tsconfigObject.compilerOptions.paths = { ...existingPaths, ...tspublishPaths };
+1 -1
View File
@@ -26,7 +26,7 @@ export class Meta {
/**
* the meta repo data
*/
public metaRepoData: interfaces.IMetaRepoData;
public metaRepoData!: interfaces.IMetaRepoData;
public smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash',
});
+2 -2
View File
@@ -1,8 +1,8 @@
import * as plugins from './mod.plugins.js';
import * as paths from '../paths.js';
export let run = (argvArg) => {
let projectInfo = new plugins.projectinfo.ProjectInfo(paths.cwd);
export let run = async (argvArg) => {
let projectInfo = await plugins.projectinfo.ProjectInfo.create(paths.cwd);
if (argvArg._[1] === 'ci') {
plugins.smartopen.openUrl(
`https://gitlab.com/${projectInfo.git.gituser}/${projectInfo.git.gitrepo}/settings/ci_cd`,
+5 -3
View File
@@ -148,7 +148,8 @@ export class DockerContainer {
const result = await this.smartshell.exec(command);
return result.exitCode === 0;
} catch (error) {
logger.log('error', `Failed to run container: ${error.message}`);
const errorMessage = error instanceof Error ? error.message : String(error);
logger.log('error', `Failed to run container: ${errorMessage}`);
return false;
}
}
@@ -177,7 +178,8 @@ export class DockerContainer {
const result = await this.smartshell.exec(`docker logs ${tailFlag} ${containerName}`);
return result.stdout;
} catch (error) {
return `Error getting logs: ${error.message}`;
const errorMessage = error instanceof Error ? error.message : String(error);
return `Error getting logs: ${errorMessage}`;
}
}
@@ -258,4 +260,4 @@ export class DockerContainer {
return null;
}
}
}
}
@@ -28,7 +28,7 @@ export interface IServiceConfig {
export class ServiceConfiguration {
private configPath: string;
private config: IServiceConfig;
private config!: IServiceConfig;
private docker: DockerContainer;
constructor() {
@@ -515,4 +515,4 @@ export class ServiceConfiguration {
logger.log('info', ` 📍 S3 Console: ${s3ConsolePort}`);
logger.log('info', ` 📍 Elasticsearch: ${esPort}`);
}
}
}
+9 -6
View File
@@ -61,13 +61,15 @@ export class ServiceManager {
default: ['mongodb', 'minio', 'elasticsearch']
});
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
const enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
this.enabledServices = enabledServices;
// Save to .smartconfig.json
await this.saveServiceConfiguration(this.enabledServices);
await this.saveServiceConfiguration(enabledServices);
} else {
this.enabledServices = gitzoneConfig.services;
logger.log('info', `🔧 Enabled services: ${this.enabledServices.join(', ')}`);
const enabledServices = gitzoneConfig.services as string[];
this.enabledServices = enabledServices;
logger.log('info', `🔧 Enabled services: ${enabledServices.join(', ')}`);
}
}
@@ -902,10 +904,11 @@ export class ServiceManager {
default: currentServices
});
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
const enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
this.enabledServices = enabledServices;
// Save to .smartconfig.json
await this.saveServiceConfiguration(this.enabledServices);
await this.saveServiceConfiguration(enabledServices);
logger.log('ok', '✅ Service configuration updated');
}
+5 -1
View File
@@ -15,7 +15,7 @@ export const isTemplate = async (templateNameArg: string) => {
};
export const getTemplate = async (templateNameArg: string) => {
if (isTemplate(templateNameArg)) {
if (await isTemplate(templateNameArg)) {
const localScafTemplate = new plugins.smartscaf.ScafTemplate(
getTemplatePath(templateNameArg),
);
@@ -50,6 +50,10 @@ export const run = async (argvArg: any) => {
}
const localScafTemplate = await getTemplate(chosenTemplate);
if (!localScafTemplate) {
logger.log('error', `Template ${chosenTemplate} not available`);
return;
}
await localScafTemplate.askCliForMissingVariables();
await localScafTemplate.writeToDisk(paths.cwd);
};
+251 -4
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",
@@ -484,6 +529,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(
@@ -710,6 +791,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 +816,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 +857,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,
+31 -13
View File
@@ -88,7 +88,10 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
const installedPackages = await pmUtil.getInstalledPackages();
const packageInfos = await getPackageUpdateInfos(pmUtil, installedPackages);
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) {
console.log("No managed @git.zone packages found installed globally.");
@@ -131,10 +134,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 +183,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 +209,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 +263,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 +295,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`);
@@ -321,7 +328,7 @@ async function handleSelfUpdate(
return false;
}
const success = await pmUtil.installLatest("@git.zone/cli");
const success = await pmUtil.installLatest("@git.zone/cli", latestVersion);
if (!success) {
console.log(
"\ngitzone self-update failed. Continuing with the current version.\n",
@@ -329,8 +336,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;
}
@@ -344,7 +353,9 @@ async function getPackageUpdateInfos(
if (!GITZONE_PACKAGES.includes(installedPackage.name)) {
continue;
}
const latestVersion = await pmUtil.getLatestVersion(installedPackage.name);
const latestVersion = await pmUtil.getLatestMatureVersion(
installedPackage.name,
);
packageInfos.push({
name: installedPackage.name,
currentVersion: installedPackage.version,
@@ -401,7 +412,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 +431,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++;
-1
View File
@@ -5,7 +5,6 @@
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
},
"exclude": ["dist_*/**/*.d.ts"]