fix(tools): handle pnpm 11 global migrations
This commit is contained in:
@@ -2,6 +2,12 @@
|
||||
|
||||
## Pending
|
||||
|
||||
### Fixes
|
||||
|
||||
- repair `gitzone tools update` for pnpm 11 global installs
|
||||
- Detects and migrates legacy pnpm global roots such as `PNPM_HOME/global/5`.
|
||||
- Runs global pnpm maintenance with project package-manager switching disabled.
|
||||
- Refreshes pnpm v10 and v11 command shims before deleting stale managed global roots.
|
||||
|
||||
|
||||
## 2026-05-14 - 2.19.1
|
||||
|
||||
@@ -3,6 +3,9 @@ import * as plugins from "./mod.plugins.js";
|
||||
export interface IInstalledPackage {
|
||||
name: string;
|
||||
version: string;
|
||||
globalDir?: string;
|
||||
packagePath?: string;
|
||||
legacy?: boolean;
|
||||
}
|
||||
|
||||
export interface IPackageUpdateInfo {
|
||||
@@ -10,6 +13,8 @@ export interface IPackageUpdateInfo {
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
needsUpdate: boolean;
|
||||
needsMigration?: boolean;
|
||||
globalDir?: string;
|
||||
}
|
||||
|
||||
export interface IPackageManagerInfo {
|
||||
@@ -19,18 +24,38 @@ export interface IPackageManagerInfo {
|
||||
needsUpdate: boolean;
|
||||
}
|
||||
|
||||
export interface ILegacyGlobalRootInfo {
|
||||
globalDir: string;
|
||||
packages: IInstalledPackage[];
|
||||
unmanagedPackageNames: string[];
|
||||
safeToDelete: boolean;
|
||||
}
|
||||
|
||||
export interface ILegacyCleanupResult {
|
||||
globalDir: string;
|
||||
deleted: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface IShimSyncResult {
|
||||
name: string;
|
||||
action: "updated" | "removed" | "skipped";
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
interface IPnpmListProject {
|
||||
path?: string;
|
||||
dependencies?: Record<string, any>;
|
||||
}
|
||||
|
||||
export class PackageManagerUtil {
|
||||
private shell = new plugins.smartshell.Smartshell({
|
||||
executor: "bash",
|
||||
});
|
||||
private pnpmCommand: string | null | undefined;
|
||||
|
||||
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;
|
||||
}
|
||||
return Boolean(await this.getPnpmCommand());
|
||||
}
|
||||
|
||||
public async getPnpmVersionInfo(): Promise<IPackageManagerInfo> {
|
||||
@@ -45,53 +70,300 @@ export class PackageManagerUtil {
|
||||
}
|
||||
|
||||
const currentVersion = await this.getCurrentPnpmVersion();
|
||||
const latestVersion = await this.getLatestVersion("pnpm", ["https://registry.npmjs.org"]);
|
||||
const latestVersion = await this.getLatestVersion("pnpm", [
|
||||
"https://registry.npmjs.org",
|
||||
]);
|
||||
|
||||
return {
|
||||
available: true,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
needsUpdate: latestVersion ? this.isNewerVersion(currentVersion, latestVersion) : false,
|
||||
needsUpdate: latestVersion
|
||||
? this.isNewerVersion(currentVersion, latestVersion)
|
||||
: false,
|
||||
};
|
||||
}
|
||||
|
||||
public async getInstalledPackages(): Promise<IInstalledPackage[]> {
|
||||
const packages: IInstalledPackage[] = [];
|
||||
const packageMap = new Map<string, IInstalledPackage>();
|
||||
const currentPackages = await this.getCurrentInstalledPackages();
|
||||
|
||||
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;
|
||||
for (const packageInfo of currentPackages) {
|
||||
packageMap.set(packageInfo.name, packageInfo);
|
||||
}
|
||||
|
||||
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/")) {
|
||||
const legacyRoots = await this.getLegacyGlobalRoots();
|
||||
for (const legacyRoot of legacyRoots) {
|
||||
for (const packageInfo of legacyRoot.packages) {
|
||||
if (!packageMap.has(packageInfo.name)) {
|
||||
packageMap.set(packageInfo.name, packageInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(packageMap.values()).sort((packageA, packageB) =>
|
||||
packageA.name.localeCompare(packageB.name),
|
||||
);
|
||||
}
|
||||
|
||||
public async getLegacyGlobalRoots(): Promise<ILegacyGlobalRootInfo[]> {
|
||||
const currentGlobalDir = await this.getCurrentGlobalDir();
|
||||
const baseDirs = new Set<string>();
|
||||
const pnpmHome = process.env.PNPM_HOME;
|
||||
|
||||
if (pnpmHome) {
|
||||
baseDirs.add(plugins.path.join(pnpmHome, "global"));
|
||||
}
|
||||
|
||||
if (currentGlobalDir) {
|
||||
baseDirs.add(plugins.path.dirname(currentGlobalDir));
|
||||
}
|
||||
|
||||
const roots: ILegacyGlobalRootInfo[] = [];
|
||||
for (const baseDir of baseDirs) {
|
||||
try {
|
||||
const entries = await plugins.fs.readdir(baseDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
packages.push({
|
||||
name,
|
||||
version: (info as any).version || "unknown",
|
||||
});
|
||||
|
||||
const globalDir = normalizePath(
|
||||
plugins.path.join(baseDir, entry.name),
|
||||
);
|
||||
if (currentGlobalDir && pathsAreEqual(globalDir, currentGlobalDir)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rootInfo = await this.inspectGlobalRoot(globalDir, false);
|
||||
if (rootInfo.packages.length > 0) {
|
||||
roots.push(rootInfo);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return packages;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return packages;
|
||||
return roots;
|
||||
}
|
||||
|
||||
public async cleanupLegacyGlobalRoots(): Promise<ILegacyCleanupResult[]> {
|
||||
const legacyRoots = await this.getLegacyGlobalRoots();
|
||||
if (legacyRoots.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentPackageNames = new Set(
|
||||
(await this.getCurrentInstalledPackages()).map(
|
||||
(packageInfo) => packageInfo.name,
|
||||
),
|
||||
);
|
||||
const cleanupResults: ILegacyCleanupResult[] = [];
|
||||
|
||||
for (const legacyRoot of legacyRoots) {
|
||||
const missingPackageNames = legacyRoot.packages
|
||||
.map((packageInfo) => packageInfo.name)
|
||||
.filter((packageName) => !currentPackageNames.has(packageName));
|
||||
|
||||
if (missingPackageNames.length > 0) {
|
||||
cleanupResults.push({
|
||||
globalDir: legacyRoot.globalDir,
|
||||
deleted: false,
|
||||
reason: `kept because ${missingPackageNames.join(", ")} are not installed in the current pnpm global root`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!legacyRoot.safeToDelete) {
|
||||
cleanupResults.push({
|
||||
globalDir: legacyRoot.globalDir,
|
||||
deleted: false,
|
||||
reason:
|
||||
legacyRoot.unmanagedPackageNames.length > 0
|
||||
? `kept because it also contains ${legacyRoot.unmanagedPackageNames.join(", ")}`
|
||||
: "kept because it is not a managed @git.zone-only global root",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const blockingShims = await this.getShimReferences(legacyRoot.globalDir);
|
||||
if (blockingShims === null) {
|
||||
cleanupResults.push({
|
||||
globalDir: legacyRoot.globalDir,
|
||||
deleted: false,
|
||||
reason:
|
||||
"kept because PNPM_HOME is not set, so command shims could not be verified",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blockingShims.length > 0) {
|
||||
cleanupResults.push({
|
||||
globalDir: legacyRoot.globalDir,
|
||||
deleted: false,
|
||||
reason: `kept because command shims still reference it: ${blockingShims.join(", ")}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await plugins.fs.rm(legacyRoot.globalDir, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
cleanupResults.push({
|
||||
globalDir: legacyRoot.globalDir,
|
||||
deleted: true,
|
||||
});
|
||||
} catch (error) {
|
||||
cleanupResults.push({
|
||||
globalDir: legacyRoot.globalDir,
|
||||
deleted: false,
|
||||
reason: `delete failed: ${(error as Error).message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return cleanupResults;
|
||||
}
|
||||
|
||||
public async syncCurrentGlobalShims(): Promise<IShimSyncResult[]> {
|
||||
const pnpmShimDirs = await this.getPnpmShimDirs();
|
||||
if (!pnpmShimDirs) {
|
||||
return [
|
||||
{
|
||||
name: "PNPM_HOME",
|
||||
action: "skipped",
|
||||
reason: "PNPM_HOME is not set",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const results: IShimSyncResult[] = [];
|
||||
const currentBinNames = new Set<string>();
|
||||
const currentPackages = await this.getCurrentInstalledPackages();
|
||||
|
||||
for (const packageInfo of currentPackages) {
|
||||
if (!packageInfo.packagePath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const packageJson = await readJson(
|
||||
plugins.path.join(packageInfo.packagePath, "package.json"),
|
||||
);
|
||||
const binNames = getPackageBinNames(packageInfo.name, packageJson);
|
||||
const nodeModulesDir = getNodeModulesDir(
|
||||
packageInfo.packagePath,
|
||||
packageInfo.name,
|
||||
);
|
||||
|
||||
for (const binName of binNames) {
|
||||
currentBinNames.add(binName);
|
||||
const sourceShim = plugins.path.join(nodeModulesDir, ".bin", binName);
|
||||
|
||||
for (const pnpmShimDir of pnpmShimDirs) {
|
||||
const destinationShim = plugins.path.join(pnpmShimDir, binName);
|
||||
|
||||
try {
|
||||
const sourceContent = await plugins.fs.readFile(sourceShim, "utf8");
|
||||
const sourceStat = await plugins.fs.stat(sourceShim);
|
||||
await plugins.fs.writeFile(
|
||||
destinationShim,
|
||||
rewriteShimForPnpmHome(sourceContent),
|
||||
"utf8",
|
||||
);
|
||||
await plugins.fs.chmod(destinationShim, sourceStat.mode);
|
||||
results.push({
|
||||
name: formatShimName(pnpmShimDir, binName),
|
||||
action: "updated",
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
name: formatShimName(pnpmShimDir, binName),
|
||||
action: "skipped",
|
||||
reason: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const legacyRoots = await this.getLegacyGlobalRoots();
|
||||
const legacyGlobalDirs = legacyRoots.map(
|
||||
(legacyRoot) => legacyRoot.globalDir,
|
||||
);
|
||||
if (legacyGlobalDirs.length === 0) {
|
||||
return results;
|
||||
}
|
||||
|
||||
for (const pnpmShimDir of pnpmShimDirs) {
|
||||
try {
|
||||
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile() || currentBinNames.has(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
||||
let content = "";
|
||||
try {
|
||||
content = await plugins.fs.readFile(filePath, "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!legacyGlobalDirs.some((legacyGlobalDir) =>
|
||||
content.includes(legacyGlobalDir),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await plugins.fs.rm(filePath, { force: true });
|
||||
results.push({
|
||||
name: formatShimName(pnpmShimDir, entry.name),
|
||||
action: "removed",
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
name: formatShimName(pnpmShimDir, entry.name),
|
||||
action: "skipped",
|
||||
reason: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
results.push({
|
||||
name: pnpmShimDir,
|
||||
action: "skipped",
|
||||
reason: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async getLatestVersion(
|
||||
packageName: string,
|
||||
registries = ["https://verdaccio.lossless.digital", "https://registry.npmjs.org"],
|
||||
registries = [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org",
|
||||
],
|
||||
): Promise<string | null> {
|
||||
for (const registry of registries) {
|
||||
const latest = await this.getLatestVersionFromRegistry(registry, packageName);
|
||||
const latest = await this.getLatestVersionFromRegistry(
|
||||
registry,
|
||||
packageName,
|
||||
);
|
||||
if (latest) {
|
||||
return latest;
|
||||
}
|
||||
@@ -99,23 +371,60 @@ export class PackageManagerUtil {
|
||||
return null;
|
||||
}
|
||||
|
||||
public async installLatest(packageName: string): Promise<boolean> {
|
||||
const packageSpecifier = `${packageName}@latest`;
|
||||
public async installLatest(
|
||||
packageName: string,
|
||||
version = "latest",
|
||||
): Promise<boolean> {
|
||||
const pnpmCommand = await this.getPnpmCommand();
|
||||
if (!pnpmCommand) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const packageSpecifier = `${packageName}@${version}`;
|
||||
console.log(` Installing ${packageSpecifier} via pnpm...`);
|
||||
|
||||
try {
|
||||
const result = await this.shell.exec(`pnpm add -g ${shellQuote(packageSpecifier)}`);
|
||||
const result = await this.shell.exec(
|
||||
`${pnpmCommand} add -g ${shellQuote(packageSpecifier)}`,
|
||||
);
|
||||
return result.exitCode === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async updatePnpm(targetVersion: string): Promise<boolean> {
|
||||
const pnpmCommand = await this.getPnpmCommand();
|
||||
if (!pnpmCommand) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const neutralDir = process.env.PNPM_HOME || "/tmp";
|
||||
|
||||
try {
|
||||
const result = await this.shell.exec(
|
||||
`${pnpmCommand} --dir ${shellQuote(neutralDir)} self-update ${shellQuote(targetVersion)}`,
|
||||
);
|
||||
this.pnpmCommand = undefined;
|
||||
const currentVersion = await this.getCurrentPnpmVersion();
|
||||
return (
|
||||
result.exitCode === 0 &&
|
||||
!this.isNewerVersion(currentVersion, targetVersion)
|
||||
);
|
||||
} 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++) {
|
||||
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;
|
||||
@@ -127,7 +436,10 @@ export class PackageManagerUtil {
|
||||
|
||||
private async getCurrentPnpmVersion(): Promise<string> {
|
||||
try {
|
||||
const result = await this.shell.execSilent("pnpm --version 2>/dev/null");
|
||||
const result = await this.execPnpmSilent("--version 2>/dev/null");
|
||||
if (!result) {
|
||||
return "unknown";
|
||||
}
|
||||
const versionMatch = result.stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
||||
return versionMatch?.[1] || "unknown";
|
||||
} catch {
|
||||
@@ -135,6 +447,265 @@ export class PackageManagerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private async getPnpmCommand(): Promise<string | null> {
|
||||
if (this.pnpmCommand !== undefined) {
|
||||
return this.pnpmCommand;
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
"pnpm --pm-on-fail=ignore",
|
||||
"pnpm --config.manage-package-manager-versions=false",
|
||||
"pnpm",
|
||||
];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
const result = await this.shell.execSilent(
|
||||
`${candidate} --version 2>/dev/null`,
|
||||
);
|
||||
if (result.exitCode === 0 && Boolean(result.stdout.trim())) {
|
||||
this.pnpmCommand = candidate;
|
||||
return this.pnpmCommand;
|
||||
}
|
||||
} catch {
|
||||
// Try the next supported pnpm invocation form.
|
||||
}
|
||||
}
|
||||
|
||||
this.pnpmCommand = null;
|
||||
return this.pnpmCommand;
|
||||
}
|
||||
|
||||
private async execPnpmSilent(commandArgs: string): Promise<any | null> {
|
||||
const pnpmCommand = await this.getPnpmCommand();
|
||||
if (!pnpmCommand) {
|
||||
return null;
|
||||
}
|
||||
return this.shell.execSilent(`${pnpmCommand} ${commandArgs}`);
|
||||
}
|
||||
|
||||
private async getPnpmListProjects(): Promise<IPnpmListProject[]> {
|
||||
try {
|
||||
const result = await this.execPnpmSilent(
|
||||
"list -g --depth=0 --json 2>/dev/null || true",
|
||||
);
|
||||
const output = result?.stdout.trim();
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = JSON.parse(output);
|
||||
return Array.isArray(data) ? data : [data];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async getCurrentGlobalDir(): Promise<string | null> {
|
||||
const listProjects = await this.getPnpmListProjects();
|
||||
for (const listProject of listProjects) {
|
||||
if (typeof listProject.path === "string" && listProject.path.length > 0) {
|
||||
return normalizeGlobalDir(listProject.path);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.execPnpmSilent("root -g 2>/dev/null");
|
||||
const output = result?.stdout.trim();
|
||||
return output ? normalizeGlobalDir(output) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getCurrentInstalledPackages(): Promise<IInstalledPackage[]> {
|
||||
const listProjects = await this.getPnpmListProjects();
|
||||
const currentGlobalDir = await this.getCurrentGlobalDir();
|
||||
const packageMap = new Map<string, IInstalledPackage>();
|
||||
|
||||
for (const listProject of listProjects) {
|
||||
const globalDir = listProject.path
|
||||
? normalizeGlobalDir(listProject.path)
|
||||
: currentGlobalDir || undefined;
|
||||
const dependencies = listProject.dependencies || {};
|
||||
for (const [name, info] of Object.entries(dependencies)) {
|
||||
if (!name.startsWith("@git.zone/")) {
|
||||
continue;
|
||||
}
|
||||
packageMap.set(name, {
|
||||
name,
|
||||
version: getDependencyVersion(info),
|
||||
globalDir,
|
||||
packagePath: getDependencyPackagePath(info),
|
||||
legacy: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (packageMap.size === 0 && currentGlobalDir) {
|
||||
const currentRootInfo = await this.inspectGlobalRoot(
|
||||
currentGlobalDir,
|
||||
true,
|
||||
);
|
||||
for (const packageInfo of currentRootInfo.packages) {
|
||||
packageMap.set(packageInfo.name, packageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(packageMap.values());
|
||||
}
|
||||
|
||||
private async inspectGlobalRoot(
|
||||
globalDir: string,
|
||||
current: boolean,
|
||||
): Promise<ILegacyGlobalRootInfo> {
|
||||
const normalizedGlobalDir = normalizeGlobalDir(globalDir);
|
||||
const rootPackageJson = await readJson(
|
||||
plugins.path.join(normalizedGlobalDir, "package.json"),
|
||||
);
|
||||
const dependencies = getDependencyMap(rootPackageJson?.dependencies);
|
||||
const packageMap = new Map<string, IInstalledPackage>();
|
||||
|
||||
for (const [name, spec] of Object.entries(dependencies)) {
|
||||
if (!name.startsWith("@git.zone/")) {
|
||||
continue;
|
||||
}
|
||||
packageMap.set(name, {
|
||||
name,
|
||||
version:
|
||||
(await this.getInstalledPackageVersion(normalizedGlobalDir, name)) ||
|
||||
normalizeDependencySpec(spec),
|
||||
globalDir: normalizedGlobalDir,
|
||||
packagePath: getPackagePath(normalizedGlobalDir, name),
|
||||
legacy: !current,
|
||||
});
|
||||
}
|
||||
|
||||
const gitZoneScopeDir = plugins.path.join(
|
||||
normalizedGlobalDir,
|
||||
"node_modules",
|
||||
"@git.zone",
|
||||
);
|
||||
try {
|
||||
const entries = await plugins.fs.readdir(gitZoneScopeDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
||||
continue;
|
||||
}
|
||||
const name = `@git.zone/${entry.name}`;
|
||||
packageMap.set(name, {
|
||||
name,
|
||||
version:
|
||||
(await this.getInstalledPackageVersion(
|
||||
normalizedGlobalDir,
|
||||
name,
|
||||
)) || "unknown",
|
||||
globalDir: normalizedGlobalDir,
|
||||
packagePath: getPackagePath(normalizedGlobalDir, name),
|
||||
legacy: !current,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// A pnpm global root may be empty or may not have node_modules materialized yet.
|
||||
}
|
||||
|
||||
const dependencyNames = Object.keys(dependencies);
|
||||
const unmanagedPackageNames = dependencyNames.filter(
|
||||
(packageName) => !packageName.startsWith("@git.zone/"),
|
||||
);
|
||||
|
||||
return {
|
||||
globalDir: normalizedGlobalDir,
|
||||
packages: Array.from(packageMap.values()).sort((packageA, packageB) =>
|
||||
packageA.name.localeCompare(packageB.name),
|
||||
),
|
||||
unmanagedPackageNames,
|
||||
safeToDelete:
|
||||
!current &&
|
||||
dependencyNames.length > 0 &&
|
||||
unmanagedPackageNames.length === 0,
|
||||
};
|
||||
}
|
||||
|
||||
private async getInstalledPackageVersion(
|
||||
globalDir: string,
|
||||
packageName: string,
|
||||
): Promise<string | null> {
|
||||
const packageJson = await readJson(
|
||||
plugins.path.join(
|
||||
globalDir,
|
||||
"node_modules",
|
||||
...packageName.split("/"),
|
||||
"package.json",
|
||||
),
|
||||
);
|
||||
return typeof packageJson?.version === "string"
|
||||
? packageJson.version
|
||||
: null;
|
||||
}
|
||||
|
||||
private async getShimReferences(
|
||||
legacyGlobalDir: string,
|
||||
): Promise<string[] | null> {
|
||||
const pnpmShimDirs = await this.getPnpmShimDirs();
|
||||
if (!pnpmShimDirs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const references: string[] = [];
|
||||
for (const pnpmShimDir of pnpmShimDirs) {
|
||||
try {
|
||||
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
||||
try {
|
||||
const content = await plugins.fs.readFile(filePath, "utf8");
|
||||
if (content.includes(legacyGlobalDir)) {
|
||||
references.push(formatShimName(pnpmShimDir, entry.name));
|
||||
}
|
||||
} catch {
|
||||
// Ignore unreadable or non-text files in PNPM_HOME.
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private async getPnpmShimDirs(): Promise<string[] | null> {
|
||||
const pnpmHome = process.env.PNPM_HOME;
|
||||
if (!pnpmHome) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidateDirs = [plugins.path.join(pnpmHome, "bin"), pnpmHome];
|
||||
const shimDirs: string[] = [];
|
||||
for (const candidateDir of candidateDirs) {
|
||||
try {
|
||||
const stat = await plugins.fs.stat(candidateDir);
|
||||
if (stat.isDirectory()) {
|
||||
shimDirs.push(normalizePath(candidateDir));
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing pnpm shim directories.
|
||||
}
|
||||
}
|
||||
|
||||
return shimDirs.length > 0 ? Array.from(new Set(shimDirs)) : null;
|
||||
}
|
||||
|
||||
private async getLatestVersionFromRegistry(
|
||||
registry: string,
|
||||
packageName: string,
|
||||
@@ -164,6 +735,125 @@ export class PackageManagerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(filePath: string): Promise<any | null> {
|
||||
try {
|
||||
const content = await plugins.fs.readFile(filePath, "utf8");
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyMap(value: unknown): Record<string, string> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
return value as Record<string, string>;
|
||||
}
|
||||
|
||||
function getDependencyVersion(info: any): string {
|
||||
if (info && typeof info === "object" && typeof info.version === "string") {
|
||||
return info.version;
|
||||
}
|
||||
if (typeof info === "string") {
|
||||
return normalizeDependencySpec(info);
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function getDependencyPackagePath(info: any): string | undefined {
|
||||
return info && typeof info === "object" && typeof info.path === "string"
|
||||
? normalizePath(info.path)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function getPackagePath(globalDir: string, packageName: string): string {
|
||||
return plugins.path.join(
|
||||
globalDir,
|
||||
"node_modules",
|
||||
...packageName.split("/"),
|
||||
);
|
||||
}
|
||||
|
||||
function getPackageBinNames(packageName: string, packageJson: any): string[] {
|
||||
const bin = packageJson?.bin;
|
||||
if (typeof bin === "string") {
|
||||
return [getDefaultBinName(packageName)];
|
||||
}
|
||||
if (!bin || typeof bin !== "object" || Array.isArray(bin)) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys(bin)
|
||||
.filter((binName) => binName.length > 0)
|
||||
.sort();
|
||||
}
|
||||
|
||||
function getDefaultBinName(packageName: string): string {
|
||||
const packageNameParts = packageName.split("/");
|
||||
return packageNameParts[packageNameParts.length - 1] || packageName;
|
||||
}
|
||||
|
||||
function getNodeModulesDir(packagePath: string, packageName: string): string {
|
||||
return packageName.startsWith("@")
|
||||
? plugins.path.dirname(plugins.path.dirname(packagePath))
|
||||
: plugins.path.dirname(packagePath);
|
||||
}
|
||||
|
||||
function rewriteShimForPnpmHome(content: string): string {
|
||||
const targetMatch = content.match(/^# cmd-shim-target=(.+)$/m);
|
||||
if (!targetMatch?.[1]) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const absoluteTarget = `"${escapeDoubleQuotedShell(targetMatch[1])}"`;
|
||||
return content.replace(
|
||||
/"\$basedir\/(?:\.\.\/)+store\/[^"\n]+"/g,
|
||||
absoluteTarget,
|
||||
);
|
||||
}
|
||||
|
||||
function escapeDoubleQuotedShell(value: string): string {
|
||||
return value.replace(/["\\$`]/g, "\\$&");
|
||||
}
|
||||
|
||||
function formatShimName(pnpmShimDir: string, binName: string): string {
|
||||
const pnpmHome = process.env.PNPM_HOME;
|
||||
if (!pnpmHome) {
|
||||
return binName;
|
||||
}
|
||||
|
||||
const relativeName = plugins.path.relative(
|
||||
pnpmHome,
|
||||
plugins.path.join(pnpmShimDir, binName),
|
||||
);
|
||||
return relativeName && !relativeName.startsWith("..")
|
||||
? relativeName
|
||||
: binName;
|
||||
}
|
||||
|
||||
function normalizeDependencySpec(spec: unknown): string {
|
||||
if (typeof spec !== "string" || spec.length === 0) {
|
||||
return "unknown";
|
||||
}
|
||||
const versionMatch = spec.match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/);
|
||||
return versionMatch?.[0] || spec;
|
||||
}
|
||||
|
||||
function normalizeGlobalDir(globalDir: string): string {
|
||||
const normalizedPath = normalizePath(globalDir);
|
||||
return plugins.path.basename(normalizedPath) === "node_modules"
|
||||
? plugins.path.dirname(normalizedPath)
|
||||
: normalizedPath;
|
||||
}
|
||||
|
||||
function normalizePath(filePath: string): string {
|
||||
return plugins.path.resolve(filePath).replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function pathsAreEqual(pathA: string, pathB: string): boolean {
|
||||
return normalizePath(pathA) === normalizePath(pathB);
|
||||
}
|
||||
|
||||
function normalizeSemver(version: string): number[] {
|
||||
return version
|
||||
.replace(/^[^\d]*/, "")
|
||||
|
||||
+219
-39
@@ -51,7 +51,9 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
|
||||
const pnpmInfo = await pmUtil.getPnpmVersionInfo();
|
||||
if (!pnpmInfo.available) {
|
||||
console.log("pnpm is required for gitzone tools update, but it was not found.");
|
||||
console.log(
|
||||
"pnpm is required for gitzone tools update, but it was not found.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,18 +61,25 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
console.log(" Name Current Latest Status");
|
||||
console.log(" ----------------------------------------------");
|
||||
const latestPnpm = (pnpmInfo.latestVersion || "unknown").padEnd(12);
|
||||
const pnpmStatus = pnpmInfo.latestVersion === null
|
||||
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(
|
||||
` ${"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;
|
||||
@@ -78,39 +87,79 @@ 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;
|
||||
|
||||
if (packageInfos.length === 0) {
|
||||
console.log("No managed @git.zone packages found installed globally.");
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
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}`,
|
||||
" 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);
|
||||
if (packagesToUpdate.length === 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`Found ${packagesToUpdate.length} package(s) with available updates.\n`);
|
||||
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 without prompts.");
|
||||
console.log(
|
||||
"Run gitzone tools update -y to update, migrate, and cleanup without prompts.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -120,7 +169,7 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
const answer = await interactInstance.askQuestion({
|
||||
type: "confirm",
|
||||
name: "confirmUpdate",
|
||||
message: "Do you want to update these packages?",
|
||||
message: "Do you want to update, migrate, and cleanup these tools?",
|
||||
default: true,
|
||||
});
|
||||
shouldUpdate = answer.value === true;
|
||||
@@ -131,7 +180,35 @@ async function runUpdate(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
await installPackages(pmUtil, packagesToUpdate.map((packageInfo) => packageInfo.name), "updated");
|
||||
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<void> {
|
||||
@@ -142,7 +219,9 @@ async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
|
||||
const pnpmAvailable = await pmUtil.detectPnpm();
|
||||
if (!pnpmAvailable) {
|
||||
console.log("pnpm is required for gitzone tools install, but it was not found.");
|
||||
console.log(
|
||||
"pnpm is required for gitzone tools install, but it was not found.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,8 +230,12 @@ async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
}
|
||||
|
||||
const installedPackages = await pmUtil.getInstalledPackages();
|
||||
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||
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.");
|
||||
@@ -163,7 +246,9 @@ async function runInstall(argvArg: any, mode: ICliMode): Promise<void> {
|
||||
|
||||
if (!mode.yes && !mode.interactive) {
|
||||
await printPackageListWithLatest(pmUtil, missingPackages);
|
||||
console.log("Run gitzone tools install -y to install all missing packages without prompts.");
|
||||
console.log(
|
||||
"Run gitzone tools install -y to install all missing packages without prompts.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,7 +295,9 @@ async function handleSelfUpdate(
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(` @git.zone/cli ${currentVersion} -> ${latestVersion} Update available\n`);
|
||||
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.");
|
||||
@@ -236,11 +323,15 @@ async function handleSelfUpdate(
|
||||
|
||||
const success = await pmUtil.installLatest("@git.zone/cli");
|
||||
if (!success) {
|
||||
console.log("\ngitzone self-update failed. Continuing with the current version.\n");
|
||||
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.");
|
||||
console.log(
|
||||
"\ngitzone has been updated. Re-run gitzone tools update to check remaining packages.",
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -261,17 +352,39 @@ async function getPackageUpdateInfos(
|
||||
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<void> {
|
||||
const installedNames = new Set(installedPackages.map((packageInfo) => packageInfo.name));
|
||||
const missingPackages = GITZONE_PACKAGES.filter((packageName) => !installedNames.has(packageName));
|
||||
const installedNames = new Set(
|
||||
installedPackages.map((packageInfo) => packageInfo.name),
|
||||
);
|
||||
const missingPackages = GITZONE_PACKAGES.filter(
|
||||
(packageName) => !installedNames.has(packageName),
|
||||
);
|
||||
if (missingPackages.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -296,14 +409,18 @@ async function printPackageListWithLatest(
|
||||
|
||||
async function installPackages(
|
||||
pmUtil: PackageManagerUtil,
|
||||
packageNames: string[],
|
||||
packageSpecs: Array<string | { name: string; version?: string }>,
|
||||
action: "installed" | "updated",
|
||||
): Promise<void> {
|
||||
): Promise<{ successCount: number; failCount: number }> {
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const packageName of packageNames) {
|
||||
const success = await pmUtil.installLatest(packageName);
|
||||
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++;
|
||||
@@ -319,6 +436,56 @@ async function installPackages(
|
||||
} else {
|
||||
console.log(`${successCount} package(s) ${action}, ${failCount} failed.`);
|
||||
}
|
||||
|
||||
return { successCount, failCount };
|
||||
}
|
||||
|
||||
async function cleanupLegacyInstalls(
|
||||
pmUtil: PackageManagerUtil,
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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 {
|
||||
@@ -327,12 +494,21 @@ export function showHelp(mode?: ICliMode): void {
|
||||
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" },
|
||||
{
|
||||
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" },
|
||||
{
|
||||
flag: "-v, --verbose",
|
||||
description: "Show package manager diagnostics",
|
||||
},
|
||||
],
|
||||
packageManager: "pnpm",
|
||||
managedPackages: GITZONE_PACKAGES,
|
||||
@@ -344,8 +520,12 @@ export function showHelp(mode?: ICliMode): void {
|
||||
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(
|
||||
" 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");
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as smartlog from '@push.rocks/smartlog';
|
||||
import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
|
||||
import * as smartconfig from '@push.rocks/smartconfig';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as projectinfo from '@push.rocks/projectinfo';
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
@@ -22,6 +23,7 @@ export {
|
||||
smartlogDestinationLocal,
|
||||
smartconfig,
|
||||
path,
|
||||
fs,
|
||||
projectinfo,
|
||||
smartcli,
|
||||
smartpath,
|
||||
|
||||
Reference in New Issue
Block a user