tspm/ts/cli.ts

371 lines
13 KiB
TypeScript

import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { Tspm, type IProcessConfig } from './classes.tspm.js';
import {
Logger,
LogLevel,
handleError,
TspmError,
ProcessError,
ConfigError,
ValidationError
} from './utils.errorhandler.js';
// Define interface for CLI arguments
interface CliArguments {
_: (string | number)[];
[key: string]: any;
}
export const run = async (): Promise<void> => {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
const tspm = new Tspm();
// Check if debug mode is enabled
const debugMode = process.env.TSPM_DEBUG === 'true';
if (debugMode) {
cliLogger.setLevel(LogLevel.DEBUG);
cliLogger.debug('Debug mode enabled');
}
const smartcliInstance = new plugins.smartcli.Smartcli();
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
// Default command - show help and list processes
smartcliInstance.standardCommand().subscribe({
next: async (argvArg: CliArguments) => {
console.log(`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`);
console.log('Usage: tspm [command] [options]');
console.log('\nCommands:');
console.log(' start <script> Start a process');
console.log(' startAsDaemon <script> Start a process in daemon mode');
console.log(' list List all processes');
console.log(' stop <id> Stop a process');
console.log(' restart <id> Restart a process');
console.log(' delete <id> Delete a process');
console.log(' describe <id> Show details for a process');
console.log('\nUse tspm [command] --help for more information about a command.');
// Show current process list
console.log('\nProcess List:');
const processes = tspm.list();
if (processes.length === 0) {
console.log(' No processes running. Use "tspm start" to start a process.');
} else {
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┐');
console.log('│ ID │ Name │ Status │ Memory │ Restarts │');
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┤');
for (const proc of processes) {
console.log(`${pad(proc.id, 8)}${pad(proc.id, 12)}${pad(proc.status, 10)}${pad(formatMemory(proc.memory), 10)}${pad(String(proc.restarts), 9)}`);
}
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┘');
}
},
});
// Start command - start a new process
smartcliInstance.addCommand('start').subscribe({
next: async (argvArg: CliArguments) => {
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!script) {
console.error('Error: Missing script argument. Usage: tspm start <script>');
return;
}
// Parse additional options
const name = argvArg.name || script;
const cwd = argvArg.cwd || process.cwd();
const memLimit = parseMemoryString(argvArg.memory || '500MB');
try {
cliLogger.debug(`Starting process with script: ${script}`);
const processConfig: IProcessConfig = {
id: argvArg.id || name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase(),
name: name,
projectDir: cwd,
command: script,
args: argvArg.args ? String(argvArg.args).split(' ') : undefined,
memoryLimitBytes: memLimit,
monitorIntervalMs: Number(argvArg.interval) || 5000,
autorestart: argvArg.autorestart !== 'false',
watch: Boolean(argvArg.watch)
};
cliLogger.debug(`Created process config: ${JSON.stringify(processConfig)}`);
await tspm.start(processConfig);
console.log(`Process ${processConfig.id} started successfully.`);
} catch (error: Error | unknown) {
const tspmError = handleError(error);
if (tspmError instanceof ValidationError) {
console.error(`Validation error: ${tspmError.message}`);
} else if (tspmError instanceof ProcessError) {
console.error(`Process error: ${tspmError.message}`);
if (debugMode) {
console.error(`Error details: ${JSON.stringify(tspmError.details)}`);
}
} else {
console.error(`Error starting process: ${tspmError.message}`);
}
cliLogger.error(tspmError);
}
},
});
// Start as daemon command
smartcliInstance.addCommand('startAsDaemon').subscribe({
next: async (argvArg: CliArguments) => {
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!script) {
console.error('Error: Missing script argument. Usage: tspm startAsDaemon <script>');
return;
}
// For daemon mode, we'll detach from the console
const daemonProcess = plugins.childProcess.spawn(
process.execPath,
[
...process.execArgv,
process.argv[1], // The tspm script path
'start',
script,
...process.argv.slice(3) // Pass other arguments
],
{
detached: true,
stdio: 'ignore',
cwd: process.cwd()
}
);
// Unref to allow parent to exit
daemonProcess.unref();
console.log(`Started process ${script} as daemon.`);
}
});
// Stop command
smartcliInstance.addCommand('stop').subscribe({
next: async (argvArg: CliArguments) => {
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!id) {
console.error('Error: Missing process ID. Usage: tspm stop <id>');
return;
}
try {
cliLogger.debug(`Stopping process: ${id}`);
await tspm.stop(id);
console.log(`Process ${id} stopped.`);
} catch (error: Error | unknown) {
const tspmError = handleError(error);
if (tspmError instanceof ValidationError) {
console.error(`Validation error: ${tspmError.message}`);
} else {
console.error(`Error stopping process: ${tspmError.message}`);
}
cliLogger.error(tspmError);
}
}
});
// Restart command
smartcliInstance.addCommand('restart').subscribe({
next: async (argvArg: CliArguments) => {
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!id) {
console.error('Error: Missing process ID. Usage: tspm restart <id>');
return;
}
try {
cliLogger.debug(`Restarting process: ${id}`);
await tspm.restart(id);
console.log(`Process ${id} restarted.`);
} catch (error: Error | unknown) {
const tspmError = handleError(error);
if (tspmError instanceof ValidationError) {
console.error(`Validation error: ${tspmError.message}`);
} else if (tspmError instanceof ProcessError) {
console.error(`Process error: ${tspmError.message}`);
} else {
console.error(`Error restarting process: ${tspmError.message}`);
}
cliLogger.error(tspmError);
}
}
});
// Delete command
smartcliInstance.addCommand('delete').subscribe({
next: async (argvArg: CliArguments) => {
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!id) {
console.error('Error: Missing process ID. Usage: tspm delete <id>');
return;
}
try {
cliLogger.debug(`Deleting process: ${id}`);
await tspm.delete(id);
console.log(`Process ${id} deleted.`);
} catch (error: Error | unknown) {
const tspmError = handleError(error);
if (tspmError instanceof ValidationError) {
console.error(`Validation error: ${tspmError.message}`);
} else if (tspmError instanceof ConfigError) {
console.error(`Configuration error: ${tspmError.message}`);
} else {
console.error(`Error deleting process: ${tspmError.message}`);
}
cliLogger.error(tspmError);
}
}
});
// List command
smartcliInstance.addCommand('list').subscribe({
next: async (argvArg: CliArguments) => {
const processes = tspm.list();
if (processes.length === 0) {
console.log('No processes running.');
return;
}
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┐');
console.log('│ ID │ Name │ Status │ Memory │ Restarts │');
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┤');
for (const proc of processes) {
console.log(`${pad(proc.id, 8)}${pad(proc.id, 12)}${pad(proc.status, 10)}${pad(formatMemory(proc.memory), 10)}${pad(String(proc.restarts), 9)}`);
}
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┘');
}
});
// Describe command
smartcliInstance.addCommand('describe').subscribe({
next: async (argvArg: CliArguments) => {
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!id) {
console.error('Error: Missing process ID. Usage: tspm describe <id>');
return;
}
const details = tspm.describe(id);
if (!details) {
console.error(`Process with ID '${id}' not found.`);
return;
}
console.log(`Details for process '${id}':`);
console.log(` Status: ${details.info.status}`);
console.log(` Memory: ${formatMemory(details.info.memory)}`);
console.log(` Restarts: ${details.info.restarts}`);
console.log(` Command: ${details.config.command}`);
console.log(` Directory: ${details.config.projectDir}`);
console.log(` Memory limit: ${formatMemory(details.config.memoryLimitBytes)}`);
if (details.config.args && details.config.args.length > 0) {
console.log(` Arguments: ${details.config.args.join(' ')}`);
}
}
});
// Logs command
smartcliInstance.addCommand('logs').subscribe({
next: async (argvArg: CliArguments) => {
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
if (!id) {
console.error('Error: Missing process ID. Usage: tspm logs <id>');
return;
}
const lines = Number(argvArg.lines || argvArg.n) || 20;
const logs = tspm.getLogs(id, lines);
if (logs.length === 0) {
console.log(`No logs found for process '${id}'.`);
return;
}
// Display logs with colors for different log types
for (const log of logs) {
const timestamp = log.timestamp.toISOString();
const prefix = `[${timestamp}] `;
switch (log.type) {
case 'stdout':
console.log(`${prefix}${log.message}`);
break;
case 'stderr':
console.error(`${prefix}${log.message}`);
break;
case 'system':
console.log(`${prefix}[SYSTEM] ${log.message}`);
break;
}
}
}
});
// Start parsing
smartcliInstance.startParse();
};
// Helper function to format memory usage
function formatMemory(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Helper function to parse memory strings like "500MB"
function parseMemoryString(memString: string): number {
const units = {
'B': 1,
'KB': 1024,
'MB': 1024 * 1024,
'GB': 1024 * 1024 * 1024,
'TB': 1024 * 1024 * 1024 * 1024
};
const match = memString.match(/^(\d+(?:\.\d+)?)\s*([KMGT]?B)$/i);
if (!match) {
throw new Error(`Invalid memory format: ${memString}. Use format like 500MB`);
}
const value = parseFloat(match[1]);
const unit = match[2].toUpperCase();
return value * units[unit];
}
// Helper function to pad strings for table display
function pad(str: string, length: number): string {
return str.padEnd(length);
}