feat(mod_services): Add global service registry and global commands for managing project containers

This commit is contained in:
2025-11-29 17:56:46 +00:00
parent ddf5023ecb
commit 847e679e92
7 changed files with 431 additions and 17 deletions

View File

@@ -1,5 +1,14 @@
# Changelog # 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) ## 2025-11-27 - 2.0.0 - BREAKING CHANGE(core)
Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules

View File

@@ -78,7 +78,7 @@
"@push.rocks/smartchok": "^1.1.1", "@push.rocks/smartchok": "^1.1.1",
"@push.rocks/smartcli": "^4.0.19", "@push.rocks/smartcli": "^4.0.19",
"@push.rocks/smartdiff": "^1.0.3", "@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/smartfs": "^1.1.0",
"@push.rocks/smartgulp": "^3.0.4", "@push.rocks/smartgulp": "^3.0.4",
"@push.rocks/smartjson": "^5.2.0", "@push.rocks/smartjson": "^5.2.0",

18
pnpm-lock.yaml generated
View File

@@ -42,8 +42,8 @@ importers:
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3 version: 1.0.3
'@push.rocks/smartfile': '@push.rocks/smartfile':
specifier: ^13.0.1 specifier: ^13.1.0
version: 13.0.1(@push.rocks/smartfs@1.1.0) version: 13.1.0
'@push.rocks/smartfs': '@push.rocks/smartfs':
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
@@ -1184,13 +1184,8 @@ packages:
'@push.rocks/smartfile@11.2.7': '@push.rocks/smartfile@11.2.7':
resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==} resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==}
'@push.rocks/smartfile@13.0.1': '@push.rocks/smartfile@13.1.0':
resolution: {integrity: sha512-phtryDFtBYHo7R2H9V3Y7VeiYQU9YzKL140gKD3bTicBgXoIYrJ6+b3mbZunSO2yQt1Vy1AxCxYXrFE/K+4grw==} resolution: {integrity: sha512-bSjH9vHl6l1nbe/gcSi4PcutFcTHUCVkMuQGGTVtn1cOgCuOXIHV04uhOXrZoKvlcSxxoiq8THolFt65lqn7cg==}
peerDependencies:
'@push.rocks/smartfs': ^1.0.0
peerDependenciesMeta:
'@push.rocks/smartfs':
optional: true
'@push.rocks/smartfm@2.2.2': '@push.rocks/smartfm@2.2.2':
resolution: {integrity: sha512-kLrBv/vWXJmB558LI5C79fWXLKOnno998vnp3opfB+uyznT2E6LkcpKsxdjwe1V/r+Z5GlhXPOWmGgHPCzUR6w==} resolution: {integrity: sha512-kLrBv/vWXJmB558LI5C79fWXLKOnno998vnp3opfB+uyznT2E6LkcpKsxdjwe1V/r+Z5GlhXPOWmGgHPCzUR6w==}
@@ -6878,11 +6873,12 @@ snapshots:
glob: 11.0.3 glob: 11.0.3
js-yaml: 4.1.0 js-yaml: 4.1.0
'@push.rocks/smartfile@13.0.1(@push.rocks/smartfs@1.1.0)': '@push.rocks/smartfile@13.1.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartfile-interfaces': 1.0.7 '@push.rocks/smartfile-interfaces': 1.0.7
'@push.rocks/smartfs': 1.1.0
'@push.rocks/smarthash': 3.2.6 '@push.rocks/smarthash': 3.2.6
'@push.rocks/smartjson': 5.2.0 '@push.rocks/smartjson': 5.2.0
'@push.rocks/smartmime': 2.0.4 '@push.rocks/smartmime': 2.0.4
@@ -6893,8 +6889,6 @@ snapshots:
'@types/js-yaml': 4.0.9 '@types/js-yaml': 4.0.9
glob: 11.0.3 glob: 11.0.3
js-yaml: 4.1.0 js-yaml: 4.1.0
optionalDependencies:
'@push.rocks/smartfs': 1.1.0
'@push.rocks/smartfm@2.2.2': '@push.rocks/smartfm@2.2.2':
dependencies: dependencies:

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/cli', 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.' 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.'
} }

View File

@@ -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<IGlobalRegistryData>;
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<IRegisteredProject, 'lastActive'>): Promise<void> {
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<void> {
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<void> {
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<boolean> {
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<string[]> {
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;
}
}

View File

@@ -2,16 +2,19 @@ import * as plugins from './mod.plugins.js';
import * as helpers from './helpers.js'; import * as helpers from './helpers.js';
import { ServiceConfiguration } from './classes.serviceconfiguration.js'; import { ServiceConfiguration } from './classes.serviceconfiguration.js';
import { DockerContainer } from './classes.dockercontainer.js'; import { DockerContainer } from './classes.dockercontainer.js';
import { GlobalRegistry } from './classes.globalregistry.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
export class ServiceManager { export class ServiceManager {
private config: ServiceConfiguration; private config: ServiceConfiguration;
private docker: DockerContainer; private docker: DockerContainer;
private enabledServices: string[] | null = null; private enabledServices: string[] | null = null;
private globalRegistry: GlobalRegistry;
constructor() { constructor() {
this.config = new ServiceConfiguration(); this.config = new ServiceConfiguration();
this.docker = new DockerContainer(); this.docker = new DockerContainer();
this.globalRegistry = GlobalRegistry.getInstance();
} }
/** /**
@@ -107,6 +110,31 @@ export class ServiceManager {
return this.enabledServices.includes(service); return this.enabledServices.includes(service);
} }
/**
* Register this project with the global registry
*/
private async registerWithGlobalRegistry(): Promise<void> {
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 * Start all enabled services
*/ */
@@ -127,6 +155,9 @@ export class ServiceManager {
await this.startElasticsearch(); await this.startElasticsearch();
first = false; first = false;
} }
// Register with global registry
await this.registerWithGlobalRegistry();
} }
/** /**
@@ -808,6 +839,15 @@ export class ServiceManager {
if (!removed) { if (!removed) {
logger.log('note', ' No containers to remove'); 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());
}
} }
/** /**

View File

@@ -1,13 +1,23 @@
import * as plugins from './mod.plugins.js'; import * as plugins from './mod.plugins.js';
import * as helpers from './helpers.js'; import * as helpers from './helpers.js';
import { ServiceManager } from './classes.servicemanager.js'; import { ServiceManager } from './classes.servicemanager.js';
import { GlobalRegistry } from './classes.globalregistry.js';
import { logger } from '../gitzone.logging.js'; import { logger } from '../gitzone.logging.js';
export const run = async (argvArg: any) => { 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(); const serviceManager = new ServiceManager();
await serviceManager.init(); await serviceManager.init();
const command = argvArg._[1] || 'help';
const service = argvArg._[2] || 'all'; const service = argvArg._[2] || 'all';
switch (command) { switch (command) {
@@ -249,4 +259,175 @@ function showHelp() {
logger.log('info', ' gitzone services config # Show configuration'); logger.log('info', ' gitzone services config # Show configuration');
logger.log('info', ' gitzone services compass # Get MongoDB Compass connection'); logger.log('info', ' gitzone services compass # Get MongoDB Compass connection');
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs'); 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}`);
}
} }