import * as plugins from "./mod.plugins.js"; import * as helpers from "./helpers.js"; import { ServiceManager } from "./classes.servicemanager.js"; import { GlobalRegistry } from "./classes.globalregistry.js"; import { logger } from "../gitzone.logging.js"; import type { ICliMode } from "../helpers.climode.js"; import { getCliMode, printJson } from "../helpers.climode.js"; import { getCliConfigValueFromData, readSmartconfigFile, setCliConfigValueInData, writeSmartconfigFile, } from "../helpers.smartconfig.js"; export const run = async (argvArg: any) => { const mode = await getCliMode(argvArg); const isGlobal = argvArg.g || argvArg.global; const command = argvArg._[1] || "help"; if (mode.help || command === "help") { showHelp(mode); return; } // Handle global commands first if (isGlobal) { await handleGlobalCommand(command); return; } const service = argvArg._[2] || "all"; switch (command) { case "config": if (service === "services" || argvArg._[2] === "services") { const serviceManager = new ServiceManager(); await serviceManager.init(); await handleConfigureServices(serviceManager); } else { await handleShowConfig(mode); } break; case "set": await handleSetServices(argvArg._[2], mode); break; case "enable": await handleEnableServices(argvArg._.slice(2), mode); break; case "disable": await handleDisableServices(argvArg._.slice(2), mode); break; case "start": case "stop": case "restart": case "status": case "compass": case "logs": case "remove": case "clean": case "reconfigure": { const serviceManager = new ServiceManager(); await serviceManager.init(); switch (command) { case "start": await handleStart(serviceManager, service); break; case "stop": await handleStop(serviceManager, service); break; case "restart": await handleRestart(serviceManager, service); break; case "status": await serviceManager.showStatus(); break; case "compass": await serviceManager.showCompassConnection(); break; case "logs": { const lines = parseInt(argvArg._[3]) || 20; await serviceManager.showLogs(service, lines); break; } case "remove": await handleRemove(serviceManager); break; case "clean": await handleClean(serviceManager); break; case "reconfigure": await serviceManager.reconfigure(); break; } break; } default: showHelp(mode); break; } }; const allowedServices = ["mongodb", "minio", "elasticsearch"]; const normalizeServiceName = (service: string): string => { switch (service) { case "mongo": case "mongodb": return "mongodb"; case "minio": case "s3": return "minio"; case "elastic": case "elasticsearch": case "es": return "elasticsearch"; default: return service; } }; async function readServicesConfig(): Promise<{ enabledServices: string[]; environment: Record | null; }> { const smartconfigData = await readSmartconfigFile(); const enabledServices = getCliConfigValueFromData( smartconfigData, "services", ); let environment: Record | null = null; const envPath = plugins.path.join(process.cwd(), ".nogit", "env.json"); if (await plugins.smartfs.file(envPath).exists()) { const envContent = (await plugins.smartfs .file(envPath) .encoding("utf8") .read()) as string; environment = JSON.parse(envContent); } return { enabledServices: Array.isArray(enabledServices) ? enabledServices : [], environment, }; } async function updateEnabledServices(services: string[]): Promise { const smartconfigData = await readSmartconfigFile(); setCliConfigValueInData(smartconfigData, "services", services); await writeSmartconfigFile(smartconfigData); } async function handleShowConfig(mode: ICliMode) { const configData = await readServicesConfig(); if (mode.json) { printJson(configData); return; } helpers.printHeader("Current Services Configuration"); logger.log( "info", `Enabled Services: ${configData.enabledServices.length > 0 ? configData.enabledServices.join(", ") : "none configured"}`, ); console.log(); if (!configData.environment) { logger.log( "note", "No .nogit/env.json found yet. Start a service once to create runtime defaults.", ); return; } const env = configData.environment; logger.log("note", "MongoDB:"); logger.log("info", ` Host: ${env.MONGODB_HOST}:${env.MONGODB_PORT}`); logger.log("info", ` Database: ${env.MONGODB_NAME}`); logger.log("info", ` User: ${env.MONGODB_USER}`); logger.log("info", ` Container: ${env.PROJECT_NAME}-mongodb`); logger.log( "info", ` Data: ${plugins.path.join(process.cwd(), ".nogit", "mongodata")}`, ); logger.log("info", ` Connection: ${env.MONGODB_URL}`); console.log(); logger.log("note", "S3/MinIO:"); logger.log("info", ` Host: ${env.S3_HOST}`); logger.log("info", ` API Port: ${env.S3_PORT}`); logger.log("info", ` Console Port: ${env.S3_CONSOLE_PORT}`); logger.log("info", ` Bucket: ${env.S3_BUCKET}`); logger.log("info", ` Container: ${env.PROJECT_NAME}-minio`); logger.log( "info", ` Data: ${plugins.path.join(process.cwd(), ".nogit", "miniodata")}`, ); logger.log("info", ` Endpoint: ${env.S3_ENDPOINT}`); console.log(); logger.log("note", "Elasticsearch:"); logger.log( "info", ` Host: ${env.ELASTICSEARCH_HOST}:${env.ELASTICSEARCH_PORT}`, ); logger.log("info", ` User: ${env.ELASTICSEARCH_USER}`); logger.log("info", ` Container: ${env.PROJECT_NAME}-elasticsearch`); logger.log( "info", ` Data: ${plugins.path.join(process.cwd(), ".nogit", "esdata")}`, ); logger.log("info", ` Connection: ${env.ELASTICSEARCH_URL}`); } async function handleSetServices(rawValue: string | undefined, mode: ICliMode) { if (!rawValue) { throw new Error("Specify a comma-separated list of services"); } const requestedServices = rawValue .split(",") .map((service) => normalizeServiceName(service.trim())) .filter(Boolean); validateRequestedServices(requestedServices); await updateEnabledServices(requestedServices); if (mode.json) { printJson({ ok: true, action: "set", enabledServices: requestedServices }); return; } logger.log("ok", `Enabled services set to: ${requestedServices.join(", ")}`); } async function handleEnableServices( requestedServices: string[], mode: ICliMode, ) { const normalizedServices = requestedServices.map((service) => normalizeServiceName(service), ); validateRequestedServices(normalizedServices); const configData = await readServicesConfig(); const nextServices = Array.from( new Set([...configData.enabledServices, ...normalizedServices]), ); await updateEnabledServices(nextServices); if (mode.json) { printJson({ ok: true, action: "enable", enabledServices: nextServices }); return; } logger.log("ok", `Enabled services: ${nextServices.join(", ")}`); } async function handleDisableServices( requestedServices: string[], mode: ICliMode, ) { const normalizedServices = requestedServices.map((service) => normalizeServiceName(service), ); validateRequestedServices(normalizedServices); const configData = await readServicesConfig(); const nextServices = configData.enabledServices.filter( (service) => !normalizedServices.includes(service), ); await updateEnabledServices(nextServices); if (mode.json) { printJson({ ok: true, action: "disable", enabledServices: nextServices }); return; } logger.log("ok", `Enabled services: ${nextServices.join(", ")}`); } function validateRequestedServices(services: string[]): void { if (services.length === 0) { throw new Error("Specify at least one service"); } const invalidServices = services.filter( (service) => !allowedServices.includes(service), ); if (invalidServices.length > 0) { throw new Error(`Unknown service(s): ${invalidServices.join(", ")}`); } } async function handleStart(serviceManager: ServiceManager, service: string) { helpers.printHeader("Starting Services"); switch (service) { case "mongo": case "mongodb": await serviceManager.startMongoDB(); break; case "minio": case "s3": await serviceManager.startMinIO(); break; case "elasticsearch": case "es": await serviceManager.startElasticsearch(); break; case "all": case "": await serviceManager.startAll(); break; default: logger.log("error", `Unknown service: ${service}`); logger.log("note", "Use: mongo, s3, elasticsearch, or all"); break; } } async function handleStop(serviceManager: ServiceManager, service: string) { helpers.printHeader("Stopping Services"); switch (service) { case "mongo": case "mongodb": await serviceManager.stopMongoDB(); break; case "minio": case "s3": await serviceManager.stopMinIO(); break; case "elasticsearch": case "es": await serviceManager.stopElasticsearch(); break; case "all": case "": await serviceManager.stopAll(); break; default: logger.log("error", `Unknown service: ${service}`); logger.log("note", "Use: mongo, s3, elasticsearch, or all"); break; } } async function handleRestart(serviceManager: ServiceManager, service: string) { helpers.printHeader("Restarting Services"); switch (service) { case "mongo": case "mongodb": await serviceManager.stopMongoDB(); await plugins.smartdelay.delayFor(2000); await serviceManager.startMongoDB(); break; case "minio": case "s3": await serviceManager.stopMinIO(); await plugins.smartdelay.delayFor(2000); await serviceManager.startMinIO(); break; case "elasticsearch": case "es": await serviceManager.stopElasticsearch(); await plugins.smartdelay.delayFor(2000); await serviceManager.startElasticsearch(); break; case "all": case "": await serviceManager.stopAll(); await plugins.smartdelay.delayFor(2000); await serviceManager.startAll(); break; default: logger.log("error", `Unknown service: ${service}`); break; } } async function handleRemove(serviceManager: ServiceManager) { helpers.printHeader("Removing Containers"); logger.log("note", "⚠️ This will remove containers but preserve data"); const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation( "Continue?", false, ); if (shouldContinue) { await serviceManager.removeContainers(); } else { logger.log("note", "Cancelled"); } } async function handleClean(serviceManager: ServiceManager) { helpers.printHeader("Clean All"); logger.log("error", "⚠️ WARNING: This will remove all containers and data!"); logger.log("error", "This action cannot be undone!"); const smartinteraction = new plugins.smartinteract.SmartInteract(); const confirmAnswer = await smartinteraction.askQuestion({ name: "confirm", type: "input", message: 'Type "yes" to confirm:', default: "no", }); if (confirmAnswer.value === "yes") { await serviceManager.removeContainers(); console.log(); await serviceManager.cleanData(); logger.log("ok", "All cleaned ✓"); } else { logger.log("note", "Cancelled"); } } async function handleConfigureServices(serviceManager: ServiceManager) { helpers.printHeader("Configure Services"); await serviceManager.configureServices(); } export function showHelp(mode?: ICliMode) { if (mode?.json) { printJson({ command: "services", usage: "gitzone services [options]", commands: [ { name: "config", description: "Show configured services and any existing runtime env.json data", }, { name: "set ", description: "Set the enabled service list without prompts", }, { name: "enable ", description: "Enable one or more services without prompts", }, { name: "disable ", description: "Disable one or more services without prompts", }, { name: "start [service]", description: "Start services" }, { name: "stop [service]", description: "Stop services" }, { name: "status", description: "Show service status" }, ], examples: [ "gitzone services config --json", "gitzone services set mongodb,minio", "gitzone services enable elasticsearch", ], }); return; } helpers.printHeader("GitZone Services Manager"); logger.log("ok", "Usage: gitzone services [command] [options]"); console.log(); logger.log("note", "Commands:"); logger.log( "info", " start [service] Start services (mongo|s3|elasticsearch|all)", ); logger.log( "info", " stop [service] Stop services (mongo|s3|elasticsearch|all)", ); logger.log( "info", " restart [service] Restart services (mongo|s3|elasticsearch|all)", ); logger.log("info", " status Show service status"); logger.log("info", " config Show current configuration"); logger.log( "info", " config services Configure which services are enabled", ); logger.log( "info", " set Set enabled services without prompts", ); logger.log("info", " enable Enable one or more services"); logger.log("info", " disable Disable one or more services"); logger.log( "info", " compass Show MongoDB Compass connection string", ); logger.log( "info", " logs [service] Show logs (mongo|s3|elasticsearch|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"); console.log(); logger.log("note", "Available Services:"); logger.log("info", " • MongoDB (mongo) - Document database"); logger.log("info", " • MinIO (s3) - S3-compatible object storage"); logger.log( "info", " • Elasticsearch (elasticsearch) - Search and analytics engine", ); console.log(); logger.log("note", "Features:"); logger.log("info", " • Auto-creates .nogit/env.json with smart defaults"); logger.log( "info", " • Random ports (20000-30000) for MongoDB/MinIO to avoid conflicts", ); logger.log("info", " • Elasticsearch uses standard port 9200"); logger.log( "info", " • Project-specific containers for multi-project support", ); logger.log("info", " • Preserves custom configuration values"); logger.log("info", " • MongoDB Compass connection support"); console.log(); logger.log("note", "Examples:"); logger.log( "info", " gitzone services start # Start all services", ); logger.log( "info", " gitzone services start mongo # Start only MongoDB", ); logger.log( "info", " gitzone services start elasticsearch # Start only Elasticsearch", ); logger.log( "info", " gitzone services stop # Stop all services", ); logger.log( "info", " gitzone services status # Check service status", ); logger.log( "info", " gitzone services config # Show configuration", ); logger.log( "info", " gitzone services config --json # Show configuration as JSON", ); logger.log( "info", " gitzone services set mongodb,minio # Configure services without prompts", ); logger.log( "info", " gitzone services compass # Get MongoDB Compass connection", ); logger.log( "info", " gitzone services logs elasticsearch # Show Elasticsearch logs", ); console.log(); logger.log("note", "Global Commands (-g/--global):"); logger.log("info", " list -g List all registered projects"); logger.log("info", " status -g Show status across all projects"); logger.log( "info", " stop -g Stop all containers across all projects", ); logger.log("info", " cleanup -g Remove stale registry entries"); console.log(); logger.log("note", "Global Examples:"); logger.log( "info", " gitzone services list -g # List all registered projects", ); logger.log( "info", " gitzone services status -g # Show global container status", ); logger.log( "info", " gitzone services stop -g # Stop all (prompts for confirmation)", ); } // ==================== Global Command Handlers ==================== async function handleGlobalCommand(command: string) { const globalRegistry = GlobalRegistry.getInstance(); switch (command) { case "list": await handleGlobalList(globalRegistry); break; case "status": await handleGlobalStatus(globalRegistry); break; case "stop": await handleGlobalStop(globalRegistry); break; case "cleanup": await handleGlobalCleanup(globalRegistry); break; case "help": default: showHelp(); break; } } async function handleGlobalList(globalRegistry: GlobalRegistry) { helpers.printHeader("Registered Projects (Global)"); const projects = await globalRegistry.getAllProjects(); const projectPaths = Object.keys(projects); if (projectPaths.length === 0) { logger.log("note", "No projects registered"); return; } for (const path of projectPaths) { const project = projects[path]; const lastActive = new Date(project.lastActive).toLocaleString(); console.log(); logger.log("ok", `📁 ${project.projectName}`); logger.log("info", ` Path: ${project.projectPath}`); logger.log("info", ` Services: ${project.enabledServices.join(", ")}`); logger.log("info", ` Last Active: ${lastActive}`); } } async function handleGlobalStatus(globalRegistry: GlobalRegistry) { helpers.printHeader("Global Service Status"); const statuses = await globalRegistry.getGlobalStatus(); if (statuses.length === 0) { logger.log("note", "No projects registered"); return; } let runningCount = 0; let totalContainers = 0; for (const project of statuses) { console.log(); logger.log("ok", `📁 ${project.projectName}`); logger.log("info", ` Path: ${project.projectPath}`); if (project.containers.length === 0) { logger.log("note", " No containers configured"); continue; } for (const container of project.containers) { totalContainers++; const statusIcon = container.status === "running" ? "🟢" : container.status === "exited" ? "🟡" : "⚪"; if (container.status === "running") runningCount++; logger.log( "info", ` ${statusIcon} ${container.name}: ${container.status}`, ); } } console.log(); logger.log( "note", `Summary: ${runningCount}/${totalContainers} containers running across ${statuses.length} project(s)`, ); } async function handleGlobalStop(globalRegistry: GlobalRegistry) { helpers.printHeader("Stop All Containers (Global)"); const statuses = await globalRegistry.getGlobalStatus(); // Count running containers let runningCount = 0; for (const project of statuses) { for (const container of project.containers) { if (container.status === "running") runningCount++; } } if (runningCount === 0) { logger.log("note", "No running containers found"); return; } logger.log( "note", `Found ${runningCount} running container(s) across ${statuses.length} project(s)`, ); console.log(); // Show what will be stopped for (const project of statuses) { const runningContainers = project.containers.filter( (c) => c.status === "running", ); if (runningContainers.length > 0) { logger.log("info", `${project.projectName}:`); for (const container of runningContainers) { logger.log("info", ` • ${container.name}`); } } } console.log(); const shouldContinue = await plugins.smartinteract.SmartInteract.getCliConfirmation( "Stop all containers?", false, ); if (!shouldContinue) { logger.log("note", "Cancelled"); return; } logger.log("note", "Stopping all containers..."); const result = await globalRegistry.stopAll(); if (result.stopped.length > 0) { logger.log("ok", `Stopped: ${result.stopped.join(", ")}`); } if (result.failed.length > 0) { logger.log("error", `Failed to stop: ${result.failed.join(", ")}`); } } async function handleGlobalCleanup(globalRegistry: GlobalRegistry) { helpers.printHeader("Cleanup Registry (Global)"); logger.log("note", "Checking for stale registry entries..."); const removed = await globalRegistry.cleanup(); if (removed.length === 0) { logger.log("ok", "No stale entries found"); return; } logger.log( "ok", `Removed ${removed.length} stale entr${removed.length === 1 ? "y" : "ies"}:`, ); for (const path of removed) { logger.log("info", ` • ${path}`); } }