@git.zone/cli 2.23.0 → 2.25.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 +1 -1
- package/dist_ts/gitzone.cli.js +11 -2
- package/dist_ts/mod_services/classes.dockercontainer.d.ts +81 -0
- package/dist_ts/mod_services/classes.dockercontainer.js +205 -10
- package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -0
- package/dist_ts/mod_services/classes.globalregistry.js +23 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +52 -1
- package/dist_ts/mod_services/classes.serviceconfiguration.js +116 -20
- package/dist_ts/mod_services/classes.servicedatamarker.d.ts +93 -0
- package/dist_ts/mod_services/classes.servicedatamarker.js +166 -0
- package/dist_ts/mod_services/classes.servicemanager.d.ts +91 -5
- package/dist_ts/mod_services/classes.servicemanager.js +274 -64
- package/dist_ts/mod_services/classes.serviceoptions.d.ts +58 -0
- package/dist_ts/mod_services/classes.serviceoptions.js +93 -0
- package/dist_ts/mod_services/classes.servicepruner.d.ts +102 -0
- package/dist_ts/mod_services/classes.servicepruner.js +410 -0
- package/dist_ts/mod_services/helpers.d.ts +15 -0
- package/dist_ts/mod_services/helpers.js +57 -1
- package/dist_ts/mod_services/index.js +307 -53
- package/dist_ts/mod_tools/classes.packagemanager.d.ts +13 -0
- package/dist_ts/mod_tools/classes.packagemanager.js +42 -1
- package/dist_ts/mod_tools/index.js +4 -1
- package/package.json +3 -2
- package/readme.hints.md +105 -0
- package/readme.md +123 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/gitzone.cli.ts +10 -0
- package/ts/mod_services/classes.dockercontainer.ts +244 -13
- package/ts/mod_services/classes.globalregistry.ts +27 -0
- package/ts/mod_services/classes.serviceconfiguration.ts +148 -27
- package/ts/mod_services/classes.servicedatamarker.ts +228 -0
- package/ts/mod_services/classes.servicemanager.ts +394 -72
- package/ts/mod_services/classes.serviceoptions.ts +135 -0
- package/ts/mod_services/classes.servicepruner.ts +532 -0
- package/ts/mod_services/helpers.ts +60 -0
- package/ts/mod_services/index.ts +437 -58
- package/ts/mod_tools/classes.packagemanager.ts +54 -0
- package/ts/mod_tools/index.ts +5 -0
package/ts/mod_services/index.ts
CHANGED
|
@@ -2,9 +2,19 @@ import * as plugins from "./mod.plugins.js";
|
|
|
2
2
|
import * as helpers from "./helpers.js";
|
|
3
3
|
import { ServiceManager } from "./classes.servicemanager.js";
|
|
4
4
|
import { GlobalRegistry } from "./classes.globalregistry.js";
|
|
5
|
+
import { getServiceDataDirectory } from "./classes.servicedatamarker.js";
|
|
6
|
+
import {
|
|
7
|
+
ServicePruner,
|
|
8
|
+
defaultStaleDays,
|
|
9
|
+
type IServicePrunePlan,
|
|
10
|
+
} from "./classes.servicepruner.js";
|
|
5
11
|
import { logger } from "../gitzone.logging.js";
|
|
6
12
|
import type { ICliMode } from "../helpers.climode.js";
|
|
7
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getCliMode,
|
|
15
|
+
printJson,
|
|
16
|
+
runWithSuppressedOutput,
|
|
17
|
+
} from "../helpers.climode.js";
|
|
8
18
|
import {
|
|
9
19
|
getCliConfigValueFromData,
|
|
10
20
|
readSmartconfigFile,
|
|
@@ -24,7 +34,7 @@ export const run = async (argvArg: any) => {
|
|
|
24
34
|
|
|
25
35
|
// Handle global commands first
|
|
26
36
|
if (isGlobal) {
|
|
27
|
-
await handleGlobalCommand(command);
|
|
37
|
+
await handleGlobalCommand(command, argvArg, mode);
|
|
28
38
|
return;
|
|
29
39
|
}
|
|
30
40
|
|
|
@@ -53,6 +63,14 @@ export const run = async (argvArg: any) => {
|
|
|
53
63
|
await handleDisableServices(argvArg._.slice(2), mode);
|
|
54
64
|
break;
|
|
55
65
|
|
|
66
|
+
case "prune":
|
|
67
|
+
await handlePrune(argvArg, mode);
|
|
68
|
+
break;
|
|
69
|
+
|
|
70
|
+
case "auth":
|
|
71
|
+
await handleAuth(argvArg._[2], argvArg._[3], mode);
|
|
72
|
+
break;
|
|
73
|
+
|
|
56
74
|
case "start":
|
|
57
75
|
case "stop":
|
|
58
76
|
case "restart":
|
|
@@ -63,11 +81,13 @@ export const run = async (argvArg: any) => {
|
|
|
63
81
|
case "clean":
|
|
64
82
|
case "reconfigure": {
|
|
65
83
|
const serviceManager = new ServiceManager();
|
|
66
|
-
|
|
84
|
+
// Initialisation is chatty. In JSON mode that chatter would land on stdout
|
|
85
|
+
// ahead of the payload and make the output unparseable.
|
|
86
|
+
await withJsonSafeOutput(mode, () => serviceManager.init());
|
|
67
87
|
|
|
68
88
|
switch (command) {
|
|
69
89
|
case "start":
|
|
70
|
-
await handleStart(serviceManager, service);
|
|
90
|
+
await handleStart(serviceManager, service, mode);
|
|
71
91
|
break;
|
|
72
92
|
|
|
73
93
|
case "stop":
|
|
@@ -79,7 +99,12 @@ export const run = async (argvArg: any) => {
|
|
|
79
99
|
break;
|
|
80
100
|
|
|
81
101
|
case "status":
|
|
102
|
+
if (mode.json) {
|
|
103
|
+
printJson(await serviceManager.collectStatus());
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
82
106
|
await serviceManager.showStatus();
|
|
107
|
+
await printStaleHint();
|
|
83
108
|
break;
|
|
84
109
|
|
|
85
110
|
case "compass":
|
|
@@ -93,11 +118,11 @@ export const run = async (argvArg: any) => {
|
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
case "remove":
|
|
96
|
-
await handleRemove(serviceManager);
|
|
121
|
+
await handleRemove(serviceManager, mode);
|
|
97
122
|
break;
|
|
98
123
|
|
|
99
124
|
case "clean":
|
|
100
|
-
await handleClean(serviceManager);
|
|
125
|
+
await handleClean(serviceManager, mode);
|
|
101
126
|
break;
|
|
102
127
|
|
|
103
128
|
case "reconfigure":
|
|
@@ -112,6 +137,21 @@ export const run = async (argvArg: any) => {
|
|
|
112
137
|
}
|
|
113
138
|
};
|
|
114
139
|
|
|
140
|
+
/**
|
|
141
|
+
* Run work that logs for humans, keeping stdout clean when the caller asked for
|
|
142
|
+
* JSON. Without this, `--json` output is preceded by log lines and cannot be
|
|
143
|
+
* parsed by a consumer such as a test suite.
|
|
144
|
+
*/
|
|
145
|
+
async function withJsonSafeOutput<T>(
|
|
146
|
+
mode: ICliMode,
|
|
147
|
+
work: () => Promise<T>,
|
|
148
|
+
): Promise<T> {
|
|
149
|
+
if (!mode.json) {
|
|
150
|
+
return work();
|
|
151
|
+
}
|
|
152
|
+
return runWithSuppressedOutput(work);
|
|
153
|
+
}
|
|
154
|
+
|
|
115
155
|
const allowedServices = ["mongodb", "minio", "elasticsearch"];
|
|
116
156
|
|
|
117
157
|
const normalizeServiceName = (service: string): string => {
|
|
@@ -193,7 +233,11 @@ async function handleShowConfig(mode: ICliMode) {
|
|
|
193
233
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`);
|
|
194
234
|
logger.log(
|
|
195
235
|
"info",
|
|
196
|
-
`
|
|
236
|
+
` Auth: ${env.MONGODB_AUTH_ENABLED === false ? "DISABLED (loopback only)" : "enabled"}`,
|
|
237
|
+
);
|
|
238
|
+
logger.log(
|
|
239
|
+
"info",
|
|
240
|
+
` Data: ${getServiceDataDirectory(process.cwd(), "mongodb")}`,
|
|
197
241
|
);
|
|
198
242
|
logger.log("info", ` Connection: ${env.MONGODB_URL}`);
|
|
199
243
|
console.log();
|
|
@@ -206,7 +250,7 @@ async function handleShowConfig(mode: ICliMode) {
|
|
|
206
250
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`);
|
|
207
251
|
logger.log(
|
|
208
252
|
"info",
|
|
209
|
-
` Data: ${
|
|
253
|
+
` Data: ${getServiceDataDirectory(process.cwd(), "minio")}`,
|
|
210
254
|
);
|
|
211
255
|
logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`);
|
|
212
256
|
console.log();
|
|
@@ -220,7 +264,7 @@ async function handleShowConfig(mode: ICliMode) {
|
|
|
220
264
|
logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`);
|
|
221
265
|
logger.log(
|
|
222
266
|
"info",
|
|
223
|
-
` Data: ${
|
|
267
|
+
` Data: ${getServiceDataDirectory(process.cwd(), "elasticsearch")}`,
|
|
224
268
|
);
|
|
225
269
|
logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`);
|
|
226
270
|
}
|
|
@@ -304,34 +348,93 @@ function validateRequestedServices(services: string[]): void {
|
|
|
304
348
|
}
|
|
305
349
|
}
|
|
306
350
|
|
|
307
|
-
async function handleStart(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
async function handleStart(
|
|
352
|
+
serviceManager: ServiceManager,
|
|
353
|
+
service: string,
|
|
354
|
+
mode: ICliMode,
|
|
355
|
+
) {
|
|
356
|
+
const startService = async (): Promise<boolean> => {
|
|
357
|
+
switch (service) {
|
|
358
|
+
case "mongo":
|
|
359
|
+
case "mongodb":
|
|
360
|
+
await serviceManager.startMongoDB();
|
|
361
|
+
break;
|
|
362
|
+
|
|
363
|
+
case "minio":
|
|
364
|
+
case "s3":
|
|
365
|
+
await serviceManager.startMinIO();
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case "elasticsearch":
|
|
369
|
+
case "es":
|
|
370
|
+
await serviceManager.startElasticsearch();
|
|
371
|
+
break;
|
|
372
|
+
|
|
373
|
+
case "all":
|
|
374
|
+
case "":
|
|
375
|
+
await serviceManager.startAll();
|
|
376
|
+
break;
|
|
377
|
+
|
|
378
|
+
default:
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
315
381
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
382
|
+
// Register on every start path so the global registry never drifts from
|
|
383
|
+
// reality; prune relies on this claim to identify tool-owned containers.
|
|
384
|
+
await serviceManager.registerWithGlobalRegistry();
|
|
385
|
+
return true;
|
|
386
|
+
};
|
|
320
387
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
388
|
+
if (mode.json) {
|
|
389
|
+
const started = await runWithSuppressedOutput(startService);
|
|
390
|
+
if (!started) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Unknown service: ${service}. Use: mongo, s3, elasticsearch, or all`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
printJson(await serviceManager.collectStatus());
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
325
398
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
399
|
+
helpers.printHeader("Starting Services");
|
|
400
|
+
if (!(await startService())) {
|
|
401
|
+
logger.log("error", `Unknown service: ${service}`);
|
|
402
|
+
logger.log("note", "Use: mongo, s3, elasticsearch, or all");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
await printStaleHint();
|
|
406
|
+
}
|
|
330
407
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Non-blocking hint that reclaimable service data exists elsewhere on the
|
|
410
|
+
* machine. Detection is automatic; deletion never is.
|
|
411
|
+
*/
|
|
412
|
+
async function printStaleHint() {
|
|
413
|
+
try {
|
|
414
|
+
const summary = await GlobalRegistry.getInstance().getStaleSummary(
|
|
415
|
+
defaultStaleDays,
|
|
416
|
+
);
|
|
417
|
+
if (summary.orphaned === 0 && summary.stale === 0) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const parts: string[] = [];
|
|
421
|
+
if (summary.stale > 0) {
|
|
422
|
+
parts.push(`${summary.stale} inactive`);
|
|
423
|
+
}
|
|
424
|
+
if (summary.orphaned > 0) {
|
|
425
|
+
parts.push(`${summary.orphaned} with a deleted directory`);
|
|
426
|
+
}
|
|
427
|
+
console.log();
|
|
428
|
+
logger.log(
|
|
429
|
+
"note",
|
|
430
|
+
`💡 ${parts.join(", ")} gitzone services project(s) registered on this machine.`,
|
|
431
|
+
);
|
|
432
|
+
logger.log(
|
|
433
|
+
"note",
|
|
434
|
+
" Review what can be reclaimed with `gitzone services prune` (read-only).",
|
|
435
|
+
);
|
|
436
|
+
} catch {
|
|
437
|
+
// A hint must never break the command that produced it.
|
|
335
438
|
}
|
|
336
439
|
}
|
|
337
440
|
|
|
@@ -404,43 +507,272 @@ async function handleRestart(serviceManager: ServiceManager, service: string) {
|
|
|
404
507
|
}
|
|
405
508
|
}
|
|
406
509
|
|
|
407
|
-
async function handleRemove(serviceManager: ServiceManager) {
|
|
510
|
+
async function handleRemove(serviceManager: ServiceManager, mode: ICliMode) {
|
|
408
511
|
helpers.printHeader("Removing Containers");
|
|
409
512
|
logger.log("note", "⚠️ This will remove containers but preserve data");
|
|
410
513
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
514
|
+
if (!mode.yes) {
|
|
515
|
+
if (!mode.interactive) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
"`services remove` needs confirmation. Re-run with --yes to remove containers non-interactively.",
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const shouldContinue =
|
|
521
|
+
await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
522
|
+
"Continue?",
|
|
523
|
+
false,
|
|
524
|
+
);
|
|
525
|
+
if (!shouldContinue) {
|
|
526
|
+
logger.log("note", "Cancelled");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
421
529
|
}
|
|
530
|
+
|
|
531
|
+
await serviceManager.removeContainers();
|
|
422
532
|
}
|
|
423
533
|
|
|
424
|
-
async function handleClean(serviceManager: ServiceManager) {
|
|
534
|
+
async function handleClean(serviceManager: ServiceManager, mode: ICliMode) {
|
|
425
535
|
helpers.printHeader("Clean All");
|
|
426
536
|
logger.log("error", "⚠️ WARNING: This will remove all containers and data!");
|
|
427
537
|
logger.log("error", "This action cannot be undone!");
|
|
538
|
+
await serviceManager.showDiskUsage();
|
|
539
|
+
console.log();
|
|
540
|
+
|
|
541
|
+
if (!mode.yes) {
|
|
542
|
+
if (!mode.interactive) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
"`services clean` destroys data and needs explicit intent. Re-run with --yes to confirm non-interactively.",
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
const smartinteraction = new plugins.smartinteract.SmartInteract();
|
|
548
|
+
const confirmAnswer = await smartinteraction.askQuestion({
|
|
549
|
+
name: "confirm",
|
|
550
|
+
type: "input",
|
|
551
|
+
message: 'Type "yes" to confirm:',
|
|
552
|
+
default: "no",
|
|
553
|
+
});
|
|
554
|
+
if (confirmAnswer.value !== "yes") {
|
|
555
|
+
logger.log("note", "Cancelled");
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
await serviceManager.removeContainers();
|
|
561
|
+
console.log();
|
|
562
|
+
await serviceManager.cleanData();
|
|
563
|
+
logger.log("ok", "All cleaned ✓");
|
|
564
|
+
}
|
|
428
565
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
566
|
+
/**
|
|
567
|
+
* Toggle MongoDB authentication for this project.
|
|
568
|
+
*
|
|
569
|
+
* Opt-in only, and never silent: disabling auth also restricts publishing to
|
|
570
|
+
* loopback and is refused outright for a non-local MONGODB_HOST.
|
|
571
|
+
*/
|
|
572
|
+
async function handleAuth(
|
|
573
|
+
rawService: string | undefined,
|
|
574
|
+
rawValue: string | undefined,
|
|
575
|
+
mode: ICliMode,
|
|
576
|
+
) {
|
|
577
|
+
const service = normalizeServiceName((rawService || "").trim());
|
|
578
|
+
if (service !== "mongodb") {
|
|
579
|
+
throw new Error(
|
|
580
|
+
"Only `mongodb` supports an auth toggle. Usage: gitzone services auth mongodb <on|off>",
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const value = (rawValue || "").trim().toLowerCase();
|
|
585
|
+
if (value !== "on" && value !== "off") {
|
|
586
|
+
throw new Error("Usage: gitzone services auth mongodb <on|off>");
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const serviceManager = new ServiceManager();
|
|
590
|
+
const configuration = await withJsonSafeOutput(mode, async () => {
|
|
591
|
+
await serviceManager.init();
|
|
592
|
+
const resolvedConfiguration = serviceManager.getConfiguration();
|
|
593
|
+
await resolvedConfiguration.setMongoAuthEnabled(value === "on");
|
|
594
|
+
return resolvedConfiguration;
|
|
435
595
|
});
|
|
436
596
|
|
|
437
|
-
if (
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
597
|
+
if (mode.json) {
|
|
598
|
+
printJson({
|
|
599
|
+
ok: true,
|
|
600
|
+
action: "auth",
|
|
601
|
+
service: "mongodb",
|
|
602
|
+
authEnabled: value === "on",
|
|
603
|
+
connectionString: configuration.getMongoConnectionString(),
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (value === "off") {
|
|
609
|
+
logger.log("error", "⚠️ MongoDB authentication is now DISABLED for this project.");
|
|
610
|
+
logger.log(
|
|
611
|
+
"note",
|
|
612
|
+
" The database will be published on 127.0.0.1 only. Do not use this for anything but local development.",
|
|
613
|
+
);
|
|
442
614
|
} else {
|
|
615
|
+
logger.log("ok", "MongoDB authentication is now enabled for this project.");
|
|
616
|
+
}
|
|
617
|
+
logger.log("info", `Connection: ${configuration.getMongoConnectionString()}`);
|
|
618
|
+
logger.log(
|
|
619
|
+
"note",
|
|
620
|
+
"Run `gitzone services start mongo` to recreate the container in the new mode.",
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Report — and only with --apply, reclaim — resources left behind by
|
|
626
|
+
* `gitzone services` across every registered project on this machine.
|
|
627
|
+
*/
|
|
628
|
+
async function handlePrune(argvArg: any, mode: ICliMode) {
|
|
629
|
+
const staleDaysRaw = argvArg["stale-days"] ?? argvArg.staleDays;
|
|
630
|
+
const staleDays =
|
|
631
|
+
staleDaysRaw === undefined ? defaultStaleDays : parseInt(String(staleDaysRaw));
|
|
632
|
+
if (!Number.isFinite(staleDays) || staleDays < 1) {
|
|
633
|
+
throw new Error("--stale-days must be a positive number of days");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const apply = Boolean(argvArg.apply);
|
|
637
|
+
const pruner = new ServicePruner({ staleDays });
|
|
638
|
+
const plan = await pruner.createPlan();
|
|
639
|
+
|
|
640
|
+
const nothingToDo =
|
|
641
|
+
plan.containersToRemove.length === 0 &&
|
|
642
|
+
plan.dataDirsToRemove.length === 0 &&
|
|
643
|
+
plan.registryEntriesToRemove.length === 0;
|
|
644
|
+
|
|
645
|
+
// Deleting data always needs intent beyond --apply. Enforced before the
|
|
646
|
+
// output branches so JSON mode cannot bypass the gate the human path applies.
|
|
647
|
+
const confirmDataRemoval = async (): Promise<boolean> => {
|
|
648
|
+
if (!apply || nothingToDo || plan.dataDirsToRemove.length === 0 || mode.yes) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
if (!mode.interactive) {
|
|
652
|
+
throw new Error(
|
|
653
|
+
"`services prune --apply` would delete data directories. Re-run with --yes to confirm non-interactively.",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
const smartinteraction = new plugins.smartinteract.SmartInteract();
|
|
657
|
+
const confirmAnswer = await smartinteraction.askQuestion({
|
|
658
|
+
name: "confirm",
|
|
659
|
+
type: "input",
|
|
660
|
+
message: `Type "yes" to permanently delete ${plan.dataDirsToRemove.length} data directory/ies:`,
|
|
661
|
+
default: "no",
|
|
662
|
+
});
|
|
663
|
+
return confirmAnswer.value === "yes";
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
if (mode.json) {
|
|
667
|
+
let applied = false;
|
|
668
|
+
if (apply && !nothingToDo) {
|
|
669
|
+
// Non-interactive by definition, so this throws unless --yes was passed.
|
|
670
|
+
await confirmDataRemoval();
|
|
671
|
+
await runWithSuppressedOutput(() => pruner.applyPlan(plan));
|
|
672
|
+
applied = true;
|
|
673
|
+
}
|
|
674
|
+
printJson({ applied, ...plan });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
printPrunePlan(plan, apply);
|
|
679
|
+
|
|
680
|
+
if (!apply || nothingToDo) {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (!(await confirmDataRemoval())) {
|
|
443
685
|
logger.log("note", "Cancelled");
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
await pruner.applyPlan(plan);
|
|
690
|
+
logger.log("ok", "Prune applied ✓");
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function printPrunePlan(plan: IServicePrunePlan, applyArg: boolean) {
|
|
694
|
+
helpers.printHeader(
|
|
695
|
+
applyArg ? "Prune Services (apply)" : "Prune Services (dry run)",
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
logger.log("info", `Inactivity threshold: ${plan.staleDays} days`);
|
|
699
|
+
if (!plan.dockerAvailable) {
|
|
700
|
+
logger.log(
|
|
701
|
+
"error",
|
|
702
|
+
"Docker is not reachable — container and data reclamation is disabled.",
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
console.log();
|
|
706
|
+
|
|
707
|
+
if (plan.projects.length === 0) {
|
|
708
|
+
logger.log("note", "No gitzone services projects known on this machine");
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
for (const project of plan.projects) {
|
|
713
|
+
const stateIcon =
|
|
714
|
+
project.state === "live"
|
|
715
|
+
? "🟢"
|
|
716
|
+
: project.state === "stale"
|
|
717
|
+
? "🟡"
|
|
718
|
+
: project.state === "orphaned"
|
|
719
|
+
? "⚫"
|
|
720
|
+
: "❓";
|
|
721
|
+
logger.log("ok", `${stateIcon} ${project.projectName} [${project.state}]`);
|
|
722
|
+
logger.log("info", ` Path: ${project.projectPath}`);
|
|
723
|
+
logger.log("info", ` ${project.stateReason}`);
|
|
724
|
+
for (const container of project.containers) {
|
|
725
|
+
const marker = container.reclaimable ? "REMOVE" : "keep";
|
|
726
|
+
logger.log(
|
|
727
|
+
"info",
|
|
728
|
+
` ${marker === "REMOVE" ? "🗑 " : " "}container ${container.name} (${container.state}) — ${marker}: ${container.reason}`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
for (const dataDir of project.dataDirectories) {
|
|
732
|
+
if (!dataDir.exists) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
const marker = dataDir.reclaimable ? "REMOVE" : "keep";
|
|
736
|
+
logger.log(
|
|
737
|
+
"info",
|
|
738
|
+
` ${marker === "REMOVE" ? "🗑 " : " "}${dataDir.service} data ${helpers.formatBytes(dataDir.sizeBytes)} — ${marker}: ${dataDir.reason}`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
console.log();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
for (const skipped of plan.skipped) {
|
|
745
|
+
logger.log("note", `skipped ${skipped.target}: ${skipped.reason}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
logger.log("note", "Summary:");
|
|
749
|
+
logger.log("info", ` Projects known: ${plan.totals.projects}`);
|
|
750
|
+
logger.log(
|
|
751
|
+
"info",
|
|
752
|
+
` Service data on disk: ${helpers.formatBytes(plan.totals.totalDataBytes)}`,
|
|
753
|
+
);
|
|
754
|
+
logger.log(
|
|
755
|
+
"info",
|
|
756
|
+
` Reclaimable now: ${helpers.formatBytes(plan.totals.reclaimableDataBytes)} across ${plan.dataDirsToRemove.length} directory/ies`,
|
|
757
|
+
);
|
|
758
|
+
if (plan.totals.blockedByRunningBytes > 0) {
|
|
759
|
+
logger.log(
|
|
760
|
+
"info",
|
|
761
|
+
` Held by running containers: ${helpers.formatBytes(plan.totals.blockedByRunningBytes)} — stop them first (\`gitzone services stop\` in the project, or \`gitzone services stop -g\`)`,
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
logger.log("info", ` Containers to remove: ${plan.containersToRemove.length}`);
|
|
765
|
+
logger.log(
|
|
766
|
+
"info",
|
|
767
|
+
` Stale registry entries to remove: ${plan.registryEntriesToRemove.length}`,
|
|
768
|
+
);
|
|
769
|
+
|
|
770
|
+
if (!applyArg) {
|
|
771
|
+
console.log();
|
|
772
|
+
logger.log(
|
|
773
|
+
"note",
|
|
774
|
+
"Dry run only. Re-run with --apply to reclaim the items marked REMOVE.",
|
|
775
|
+
);
|
|
444
776
|
}
|
|
445
777
|
}
|
|
446
778
|
|
|
@@ -475,11 +807,25 @@ export function showHelp(mode?: ICliMode) {
|
|
|
475
807
|
{ name: "start [service]", description: "Start services" },
|
|
476
808
|
{ name: "stop [service]", description: "Stop services" },
|
|
477
809
|
{ name: "status", description: "Show service status" },
|
|
810
|
+
{
|
|
811
|
+
name: "auth mongodb <on|off>",
|
|
812
|
+
description:
|
|
813
|
+
"Toggle MongoDB authentication; off publishes on loopback only",
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
name: "prune",
|
|
817
|
+
description:
|
|
818
|
+
"Report reclaimable containers, data and registry entries (dry run; --apply to reclaim)",
|
|
819
|
+
},
|
|
478
820
|
],
|
|
479
821
|
examples: [
|
|
480
822
|
"gitzone services config --json",
|
|
823
|
+
"gitzone services status --json",
|
|
481
824
|
"gitzone services set mongodb,minio",
|
|
482
825
|
"gitzone services enable elasticsearch",
|
|
826
|
+
"gitzone services auth mongodb off",
|
|
827
|
+
"gitzone services prune",
|
|
828
|
+
"gitzone services prune --apply --yes",
|
|
483
829
|
],
|
|
484
830
|
});
|
|
485
831
|
return;
|
|
@@ -524,11 +870,32 @@ export function showHelp(mode?: ICliMode) {
|
|
|
524
870
|
" logs [service] Show logs (mongo|s3|elasticsearch|all) [lines]",
|
|
525
871
|
);
|
|
526
872
|
logger.log("info", " reconfigure Reassign ports and restart services");
|
|
527
|
-
logger.log(
|
|
873
|
+
logger.log(
|
|
874
|
+
"info",
|
|
875
|
+
" auth mongodb <on|off> Toggle MongoDB auth (off = loopback only) ⚠️",
|
|
876
|
+
);
|
|
877
|
+
logger.log(
|
|
878
|
+
"info",
|
|
879
|
+
" prune Report reclaimable data/containers machine-wide",
|
|
880
|
+
);
|
|
881
|
+
logger.log("info", " remove Remove all containers (keeps data)");
|
|
528
882
|
logger.log("info", " clean Remove all containers and data ⚠️");
|
|
529
883
|
logger.log("info", " help Show this help message");
|
|
530
884
|
console.log();
|
|
531
885
|
|
|
886
|
+
logger.log("note", "Cleanup levels (least to most destructive):");
|
|
887
|
+
logger.log("info", " stop Containers stay, data stays — fully resumable");
|
|
888
|
+
logger.log("info", " remove Containers removed, data kept — resumable");
|
|
889
|
+
logger.log(
|
|
890
|
+
"info",
|
|
891
|
+
" clean Containers and this project's data removed — irreversible, needs typed confirmation or --yes",
|
|
892
|
+
);
|
|
893
|
+
logger.log(
|
|
894
|
+
"info",
|
|
895
|
+
" prune Machine-wide reclamation of inactive/orphaned projects — dry run unless --apply",
|
|
896
|
+
);
|
|
897
|
+
console.log();
|
|
898
|
+
|
|
532
899
|
logger.log("note", "Available Services:");
|
|
533
900
|
logger.log("info", " • MongoDB (mongo) - Document database");
|
|
534
901
|
logger.log("info", " • MinIO (s3) - S3-compatible object storage");
|
|
@@ -604,6 +971,10 @@ export function showHelp(mode?: ICliMode) {
|
|
|
604
971
|
" stop -g Stop all containers across all projects",
|
|
605
972
|
);
|
|
606
973
|
logger.log("info", " cleanup -g Remove stale registry entries");
|
|
974
|
+
logger.log(
|
|
975
|
+
"info",
|
|
976
|
+
" prune Report/reclaim inactive project data (--stale-days N, --apply)",
|
|
977
|
+
);
|
|
607
978
|
console.log();
|
|
608
979
|
|
|
609
980
|
logger.log("note", "Global Examples:");
|
|
@@ -623,7 +994,11 @@ export function showHelp(mode?: ICliMode) {
|
|
|
623
994
|
|
|
624
995
|
// ==================== Global Command Handlers ====================
|
|
625
996
|
|
|
626
|
-
async function handleGlobalCommand(
|
|
997
|
+
async function handleGlobalCommand(
|
|
998
|
+
command: string,
|
|
999
|
+
argvArg: any,
|
|
1000
|
+
mode: ICliMode,
|
|
1001
|
+
) {
|
|
627
1002
|
const globalRegistry = GlobalRegistry.getInstance();
|
|
628
1003
|
|
|
629
1004
|
switch (command) {
|
|
@@ -639,6 +1014,10 @@ async function handleGlobalCommand(command: string) {
|
|
|
639
1014
|
await handleGlobalStop(globalRegistry);
|
|
640
1015
|
break;
|
|
641
1016
|
|
|
1017
|
+
case "prune":
|
|
1018
|
+
await handlePrune(argvArg, mode);
|
|
1019
|
+
break;
|
|
1020
|
+
|
|
642
1021
|
case "cleanup":
|
|
643
1022
|
await handleGlobalCleanup(globalRegistry);
|
|
644
1023
|
break;
|