@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.
Files changed (38) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/gitzone.cli.js +11 -2
  3. package/dist_ts/mod_services/classes.dockercontainer.d.ts +81 -0
  4. package/dist_ts/mod_services/classes.dockercontainer.js +205 -10
  5. package/dist_ts/mod_services/classes.globalregistry.d.ts +10 -0
  6. package/dist_ts/mod_services/classes.globalregistry.js +23 -1
  7. package/dist_ts/mod_services/classes.serviceconfiguration.d.ts +52 -1
  8. package/dist_ts/mod_services/classes.serviceconfiguration.js +116 -20
  9. package/dist_ts/mod_services/classes.servicedatamarker.d.ts +93 -0
  10. package/dist_ts/mod_services/classes.servicedatamarker.js +166 -0
  11. package/dist_ts/mod_services/classes.servicemanager.d.ts +91 -5
  12. package/dist_ts/mod_services/classes.servicemanager.js +274 -64
  13. package/dist_ts/mod_services/classes.serviceoptions.d.ts +58 -0
  14. package/dist_ts/mod_services/classes.serviceoptions.js +93 -0
  15. package/dist_ts/mod_services/classes.servicepruner.d.ts +102 -0
  16. package/dist_ts/mod_services/classes.servicepruner.js +410 -0
  17. package/dist_ts/mod_services/helpers.d.ts +15 -0
  18. package/dist_ts/mod_services/helpers.js +57 -1
  19. package/dist_ts/mod_services/index.js +307 -53
  20. package/dist_ts/mod_tools/classes.packagemanager.d.ts +13 -0
  21. package/dist_ts/mod_tools/classes.packagemanager.js +42 -1
  22. package/dist_ts/mod_tools/index.js +4 -1
  23. package/package.json +3 -2
  24. package/readme.hints.md +105 -0
  25. package/readme.md +123 -5
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/gitzone.cli.ts +10 -0
  28. package/ts/mod_services/classes.dockercontainer.ts +244 -13
  29. package/ts/mod_services/classes.globalregistry.ts +27 -0
  30. package/ts/mod_services/classes.serviceconfiguration.ts +148 -27
  31. package/ts/mod_services/classes.servicedatamarker.ts +228 -0
  32. package/ts/mod_services/classes.servicemanager.ts +394 -72
  33. package/ts/mod_services/classes.serviceoptions.ts +135 -0
  34. package/ts/mod_services/classes.servicepruner.ts +532 -0
  35. package/ts/mod_services/helpers.ts +60 -0
  36. package/ts/mod_services/index.ts +437 -58
  37. package/ts/mod_tools/classes.packagemanager.ts +54 -0
  38. package/ts/mod_tools/index.ts +5 -0
@@ -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 { getCliMode, printJson } from "../helpers.climode.js";
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
- await serviceManager.init();
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
- ` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`,
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: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`,
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: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`,
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(serviceManager: ServiceManager, service: string) {
308
- helpers.printHeader("Starting Services");
309
-
310
- switch (service) {
311
- case "mongo":
312
- case "mongodb":
313
- await serviceManager.startMongoDB();
314
- break;
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
- case "minio":
317
- case "s3":
318
- await serviceManager.startMinIO();
319
- break;
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
- case "elasticsearch":
322
- case "es":
323
- await serviceManager.startElasticsearch();
324
- break;
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
- case "all":
327
- case "":
328
- await serviceManager.startAll();
329
- break;
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
- default:
332
- logger.log("error", `Unknown service: ${service}`);
333
- logger.log("note", "Use: mongo, s3, elasticsearch, or all");
334
- break;
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
- const shouldContinue =
412
- await plugins.smartinteract.SmartInteract.getCliConfirmation(
413
- "Continue?",
414
- false,
415
- );
416
-
417
- if (shouldContinue) {
418
- await serviceManager.removeContainers();
419
- } else {
420
- logger.log("note", "Cancelled");
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
- const smartinteraction = new plugins.smartinteract.SmartInteract();
430
- const confirmAnswer = await smartinteraction.askQuestion({
431
- name: "confirm",
432
- type: "input",
433
- message: 'Type "yes" to confirm:',
434
- default: "no",
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 (confirmAnswer.value === "yes") {
438
- await serviceManager.removeContainers();
439
- console.log();
440
- await serviceManager.cleanData();
441
- logger.log("ok", "All cleaned ✓");
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("info", " remove Remove all containers");
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(command: string) {
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;