@git.zone/cli 2.25.2 → 3.0.1
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 +2 -2
- package/dist_ts/mod_config/index.js +10 -3
- package/dist_ts/mod_docker/classes.dockerpruner.d.ts +87 -0
- package/dist_ts/mod_docker/classes.dockerpruner.js +262 -0
- package/dist_ts/mod_docker/index.d.ts +2 -0
- package/dist_ts/mod_docker/index.js +167 -6
- package/dist_ts/mod_format/index.js +8 -3
- package/dist_ts/mod_meta/meta.classes.meta.d.ts +11 -0
- package/dist_ts/mod_meta/meta.classes.meta.js +61 -8
- package/dist_ts/mod_services/classes.dockercontainer.d.ts +9 -0
- package/dist_ts/mod_services/classes.dockercontainer.js +48 -1
- package/dist_ts/mod_services/classes.globalregistry.d.ts +7 -1
- package/dist_ts/mod_services/classes.globalregistry.js +29 -7
- package/dist_ts/mod_services/classes.servicemanager.d.ts +6 -4
- package/dist_ts/mod_services/classes.servicemanager.js +42 -16
- package/dist_ts/mod_services/index.js +6 -3
- package/dist_ts/mod_standard/index.js +10 -2
- package/dist_ts/mod_tools/classes.packagemanager.js +22 -1
- package/package.json +1 -1
- package/readme.hints.md +30 -0
- package/readme.md +29 -4
- package/readme.plan.md +31 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_config/index.ts +15 -2
- package/ts/mod_docker/classes.dockerpruner.ts +334 -0
- package/ts/mod_docker/index.ts +231 -6
- package/ts/mod_format/index.ts +17 -2
- package/ts/mod_meta/meta.classes.meta.ts +82 -7
- package/ts/mod_services/classes.dockercontainer.ts +72 -0
- package/ts/mod_services/classes.globalregistry.ts +32 -7
- package/ts/mod_services/classes.servicemanager.ts +57 -16
- package/ts/mod_services/index.ts +9 -2
- package/ts/mod_standard/index.ts +9 -1
- package/ts/mod_tools/classes.packagemanager.ts +25 -0
|
@@ -100,8 +100,7 @@ export class Meta {
|
|
|
100
100
|
`cd ${this.cwd} && git pull origin master`,
|
|
101
101
|
);
|
|
102
102
|
if (gitCleanArg) {
|
|
103
|
-
|
|
104
|
-
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
|
|
103
|
+
await this.cleanUntrackedWithConsent();
|
|
105
104
|
}
|
|
106
105
|
logger.log('info', `syncing to remote origin master`);
|
|
107
106
|
await this.smartshellInstance.exec(
|
|
@@ -109,6 +108,54 @@ export class Meta {
|
|
|
109
108
|
);
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Remove untracked files from the meta repository, but only after showing
|
|
113
|
+
* exactly what would go and getting a yes.
|
|
114
|
+
*
|
|
115
|
+
* This used to be an unconditional `git clean -fd` on a code path that every
|
|
116
|
+
* meta subcommand reaches, including `meta update`. It deletes untracked,
|
|
117
|
+
* non-ignored files — which is a user's uncommitted scratch work, not the
|
|
118
|
+
* "old directories" the log line claimed, since cloned project directories
|
|
119
|
+
* are normally gitignored and `-fd` does not touch ignored paths.
|
|
120
|
+
*/
|
|
121
|
+
private async cleanUntrackedWithConsent(): Promise<void> {
|
|
122
|
+
const dryRun = await this.smartshellInstance.execSilent(
|
|
123
|
+
`cd ${this.cwd} && git clean -nd`,
|
|
124
|
+
);
|
|
125
|
+
if (dryRun.exitCode !== 0) {
|
|
126
|
+
logger.log('note', 'could not determine untracked files; skipping cleanup');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const candidates = (dryRun.stdout || '')
|
|
131
|
+
.trim()
|
|
132
|
+
.split(/\r?\n/)
|
|
133
|
+
.map((line) => line.replace(/^Would remove\s+/, '').trim())
|
|
134
|
+
.filter(Boolean);
|
|
135
|
+
|
|
136
|
+
if (candidates.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logger.log('note', `${candidates.length} untracked path(s) in the meta repository:`);
|
|
141
|
+
for (const candidate of candidates) {
|
|
142
|
+
logger.log('info', ` ${candidate}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const shouldClean =
|
|
146
|
+
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
147
|
+
'Delete these untracked paths?',
|
|
148
|
+
false,
|
|
149
|
+
);
|
|
150
|
+
if (!shouldClean) {
|
|
151
|
+
logger.log('note', 'keeping untracked paths');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
logger.log('info', 'cleaning the repository from untracked paths');
|
|
156
|
+
await this.smartshellInstance.exec(`cd ${this.cwd} && git clean -fd`);
|
|
157
|
+
}
|
|
158
|
+
|
|
112
159
|
/**
|
|
113
160
|
* update the locally cloned repositories
|
|
114
161
|
*/
|
|
@@ -299,17 +346,45 @@ export class Meta {
|
|
|
299
346
|
return;
|
|
300
347
|
}
|
|
301
348
|
|
|
349
|
+
// The name comes from .meta.json, which is just a file on disk: a key of
|
|
350
|
+
// `../something` would otherwise resolve outside the meta repo and be
|
|
351
|
+
// deleted. Require the target to be a direct child of cwd.
|
|
352
|
+
const targetDirectory = plugins.path.resolve(paths.cwd, projectNameArg);
|
|
353
|
+
const expectedParent = plugins.path.resolve(paths.cwd);
|
|
354
|
+
if (
|
|
355
|
+
plugins.path.dirname(targetDirectory) !== expectedParent ||
|
|
356
|
+
targetDirectory === expectedParent
|
|
357
|
+
) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Refusing to remove "${projectNameArg}": it does not resolve to a direct child of ${expectedParent}`,
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Deleting a clone can destroy uncommitted or unpushed work, so it is
|
|
364
|
+
// confirmed rather than assumed.
|
|
365
|
+
const targetExists = await plugins.smartfs.directory(targetDirectory).exists();
|
|
366
|
+
if (targetExists) {
|
|
367
|
+
const shouldContinue =
|
|
368
|
+
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
369
|
+
`Delete directory ${targetDirectory} and everything in it?`,
|
|
370
|
+
false,
|
|
371
|
+
);
|
|
372
|
+
if (!shouldContinue) {
|
|
373
|
+
logger.log('note', 'Cancelled — nothing was removed');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
302
378
|
delete this.metaRepoData.projects[projectNameArg];
|
|
303
379
|
|
|
304
380
|
logger.log('info', 'removing project from .meta.json');
|
|
305
381
|
await this.sortMetaRepoData();
|
|
306
382
|
await this.writeToDisk();
|
|
307
383
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.directory(
|
|
311
|
-
|
|
312
|
-
.delete();
|
|
384
|
+
if (targetExists) {
|
|
385
|
+
logger.log('info', 'removing directory from cwd');
|
|
386
|
+
await plugins.smartfs.directory(targetDirectory).recursive().delete();
|
|
387
|
+
}
|
|
313
388
|
await this.updateLocalRepos();
|
|
314
389
|
}
|
|
315
390
|
}
|
|
@@ -4,6 +4,15 @@ import { logger } from '../gitzone.logging.js';
|
|
|
4
4
|
|
|
5
5
|
export type ContainerStatus = 'running' | 'stopped' | 'not_exists';
|
|
6
6
|
|
|
7
|
+
export interface IDockerUlimit {
|
|
8
|
+
soft: number;
|
|
9
|
+
hard: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type TDockerUlimits = Record<string, IDockerUlimit>;
|
|
13
|
+
|
|
14
|
+
export type TDockerUlimitMatch = 'match' | 'mismatch' | 'unavailable';
|
|
15
|
+
|
|
7
16
|
export interface IDockerRunOptions {
|
|
8
17
|
name: string;
|
|
9
18
|
image: string;
|
|
@@ -11,6 +20,7 @@ export interface IDockerRunOptions {
|
|
|
11
20
|
volumes?: { [key: string]: string };
|
|
12
21
|
environment?: { [key: string]: string };
|
|
13
22
|
labels?: { [key: string]: string };
|
|
23
|
+
ulimits?: TDockerUlimits;
|
|
14
24
|
restart?: string;
|
|
15
25
|
command?: string;
|
|
16
26
|
}
|
|
@@ -29,6 +39,66 @@ export interface IContainerInspectInfo {
|
|
|
29
39
|
/** Single-quote a value for safe interpolation into a bash command. */
|
|
30
40
|
export const shellQuote = (valueArg: string): string => `'${valueArg.replace(/'/g, `'"'"'`)}'`;
|
|
31
41
|
|
|
42
|
+
const assertDockerUlimit = (nameArg: string, limitArg: IDockerUlimit): void => {
|
|
43
|
+
if (!/^[a-z][a-z0-9_-]*$/u.test(nameArg)) {
|
|
44
|
+
throw new Error(`Invalid Docker ulimit name: ${nameArg}`);
|
|
45
|
+
}
|
|
46
|
+
if (
|
|
47
|
+
!Number.isSafeInteger(limitArg.soft) ||
|
|
48
|
+
!Number.isSafeInteger(limitArg.hard) ||
|
|
49
|
+
limitArg.soft <= 0 ||
|
|
50
|
+
limitArg.hard <= 0 ||
|
|
51
|
+
limitArg.hard < limitArg.soft
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(`Invalid Docker ulimit values for ${nameArg}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const renderDockerUlimits = (ulimitsArg: TDockerUlimits = {}): string => {
|
|
58
|
+
let rendered = '';
|
|
59
|
+
for (const [name, limit] of Object.entries(ulimitsArg)) {
|
|
60
|
+
assertDockerUlimit(name, limit);
|
|
61
|
+
rendered += ` --ulimit ${shellQuote(`${name}=${limit.soft}:${limit.hard}`)}`;
|
|
62
|
+
}
|
|
63
|
+
return rendered;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const matchDockerUlimits = (
|
|
67
|
+
inspectDataArg: unknown,
|
|
68
|
+
expectedArg: TDockerUlimits,
|
|
69
|
+
): TDockerUlimitMatch => {
|
|
70
|
+
if (!Array.isArray(inspectDataArg) || inspectDataArg.length !== 1) {
|
|
71
|
+
return 'unavailable';
|
|
72
|
+
}
|
|
73
|
+
const container = inspectDataArg[0] as {
|
|
74
|
+
HostConfig?: {
|
|
75
|
+
Ulimits?: Array<{ Name?: unknown; Soft?: unknown; Hard?: unknown }> | null;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
if (!container?.HostConfig) {
|
|
79
|
+
return 'unavailable';
|
|
80
|
+
}
|
|
81
|
+
if (!Object.hasOwn(container.HostConfig, 'Ulimits')) {
|
|
82
|
+
return 'unavailable';
|
|
83
|
+
}
|
|
84
|
+
const actual = container.HostConfig.Ulimits ?? [];
|
|
85
|
+
if (!Array.isArray(actual)) {
|
|
86
|
+
return 'unavailable';
|
|
87
|
+
}
|
|
88
|
+
for (const [name, expected] of Object.entries(expectedArg)) {
|
|
89
|
+
assertDockerUlimit(name, expected);
|
|
90
|
+
const matchingName = actual.filter((entry) => entry?.Name === name);
|
|
91
|
+
if (
|
|
92
|
+
matchingName.length !== 1 ||
|
|
93
|
+
matchingName[0].Soft !== expected.soft ||
|
|
94
|
+
matchingName[0].Hard !== expected.hard
|
|
95
|
+
) {
|
|
96
|
+
return 'mismatch';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return 'match';
|
|
100
|
+
};
|
|
101
|
+
|
|
32
102
|
export class DockerContainer {
|
|
33
103
|
private smartshell: plugins.smartshell.Smartshell;
|
|
34
104
|
/**
|
|
@@ -171,6 +241,8 @@ export class DockerContainer {
|
|
|
171
241
|
}
|
|
172
242
|
}
|
|
173
243
|
|
|
244
|
+
command += renderDockerUlimits(options.ulimits);
|
|
245
|
+
|
|
174
246
|
// Add restart policy
|
|
175
247
|
if (options.restart) {
|
|
176
248
|
command += ` --restart ${options.restart}`;
|
|
@@ -200,18 +200,43 @@ export class GlobalRegistry {
|
|
|
200
200
|
/**
|
|
201
201
|
* Remove stale registry entries (projects that no longer exist on disk)
|
|
202
202
|
*/
|
|
203
|
-
public async cleanup(): Promise<string[]> {
|
|
203
|
+
public async cleanup(): Promise<{ removed: string[]; kept: Array<{ projectPath: string; reason: string }> }> {
|
|
204
204
|
const projects = await this.getAllProjects();
|
|
205
205
|
const removed: string[] = [];
|
|
206
|
+
const kept: Array<{ projectPath: string; reason: string }> = [];
|
|
206
207
|
|
|
207
|
-
for (const projectPath of Object.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await this.unregisterProject(projectPath);
|
|
211
|
-
removed.push(projectPath);
|
|
208
|
+
for (const [projectPath, project] of Object.entries(projects)) {
|
|
209
|
+
if (await plugins.smartfs.directory(projectPath).exists()) {
|
|
210
|
+
continue;
|
|
212
211
|
}
|
|
212
|
+
|
|
213
|
+
// Same rule ServicePruner enforces: for containers created before labels
|
|
214
|
+
// existed, this registry entry is the only thing that can still identify
|
|
215
|
+
// them. Dropping it while one exists would hide the container from prune,
|
|
216
|
+
// `stop -g` and `cleanup -g` alike, leaving it running forever under
|
|
217
|
+
// `restart: unless-stopped`.
|
|
218
|
+
const survivingContainers: string[] = [];
|
|
219
|
+
for (const containerName of Object.values(project.containers)) {
|
|
220
|
+
if (!containerName) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if ((await this.docker.getStatus(containerName)) !== 'not_exists') {
|
|
224
|
+
survivingContainers.push(containerName);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (survivingContainers.length > 0) {
|
|
229
|
+
kept.push({
|
|
230
|
+
projectPath,
|
|
231
|
+
reason: `container(s) still exist: ${survivingContainers.join(', ')}`,
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await this.unregisterProject(projectPath);
|
|
237
|
+
removed.push(projectPath);
|
|
213
238
|
}
|
|
214
239
|
|
|
215
|
-
return removed;
|
|
240
|
+
return { removed, kept };
|
|
216
241
|
}
|
|
217
242
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import * as plugins from './mod.plugins.js';
|
|
2
2
|
import * as helpers from './helpers.js';
|
|
3
3
|
import { ServiceConfiguration } from './classes.serviceconfiguration.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DockerContainer,
|
|
6
|
+
matchDockerUlimits,
|
|
7
|
+
type TDockerUlimits,
|
|
8
|
+
} from './classes.dockercontainer.js';
|
|
5
9
|
import { GlobalRegistry } from './classes.globalregistry.js';
|
|
6
10
|
import {
|
|
7
11
|
getServiceContainerLabels,
|
|
@@ -44,6 +48,13 @@ export interface IServicesStatus {
|
|
|
44
48
|
totalDataBytes: number;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
export const mongoServiceUlimits = {
|
|
52
|
+
nofile: {
|
|
53
|
+
soft: 65_536,
|
|
54
|
+
hard: 65_536,
|
|
55
|
+
},
|
|
56
|
+
} as const satisfies TDockerUlimits;
|
|
57
|
+
|
|
47
58
|
export class ServiceManager {
|
|
48
59
|
private config: ServiceConfiguration;
|
|
49
60
|
private docker: DockerContainer;
|
|
@@ -297,24 +308,60 @@ export class ServiceManager {
|
|
|
297
308
|
|
|
298
309
|
const status = await this.docker.getStatus(containers.mongo);
|
|
299
310
|
|
|
300
|
-
// Containers created before replica-set support, with a
|
|
301
|
-
// running the other auth mode must be recreated; the data
|
|
311
|
+
// Containers created before replica-set or managed-limit support, with a
|
|
312
|
+
// changed port, or running the other auth mode must be recreated; the data
|
|
313
|
+
// is preserved.
|
|
302
314
|
let needsRecreate = false;
|
|
303
315
|
if (status !== 'not_exists') {
|
|
304
|
-
const
|
|
316
|
+
const inspectInfo = await this.docker.inspect(containers.mongo);
|
|
317
|
+
if (!Array.isArray(inspectInfo) || inspectInfo.length !== 1) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Unable to inspect MongoDB container ${containers.mongo}; refusing to recreate it.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
const inspectedContainer = inspectInfo[0];
|
|
323
|
+
if (
|
|
324
|
+
!inspectedContainer?.Config ||
|
|
325
|
+
!Array.isArray(inspectedContainer.Config.Cmd) ||
|
|
326
|
+
!inspectedContainer.HostConfig ||
|
|
327
|
+
!Object.hasOwn(inspectedContainer.HostConfig, 'PortBindings')
|
|
328
|
+
) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`Incomplete inspection for MongoDB container ${containers.mongo}; refusing to recreate it.`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const containerCmd = inspectedContainer.Config.Cmd;
|
|
305
334
|
const hasReplSet = containerCmd.includes('--replSet');
|
|
306
335
|
const hasKeyFile = containerCmd.includes('--keyFile');
|
|
307
|
-
const
|
|
336
|
+
const mongoPortBindings =
|
|
337
|
+
inspectedContainer.HostConfig.PortBindings?.[`${config.MONGODB_PORT}/tcp`];
|
|
308
338
|
const portMatches =
|
|
309
|
-
|
|
339
|
+
Array.isArray(mongoPortBindings) &&
|
|
340
|
+
mongoPortBindings.length > 0 &&
|
|
341
|
+
mongoPortBindings.some(
|
|
342
|
+
(bindingArg: { HostPort?: string }) => bindingArg?.HostPort === config.MONGODB_PORT,
|
|
343
|
+
);
|
|
310
344
|
const authMatches = hasKeyFile === this.config.isMongoAuthEnabled();
|
|
311
|
-
|
|
345
|
+
const ulimitMatch = matchDockerUlimits(inspectInfo, mongoServiceUlimits);
|
|
346
|
+
if (ulimitMatch === 'unavailable') {
|
|
347
|
+
throw new Error(
|
|
348
|
+
`Unable to inspect MongoDB container limits for ${containers.mongo}; refusing to recreate it.`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
needsRecreate =
|
|
352
|
+
!hasReplSet ||
|
|
353
|
+
!portMatches ||
|
|
354
|
+
!authMatches ||
|
|
355
|
+
ulimitMatch === 'mismatch';
|
|
312
356
|
if (!authMatches) {
|
|
313
357
|
logger.log(
|
|
314
358
|
'note',
|
|
315
359
|
` Auth mode changed to ${this.config.isMongoAuthEnabled() ? 'enabled' : 'disabled'}, recreating container...`,
|
|
316
360
|
);
|
|
317
361
|
}
|
|
362
|
+
if (ulimitMatch === 'mismatch') {
|
|
363
|
+
logger.log('note', ' MongoDB file-descriptor limit changed, recreating container...');
|
|
364
|
+
}
|
|
318
365
|
}
|
|
319
366
|
|
|
320
367
|
switch (status) {
|
|
@@ -323,7 +370,7 @@ export class ServiceManager {
|
|
|
323
370
|
logger.log('ok', ' Already running ✓');
|
|
324
371
|
break;
|
|
325
372
|
}
|
|
326
|
-
logger.log('note', '
|
|
373
|
+
logger.log('note', ' MongoDB service configuration changed, recreating container...');
|
|
327
374
|
await this.docker.remove(containers.mongo, true);
|
|
328
375
|
await this.createMongoContainer();
|
|
329
376
|
break;
|
|
@@ -420,6 +467,7 @@ export class ServiceManager {
|
|
|
420
467
|
},
|
|
421
468
|
environment,
|
|
422
469
|
labels,
|
|
470
|
+
ulimits: mongoServiceUlimits,
|
|
423
471
|
restart: 'unless-stopped',
|
|
424
472
|
command
|
|
425
473
|
});
|
|
@@ -428,6 +476,7 @@ export class ServiceManager {
|
|
|
428
476
|
logger.log('ok', ' Created and started ✓');
|
|
429
477
|
} else {
|
|
430
478
|
logger.log('error', ' Failed to create container');
|
|
479
|
+
throw new Error(`Failed to create MongoDB container ${containers.mongo}.`);
|
|
431
480
|
}
|
|
432
481
|
}
|
|
433
482
|
|
|
@@ -451,14 +500,6 @@ export class ServiceManager {
|
|
|
451
500
|
});
|
|
452
501
|
}
|
|
453
502
|
|
|
454
|
-
/**
|
|
455
|
-
* The mongod argv the container was created with
|
|
456
|
-
*/
|
|
457
|
-
private async getMongoContainerCmd(containerName: string): Promise<string[]> {
|
|
458
|
-
const info = await this.docker.inspect(containerName);
|
|
459
|
-
return info?.[0]?.Config?.Cmd ?? [];
|
|
460
|
-
}
|
|
461
|
-
|
|
462
503
|
/**
|
|
463
504
|
* Make sure the configured root user exists when auth is enabled.
|
|
464
505
|
*
|
package/ts/mod_services/index.ts
CHANGED
|
@@ -1162,10 +1162,17 @@ async function handleGlobalCleanup(globalRegistry: GlobalRegistry) {
|
|
|
1162
1162
|
helpers.printHeader("Cleanup Registry (Global)");
|
|
1163
1163
|
|
|
1164
1164
|
logger.log("note", "Checking for stale registry entries...");
|
|
1165
|
-
const removed = await globalRegistry.cleanup();
|
|
1165
|
+
const { removed, kept } = await globalRegistry.cleanup();
|
|
1166
|
+
|
|
1167
|
+
for (const keptEntry of kept) {
|
|
1168
|
+
logger.log(
|
|
1169
|
+
"note",
|
|
1170
|
+
` kept ${keptEntry.projectPath}: ${keptEntry.reason} — removing the entry would make them unidentifiable`,
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1166
1173
|
|
|
1167
1174
|
if (removed.length === 0) {
|
|
1168
|
-
logger.log("ok", "No stale entries
|
|
1175
|
+
logger.log("ok", "No stale entries removed");
|
|
1169
1176
|
return;
|
|
1170
1177
|
}
|
|
1171
1178
|
|
package/ts/mod_standard/index.ts
CHANGED
|
@@ -26,7 +26,10 @@ const commandSummaries: ICommandHelpSummary[] = [
|
|
|
26
26
|
{ name: "tools", description: "Manage the global @git.zone toolchain" },
|
|
27
27
|
{ name: "template", description: "Create a project from a template" },
|
|
28
28
|
{ name: "open", description: "Open project assets and CI pages" },
|
|
29
|
-
{
|
|
29
|
+
{
|
|
30
|
+
name: "docker",
|
|
31
|
+
description: "Report and reclaim Docker resources created by git.zone tooling",
|
|
32
|
+
},
|
|
30
33
|
{
|
|
31
34
|
name: "deprecate",
|
|
32
35
|
description: "Deprecate npm packages across registries",
|
|
@@ -245,6 +248,11 @@ async function showCommandHelp(
|
|
|
245
248
|
modTools.showHelp(mode);
|
|
246
249
|
return true;
|
|
247
250
|
}
|
|
251
|
+
case "docker": {
|
|
252
|
+
const modDocker = await import("../mod_docker/index.js");
|
|
253
|
+
modDocker.showHelp(mode);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
248
256
|
default:
|
|
249
257
|
return false;
|
|
250
258
|
}
|
|
@@ -362,6 +362,26 @@ export class PackageManagerUtil {
|
|
|
362
362
|
return results;
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
// Only shims belonging to packages this tool manages may be removed.
|
|
366
|
+
// Pointing at a legacy root is not sufficient evidence of ownership: a
|
|
367
|
+
// legacy root retained precisely because it holds unmanaged packages also
|
|
368
|
+
// holds their shims, and deleting those silently breaks unrelated global
|
|
369
|
+
// CLIs that have nothing to do with @git.zone.
|
|
370
|
+
const managedLegacyBinNames = new Set<string>();
|
|
371
|
+
for (const legacyRoot of legacyRoots) {
|
|
372
|
+
for (const packageInfo of legacyRoot.packages) {
|
|
373
|
+
if (!packageInfo.packagePath) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const packageJson = await readJson(
|
|
377
|
+
plugins.path.join(packageInfo.packagePath, "package.json"),
|
|
378
|
+
);
|
|
379
|
+
for (const binName of getPackageBinNames(packageInfo.name, packageJson)) {
|
|
380
|
+
managedLegacyBinNames.add(binName);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
365
385
|
for (const pnpmShimDir of pnpmShimDirs) {
|
|
366
386
|
try {
|
|
367
387
|
const entries = await plugins.fs.readdir(pnpmShimDir, {
|
|
@@ -372,6 +392,11 @@ export class PackageManagerUtil {
|
|
|
372
392
|
continue;
|
|
373
393
|
}
|
|
374
394
|
|
|
395
|
+
if (!managedLegacyBinNames.has(entry.name)) {
|
|
396
|
+
// Not a binary of a package this tool manages — leave it alone.
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
375
400
|
const filePath = plugins.path.join(pnpmShimDir, entry.name);
|
|
376
401
|
let content = "";
|
|
377
402
|
try {
|