From 847e679e929e1a8b9c60182abb1801487283fb1e Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 29 Nov 2025 17:56:46 +0000 Subject: [PATCH] feat(mod_services): Add global service registry and global commands for managing project containers --- changelog.md | 9 + package.json | 2 +- pnpm-lock.yaml | 18 +- ts/00_commitinfo_data.ts | 2 +- ts/mod_services/classes.globalregistry.ts | 190 ++++++++++++++++++++++ ts/mod_services/classes.servicemanager.ts | 40 +++++ ts/mod_services/index.ts | 187 ++++++++++++++++++++- 7 files changed, 431 insertions(+), 17 deletions(-) create mode 100644 ts/mod_services/classes.globalregistry.ts diff --git a/changelog.md b/changelog.md index b239228..edc7d23 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,14 @@ # Changelog +## 2025-11-29 - 2.1.0 - feat(mod_services) +Add global service registry and global commands for managing project containers + +- Introduce GlobalRegistry class to track registered projects, their containers, ports and last activity (ts/mod_services/classes.globalregistry.ts) +- Add global CLI mode for services (use -g/--global) with commands: list, status, stop, cleanup (ts/mod_services/index.ts) +- ServiceManager now registers the current project with the global registry when starting services and unregisters when all containers are removed (ts/mod_services/classes.servicemanager.ts) +- Global handlers to list projects, show aggregated status, stop containers across projects and cleanup stale entries +- Bump dependency @push.rocks/smartfile to ^13.1.0 in package.json + ## 2025-11-27 - 2.0.0 - BREAKING CHANGE(core) Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules diff --git a/package.json b/package.json index deea34f..3a3dd66 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "@push.rocks/smartchok": "^1.1.1", "@push.rocks/smartcli": "^4.0.19", "@push.rocks/smartdiff": "^1.0.3", - "@push.rocks/smartfile": "^13.0.1", + "@push.rocks/smartfile": "^13.1.0", "@push.rocks/smartfs": "^1.1.0", "@push.rocks/smartgulp": "^3.0.4", "@push.rocks/smartjson": "^5.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cb8177..1d47835 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@push.rocks/smartfile': - specifier: ^13.0.1 - version: 13.0.1(@push.rocks/smartfs@1.1.0) + specifier: ^13.1.0 + version: 13.1.0 '@push.rocks/smartfs': specifier: ^1.1.0 version: 1.1.0 @@ -1184,13 +1184,8 @@ packages: '@push.rocks/smartfile@11.2.7': resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==} - '@push.rocks/smartfile@13.0.1': - resolution: {integrity: sha512-phtryDFtBYHo7R2H9V3Y7VeiYQU9YzKL140gKD3bTicBgXoIYrJ6+b3mbZunSO2yQt1Vy1AxCxYXrFE/K+4grw==} - peerDependencies: - '@push.rocks/smartfs': ^1.0.0 - peerDependenciesMeta: - '@push.rocks/smartfs': - optional: true + '@push.rocks/smartfile@13.1.0': + resolution: {integrity: sha512-bSjH9vHl6l1nbe/gcSi4PcutFcTHUCVkMuQGGTVtn1cOgCuOXIHV04uhOXrZoKvlcSxxoiq8THolFt65lqn7cg==} '@push.rocks/smartfm@2.2.2': resolution: {integrity: sha512-kLrBv/vWXJmB558LI5C79fWXLKOnno998vnp3opfB+uyznT2E6LkcpKsxdjwe1V/r+Z5GlhXPOWmGgHPCzUR6w==} @@ -6878,11 +6873,12 @@ snapshots: glob: 11.0.3 js-yaml: 4.1.0 - '@push.rocks/smartfile@13.0.1(@push.rocks/smartfs@1.1.0)': + '@push.rocks/smartfile@13.1.0': dependencies: '@push.rocks/lik': 6.2.2 '@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartfile-interfaces': 1.0.7 + '@push.rocks/smartfs': 1.1.0 '@push.rocks/smarthash': 3.2.6 '@push.rocks/smartjson': 5.2.0 '@push.rocks/smartmime': 2.0.4 @@ -6893,8 +6889,6 @@ snapshots: '@types/js-yaml': 4.0.9 glob: 11.0.3 js-yaml: 4.1.0 - optionalDependencies: - '@push.rocks/smartfs': 1.1.0 '@push.rocks/smartfm@2.2.2': dependencies: diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 526db48..b3da7f3 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/cli', - version: '2.0.0', + version: '2.1.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.' } diff --git a/ts/mod_services/classes.globalregistry.ts b/ts/mod_services/classes.globalregistry.ts new file mode 100644 index 0000000..2171558 --- /dev/null +++ b/ts/mod_services/classes.globalregistry.ts @@ -0,0 +1,190 @@ +import * as plugins from '../plugins.js'; +import { DockerContainer } from './classes.dockercontainer.js'; +import { logger } from '../gitzone.logging.js'; + +export interface IRegisteredProject { + projectPath: string; + projectName: string; + containers: { + mongo?: string; + minio?: string; + elasticsearch?: string; + }; + ports: { + mongo?: number; + s3?: number; + s3Console?: number; + elasticsearch?: number; + }; + enabledServices: string[]; + lastActive: number; +} + +export interface IGlobalRegistryData { + projects: { [projectPath: string]: IRegisteredProject }; +} + +export class GlobalRegistry { + private static instance: GlobalRegistry | null = null; + private kvStore: plugins.npmextra.KeyValueStore; + private docker: DockerContainer; + + private constructor() { + this.kvStore = new plugins.npmextra.KeyValueStore({ + typeArg: 'userHomeDir', + identityArg: 'gitzone-services', + }); + this.docker = new DockerContainer(); + } + + /** + * Get the singleton instance + */ + public static getInstance(): GlobalRegistry { + if (!GlobalRegistry.instance) { + GlobalRegistry.instance = new GlobalRegistry(); + } + return GlobalRegistry.instance; + } + + /** + * Register or update a project in the global registry + */ + public async registerProject(data: Omit): Promise { + const allData = await this.kvStore.readAll(); + const projects = allData.projects || {}; + + projects[data.projectPath] = { + ...data, + lastActive: Date.now(), + }; + + await this.kvStore.writeKey('projects', projects); + } + + /** + * Remove a project from the registry + */ + public async unregisterProject(projectPath: string): Promise { + const allData = await this.kvStore.readAll(); + const projects = allData.projects || {}; + + if (projects[projectPath]) { + delete projects[projectPath]; + await this.kvStore.writeKey('projects', projects); + } + } + + /** + * Update the lastActive timestamp for a project + */ + public async touchProject(projectPath: string): Promise { + const allData = await this.kvStore.readAll(); + const projects = allData.projects || {}; + + if (projects[projectPath]) { + projects[projectPath].lastActive = Date.now(); + await this.kvStore.writeKey('projects', projects); + } + } + + /** + * Get all registered projects + */ + public async getAllProjects(): Promise<{ [path: string]: IRegisteredProject }> { + const allData = await this.kvStore.readAll(); + return allData.projects || {}; + } + + /** + * Check if a project is registered + */ + public async isRegistered(projectPath: string): Promise { + const projects = await this.getAllProjects(); + return !!projects[projectPath]; + } + + /** + * Get status of all containers across all registered projects + */ + public async getGlobalStatus(): Promise< + Array<{ + projectPath: string; + projectName: string; + containers: Array<{ name: string; status: string }>; + lastActive: number; + }> + > { + const projects = await this.getAllProjects(); + const result: Array<{ + projectPath: string; + projectName: string; + containers: Array<{ name: string; status: string }>; + lastActive: number; + }> = []; + + for (const [path, project] of Object.entries(projects)) { + const containerStatuses: Array<{ name: string; status: string }> = []; + + for (const containerName of Object.values(project.containers)) { + if (containerName) { + const status = await this.docker.getStatus(containerName); + containerStatuses.push({ name: containerName, status }); + } + } + + result.push({ + projectPath: path, + projectName: project.projectName, + containers: containerStatuses, + lastActive: project.lastActive, + }); + } + + return result; + } + + /** + * Stop all containers across all registered projects + */ + public async stopAll(): Promise<{ stopped: string[]; failed: string[] }> { + const projects = await this.getAllProjects(); + const stopped: string[] = []; + const failed: string[] = []; + + for (const project of Object.values(projects)) { + for (const containerName of Object.values(project.containers)) { + if (containerName) { + const status = await this.docker.getStatus(containerName); + if (status === 'running') { + if (await this.docker.stop(containerName)) { + stopped.push(containerName); + } else { + failed.push(containerName); + } + } + } + } + } + + return { stopped, failed }; + } + + /** + * Remove stale registry entries (projects that no longer exist on disk) + */ + public async cleanup(): Promise { + const projects = await this.getAllProjects(); + const removed: string[] = []; + + for (const projectPath of Object.keys(projects)) { + const exists = await plugins.smartfs.directory(projectPath).exists(); + if (!exists) { + await this.unregisterProject(projectPath); + removed.push(projectPath); + } + } + + return removed; + } +} diff --git a/ts/mod_services/classes.servicemanager.ts b/ts/mod_services/classes.servicemanager.ts index a4b889a..1625eb7 100644 --- a/ts/mod_services/classes.servicemanager.ts +++ b/ts/mod_services/classes.servicemanager.ts @@ -2,16 +2,19 @@ import * as plugins from './mod.plugins.js'; import * as helpers from './helpers.js'; import { ServiceConfiguration } from './classes.serviceconfiguration.js'; import { DockerContainer } from './classes.dockercontainer.js'; +import { GlobalRegistry } from './classes.globalregistry.js'; import { logger } from '../gitzone.logging.js'; export class ServiceManager { private config: ServiceConfiguration; private docker: DockerContainer; private enabledServices: string[] | null = null; + private globalRegistry: GlobalRegistry; constructor() { this.config = new ServiceConfiguration(); this.docker = new DockerContainer(); + this.globalRegistry = GlobalRegistry.getInstance(); } /** @@ -107,6 +110,31 @@ export class ServiceManager { return this.enabledServices.includes(service); } + /** + * Register this project with the global registry + */ + private async registerWithGlobalRegistry(): Promise { + const config = this.config.getConfig(); + const containers = this.config.getContainerNames(); + + await this.globalRegistry.registerProject({ + projectPath: process.cwd(), + projectName: config.PROJECT_NAME, + containers: { + mongo: containers.mongo, + minio: containers.minio, + elasticsearch: containers.elasticsearch, + }, + ports: { + mongo: parseInt(config.MONGODB_PORT), + s3: parseInt(config.S3_PORT), + s3Console: parseInt(config.S3_CONSOLE_PORT), + elasticsearch: parseInt(config.ELASTICSEARCH_PORT), + }, + enabledServices: this.enabledServices || ['mongodb', 'minio', 'elasticsearch'], + }); + } + /** * Start all enabled services */ @@ -127,6 +155,9 @@ export class ServiceManager { await this.startElasticsearch(); first = false; } + + // Register with global registry + await this.registerWithGlobalRegistry(); } /** @@ -808,6 +839,15 @@ export class ServiceManager { if (!removed) { logger.log('note', ' No containers to remove'); } + + // Check if all containers are gone, then unregister from global registry + const mongoExists = await this.docker.exists(containers.mongo); + const minioExists = await this.docker.exists(containers.minio); + const esExists = await this.docker.exists(containers.elasticsearch); + + if (!mongoExists && !minioExists && !esExists) { + await this.globalRegistry.unregisterProject(process.cwd()); + } } /** diff --git a/ts/mod_services/index.ts b/ts/mod_services/index.ts index 3b81b02..00a677c 100644 --- a/ts/mod_services/index.ts +++ b/ts/mod_services/index.ts @@ -1,15 +1,25 @@ 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'; export const run = async (argvArg: any) => { + const isGlobal = argvArg.g || argvArg.global; + const command = argvArg._[1] || 'help'; + + // Handle global commands first + if (isGlobal) { + await handleGlobalCommand(command); + return; + } + + // Local project commands const serviceManager = new ServiceManager(); await serviceManager.init(); - - const command = argvArg._[1] || 'help'; + const service = argvArg._[2] || 'all'; - + switch (command) { case 'start': await handleStart(serviceManager, service); @@ -249,4 +259,175 @@ function showHelp() { logger.log('info', ' gitzone services config # Show configuration'); 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}`); + } } \ No newline at end of file