Files
cli/ts/mod_services/index.ts
T

801 lines
21 KiB
TypeScript

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<string, any> | null;
}> {
const smartconfigData = await readSmartconfigFile();
const enabledServices = getCliConfigValueFromData(
smartconfigData,
"services",
);
let environment: Record<string, any> | 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<void> {
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 <command> [options]",
commands: [
{
name: "config",
description:
"Show configured services and any existing runtime env.json data",
},
{
name: "set <csv>",
description: "Set the enabled service list without prompts",
},
{
name: "enable <service...>",
description: "Enable one or more services without prompts",
},
{
name: "disable <service...>",
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 <csv> Set enabled services without prompts",
);
logger.log("info", " enable <svc...> Enable one or more services");
logger.log("info", " disable <svc...> 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}`);
}
}