feat(services): Add Docker port mapping sync and reconfigure workflow for local services
This commit is contained in:
		
							
								
								
									
										12
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								changelog.md
									
									
									
									
									
								
							| @@ -1,5 +1,17 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## 2025-08-16 - 1.18.0 - feat(services) | ||||
| Add Docker port mapping sync and reconfigure workflow for local services | ||||
|  | ||||
| - Add getPortMappings to DockerContainer to extract port bindings from docker inspect output | ||||
| - Sync existing container port mappings into .nogit/env.json when loading/creating service configuration | ||||
| - Validate and automatically update ports only when containers are not present; preserve container ports when containers exist | ||||
| - Recreate containers automatically if detected container port mappings differ from configuration (MongoDB and MinIO) | ||||
| - Add reconfigure method and new CLI command to reassign ports and optionally restart services | ||||
| - Improve status output to show configured ports and port availability information | ||||
| - Minor helpers and imports updated (DockerContainer injected into ServiceConfiguration) | ||||
| - Add .claude/settings.local.json (local permissions config) to repository | ||||
|  | ||||
| ## 2025-08-15 - 1.17.5 - fix(services) | ||||
| Update S3 credentials naming and add S3_ENDPOINT/S3_USESSL support for improved MinIO integration | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,6 @@ | ||||
|  */ | ||||
| export const commitinfo = { | ||||
|   name: '@git.zone/cli', | ||||
|   version: '1.17.5', | ||||
|   version: '1.18.0', | ||||
|   description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.' | ||||
| } | ||||
|   | ||||
| @@ -215,7 +215,7 @@ export class DockerContainer { | ||||
|    */ | ||||
|   public async inspect(containerName: string): Promise<any> { | ||||
|     try { | ||||
|       const result = await this.smartshell.exec(`docker inspect ${containerName}`); | ||||
|       const result = await this.smartshell.exec(`docker inspect ${containerName} 2>/dev/null`); | ||||
|       if (result.exitCode === 0) { | ||||
|         return JSON.parse(result.stdout); | ||||
|       } | ||||
| @@ -224,4 +224,38 @@ export class DockerContainer { | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Get port mappings for a container | ||||
|    */ | ||||
|   public async getPortMappings(containerName: string): Promise<{ [key: string]: string } | null> { | ||||
|     try { | ||||
|       // Use docker inspect without format to get full JSON, then extract PortBindings | ||||
|       const result = await this.smartshell.exec(`docker inspect ${containerName} 2>/dev/null`); | ||||
|        | ||||
|       if (result.exitCode === 0 && result.stdout) { | ||||
|         const inspectData = JSON.parse(result.stdout); | ||||
|         if (inspectData && inspectData[0] && inspectData[0].HostConfig && inspectData[0].HostConfig.PortBindings) { | ||||
|           const portBindings = inspectData[0].HostConfig.PortBindings; | ||||
|           const mappings: { [key: string]: string } = {}; | ||||
|            | ||||
|           // Convert Docker's port binding format to simple host:container mapping | ||||
|           for (const [containerPort, hostBindings] of Object.entries(portBindings)) { | ||||
|             if (Array.isArray(hostBindings) && hostBindings.length > 0) { | ||||
|               const hostPort = (hostBindings[0] as any).HostPort; | ||||
|               if (hostPort) { | ||||
|                 mappings[containerPort.replace('/tcp', '').replace('/udp', '')] = hostPort; | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|            | ||||
|           return mappings; | ||||
|         } | ||||
|       } | ||||
|       return null; | ||||
|     } catch (error) { | ||||
|       // Silently fail - container might not exist | ||||
|       return null; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +1,7 @@ | ||||
| import * as plugins from './mod.plugins.js'; | ||||
| import * as helpers from './helpers.js'; | ||||
| import { logger } from '../gitzone.logging.js'; | ||||
| import { DockerContainer } from './classes.dockercontainer.js'; | ||||
|  | ||||
| export interface IServiceConfig { | ||||
|   PROJECT_NAME: string; | ||||
| @@ -23,9 +24,11 @@ export interface IServiceConfig { | ||||
| export class ServiceConfiguration { | ||||
|   private configPath: string; | ||||
|   private config: IServiceConfig; | ||||
|   private docker: DockerContainer; | ||||
|    | ||||
|   constructor() { | ||||
|     this.configPath = plugins.path.join(process.cwd(), '.nogit', 'env.json'); | ||||
|     this.docker = new DockerContainer(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
| @@ -41,6 +44,9 @@ export class ServiceConfiguration { | ||||
|       await this.createDefaultConfig(); | ||||
|     } | ||||
|      | ||||
|     // Sync ports from existing Docker containers if they exist | ||||
|     await this.syncPortsFromDocker(); | ||||
|      | ||||
|     return this.config; | ||||
|   } | ||||
|    | ||||
| @@ -280,4 +286,147 @@ export class ServiceConfiguration { | ||||
|       minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata') | ||||
|     }; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Sync port configuration from existing Docker containers | ||||
|    */ | ||||
|   private async syncPortsFromDocker(): Promise<void> { | ||||
|     const containers = this.getContainerNames(); | ||||
|     let updated = false; | ||||
|      | ||||
|     // Check MongoDB container | ||||
|     const mongoStatus = await this.docker.getStatus(containers.mongo); | ||||
|     if (mongoStatus !== 'not_exists') { | ||||
|       const portMappings = await this.docker.getPortMappings(containers.mongo); | ||||
|       if (portMappings && portMappings['27017']) { | ||||
|         const dockerPort = portMappings['27017']; | ||||
|         if (this.config.MONGODB_PORT !== dockerPort) { | ||||
|           this.config.MONGODB_PORT = dockerPort; | ||||
|           updated = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     // Check MinIO container | ||||
|     const minioStatus = await this.docker.getStatus(containers.minio); | ||||
|     if (minioStatus !== 'not_exists') { | ||||
|       const portMappings = await this.docker.getPortMappings(containers.minio); | ||||
|       if (portMappings) { | ||||
|         if (portMappings['9000']) { | ||||
|           const dockerPort = portMappings['9000']; | ||||
|           if (this.config.S3_PORT !== dockerPort) { | ||||
|             this.config.S3_PORT = dockerPort; | ||||
|             updated = true; | ||||
|           } | ||||
|         } | ||||
|         if (portMappings['9001']) { | ||||
|           const dockerPort = portMappings['9001']; | ||||
|           if (this.config.S3_CONSOLE_PORT !== dockerPort) { | ||||
|             this.config.S3_CONSOLE_PORT = dockerPort; | ||||
|             updated = true; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     if (updated) { | ||||
|       // Update derived fields | ||||
|       this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`; | ||||
|       const protocol = this.config.S3_USESSL ? 'https' : 'http'; | ||||
|       this.config.S3_ENDPOINT = `${protocol}://${this.config.S3_HOST}:${this.config.S3_PORT}`; | ||||
|        | ||||
|       await this.saveConfig(); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Validate and update ports if they're not available | ||||
|    */ | ||||
|   public async validateAndUpdatePorts(): Promise<boolean> { | ||||
|     let updated = false; | ||||
|     const containers = this.getContainerNames(); | ||||
|      | ||||
|     // Check if containers exist - if they do, ports are fine | ||||
|     const mongoExists = await this.docker.exists(containers.mongo); | ||||
|     const minioExists = await this.docker.exists(containers.minio); | ||||
|      | ||||
|     // Only check port availability if containers don't exist | ||||
|     if (!mongoExists) { | ||||
|       const mongoPort = parseInt(this.config.MONGODB_PORT); | ||||
|       if (!(await helpers.isPortAvailable(mongoPort))) { | ||||
|         logger.log('note', `⚠️  MongoDB port ${mongoPort} is in use, finding new port...`); | ||||
|         const newPort = await helpers.getRandomAvailablePort(); | ||||
|         this.config.MONGODB_PORT = newPort.toString(); | ||||
|         logger.log('ok', `✅ New MongoDB port: ${newPort}`); | ||||
|         updated = true; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     if (!minioExists) { | ||||
|       const s3Port = parseInt(this.config.S3_PORT); | ||||
|       const s3ConsolePort = parseInt(this.config.S3_CONSOLE_PORT); | ||||
|        | ||||
|       if (!(await helpers.isPortAvailable(s3Port))) { | ||||
|         logger.log('note', `⚠️  S3 API port ${s3Port} is in use, finding new port...`); | ||||
|         const newPort = await helpers.getRandomAvailablePort(); | ||||
|         this.config.S3_PORT = newPort.toString(); | ||||
|         logger.log('ok', `✅ New S3 API port: ${newPort}`); | ||||
|         updated = true; | ||||
|       } | ||||
|        | ||||
|       if (!(await helpers.isPortAvailable(s3ConsolePort))) { | ||||
|         logger.log('note', `⚠️  S3 Console port ${s3ConsolePort} is in use, finding new port...`); | ||||
|         let newPort = parseInt(this.config.S3_PORT) + 1; | ||||
|         while (!(await helpers.isPortAvailable(newPort))) { | ||||
|           newPort++; | ||||
|         } | ||||
|         this.config.S3_CONSOLE_PORT = newPort.toString(); | ||||
|         logger.log('ok', `✅ New S3 Console port: ${newPort}`); | ||||
|         updated = true; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     if (updated) { | ||||
|       // Update derived fields | ||||
|       this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`; | ||||
|       const protocol = this.config.S3_USESSL ? 'https' : 'http'; | ||||
|       this.config.S3_ENDPOINT = `${protocol}://${this.config.S3_HOST}:${this.config.S3_PORT}`; | ||||
|        | ||||
|       await this.saveConfig(); | ||||
|     } | ||||
|      | ||||
|     return updated; | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Force reconfigure all ports with new available ones | ||||
|    */ | ||||
|   public async reconfigurePorts(): Promise<void> { | ||||
|     logger.log('note', '🔄 Finding new available ports...'); | ||||
|      | ||||
|     const mongoPort = await helpers.getRandomAvailablePort(); | ||||
|     const s3Port = await helpers.getRandomAvailablePort(); | ||||
|     let s3ConsolePort = s3Port + 1; | ||||
|      | ||||
|     // Ensure console port is also available | ||||
|     while (!(await helpers.isPortAvailable(s3ConsolePort))) { | ||||
|       s3ConsolePort++; | ||||
|     } | ||||
|      | ||||
|     this.config.MONGODB_PORT = mongoPort.toString(); | ||||
|     this.config.S3_PORT = s3Port.toString(); | ||||
|     this.config.S3_CONSOLE_PORT = s3ConsolePort.toString(); | ||||
|      | ||||
|     // Update derived fields | ||||
|     this.config.MONGODB_URL = `mongodb://${this.config.MONGODB_USER}:${this.config.MONGODB_PASS}@${this.config.MONGODB_HOST}:${this.config.MONGODB_PORT}/${this.config.MONGODB_NAME}?authSource=admin`; | ||||
|     const protocol = this.config.S3_USESSL ? 'https' : 'http'; | ||||
|     this.config.S3_ENDPOINT = `${protocol}://${this.config.S3_HOST}:${this.config.S3_PORT}`; | ||||
|      | ||||
|     await this.saveConfig(); | ||||
|      | ||||
|     logger.log('ok', '✅ New port configuration:'); | ||||
|     logger.log('info', `  📍 MongoDB: ${mongoPort}`); | ||||
|     logger.log('info', `  📍 S3 API: ${s3Port}`); | ||||
|     logger.log('info', `  📍 S3 Console: ${s3ConsolePort}`); | ||||
|   } | ||||
| } | ||||
| @@ -26,6 +26,9 @@ export class ServiceManager { | ||||
|     // Load or create configuration | ||||
|     await this.config.loadOrCreate(); | ||||
|     logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`); | ||||
|      | ||||
|     // Validate and update ports if needed | ||||
|     await this.config.validateAndUpdatePorts(); | ||||
|   } | ||||
|    | ||||
|   /** | ||||
| @@ -49,10 +52,42 @@ export class ServiceManager { | ||||
|         break; | ||||
|          | ||||
|       case 'stopped': | ||||
|         if (await this.docker.start(containers.mongo)) { | ||||
|           logger.log('ok', '  Started ✓'); | ||||
|         // Check if port mapping matches config | ||||
|         const mongoPortMappings = await this.docker.getPortMappings(containers.mongo); | ||||
|         if (mongoPortMappings && mongoPortMappings['27017'] !== config.MONGODB_PORT) { | ||||
|           logger.log('note', '  Port configuration changed, recreating container...'); | ||||
|           await this.docker.remove(containers.mongo, true); | ||||
|           // Fall through to create new container | ||||
|           const success = await this.docker.run({ | ||||
|             name: containers.mongo, | ||||
|             image: 'mongo:7.0', | ||||
|             ports: { | ||||
|               [`0.0.0.0:${config.MONGODB_PORT}`]: '27017' | ||||
|             }, | ||||
|             volumes: { | ||||
|               [directories.mongo]: '/data/db' | ||||
|             }, | ||||
|             environment: { | ||||
|               MONGO_INITDB_ROOT_USERNAME: config.MONGODB_USER, | ||||
|               MONGO_INITDB_ROOT_PASSWORD: config.MONGODB_PASS, | ||||
|               MONGO_INITDB_DATABASE: config.MONGODB_NAME | ||||
|             }, | ||||
|             restart: 'unless-stopped', | ||||
|             command: '--bind_ip_all' | ||||
|           }); | ||||
|            | ||||
|           if (success) { | ||||
|             logger.log('ok', '  Recreated with new port ✓'); | ||||
|           } else { | ||||
|             logger.log('error', '  Failed to recreate container'); | ||||
|           } | ||||
|         } else { | ||||
|           logger.log('error', '  Failed to start'); | ||||
|           // Ports match, just start the container | ||||
|           if (await this.docker.start(containers.mongo)) { | ||||
|             logger.log('ok', '  Started ✓'); | ||||
|           } else { | ||||
|             logger.log('error', '  Failed to start'); | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|          | ||||
| @@ -116,10 +151,60 @@ export class ServiceManager { | ||||
|         break; | ||||
|          | ||||
|       case 'stopped': | ||||
|         if (await this.docker.start(containers.minio)) { | ||||
|           logger.log('ok', '  Started ✓'); | ||||
|         // Check if port mapping matches config | ||||
|         const minioPortMappings = await this.docker.getPortMappings(containers.minio); | ||||
|         if (minioPortMappings &&  | ||||
|             (minioPortMappings['9000'] !== config.S3_PORT ||  | ||||
|              minioPortMappings['9001'] !== config.S3_CONSOLE_PORT)) { | ||||
|           logger.log('note', '  Port configuration changed, recreating container...'); | ||||
|           await this.docker.remove(containers.minio, true); | ||||
|           // Fall through to create new container | ||||
|           const success = await this.docker.run({ | ||||
|             name: containers.minio, | ||||
|             image: 'minio/minio', | ||||
|             ports: { | ||||
|               [config.S3_PORT]: '9000', | ||||
|               [config.S3_CONSOLE_PORT]: '9001' | ||||
|             }, | ||||
|             volumes: { | ||||
|               [directories.minio]: '/data' | ||||
|             }, | ||||
|             environment: { | ||||
|               MINIO_ROOT_USER: config.S3_ACCESSKEY, | ||||
|               MINIO_ROOT_PASSWORD: config.S3_SECRETKEY | ||||
|             }, | ||||
|             restart: 'unless-stopped', | ||||
|             command: 'server /data --console-address ":9001"' | ||||
|           }); | ||||
|            | ||||
|           if (success) { | ||||
|             logger.log('ok', '  Recreated with new ports ✓'); | ||||
|              | ||||
|             // Wait for MinIO to be ready | ||||
|             await plugins.smartdelay.delayFor(3000); | ||||
|              | ||||
|             // Create default bucket | ||||
|             await this.docker.exec( | ||||
|               containers.minio, | ||||
|               `mc alias set local http://localhost:9000 ${config.S3_ACCESSKEY} ${config.S3_SECRETKEY}` | ||||
|             ); | ||||
|              | ||||
|             await this.docker.exec( | ||||
|               containers.minio, | ||||
|               `mc mb local/${config.S3_BUCKET}` | ||||
|             ); | ||||
|              | ||||
|             logger.log('ok', `  Bucket '${config.S3_BUCKET}' created ✓`); | ||||
|           } else { | ||||
|             logger.log('error', '  Failed to recreate container'); | ||||
|           } | ||||
|         } else { | ||||
|           logger.log('error', '  Failed to start'); | ||||
|           // Ports match, just start the container | ||||
|           if (await this.docker.start(containers.minio)) { | ||||
|             logger.log('ok', '  Started ✓'); | ||||
|           } else { | ||||
|             logger.log('error', '  Failed to start'); | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|          | ||||
| @@ -233,6 +318,7 @@ export class ServiceManager { | ||||
|       case 'running': | ||||
|         logger.log('ok', '📦 MongoDB: 🟢 Running'); | ||||
|         logger.log('info', `   ├─ Container: ${containers.mongo}`); | ||||
|         logger.log('info', `   ├─ Port: ${config.MONGODB_PORT}`); | ||||
|         logger.log('info', `   ├─ Connection: ${this.config.getMongoConnectionString()}`); | ||||
|          | ||||
|         // Show Compass connection string | ||||
| @@ -242,10 +328,19 @@ export class ServiceManager { | ||||
|         break; | ||||
|       case 'stopped': | ||||
|         logger.log('note', '📦 MongoDB: 🟡 Stopped'); | ||||
|         logger.log('info', `   └─ Container: ${containers.mongo}`); | ||||
|         logger.log('info', `   ├─ Container: ${containers.mongo}`); | ||||
|         logger.log('info', `   └─ Port: ${config.MONGODB_PORT}`); | ||||
|         break; | ||||
|       case 'not_exists': | ||||
|         logger.log('info', '📦 MongoDB: ⚪ Not installed'); | ||||
|         // Check port availability | ||||
|         const mongoPort = parseInt(config.MONGODB_PORT); | ||||
|         const mongoAvailable = await helpers.isPortAvailable(mongoPort); | ||||
|         if (!mongoAvailable) { | ||||
|           logger.log('error', `   └─ ⚠️  Port ${mongoPort} is in use by another process`); | ||||
|         } else { | ||||
|           logger.log('info', `   └─ Port ${mongoPort} is available`); | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|      | ||||
| @@ -261,10 +356,33 @@ export class ServiceManager { | ||||
|         break; | ||||
|       case 'stopped': | ||||
|         logger.log('note', '📦 S3/MinIO: 🟡 Stopped'); | ||||
|         logger.log('info', `   └─ Container: ${containers.minio}`); | ||||
|         logger.log('info', `   ├─ Container: ${containers.minio}`); | ||||
|         logger.log('info', `   ├─ API Port: ${config.S3_PORT}`); | ||||
|         logger.log('info', `   └─ Console Port: ${config.S3_CONSOLE_PORT}`); | ||||
|         break; | ||||
|       case 'not_exists': | ||||
|         logger.log('info', '📦 S3/MinIO: ⚪ Not installed'); | ||||
|         // Check port availability | ||||
|         const s3Port = parseInt(config.S3_PORT); | ||||
|         const s3ConsolePort = parseInt(config.S3_CONSOLE_PORT); | ||||
|         const s3Available = await helpers.isPortAvailable(s3Port); | ||||
|         const consoleAvailable = await helpers.isPortAvailable(s3ConsolePort); | ||||
|          | ||||
|         if (!s3Available || !consoleAvailable) { | ||||
|           if (!s3Available) { | ||||
|             logger.log('error', `   ├─ ⚠️  API Port ${s3Port} is in use`); | ||||
|           } else { | ||||
|             logger.log('info', `   ├─ API Port ${s3Port} is available`); | ||||
|           } | ||||
|           if (!consoleAvailable) { | ||||
|             logger.log('error', `   └─ ⚠️  Console Port ${s3ConsolePort} is in use`); | ||||
|           } else { | ||||
|             logger.log('info', `   └─ Console Port ${s3ConsolePort} is available`); | ||||
|           } | ||||
|         } else { | ||||
|           logger.log('info', `   ├─ API Port ${s3Port} is available`); | ||||
|           logger.log('info', `   └─ Console Port ${s3ConsolePort} is available`); | ||||
|         } | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| @@ -422,4 +540,44 @@ export class ServiceManager { | ||||
|       logger.log('note', '  No data to clean'); | ||||
|     } | ||||
|   } | ||||
|    | ||||
|   /** | ||||
|    * Reconfigure services with new ports | ||||
|    */ | ||||
|   public async reconfigure(): Promise<void> { | ||||
|     helpers.printHeader('Reconfiguring Services'); | ||||
|      | ||||
|     const containers = this.config.getContainerNames(); | ||||
|      | ||||
|     // Stop existing containers | ||||
|     logger.log('note', '🛑 Stopping existing containers...'); | ||||
|      | ||||
|     if (await this.docker.exists(containers.mongo)) { | ||||
|       await this.docker.stop(containers.mongo); | ||||
|       logger.log('ok', '  MongoDB stopped ✓'); | ||||
|     } | ||||
|      | ||||
|     if (await this.docker.exists(containers.minio)) { | ||||
|       await this.docker.stop(containers.minio); | ||||
|       logger.log('ok', '  S3/MinIO stopped ✓'); | ||||
|     } | ||||
|      | ||||
|     // Reconfigure ports | ||||
|     await this.config.reconfigurePorts(); | ||||
|      | ||||
|     // Ask if user wants to restart services | ||||
|     const smartinteract = new plugins.smartinteract.SmartInteract(); | ||||
|     const response = await smartinteract.askQuestion({ | ||||
|       name: 'restart', | ||||
|       type: 'confirm', | ||||
|       message: 'Do you want to start services with new ports?', | ||||
|       default: true | ||||
|     }); | ||||
|      | ||||
|     if (response.value) { | ||||
|       console.log(); | ||||
|       await this.startMongoDB(); | ||||
|       await this.startMinIO(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -48,6 +48,10 @@ export const run = async (argvArg: any) => { | ||||
|       await handleClean(serviceManager); | ||||
|       break; | ||||
|        | ||||
|     case 'reconfigure': | ||||
|       await serviceManager.reconfigure(); | ||||
|       break; | ||||
|        | ||||
|     case 'help': | ||||
|     default: | ||||
|       showHelp(); | ||||
| @@ -195,6 +199,7 @@ function showHelp() { | ||||
|   logger.log('info', '  config            Show current configuration'); | ||||
|   logger.log('info', '  compass           Show MongoDB Compass connection string'); | ||||
|   logger.log('info', '  logs [service]    Show logs (mongo|s3|all) [lines]'); | ||||
|   logger.log('info', '  reconfigure       Reassign ports and restart services'); | ||||
|   logger.log('info', '  remove            Remove all containers'); | ||||
|   logger.log('info', '  clean             Remove all containers and data ⚠️'); | ||||
|   logger.log('info', '  help              Show this help message'); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user