feat(services): Add comprehensive development services management (v1.17.0)
- Implemented gitzone services command for managing MongoDB and MinIO containers - Added smart port assignment (20000-30000 range) to avoid conflicts - Project-specific container names for complete isolation - Data persistence in .nogit/ directories - MongoDB Compass connection string generation with network IP detection - Auto-configuration via .nogit/env.json with secure defaults - Commands: start, stop, restart, status, config, compass, logs, remove, clean - Interactive confirmations for destructive operations - Comprehensive documentation and Task Venture Capital GmbH legal update
This commit is contained in:
148
ts/mod_services/helpers.ts
Normal file
148
ts/mod_services/helpers.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import * as plugins from './mod.plugins.js';
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* Check if a port is available
|
||||
*/
|
||||
export const isPortAvailable = async (port: number): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once('error', () => {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
server.once('listening', () => {
|
||||
server.close();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
server.listen(port, '0.0.0.0');
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a random available port between 20000 and 30000
|
||||
*/
|
||||
export const getRandomAvailablePort = async (): Promise<number> => {
|
||||
const maxAttempts = 100;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const port = Math.floor(Math.random() * 10001) + 20000;
|
||||
if (await isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: let the system assign a port
|
||||
return 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the project name from package.json or directory
|
||||
*/
|
||||
export const getProjectName = (): 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 (packageJson.name) {
|
||||
// Sanitize: @fin.cx/skr → fin-cx-skr
|
||||
return packageJson.name.replace(/@/g, '').replace(/[\/\.]/g, '-');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors and fall back to directory name
|
||||
}
|
||||
|
||||
return plugins.path.basename(process.cwd());
|
||||
};
|
||||
|
||||
/**
|
||||
* Print colored message to console
|
||||
*/
|
||||
export const printMessage = (message: string, color?: 'green' | 'yellow' | 'red' | 'blue' | 'magenta' | 'cyan') => {
|
||||
const logger = new plugins.smartlog.ConsoleLog();
|
||||
|
||||
switch (color) {
|
||||
case 'green':
|
||||
logger.log('ok', message);
|
||||
break;
|
||||
case 'yellow':
|
||||
logger.log('note', message);
|
||||
break;
|
||||
case 'red':
|
||||
logger.log('error', message);
|
||||
break;
|
||||
case 'blue':
|
||||
case 'magenta':
|
||||
case 'cyan':
|
||||
logger.log('info', message);
|
||||
break;
|
||||
default:
|
||||
logger.log('info', message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Print a header with decorative lines
|
||||
*/
|
||||
export const printHeader = (title: string) => {
|
||||
console.log();
|
||||
printMessage('═══════════════════════════════════════════════════════════════', 'cyan');
|
||||
printMessage(` ${title}`, 'cyan');
|
||||
printMessage('═══════════════════════════════════════════════════════════════', 'cyan');
|
||||
console.log();
|
||||
};
|
||||
|
||||
/**
|
||||
* Format bytes to human readable string
|
||||
*/
|
||||
export const formatBytes = (bytes: number): string => {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the local network IP address
|
||||
*/
|
||||
export const getLocalNetworkIp = async (): Promise<string> => {
|
||||
const smartnetworkInstance = new plugins.smartnetwork.SmartNetwork();
|
||||
const gateways = await smartnetworkInstance.getGateways();
|
||||
|
||||
// Find the best local IP from network interfaces
|
||||
for (const interfaceName of Object.keys(gateways)) {
|
||||
const interfaces = gateways[interfaceName];
|
||||
for (const iface of interfaces) {
|
||||
// Skip loopback and internal interfaces
|
||||
if (!iface.internal && iface.family === 'IPv4') {
|
||||
const address = iface.address;
|
||||
// Prefer LAN IPs
|
||||
if (address.startsWith('192.168.') || address.startsWith('10.') || address.startsWith('172.')) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get any non-internal IPv4
|
||||
for (const interfaceName of Object.keys(gateways)) {
|
||||
const interfaces = gateways[interfaceName];
|
||||
for (const iface of interfaces) {
|
||||
if (!iface.internal && iface.family === 'IPv4') {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: localhost
|
||||
return 'localhost';
|
||||
};
|
Reference in New Issue
Block a user