@git.zone/cli 2.19.1 → 2.19.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mod_commit/index.js +33 -5
- package/dist_ts/mod_tools/classes.packagemanager.d.ts +36 -1
- package/dist_ts/mod_tools/classes.packagemanager.js +541 -34
- package/dist_ts/mod_tools/index.js +128 -26
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_commit/index.ts +32 -4
- package/ts/mod_tools/classes.packagemanager.ts +724 -34
- package/ts/mod_tools/index.ts +225 -45
- package/ts/plugins.ts +2 -0
|
@@ -3,6 +3,9 @@ import * as plugins from "./mod.plugins.js";
|
|
|
3
3
|
export interface IInstalledPackage {
|
|
4
4
|
name: string;
|
|
5
5
|
version: string;
|
|
6
|
+
globalDir?: string;
|
|
7
|
+
packagePath?: string;
|
|
8
|
+
legacy?: boolean;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export interface IPackageUpdateInfo {
|
|
@@ -10,6 +13,8 @@ export interface IPackageUpdateInfo {
|
|
|
10
13
|
currentVersion: string;
|
|
11
14
|
latestVersion: string;
|
|
12
15
|
needsUpdate: boolean;
|
|
16
|
+
needsMigration?: boolean;
|
|
17
|
+
globalDir?: string;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
export interface IPackageManagerInfo {
|
|
@@ -19,18 +24,38 @@ export interface IPackageManagerInfo {
|
|
|
19
24
|
needsUpdate: boolean;
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
export interface ILegacyGlobalRootInfo {
|
|
28
|
+
globalDir: string;
|
|
29
|
+
packages: IInstalledPackage[];
|
|
30
|
+
unmanagedPackageNames: string[];
|
|
31
|
+
safeToDelete: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ILegacyCleanupResult {
|
|
35
|
+
globalDir: string;
|
|
36
|
+
deleted: boolean;
|
|
37
|
+
reason?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IShimSyncResult {
|
|
41
|
+
name: string;
|
|
42
|
+
action: "updated" | "removed" | "skipped";
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface IPnpmListProject {
|
|
47
|
+
path?: string;
|
|
48
|
+
dependencies?: Record<string, any>;
|
|
49
|
+
}
|
|
50
|
+
|
|
22
51
|
export class PackageManagerUtil {
|
|
23
52
|
private shell = new plugins.smartshell.Smartshell({
|
|
24
53
|
executor: "bash",
|
|
25
54
|
});
|
|
55
|
+
private pnpmCommand: string | null | undefined;
|
|
26
56
|
|
|
27
57
|
public async detectPnpm(): Promise<boolean> {
|
|
28
|
-
|
|
29
|
-
const result = await this.shell.execSilent("pnpm --version 2>/dev/null");
|
|
30
|
-
return result.exitCode === 0 && Boolean(result.stdout.trim());
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
58
|
+
return Boolean(await this.getPnpmCommand());
|
|
34
59
|
}
|
|
35
60
|
|
|
36
61
|
public async getPnpmVersionInfo(): Promise<IPackageManagerInfo> {
|
|
@@ -45,53 +70,300 @@ export class PackageManagerUtil {
|
|
|
45
70
|
}
|
|
46
71
|
|
|
47
72
|
const currentVersion = await this.getCurrentPnpmVersion();
|
|
48
|
-
const latestVersion = await this.getLatestVersion("pnpm", [
|
|
73
|
+
const latestVersion = await this.getLatestVersion("pnpm", [
|
|
74
|
+
"https://registry.npmjs.org",
|
|
75
|
+
]);
|
|
49
76
|
|
|
50
77
|
return {
|
|
51
78
|
available: true,
|
|
52
79
|
currentVersion,
|
|
53
80
|
latestVersion,
|
|
54
|
-
needsUpdate: latestVersion
|
|
81
|
+
needsUpdate: latestVersion
|
|
82
|
+
? this.isNewerVersion(currentVersion, latestVersion)
|
|
83
|
+
: false,
|
|
55
84
|
};
|
|
56
85
|
}
|
|
57
86
|
|
|
58
87
|
public async getInstalledPackages(): Promise<IInstalledPackage[]> {
|
|
59
|
-
const
|
|
88
|
+
const packageMap = new Map<string, IInstalledPackage>();
|
|
89
|
+
const currentPackages = await this.getCurrentInstalledPackages();
|
|
60
90
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
for (const packageInfo of currentPackages) {
|
|
92
|
+
packageMap.set(packageInfo.name, packageInfo);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const legacyRoots = await this.getLegacyGlobalRoots();
|
|
96
|
+
for (const legacyRoot of legacyRoots) {
|
|
97
|
+
for (const packageInfo of legacyRoot.packages) {
|
|
98
|
+
if (!packageMap.has(packageInfo.name)) {
|
|
99
|
+
packageMap.set(packageInfo.name, packageInfo);
|
|
100
|
+
}
|
|
66
101
|
}
|
|
102
|
+
}
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
104
|
+
return Array.from(packageMap.values()).sort((packageA, packageB) =>
|
|
105
|
+
packageA.name.localeCompare(packageB.name),
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public async getLegacyGlobalRoots(): Promise<ILegacyGlobalRootInfo[]> {
|
|
110
|
+
const currentGlobalDir = await this.getCurrentGlobalDir();
|
|
111
|
+
const baseDirs = new Set<string>();
|
|
112
|
+
const pnpmHome = process.env.PNPM_HOME;
|
|
113
|
+
|
|
114
|
+
if (pnpmHome) {
|
|
115
|
+
baseDirs.add(plugins.path.join(pnpmHome, "global"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (currentGlobalDir) {
|
|
119
|
+
baseDirs.add(plugins.path.dirname(currentGlobalDir));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const roots: ILegacyGlobalRootInfo[] = [];
|
|
123
|
+
for (const baseDir of baseDirs) {
|
|
124
|
+
try {
|
|
125
|
+
const entries = await plugins.fs.readdir(baseDir, {
|
|
126
|
+
withFileTypes: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
if (!entry.isDirectory()) {
|
|
74
131
|
continue;
|
|
75
132
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
133
|
+
|
|
134
|
+
const globalDir = normalizePath(
|
|
135
|
+
plugins.path.join(baseDir, entry.name),
|
|
136
|
+
);
|
|
137
|
+
if (currentGlobalDir && pathsAreEqual(globalDir, currentGlobalDir)) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const rootInfo = await this.inspectGlobalRoot(globalDir, false);
|
|
142
|
+
if (rootInfo.packages.length > 0) {
|
|
143
|
+
roots.push(rootInfo);
|
|
144
|
+
}
|
|
80
145
|
}
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return roots;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
public async cleanupLegacyGlobalRoots(): Promise<ILegacyCleanupResult[]> {
|
|
155
|
+
const legacyRoots = await this.getLegacyGlobalRoots();
|
|
156
|
+
if (legacyRoots.length === 0) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const currentPackageNames = new Set(
|
|
161
|
+
(await this.getCurrentInstalledPackages()).map(
|
|
162
|
+
(packageInfo) => packageInfo.name,
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
const cleanupResults: ILegacyCleanupResult[] = [];
|
|
166
|
+
|
|
167
|
+
for (const legacyRoot of legacyRoots) {
|
|
168
|
+
const missingPackageNames = legacyRoot.packages
|
|
169
|
+
.map((packageInfo) => packageInfo.name)
|
|
170
|
+
.filter((packageName) => !currentPackageNames.has(packageName));
|
|
171
|
+
|
|
172
|
+
if (missingPackageNames.length > 0) {
|
|
173
|
+
cleanupResults.push({
|
|
174
|
+
globalDir: legacyRoot.globalDir,
|
|
175
|
+
deleted: false,
|
|
176
|
+
reason: `kept because ${missingPackageNames.join(", ")} are not installed in the current pnpm global root`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!legacyRoot.safeToDelete) {
|
|
182
|
+
cleanupResults.push({
|
|
183
|
+
globalDir: legacyRoot.globalDir,
|
|
184
|
+
deleted: false,
|
|
185
|
+
reason:
|
|
186
|
+
legacyRoot.unmanagedPackageNames.length > 0
|
|
187
|
+
? `kept because it also contains ${legacyRoot.unmanagedPackageNames.join(", ")}`
|
|
188
|
+
: "kept because it is not a managed @git.zone-only global root",
|
|
189
|
+
});
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const blockingShims = await this.getShimReferences(legacyRoot.globalDir);
|
|
194
|
+
if (blockingShims === null) {
|
|
195
|
+
cleanupResults.push({
|
|
196
|
+
globalDir: legacyRoot.globalDir,
|
|
197
|
+
deleted: false,
|
|
198
|
+
reason:
|
|
199
|
+
"kept because PNPM_HOME is not set, so command shims could not be verified",
|
|
200
|
+
});
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (blockingShims.length > 0) {
|
|
205
|
+
cleanupResults.push({
|
|
206
|
+
globalDir: legacyRoot.globalDir,
|
|
207
|
+
deleted: false,
|
|
208
|
+
reason: `kept because command shims still reference it: ${blockingShims.join(", ")}`,
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await plugins.fs.rm(legacyRoot.globalDir, {
|
|
215
|
+
recursive: true,
|
|
216
|
+
force: true,
|
|
217
|
+
});
|
|
218
|
+
cleanupResults.push({
|
|
219
|
+
globalDir: legacyRoot.globalDir,
|
|
220
|
+
deleted: true,
|
|
221
|
+
});
|
|
222
|
+
} catch (error) {
|
|
223
|
+
cleanupResults.push({
|
|
224
|
+
globalDir: legacyRoot.globalDir,
|
|
225
|
+
deleted: false,
|
|
226
|
+
reason: `delete failed: ${(error as Error).message}`,
|
|
227
|
+
});
|
|
81
228
|
}
|
|
82
|
-
} catch {
|
|
83
|
-
return packages;
|
|
84
229
|
}
|
|
85
230
|
|
|
86
|
-
return
|
|
231
|
+
return cleanupResults;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public async syncCurrentGlobalShims(): Promise<IShimSyncResult[]> {
|
|
235
|
+
const pnpmShimDirs = await this.getPnpmShimDirs();
|
|
236
|
+
if (!pnpmShimDirs) {
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
name: "PNPM_HOME",
|
|
240
|
+
action: "skipped",
|
|
241
|
+
reason: "PNPM_HOME is not set",
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const results: IShimSyncResult[] = [];
|
|
247
|
+
const currentBinNames = new Set<string>();
|
|
248
|
+
const currentPackages = await this.getCurrentInstalledPackages();
|
|
249
|
+
|
|
250
|
+
for (const packageInfo of currentPackages) {
|
|
251
|
+
if (!packageInfo.packagePath) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const packageJson = await readJson(
|
|
256
|
+
plugins.path.join(packageInfo.packagePath, "package.json"),
|
|
257
|
+
);
|
|
258
|
+
const binNames = getPackageBinNames(packageInfo.name, packageJson);
|
|
259
|
+
const nodeModulesDir = getNodeModulesDir(
|
|
260
|
+
packageInfo.packagePath,
|
|
261
|
+
packageInfo.name,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
for (const binName of binNames) {
|
|
265
|
+
currentBinNames.add(binName);
|
|
266
|
+
const sourceShim = plugins.path.join(nodeModulesDir, ".bin", binName);
|
|
267
|
+
|
|
268
|
+
for (const pnpmShimDir of pnpmShimDirs) {
|
|
269
|
+
const destinationShim = plugins.path.join(pnpmShimDir, binName);
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const sourceContent = await plugins.fs.readFile(sourceShim, "utf8");
|
|
273
|
+
const sourceStat = await plugins.fs.stat(sourceShim);
|
|
274
|
+
await plugins.fs.writeFile(
|
|
275
|
+
destinationShim,
|
|
276
|
+
rewriteShimForPnpmHome(sourceContent),
|
|
277
|
+
"utf8",
|
|
278
|
+
);
|
|
279
|
+
await plugins.fs.chmod(destinationShim, sourceStat.mode);
|
|
280
|
+
results.push({
|
|
281
|
+
name: formatShimName(pnpmShimDir, binName),
|
|
282
|
+
action: "updated",
|
|
283
|
+
});
|
|
284
|
+
} catch (error) {
|
|
285
|
+
results.push({
|
|
286
|
+
name: formatShimName(pnpmShimDir, binName),
|
|
287
|
+
action: "skipped",
|
|
288
|
+
reason: (error as Error).message,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const legacyRoots = await this.getLegacyGlobalRoots();
|
|
296
|
+
const legacyGlobalDirs = legacyRoots.map(
|
|
297
|
+
(legacyRoot) => legacyRoot.globalDir,
|
|
298
|
+
);
|
|
299
|
+
if (legacyGlobalDirs.length === 0) {
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
for (const pnpmShimDir of pnpmShimDirs) {
|
|
304
|
+
try {
|
|
305
|
+
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
|
306
|
+
withFileTypes: true,
|
|
307
|
+
});
|
|
308
|
+
for (const entry of entries) {
|
|
309
|
+
if (!entry.isFile() || currentBinNames.has(entry.name)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
|
314
|
+
let content = "";
|
|
315
|
+
try {
|
|
316
|
+
content = await plugins.fs.readFile(filePath, "utf8");
|
|
317
|
+
} catch {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
!legacyGlobalDirs.some((legacyGlobalDir) =>
|
|
323
|
+
content.includes(legacyGlobalDir),
|
|
324
|
+
)
|
|
325
|
+
) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
await plugins.fs.rm(filePath, { force: true });
|
|
331
|
+
results.push({
|
|
332
|
+
name: formatShimName(pnpmShimDir, entry.name),
|
|
333
|
+
action: "removed",
|
|
334
|
+
});
|
|
335
|
+
} catch (error) {
|
|
336
|
+
results.push({
|
|
337
|
+
name: formatShimName(pnpmShimDir, entry.name),
|
|
338
|
+
action: "skipped",
|
|
339
|
+
reason: (error as Error).message,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
results.push({
|
|
345
|
+
name: pnpmShimDir,
|
|
346
|
+
action: "skipped",
|
|
347
|
+
reason: (error as Error).message,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return results;
|
|
87
353
|
}
|
|
88
354
|
|
|
89
355
|
public async getLatestVersion(
|
|
90
356
|
packageName: string,
|
|
91
|
-
registries = [
|
|
357
|
+
registries = [
|
|
358
|
+
"https://verdaccio.lossless.digital",
|
|
359
|
+
"https://registry.npmjs.org",
|
|
360
|
+
],
|
|
92
361
|
): Promise<string | null> {
|
|
93
362
|
for (const registry of registries) {
|
|
94
|
-
const latest = await this.getLatestVersionFromRegistry(
|
|
363
|
+
const latest = await this.getLatestVersionFromRegistry(
|
|
364
|
+
registry,
|
|
365
|
+
packageName,
|
|
366
|
+
);
|
|
95
367
|
if (latest) {
|
|
96
368
|
return latest;
|
|
97
369
|
}
|
|
@@ -99,23 +371,60 @@ export class PackageManagerUtil {
|
|
|
99
371
|
return null;
|
|
100
372
|
}
|
|
101
373
|
|
|
102
|
-
public async installLatest(
|
|
103
|
-
|
|
374
|
+
public async installLatest(
|
|
375
|
+
packageName: string,
|
|
376
|
+
version = "latest",
|
|
377
|
+
): Promise<boolean> {
|
|
378
|
+
const pnpmCommand = await this.getPnpmCommand();
|
|
379
|
+
if (!pnpmCommand) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const packageSpecifier = `${packageName}@${version}`;
|
|
104
384
|
console.log(` Installing ${packageSpecifier} via pnpm...`);
|
|
105
385
|
|
|
106
386
|
try {
|
|
107
|
-
const result = await this.shell.exec(
|
|
387
|
+
const result = await this.shell.exec(
|
|
388
|
+
`${pnpmCommand} add -g ${shellQuote(packageSpecifier)}`,
|
|
389
|
+
);
|
|
108
390
|
return result.exitCode === 0;
|
|
109
391
|
} catch {
|
|
110
392
|
return false;
|
|
111
393
|
}
|
|
112
394
|
}
|
|
113
395
|
|
|
396
|
+
public async updatePnpm(targetVersion: string): Promise<boolean> {
|
|
397
|
+
const pnpmCommand = await this.getPnpmCommand();
|
|
398
|
+
if (!pnpmCommand) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const neutralDir = process.env.PNPM_HOME || "/tmp";
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const result = await this.shell.exec(
|
|
406
|
+
`${pnpmCommand} --dir ${shellQuote(neutralDir)} self-update ${shellQuote(targetVersion)}`,
|
|
407
|
+
);
|
|
408
|
+
this.pnpmCommand = undefined;
|
|
409
|
+
const currentVersion = await this.getCurrentPnpmVersion();
|
|
410
|
+
return (
|
|
411
|
+
result.exitCode === 0 &&
|
|
412
|
+
!this.isNewerVersion(currentVersion, targetVersion)
|
|
413
|
+
);
|
|
414
|
+
} catch {
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
114
419
|
public isNewerVersion(current: string, latest: string): boolean {
|
|
115
420
|
const currentParts = normalizeSemver(current);
|
|
116
421
|
const latestParts = normalizeSemver(latest);
|
|
117
422
|
|
|
118
|
-
for (
|
|
423
|
+
for (
|
|
424
|
+
let i = 0;
|
|
425
|
+
i < Math.max(currentParts.length, latestParts.length);
|
|
426
|
+
i++
|
|
427
|
+
) {
|
|
119
428
|
const currentPart = currentParts[i] || 0;
|
|
120
429
|
const latestPart = latestParts[i] || 0;
|
|
121
430
|
if (latestPart > currentPart) return true;
|
|
@@ -127,7 +436,10 @@ export class PackageManagerUtil {
|
|
|
127
436
|
|
|
128
437
|
private async getCurrentPnpmVersion(): Promise<string> {
|
|
129
438
|
try {
|
|
130
|
-
const result = await this.
|
|
439
|
+
const result = await this.execPnpmSilent("--version 2>/dev/null");
|
|
440
|
+
if (!result) {
|
|
441
|
+
return "unknown";
|
|
442
|
+
}
|
|
131
443
|
const versionMatch = result.stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
|
132
444
|
return versionMatch?.[1] || "unknown";
|
|
133
445
|
} catch {
|
|
@@ -135,6 +447,265 @@ export class PackageManagerUtil {
|
|
|
135
447
|
}
|
|
136
448
|
}
|
|
137
449
|
|
|
450
|
+
private async getPnpmCommand(): Promise<string | null> {
|
|
451
|
+
if (this.pnpmCommand !== undefined) {
|
|
452
|
+
return this.pnpmCommand;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const candidates = [
|
|
456
|
+
"pnpm --pm-on-fail=ignore",
|
|
457
|
+
"pnpm --config.manage-package-manager-versions=false",
|
|
458
|
+
"pnpm",
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
for (const candidate of candidates) {
|
|
462
|
+
try {
|
|
463
|
+
const result = await this.shell.execSilent(
|
|
464
|
+
`${candidate} --version 2>/dev/null`,
|
|
465
|
+
);
|
|
466
|
+
if (result.exitCode === 0 && Boolean(result.stdout.trim())) {
|
|
467
|
+
this.pnpmCommand = candidate;
|
|
468
|
+
return this.pnpmCommand;
|
|
469
|
+
}
|
|
470
|
+
} catch {
|
|
471
|
+
// Try the next supported pnpm invocation form.
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
this.pnpmCommand = null;
|
|
476
|
+
return this.pnpmCommand;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private async execPnpmSilent(commandArgs: string): Promise<any | null> {
|
|
480
|
+
const pnpmCommand = await this.getPnpmCommand();
|
|
481
|
+
if (!pnpmCommand) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return this.shell.execSilent(`${pnpmCommand} ${commandArgs}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private async getPnpmListProjects(): Promise<IPnpmListProject[]> {
|
|
488
|
+
try {
|
|
489
|
+
const result = await this.execPnpmSilent(
|
|
490
|
+
"list -g --depth=0 --json 2>/dev/null || true",
|
|
491
|
+
);
|
|
492
|
+
const output = result?.stdout.trim();
|
|
493
|
+
if (!output) {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const data = JSON.parse(output);
|
|
498
|
+
return Array.isArray(data) ? data : [data];
|
|
499
|
+
} catch {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private async getCurrentGlobalDir(): Promise<string | null> {
|
|
505
|
+
const listProjects = await this.getPnpmListProjects();
|
|
506
|
+
for (const listProject of listProjects) {
|
|
507
|
+
if (typeof listProject.path === "string" && listProject.path.length > 0) {
|
|
508
|
+
return normalizeGlobalDir(listProject.path);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const result = await this.execPnpmSilent("root -g 2>/dev/null");
|
|
514
|
+
const output = result?.stdout.trim();
|
|
515
|
+
return output ? normalizeGlobalDir(output) : null;
|
|
516
|
+
} catch {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private async getCurrentInstalledPackages(): Promise<IInstalledPackage[]> {
|
|
522
|
+
const listProjects = await this.getPnpmListProjects();
|
|
523
|
+
const currentGlobalDir = await this.getCurrentGlobalDir();
|
|
524
|
+
const packageMap = new Map<string, IInstalledPackage>();
|
|
525
|
+
|
|
526
|
+
for (const listProject of listProjects) {
|
|
527
|
+
const globalDir = listProject.path
|
|
528
|
+
? normalizeGlobalDir(listProject.path)
|
|
529
|
+
: currentGlobalDir || undefined;
|
|
530
|
+
const dependencies = listProject.dependencies || {};
|
|
531
|
+
for (const [name, info] of Object.entries(dependencies)) {
|
|
532
|
+
if (!name.startsWith("@git.zone/")) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
packageMap.set(name, {
|
|
536
|
+
name,
|
|
537
|
+
version: getDependencyVersion(info),
|
|
538
|
+
globalDir,
|
|
539
|
+
packagePath: getDependencyPackagePath(info),
|
|
540
|
+
legacy: false,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (packageMap.size === 0 && currentGlobalDir) {
|
|
546
|
+
const currentRootInfo = await this.inspectGlobalRoot(
|
|
547
|
+
currentGlobalDir,
|
|
548
|
+
true,
|
|
549
|
+
);
|
|
550
|
+
for (const packageInfo of currentRootInfo.packages) {
|
|
551
|
+
packageMap.set(packageInfo.name, packageInfo);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return Array.from(packageMap.values());
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private async inspectGlobalRoot(
|
|
559
|
+
globalDir: string,
|
|
560
|
+
current: boolean,
|
|
561
|
+
): Promise<ILegacyGlobalRootInfo> {
|
|
562
|
+
const normalizedGlobalDir = normalizeGlobalDir(globalDir);
|
|
563
|
+
const rootPackageJson = await readJson(
|
|
564
|
+
plugins.path.join(normalizedGlobalDir, "package.json"),
|
|
565
|
+
);
|
|
566
|
+
const dependencies = getDependencyMap(rootPackageJson?.dependencies);
|
|
567
|
+
const packageMap = new Map<string, IInstalledPackage>();
|
|
568
|
+
|
|
569
|
+
for (const [name, spec] of Object.entries(dependencies)) {
|
|
570
|
+
if (!name.startsWith("@git.zone/")) {
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
packageMap.set(name, {
|
|
574
|
+
name,
|
|
575
|
+
version:
|
|
576
|
+
(await this.getInstalledPackageVersion(normalizedGlobalDir, name)) ||
|
|
577
|
+
normalizeDependencySpec(spec),
|
|
578
|
+
globalDir: normalizedGlobalDir,
|
|
579
|
+
packagePath: getPackagePath(normalizedGlobalDir, name),
|
|
580
|
+
legacy: !current,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const gitZoneScopeDir = plugins.path.join(
|
|
585
|
+
normalizedGlobalDir,
|
|
586
|
+
"node_modules",
|
|
587
|
+
"@git.zone",
|
|
588
|
+
);
|
|
589
|
+
try {
|
|
590
|
+
const entries = await plugins.fs.readdir(gitZoneScopeDir, {
|
|
591
|
+
withFileTypes: true,
|
|
592
|
+
});
|
|
593
|
+
for (const entry of entries) {
|
|
594
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const name = `@git.zone/${entry.name}`;
|
|
598
|
+
packageMap.set(name, {
|
|
599
|
+
name,
|
|
600
|
+
version:
|
|
601
|
+
(await this.getInstalledPackageVersion(
|
|
602
|
+
normalizedGlobalDir,
|
|
603
|
+
name,
|
|
604
|
+
)) || "unknown",
|
|
605
|
+
globalDir: normalizedGlobalDir,
|
|
606
|
+
packagePath: getPackagePath(normalizedGlobalDir, name),
|
|
607
|
+
legacy: !current,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
// A pnpm global root may be empty or may not have node_modules materialized yet.
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const dependencyNames = Object.keys(dependencies);
|
|
615
|
+
const unmanagedPackageNames = dependencyNames.filter(
|
|
616
|
+
(packageName) => !packageName.startsWith("@git.zone/"),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
return {
|
|
620
|
+
globalDir: normalizedGlobalDir,
|
|
621
|
+
packages: Array.from(packageMap.values()).sort((packageA, packageB) =>
|
|
622
|
+
packageA.name.localeCompare(packageB.name),
|
|
623
|
+
),
|
|
624
|
+
unmanagedPackageNames,
|
|
625
|
+
safeToDelete:
|
|
626
|
+
!current &&
|
|
627
|
+
dependencyNames.length > 0 &&
|
|
628
|
+
unmanagedPackageNames.length === 0,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private async getInstalledPackageVersion(
|
|
633
|
+
globalDir: string,
|
|
634
|
+
packageName: string,
|
|
635
|
+
): Promise<string | null> {
|
|
636
|
+
const packageJson = await readJson(
|
|
637
|
+
plugins.path.join(
|
|
638
|
+
globalDir,
|
|
639
|
+
"node_modules",
|
|
640
|
+
...packageName.split("/"),
|
|
641
|
+
"package.json",
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
return typeof packageJson?.version === "string"
|
|
645
|
+
? packageJson.version
|
|
646
|
+
: null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
private async getShimReferences(
|
|
650
|
+
legacyGlobalDir: string,
|
|
651
|
+
): Promise<string[] | null> {
|
|
652
|
+
const pnpmShimDirs = await this.getPnpmShimDirs();
|
|
653
|
+
if (!pnpmShimDirs) {
|
|
654
|
+
return null;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const references: string[] = [];
|
|
658
|
+
for (const pnpmShimDir of pnpmShimDirs) {
|
|
659
|
+
try {
|
|
660
|
+
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
|
661
|
+
withFileTypes: true,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
for (const entry of entries) {
|
|
665
|
+
if (!entry.isFile()) {
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
|
670
|
+
try {
|
|
671
|
+
const content = await plugins.fs.readFile(filePath, "utf8");
|
|
672
|
+
if (content.includes(legacyGlobalDir)) {
|
|
673
|
+
references.push(formatShimName(pnpmShimDir, entry.name));
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
// Ignore unreadable or non-text files in PNPM_HOME.
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return references;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private async getPnpmShimDirs(): Promise<string[] | null> {
|
|
688
|
+
const pnpmHome = process.env.PNPM_HOME;
|
|
689
|
+
if (!pnpmHome) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const candidateDirs = [plugins.path.join(pnpmHome, "bin"), pnpmHome];
|
|
694
|
+
const shimDirs: string[] = [];
|
|
695
|
+
for (const candidateDir of candidateDirs) {
|
|
696
|
+
try {
|
|
697
|
+
const stat = await plugins.fs.stat(candidateDir);
|
|
698
|
+
if (stat.isDirectory()) {
|
|
699
|
+
shimDirs.push(normalizePath(candidateDir));
|
|
700
|
+
}
|
|
701
|
+
} catch {
|
|
702
|
+
// Ignore missing pnpm shim directories.
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return shimDirs.length > 0 ? Array.from(new Set(shimDirs)) : null;
|
|
707
|
+
}
|
|
708
|
+
|
|
138
709
|
private async getLatestVersionFromRegistry(
|
|
139
710
|
registry: string,
|
|
140
711
|
packageName: string,
|
|
@@ -164,6 +735,125 @@ export class PackageManagerUtil {
|
|
|
164
735
|
}
|
|
165
736
|
}
|
|
166
737
|
|
|
738
|
+
async function readJson(filePath: string): Promise<any | null> {
|
|
739
|
+
try {
|
|
740
|
+
const content = await plugins.fs.readFile(filePath, "utf8");
|
|
741
|
+
return JSON.parse(content);
|
|
742
|
+
} catch {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function getDependencyMap(value: unknown): Record<string, string> {
|
|
748
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
749
|
+
return {};
|
|
750
|
+
}
|
|
751
|
+
return value as Record<string, string>;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function getDependencyVersion(info: any): string {
|
|
755
|
+
if (info && typeof info === "object" && typeof info.version === "string") {
|
|
756
|
+
return info.version;
|
|
757
|
+
}
|
|
758
|
+
if (typeof info === "string") {
|
|
759
|
+
return normalizeDependencySpec(info);
|
|
760
|
+
}
|
|
761
|
+
return "unknown";
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function getDependencyPackagePath(info: any): string | undefined {
|
|
765
|
+
return info && typeof info === "object" && typeof info.path === "string"
|
|
766
|
+
? normalizePath(info.path)
|
|
767
|
+
: undefined;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function getPackagePath(globalDir: string, packageName: string): string {
|
|
771
|
+
return plugins.path.join(
|
|
772
|
+
globalDir,
|
|
773
|
+
"node_modules",
|
|
774
|
+
...packageName.split("/"),
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function getPackageBinNames(packageName: string, packageJson: any): string[] {
|
|
779
|
+
const bin = packageJson?.bin;
|
|
780
|
+
if (typeof bin === "string") {
|
|
781
|
+
return [getDefaultBinName(packageName)];
|
|
782
|
+
}
|
|
783
|
+
if (!bin || typeof bin !== "object" || Array.isArray(bin)) {
|
|
784
|
+
return [];
|
|
785
|
+
}
|
|
786
|
+
return Object.keys(bin)
|
|
787
|
+
.filter((binName) => binName.length > 0)
|
|
788
|
+
.sort();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function getDefaultBinName(packageName: string): string {
|
|
792
|
+
const packageNameParts = packageName.split("/");
|
|
793
|
+
return packageNameParts[packageNameParts.length - 1] || packageName;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function getNodeModulesDir(packagePath: string, packageName: string): string {
|
|
797
|
+
return packageName.startsWith("@")
|
|
798
|
+
? plugins.path.dirname(plugins.path.dirname(packagePath))
|
|
799
|
+
: plugins.path.dirname(packagePath);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function rewriteShimForPnpmHome(content: string): string {
|
|
803
|
+
const targetMatch = content.match(/^# cmd-shim-target=(.+)$/m);
|
|
804
|
+
if (!targetMatch?.[1]) {
|
|
805
|
+
return content;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const absoluteTarget = `"${escapeDoubleQuotedShell(targetMatch[1])}"`;
|
|
809
|
+
return content.replace(
|
|
810
|
+
/"\$basedir\/(?:\.\.\/)+store\/[^"\n]+"/g,
|
|
811
|
+
absoluteTarget,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function escapeDoubleQuotedShell(value: string): string {
|
|
816
|
+
return value.replace(/["\\$`]/g, "\\$&");
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function formatShimName(pnpmShimDir: string, binName: string): string {
|
|
820
|
+
const pnpmHome = process.env.PNPM_HOME;
|
|
821
|
+
if (!pnpmHome) {
|
|
822
|
+
return binName;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const relativeName = plugins.path.relative(
|
|
826
|
+
pnpmHome,
|
|
827
|
+
plugins.path.join(pnpmShimDir, binName),
|
|
828
|
+
);
|
|
829
|
+
return relativeName && !relativeName.startsWith("..")
|
|
830
|
+
? relativeName
|
|
831
|
+
: binName;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function normalizeDependencySpec(spec: unknown): string {
|
|
835
|
+
if (typeof spec !== "string" || spec.length === 0) {
|
|
836
|
+
return "unknown";
|
|
837
|
+
}
|
|
838
|
+
const versionMatch = spec.match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/);
|
|
839
|
+
return versionMatch?.[0] || spec;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function normalizeGlobalDir(globalDir: string): string {
|
|
843
|
+
const normalizedPath = normalizePath(globalDir);
|
|
844
|
+
return plugins.path.basename(normalizedPath) === "node_modules"
|
|
845
|
+
? plugins.path.dirname(normalizedPath)
|
|
846
|
+
: normalizedPath;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function normalizePath(filePath: string): string {
|
|
850
|
+
return plugins.path.resolve(filePath).replace(/\/+$/, "");
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function pathsAreEqual(pathA: string, pathB: string): boolean {
|
|
854
|
+
return normalizePath(pathA) === normalizePath(pathB);
|
|
855
|
+
}
|
|
856
|
+
|
|
167
857
|
function normalizeSemver(version: string): number[] {
|
|
168
858
|
return version
|
|
169
859
|
.replace(/^[^\d]*/, "")
|