@git.zone/cli 2.25.2 → 3.0.0
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.globalregistry.d.ts +7 -1
- package/dist_ts/mod_services/classes.globalregistry.js +29 -7
- 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 +24 -3
- 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.globalregistry.ts +32 -7
- 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
|
}
|
|
@@ -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
|
}
|
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 {
|