BREAKING CHANGE(core): Migrate filesystem to smartfs (async) and add Elasticsearch service support; refactor format/commit/meta modules

This commit is contained in:
2025-11-27 21:32:34 +00:00
parent 2f3d67f9e3
commit e1d28bc10a
30 changed files with 2217 additions and 995 deletions

View File

@@ -19,6 +19,11 @@ export interface IServiceConfig {
S3_BUCKET: string;
S3_ENDPOINT: string;
S3_USESSL: boolean;
ELASTICSEARCH_HOST: string;
ELASTICSEARCH_PORT: string;
ELASTICSEARCH_USER: string;
ELASTICSEARCH_PASS: string;
ELASTICSEARCH_URL: string;
}
export class ServiceConfiguration {
@@ -61,10 +66,10 @@ export class ServiceConfiguration {
* Save the configuration to file
*/
public async saveConfig(): Promise<void> {
await plugins.smartfile.memory.toFs(
JSON.stringify(this.config, null, 2),
this.configPath
);
await plugins.smartfs
.file(this.configPath)
.encoding('utf8')
.write(JSON.stringify(this.config, null, 2));
}
/**
@@ -72,21 +77,24 @@ export class ServiceConfiguration {
*/
private async ensureNogitDirectory(): Promise<void> {
const nogitPath = plugins.path.join(process.cwd(), '.nogit');
await plugins.smartfile.fs.ensureDir(nogitPath);
await plugins.smartfs.directory(nogitPath).recursive().create();
}
/**
* Check if configuration file exists
*/
private async configExists(): Promise<boolean> {
return plugins.smartfile.fs.fileExists(this.configPath);
return plugins.smartfs.file(this.configPath).exists();
}
/**
* Load configuration from file
*/
private async loadConfig(): Promise<void> {
const configContent = plugins.smartfile.fs.toStringSync(this.configPath);
const configContent = (await plugins.smartfs
.file(this.configPath)
.encoding('utf8')
.read()) as string;
this.config = JSON.parse(configContent);
}
@@ -94,16 +102,16 @@ export class ServiceConfiguration {
* Create default configuration
*/
private async createDefaultConfig(): Promise<void> {
const projectName = helpers.getProjectName();
const projectName = await helpers.getProjectName();
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++;
}
const mongoUser = 'defaultadmin';
const mongoPass = 'defaultpass';
const mongoHost = 'localhost';
@@ -111,7 +119,11 @@ export class ServiceConfiguration {
const mongoPortStr = mongoPort.toString();
const s3Host = 'localhost';
const s3PortStr = s3Port.toString();
const esHost = 'localhost';
const esPort = '9200';
const esUser = 'elastic';
const esPass = 'elastic';
this.config = {
PROJECT_NAME: projectName,
MONGODB_HOST: mongoHost,
@@ -127,22 +139,28 @@ export class ServiceConfiguration {
S3_SECRETKEY: 'defaultpass',
S3_BUCKET: `${projectName}-documents`,
S3_ENDPOINT: s3Host,
S3_USESSL: false
S3_USESSL: false,
ELASTICSEARCH_HOST: esHost,
ELASTICSEARCH_PORT: esPort,
ELASTICSEARCH_USER: esUser,
ELASTICSEARCH_PASS: esPass,
ELASTICSEARCH_URL: `http://${esUser}:${esPass}@${esHost}:${esPort}`
};
await this.saveConfig();
logger.log('ok', '✅ Created .nogit/env.json with project defaults');
logger.log('info', `📍 MongoDB port: ${mongoPort}`);
logger.log('info', `📍 S3 API port: ${s3Port}`);
logger.log('info', `📍 S3 Console port: ${s3ConsolePort}`);
logger.log('info', `📍 Elasticsearch port: ${esPort}`);
}
/**
* Update missing fields in existing configuration
*/
private async updateMissingFields(): Promise<void> {
const projectName = helpers.getProjectName();
const projectName = await helpers.getProjectName();
let updated = false;
const fieldsAdded: string[] = [];
@@ -249,7 +267,39 @@ export class ServiceConfiguration {
fieldsAdded.push('S3_ENDPOINT');
updated = true;
}
if (!this.config.ELASTICSEARCH_HOST) {
this.config.ELASTICSEARCH_HOST = 'localhost';
fieldsAdded.push('ELASTICSEARCH_HOST');
updated = true;
}
if (!this.config.ELASTICSEARCH_PORT) {
this.config.ELASTICSEARCH_PORT = '9200';
fieldsAdded.push('ELASTICSEARCH_PORT');
updated = true;
}
if (!this.config.ELASTICSEARCH_USER) {
this.config.ELASTICSEARCH_USER = 'elastic';
fieldsAdded.push('ELASTICSEARCH_USER');
updated = true;
}
if (!this.config.ELASTICSEARCH_PASS) {
this.config.ELASTICSEARCH_PASS = 'elastic';
fieldsAdded.push('ELASTICSEARCH_PASS');
updated = true;
}
// Always update ELASTICSEARCH_URL based on current settings
const oldEsUrl = this.config.ELASTICSEARCH_URL;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
if (oldEsUrl !== this.config.ELASTICSEARCH_URL) {
fieldsAdded.push('ELASTICSEARCH_URL');
updated = true;
}
if (updated) {
await this.saveConfig();
logger.log('ok', `✅ Added missing fields: ${fieldsAdded.join(', ')}`);
@@ -272,17 +322,19 @@ export class ServiceConfiguration {
public getContainerNames() {
return {
mongo: `${this.config.PROJECT_NAME}-mongodb`,
minio: `${this.config.PROJECT_NAME}-minio`
minio: `${this.config.PROJECT_NAME}-minio`,
elasticsearch: `${this.config.PROJECT_NAME}-elasticsearch`
};
}
/**
* Get data directories
*/
public getDataDirectories() {
return {
mongo: plugins.path.join(process.cwd(), '.nogit', 'mongodata'),
minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata')
minio: plugins.path.join(process.cwd(), '.nogit', 'miniodata'),
elasticsearch: plugins.path.join(process.cwd(), '.nogit', 'esdata')
};
}
@@ -330,12 +382,27 @@ export class ServiceConfiguration {
}
}
}
// Check Elasticsearch container
const esStatus = await this.docker.getStatus(containers.elasticsearch);
if (esStatus !== 'not_exists') {
const portMappings = await this.docker.getPortMappings(containers.elasticsearch);
if (portMappings && portMappings['9200']) {
const dockerPort = portMappings['9200'];
if (this.config.ELASTICSEARCH_PORT !== dockerPort) {
logger.log('note', `📍 Syncing Elasticsearch port from Docker: ${dockerPort}`);
this.config.ELASTICSEARCH_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`;
this.config.S3_ENDPOINT = this.config.S3_HOST;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
await this.saveConfig();
logger.log('ok', '✅ Configuration synced with Docker containers');
}
@@ -347,11 +414,12 @@ export class ServiceConfiguration {
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);
const esExists = await this.docker.exists(containers.elasticsearch);
// Only check port availability if containers don't exist
if (!mongoExists) {
const mongoPort = parseInt(this.config.MONGODB_PORT);
@@ -363,11 +431,11 @@ export class ServiceConfiguration {
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();
@@ -375,7 +443,7 @@ export class ServiceConfiguration {
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;
@@ -387,15 +455,27 @@ export class ServiceConfiguration {
updated = true;
}
}
if (!esExists) {
const esPort = parseInt(this.config.ELASTICSEARCH_PORT);
if (!(await helpers.isPortAvailable(esPort))) {
logger.log('note', `⚠️ Elasticsearch port ${esPort} is in use, finding new port...`);
const newPort = await helpers.getRandomAvailablePort();
this.config.ELASTICSEARCH_PORT = newPort.toString();
logger.log('ok', `✅ New Elasticsearch 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`;
this.config.S3_ENDPOINT = this.config.S3_HOST;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_PORT}`;
await this.saveConfig();
}
return updated;
}
@@ -404,29 +484,35 @@ export class ServiceConfiguration {
*/
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++;
}
// Elasticsearch uses standard port 9200
const esPort = '9200';
this.config.MONGODB_PORT = mongoPort.toString();
this.config.S3_PORT = s3Port.toString();
this.config.S3_CONSOLE_PORT = s3ConsolePort.toString();
this.config.ELASTICSEARCH_PORT = esPort;
// 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`;
this.config.S3_ENDPOINT = this.config.S3_HOST;
this.config.ELASTICSEARCH_URL = `http://${this.config.ELASTICSEARCH_USER}:${this.config.ELASTICSEARCH_PASS}@${this.config.ELASTICSEARCH_HOST}:${this.config.ELASTICSEARCH_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}`);
logger.log('info', ` 📍 Elasticsearch: ${esPort}`);
}
}

View File

@@ -7,12 +7,13 @@ import { logger } from '../gitzone.logging.js';
export class ServiceManager {
private config: ServiceConfiguration;
private docker: DockerContainer;
private enabledServices: string[] | null = null;
constructor() {
this.config = new ServiceConfiguration();
this.docker = new DockerContainer();
}
/**
* Initialize the service manager
*/
@@ -22,15 +23,134 @@ export class ServiceManager {
logger.log('error', 'Error: Docker is not installed. Please install Docker first.');
process.exit(1);
}
// Load or create configuration
await this.config.loadOrCreate();
logger.log('info', `📋 Project: ${this.config.getConfig().PROJECT_NAME}`);
// Load service selection from npmextra.json
await this.loadServiceConfiguration();
// Validate and update ports if needed
await this.config.validateAndUpdatePorts();
}
/**
* Load service configuration from npmextra.json
*/
private async loadServiceConfiguration(): Promise<void> {
const npmextraConfig = new plugins.npmextra.Npmextra(process.cwd());
const gitzoneConfig = npmextraConfig.dataFor<any>('gitzone', {});
// Check if services array exists
if (!gitzoneConfig.services || !Array.isArray(gitzoneConfig.services) || gitzoneConfig.services.length === 0) {
// Prompt user to select services
const smartinteract = new plugins.smartinteract.SmartInteract();
const response = await smartinteract.askQuestion({
name: 'services',
type: 'checkbox',
message: 'Which services do you want to enable for this project?',
choices: [
{ name: 'MongoDB', value: 'mongodb' },
{ name: 'MinIO (S3)', value: 'minio' },
{ name: 'Elasticsearch', value: 'elasticsearch' }
],
default: ['mongodb', 'minio', 'elasticsearch']
});
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
// Save to npmextra.json
await this.saveServiceConfiguration(this.enabledServices);
} else {
this.enabledServices = gitzoneConfig.services;
logger.log('info', `🔧 Enabled services: ${this.enabledServices.join(', ')}`);
}
}
/**
* Save service configuration to npmextra.json
*/
private async saveServiceConfiguration(services: string[]): Promise<void> {
const npmextraPath = plugins.path.join(process.cwd(), 'npmextra.json');
let npmextraData: any = {};
// Read existing npmextra.json if it exists
if (await plugins.smartfs.file(npmextraPath).exists()) {
const content = await plugins.smartfs.file(npmextraPath).encoding('utf8').read();
npmextraData = JSON.parse(content as string);
}
// Update gitzone.services
if (!npmextraData.gitzone) {
npmextraData.gitzone = {};
}
npmextraData.gitzone.services = services;
// Write back to npmextra.json
await plugins.smartfs
.file(npmextraPath)
.encoding('utf8')
.write(JSON.stringify(npmextraData, null, 2));
logger.log('ok', `✅ Saved service configuration to npmextra.json`);
logger.log('info', `🔧 Enabled services: ${services.join(', ')}`);
}
/**
* Check if a service is enabled
*/
private isServiceEnabled(service: string): boolean {
if (!this.enabledServices) {
return true; // If no configuration, enable all
}
return this.enabledServices.includes(service);
}
/**
* Start all enabled services
*/
public async startAll(): Promise<void> {
let first = true;
if (this.isServiceEnabled('mongodb')) {
if (!first) console.log();
await this.startMongoDB();
first = false;
}
if (this.isServiceEnabled('minio')) {
if (!first) console.log();
await this.startMinIO();
first = false;
}
if (this.isServiceEnabled('elasticsearch')) {
if (!first) console.log();
await this.startElasticsearch();
first = false;
}
}
/**
* Stop all enabled services
*/
public async stopAll(): Promise<void> {
let first = true;
if (this.isServiceEnabled('mongodb')) {
if (!first) console.log();
await this.stopMongoDB();
first = false;
}
if (this.isServiceEnabled('minio')) {
if (!first) console.log();
await this.stopMinIO();
first = false;
}
if (this.isServiceEnabled('elasticsearch')) {
if (!first) console.log();
await this.stopElasticsearch();
first = false;
}
}
/**
* Start MongoDB service
*/
@@ -42,7 +162,7 @@ export class ServiceManager {
const directories = this.config.getDataDirectories();
// Ensure data directory exists
await plugins.smartfile.fs.ensureDir(directories.mongo);
await plugins.smartfs.directory(directories.mongo).recursive().create();
const status = await this.docker.getStatus(containers.mongo);
@@ -141,7 +261,7 @@ export class ServiceManager {
const directories = this.config.getDataDirectories();
// Ensure data directory exists
await plugins.smartfile.fs.ensureDir(directories.minio);
await plugins.smartfs.directory(directories.minio).recursive().create();
const status = await this.docker.getStatus(containers.minio);
@@ -259,7 +379,103 @@ export class ServiceManager {
logger.log('info', ` API: http://${config.S3_HOST}:${config.S3_PORT}`);
logger.log('info', ` Console: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT} (login: ${config.S3_ACCESSKEY}/***)`);
}
/**
* Start Elasticsearch service
*/
public async startElasticsearch(): Promise<void> {
logger.log('note', '📦 Elasticsearch:');
const config = this.config.getConfig();
const containers = this.config.getContainerNames();
const directories = this.config.getDataDirectories();
// Ensure data directory exists
await plugins.smartfs.directory(directories.elasticsearch).recursive().create();
const status = await this.docker.getStatus(containers.elasticsearch);
switch (status) {
case 'running':
logger.log('ok', ' Already running ✓');
break;
case 'stopped':
// Check if port mapping matches config
const esPortMappings = await this.docker.getPortMappings(containers.elasticsearch);
if (esPortMappings && esPortMappings['9200'] !== config.ELASTICSEARCH_PORT) {
logger.log('note', ' Port configuration changed, recreating container...');
await this.docker.remove(containers.elasticsearch, true);
// Fall through to create new container
const success = await this.docker.run({
name: containers.elasticsearch,
image: 'elasticsearch:8.11.0',
ports: {
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
},
volumes: {
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
},
environment: {
'discovery.type': 'single-node',
'xpack.security.enabled': 'true',
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
},
restart: 'unless-stopped'
});
if (success) {
logger.log('ok', ' Recreated with new port ✓');
} else {
logger.log('error', ' Failed to recreate container');
}
} else {
// Ports match, just start the container
if (await this.docker.start(containers.elasticsearch)) {
logger.log('ok', ' Started ✓');
} else {
logger.log('error', ' Failed to start');
}
}
break;
case 'not_exists':
logger.log('note', ' Creating container...');
const success = await this.docker.run({
name: containers.elasticsearch,
image: 'elasticsearch:8.11.0',
ports: {
[`0.0.0.0:${config.ELASTICSEARCH_PORT}`]: '9200'
},
volumes: {
[directories.elasticsearch]: '/usr/share/elasticsearch/data'
},
environment: {
'discovery.type': 'single-node',
'xpack.security.enabled': 'true',
'ELASTIC_PASSWORD': config.ELASTICSEARCH_PASS,
'ES_JAVA_OPTS': '-Xms512m -Xmx512m'
},
restart: 'unless-stopped'
});
if (success) {
logger.log('ok', ' Created and started ✓');
} else {
logger.log('error', ' Failed to create container');
}
break;
}
logger.log('info', ` Container: ${containers.elasticsearch}`);
logger.log('info', ` Port: ${config.ELASTICSEARCH_PORT}`);
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
logger.log('info', ` Username: ${config.ELASTICSEARCH_USER}`);
logger.log('info', ` Password: ${config.ELASTICSEARCH_PASS}`);
}
/**
* Stop MongoDB service
*/
@@ -285,10 +501,10 @@ export class ServiceManager {
*/
public async stopMinIO(): Promise<void> {
logger.log('note', '📦 S3/MinIO:');
const containers = this.config.getContainerNames();
const status = await this.docker.getStatus(containers.minio);
if (status === 'running') {
if (await this.docker.stop(containers.minio)) {
logger.log('ok', ' Stopped ✓');
@@ -299,7 +515,27 @@ export class ServiceManager {
logger.log('note', ' Not running');
}
}
/**
* Stop Elasticsearch service
*/
public async stopElasticsearch(): Promise<void> {
logger.log('note', '📦 Elasticsearch:');
const containers = this.config.getContainerNames();
const status = await this.docker.getStatus(containers.elasticsearch);
if (status === 'running') {
if (await this.docker.stop(containers.elasticsearch)) {
logger.log('ok', ' Stopped ✓');
} else {
logger.log('error', ' Failed to stop');
}
} else {
logger.log('note', ' Not running');
}
}
/**
* Show service status
*/
@@ -385,8 +621,36 @@ export class ServiceManager {
}
break;
}
// Elasticsearch status
const esStatus = await this.docker.getStatus(containers.elasticsearch);
switch (esStatus) {
case 'running':
logger.log('ok', '📦 Elasticsearch: 🟢 Running');
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
logger.log('info', ` ├─ Port: ${config.ELASTICSEARCH_PORT}`);
logger.log('info', ` ├─ Connection: ${config.ELASTICSEARCH_URL}`);
logger.log('info', ` └─ Credentials: ${config.ELASTICSEARCH_USER}/${config.ELASTICSEARCH_PASS}`);
break;
case 'stopped':
logger.log('note', '📦 Elasticsearch: 🟡 Stopped');
logger.log('info', ` ├─ Container: ${containers.elasticsearch}`);
logger.log('info', ` └─ Port: ${config.ELASTICSEARCH_PORT}`);
break;
case 'not_exists':
logger.log('info', '📦 Elasticsearch: ⚪ Not installed');
// Check port availability
const esPort = parseInt(config.ELASTICSEARCH_PORT);
const esAvailable = await helpers.isPortAvailable(esPort);
if (!esAvailable) {
logger.log('error', ` └─ ⚠️ Port ${esPort} is in use by another process`);
} else {
logger.log('info', ` └─ Port ${esPort} is available`);
}
break;
}
}
/**
* Show configuration
*/
@@ -420,6 +684,15 @@ export class ServiceManager {
logger.log('info', ` Data: ${this.config.getDataDirectories().minio}`);
logger.log('info', ` Endpoint: ${config.S3_ENDPOINT}`);
logger.log('info', ` Console URL: http://${config.S3_HOST}:${config.S3_CONSOLE_PORT}`);
console.log();
logger.log('note', 'Elasticsearch:');
logger.log('info', ` Host: ${config.ELASTICSEARCH_HOST}:${config.ELASTICSEARCH_PORT}`);
logger.log('info', ` User: ${config.ELASTICSEARCH_USER}`);
logger.log('info', ' Password: ***');
logger.log('info', ` Container: ${this.config.getContainerNames().elasticsearch}`);
logger.log('info', ` Data: ${this.config.getDataDirectories().elasticsearch}`);
logger.log('info', ` Connection: ${config.ELASTICSEARCH_URL}`);
}
/**
@@ -477,16 +750,29 @@ export class ServiceManager {
logger.log('note', 'S3/MinIO container is not running');
}
break;
case 'elasticsearch':
case 'es':
if (await this.docker.isRunning(containers.elasticsearch)) {
helpers.printHeader(`Elasticsearch Logs (last ${lines} lines)`);
const logs = await this.docker.logs(containers.elasticsearch, lines);
console.log(logs);
} else {
logger.log('note', 'Elasticsearch container is not running');
}
break;
case 'all':
case '':
await this.showLogs('mongo', lines);
console.log();
await this.showLogs('minio', lines);
console.log();
await this.showLogs('elasticsearch', lines);
break;
default:
logger.log('note', 'Usage: gitzone services logs [mongo|s3|all] [lines]');
logger.log('note', 'Usage: gitzone services logs [mongo|s3|elasticsearch|all] [lines]');
break;
}
}
@@ -497,21 +783,28 @@ export class ServiceManager {
public async removeContainers(): Promise<void> {
const containers = this.config.getContainerNames();
let removed = false;
if (await this.docker.exists(containers.mongo)) {
if (await this.docker.remove(containers.mongo, true)) {
logger.log('ok', ' MongoDB container removed ✓');
removed = true;
}
}
if (await this.docker.exists(containers.minio)) {
if (await this.docker.remove(containers.minio, true)) {
logger.log('ok', ' S3/MinIO container removed ✓');
removed = true;
}
}
if (await this.docker.exists(containers.elasticsearch)) {
if (await this.docker.remove(containers.elasticsearch, true)) {
logger.log('ok', ' Elasticsearch container removed ✓');
removed = true;
}
}
if (!removed) {
logger.log('note', ' No containers to remove');
}
@@ -523,24 +816,60 @@ export class ServiceManager {
public async cleanData(): Promise<void> {
const directories = this.config.getDataDirectories();
let cleaned = false;
if (await plugins.smartfile.fs.fileExists(directories.mongo)) {
await plugins.smartfile.fs.remove(directories.mongo);
if (await plugins.smartfs.directory(directories.mongo).exists()) {
await plugins.smartfs.directory(directories.mongo).recursive().delete();
logger.log('ok', ' MongoDB data removed ✓');
cleaned = true;
}
if (await plugins.smartfile.fs.fileExists(directories.minio)) {
await plugins.smartfile.fs.remove(directories.minio);
if (await plugins.smartfs.directory(directories.minio).exists()) {
await plugins.smartfs.directory(directories.minio).recursive().delete();
logger.log('ok', ' S3/MinIO data removed ✓');
cleaned = true;
}
if (await plugins.smartfs.directory(directories.elasticsearch).exists()) {
await plugins.smartfs.directory(directories.elasticsearch).recursive().delete();
logger.log('ok', ' Elasticsearch data removed ✓');
cleaned = true;
}
if (!cleaned) {
logger.log('note', ' No data to clean');
}
}
/**
* Configure which services are enabled
*/
public async configureServices(): Promise<void> {
logger.log('note', 'Select which services to enable for this project:');
console.log();
const currentServices = this.enabledServices || ['mongodb', 'minio', 'elasticsearch'];
const smartinteract = new plugins.smartinteract.SmartInteract();
const response = await smartinteract.askQuestion({
name: 'services',
type: 'checkbox',
message: 'Which services do you want to enable?',
choices: [
{ name: 'MongoDB', value: 'mongodb' },
{ name: 'MinIO (S3)', value: 'minio' },
{ name: 'Elasticsearch', value: 'elasticsearch' }
],
default: currentServices
});
this.enabledServices = response.value || ['mongodb', 'minio', 'elasticsearch'];
// Save to npmextra.json
await this.saveServiceConfiguration(this.enabledServices);
logger.log('ok', '✅ Service configuration updated');
}
/**
* Reconfigure services with new ports
*/
@@ -551,20 +880,25 @@ export class ServiceManager {
// 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 ✓');
}
if (await this.docker.exists(containers.elasticsearch)) {
await this.docker.stop(containers.elasticsearch);
logger.log('ok', ' Elasticsearch 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({
@@ -573,11 +907,10 @@ export class ServiceManager {
message: 'Do you want to start services with new ports?',
default: true
});
if (response.value) {
console.log();
await this.startMongoDB();
await this.startMinIO();
await this.startAll();
}
}
}

View File

@@ -42,11 +42,15 @@ export const getRandomAvailablePort = async (): Promise<number> => {
/**
* Get the project name from package.json or directory
*/
export const getProjectName = (): string => {
export const getProjectName = async (): Promise<string> => {
try {
const packageJsonPath = plugins.path.join(process.cwd(), 'package.json');
if (plugins.smartfile.fs.fileExistsSync(packageJsonPath)) {
const packageJson = plugins.smartfile.fs.toObjectSync(packageJsonPath);
if (await plugins.smartfs.file(packageJsonPath).exists()) {
const content = (await plugins.smartfs
.file(packageJsonPath)
.encoding('utf8')
.read()) as string;
const packageJson = JSON.parse(content);
if (packageJson.name) {
// Sanitize: @fin.cx/skr → fin-cx-skr
return packageJson.name.replace(/@/g, '').replace(/[\/\.]/g, '-');
@@ -55,7 +59,7 @@ export const getProjectName = (): string => {
} catch (error) {
// Ignore errors and fall back to directory name
}
return plugins.path.basename(process.cwd());
};

View File

@@ -28,9 +28,13 @@ export const run = async (argvArg: any) => {
break;
case 'config':
await serviceManager.showConfig();
if (service === 'services' || argvArg._[2] === 'services') {
await handleConfigureServices(serviceManager);
} else {
await serviceManager.showConfig();
}
break;
case 'compass':
await serviceManager.showCompassConnection();
break;
@@ -61,63 +65,69 @@ export const run = async (argvArg: any) => {
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.startMongoDB();
console.log();
await serviceManager.startMinIO();
await serviceManager.startAll();
break;
default:
logger.log('error', `Unknown service: ${service}`);
logger.log('note', 'Use: mongo, s3, or all');
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.stopMongoDB();
console.log();
await serviceManager.stopMinIO();
await serviceManager.stopAll();
break;
default:
logger.log('error', `Unknown service: ${service}`);
logger.log('note', 'Use: mongo, s3, or all');
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':
@@ -125,24 +135,28 @@ async function handleRestart(serviceManager: ServiceManager, service: string) {
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.stopMongoDB();
await serviceManager.stopMinIO();
await serviceManager.stopAll();
await plugins.smartdelay.delayFor(2000);
await serviceManager.startMongoDB();
console.log();
await serviceManager.startMinIO();
await serviceManager.startAll();
break;
default:
logger.log('error', `Unknown service: ${service}`);
break;
@@ -166,7 +180,7 @@ 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',
@@ -174,7 +188,7 @@ async function handleClean(serviceManager: ServiceManager) {
message: 'Type "yes" to confirm:',
default: 'no'
});
if (confirmAnswer.value === 'yes') {
await serviceManager.removeContainers();
console.log();
@@ -185,40 +199,54 @@ async function handleClean(serviceManager: ServiceManager) {
}
}
async function handleConfigureServices(serviceManager: ServiceManager) {
helpers.printHeader('Configure Services');
await serviceManager.configureServices();
}
function showHelp() {
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|all)');
logger.log('info', ' stop [service] Stop services (mongo|s3|all)');
logger.log('info', ' restart [service] Restart services (mongo|s3|all)');
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', ' compass Show MongoDB Compass connection string');
logger.log('info', ' logs [service] Show logs (mongo|s3|all) [lines]');
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) to avoid conflicts');
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 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 compass # Get MongoDB Compass connection');
logger.log('info', ' gitzone services logs mongo 50 # Show last 50 lines of MongoDB logs');
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 compass # Get MongoDB Compass connection');
logger.log('info', ' gitzone services logs elasticsearch # Show Elasticsearch logs');
}