update
This commit is contained in:
26
ts/cli/commands/batch/restart-all.ts
Normal file
26
ts/cli/commands/batch/restart-all.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerRestartAllCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'restart-all', async (_argvArg: CliArguments) => {
|
||||
console.log('Restarting all processes...');
|
||||
const response = await tspmIpcClient.request('restartAll', {});
|
||||
|
||||
if (response.restarted.length > 0) {
|
||||
console.log(`✓ Restarted ${response.restarted.length} processes:`);
|
||||
for (const id of response.restarted) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.failed.length > 0) {
|
||||
console.log(`✗ Failed to restart ${response.failed.length} processes:`);
|
||||
for (const failure of response.failed) {
|
||||
console.log(` - ${failure.id}: ${failure.error}`);
|
||||
}
|
||||
process.exitCode = 1; // Signal partial failure
|
||||
}
|
||||
}, { actionLabel: 'restart all processes' });
|
||||
}
|
26
ts/cli/commands/batch/start-all.ts
Normal file
26
ts/cli/commands/batch/start-all.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerStartAllCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'start-all', async (_argvArg: CliArguments) => {
|
||||
console.log('Starting all processes...');
|
||||
const response = await tspmIpcClient.request('startAll', {});
|
||||
|
||||
if (response.started.length > 0) {
|
||||
console.log(`✓ Started ${response.started.length} processes:`);
|
||||
for (const id of response.started) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.failed.length > 0) {
|
||||
console.log(`✗ Failed to start ${response.failed.length} processes:`);
|
||||
for (const failure of response.failed) {
|
||||
console.log(` - ${failure.id}: ${failure.error}`);
|
||||
}
|
||||
process.exitCode = 1; // Signal partial failure
|
||||
}
|
||||
}, { actionLabel: 'start all processes' });
|
||||
}
|
26
ts/cli/commands/batch/stop-all.ts
Normal file
26
ts/cli/commands/batch/stop-all.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerStopAllCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'stop-all', async (_argvArg: CliArguments) => {
|
||||
console.log('Stopping all processes...');
|
||||
const response = await tspmIpcClient.request('stopAll', {});
|
||||
|
||||
if (response.stopped.length > 0) {
|
||||
console.log(`✓ Stopped ${response.stopped.length} processes:`);
|
||||
for (const id of response.stopped) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.failed.length > 0) {
|
||||
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
|
||||
for (const failure of response.failed) {
|
||||
console.log(` - ${failure.id}: ${failure.error}`);
|
||||
}
|
||||
process.exitCode = 1; // Signal partial failure
|
||||
}
|
||||
}, { actionLabel: 'stop all processes' });
|
||||
}
|
138
ts/cli/commands/daemon/index.ts
Normal file
138
ts/cli/commands/daemon/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import * as paths from '../../../paths.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import { Logger } from '../../../utils.errorhandler.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { formatMemory } from '../../helpers/memory.js';
|
||||
|
||||
export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
const cliLogger = new Logger('CLI');
|
||||
|
||||
smartcli.addCommand('daemon').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
const subCommand = argvArg._[1];
|
||||
|
||||
switch (subCommand) {
|
||||
case 'start':
|
||||
try {
|
||||
const status = await tspmIpcClient.getDaemonStatus();
|
||||
if (status) {
|
||||
console.log('TSPM daemon is already running');
|
||||
console.log(` PID: ${status.pid}`);
|
||||
console.log(
|
||||
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
||||
);
|
||||
console.log(` Processes: ${status.processCount}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting TSPM daemon manually...');
|
||||
|
||||
// Import spawn to start daemon process
|
||||
const { spawn } = await import('child_process');
|
||||
const daemonScript = plugins.path.join(
|
||||
paths.packageDir,
|
||||
'dist_ts',
|
||||
'daemon.js',
|
||||
);
|
||||
|
||||
// Start daemon as a detached background process
|
||||
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: {
|
||||
...process.env,
|
||||
TSPM_DAEMON_MODE: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
// Detach the daemon so it continues running after CLI exits
|
||||
daemonProcess.unref();
|
||||
|
||||
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
|
||||
|
||||
// Wait for daemon to be ready
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const newStatus = await tspmIpcClient.getDaemonStatus();
|
||||
if (newStatus) {
|
||||
console.log('✓ TSPM daemon started successfully');
|
||||
console.log(` PID: ${newStatus.pid}`);
|
||||
console.log('\nNote: This daemon will run until you stop it or logout.');
|
||||
console.log('For automatic startup, use "tspm enable" instead.');
|
||||
}
|
||||
|
||||
// Disconnect from the daemon after starting
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch (error) {
|
||||
console.error('Error starting daemon:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'start-service':
|
||||
// This is called by systemd - start the daemon directly
|
||||
console.log('Starting TSPM daemon for systemd service...');
|
||||
const { startDaemon } = await import('../../../classes.daemon.js');
|
||||
await startDaemon();
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
try {
|
||||
console.log('Stopping TSPM daemon...');
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
console.log('✓ TSPM daemon stopped successfully');
|
||||
|
||||
// Disconnect from the daemon after stopping
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch (error) {
|
||||
console.error('Error stopping daemon:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
try {
|
||||
const status = await tspmIpcClient.getDaemonStatus();
|
||||
if (!status) {
|
||||
console.log('TSPM daemon is not running');
|
||||
console.log('Use "tspm daemon start" to start it');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('TSPM Daemon Status:');
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${status.status}`);
|
||||
console.log(`PID: ${status.pid}`);
|
||||
console.log(
|
||||
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
||||
);
|
||||
console.log(`Processes: ${status.processCount}`);
|
||||
console.log(
|
||||
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
|
||||
);
|
||||
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
|
||||
|
||||
// Disconnect from daemon after getting status
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch (error) {
|
||||
console.error('Error getting daemon status:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: tspm daemon <command>');
|
||||
console.log('\nCommands:');
|
||||
console.log(' start Start the TSPM daemon');
|
||||
console.log(' stop Stop the TSPM daemon');
|
||||
console.log(' status Show daemon status');
|
||||
break;
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
96
ts/cli/commands/default.ts
Normal file
96
ts/cli/commands/default.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import * as paths from '../../paths.js';
|
||||
import { tspmIpcClient } from '../../classes.ipcclient.js';
|
||||
import { Logger } from '../../utils.errorhandler.js';
|
||||
import type { CliArguments } from '../types.js';
|
||||
import { pad } from '../helpers/formatting.js';
|
||||
import { formatMemory } from '../helpers/memory.js';
|
||||
|
||||
export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
const cliLogger = new Logger('CLI');
|
||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
|
||||
smartcli.standardCommand().subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
console.log(
|
||||
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
|
||||
);
|
||||
console.log('Usage: tspm [command] [options]');
|
||||
console.log('\nService Management:');
|
||||
console.log(' enable Enable TSPM as system service (systemd)');
|
||||
console.log(' disable Disable TSPM system service');
|
||||
console.log('\nProcess Commands:');
|
||||
console.log(' start <script> Start a process');
|
||||
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(' logs <id> Show logs for a process');
|
||||
console.log(' start-all Start all saved processes');
|
||||
console.log(' stop-all Stop all processes');
|
||||
console.log(' restart-all Restart all processes');
|
||||
console.log('\nDaemon Commands:');
|
||||
console.log(' daemon start Start daemon manually (current session)');
|
||||
console.log(' daemon stop Stop the daemon');
|
||||
console.log(' daemon status Show daemon status');
|
||||
console.log(
|
||||
'\nUse tspm [command] --help for more information about a command.',
|
||||
);
|
||||
|
||||
// Show current process list
|
||||
console.log('\nProcess List:');
|
||||
|
||||
try {
|
||||
const response = await tspmIpcClient.request('list', {});
|
||||
const processes = response.processes;
|
||||
|
||||
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) {
|
||||
const statusColor =
|
||||
proc.status === 'online'
|
||||
? '\x1b[32m'
|
||||
: proc.status === 'errored'
|
||||
? '\x1b[31m'
|
||||
: '\x1b[33m';
|
||||
const resetColor = '\x1b[0m';
|
||||
|
||||
console.log(
|
||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
|
||||
);
|
||||
}
|
||||
|
||||
// Disconnect from daemon after getting list
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch (error) {
|
||||
console.error('Error: TSPM daemon is not running.');
|
||||
console.log('\nTo start the daemon, run one of:');
|
||||
console.log(' tspm daemon start - Start for this session only');
|
||||
console.log(' tspm enable - Enable as system service (recommended)');
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
24
ts/cli/commands/process/delete.ts
Normal file
24
ts/cli/commands/process/delete.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'delete', async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm delete <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Deleting process: ${id}`);
|
||||
const response = await tspmIpcClient.request('delete', { id });
|
||||
|
||||
if (response.success) {
|
||||
console.log(`✓ ${response.message}`);
|
||||
} else {
|
||||
console.error(`✗ Failed to delete process: ${response.message}`);
|
||||
}
|
||||
}, { actionLabel: 'delete process' });
|
||||
}
|
38
ts/cli/commands/process/describe.ts
Normal file
38
ts/cli/commands/process/describe.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
import { formatMemory } from '../../helpers/memory.js';
|
||||
|
||||
export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'describe', async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm describe <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await tspmIpcClient.request('describe', { id });
|
||||
|
||||
console.log(`Process Details: ${id}`);
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${response.processInfo.status}`);
|
||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||
console.log(`Memory: ${formatMemory(response.processInfo.memory)}`);
|
||||
console.log(`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`);
|
||||
console.log(`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`);
|
||||
console.log(`Restarts: ${response.processInfo.restarts}`);
|
||||
console.log('\nConfiguration:');
|
||||
console.log(`Command: ${response.config.command}`);
|
||||
console.log(`Directory: ${response.config.projectDir}`);
|
||||
console.log(`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`);
|
||||
console.log(`Auto-restart: ${response.config.autorestart}`);
|
||||
if (response.config.watch) {
|
||||
console.log(`Watch: enabled`);
|
||||
if (response.config.watchPaths) {
|
||||
console.log(`Watch Paths: ${response.config.watchPaths.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}, { actionLabel: 'describe process' });
|
||||
}
|
37
ts/cli/commands/process/list.ts
Normal file
37
ts/cli/commands/process/list.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
import { pad } from '../../helpers/formatting.js';
|
||||
import { formatMemory } from '../../helpers/memory.js';
|
||||
|
||||
export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'list', async (_argvArg: CliArguments) => {
|
||||
const response = await tspmIpcClient.request('list', {});
|
||||
const processes = response.processes;
|
||||
|
||||
if (processes.length === 0) {
|
||||
console.log('No processes running.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Process List:');
|
||||
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐');
|
||||
console.log('│ ID │ Name │ Status │ PID │ Memory │ Restarts │');
|
||||
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤');
|
||||
|
||||
for (const proc of processes) {
|
||||
const statusColor =
|
||||
proc.status === 'online' ? '\x1b[32m' :
|
||||
proc.status === 'errored' ? '\x1b[31m' :
|
||||
'\x1b[33m';
|
||||
const resetColor = '\x1b[0m';
|
||||
|
||||
console.log(
|
||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘');
|
||||
}, { actionLabel: 'list processes' });
|
||||
}
|
73
ts/cli/commands/process/logs.ts
Normal file
73
ts/cli/commands/process/logs.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
import { getBool, getNumber } from '../../helpers/argv.js';
|
||||
import { formatLog } from '../../helpers/formatting.js';
|
||||
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
|
||||
|
||||
export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'logs', async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm logs <id> [options]');
|
||||
console.log('\nOptions:');
|
||||
console.log(' --lines <n> Number of lines to show (default: 50)');
|
||||
console.log(' --follow Stream logs in real-time (like tail -f)');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = getNumber(argvArg, 'lines', 50);
|
||||
const follow = getBool(argvArg, 'follow', 'f');
|
||||
|
||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||
|
||||
if (!follow) {
|
||||
// One-shot mode - auto-disconnect handled by registerIpcCommand
|
||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||
console.log('─'.repeat(60));
|
||||
for (const log of response.logs) {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Streaming mode
|
||||
console.log(`Logs for process: ${id} (streaming...)`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
let lastSeq = 0;
|
||||
for (const log of response.logs) {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
|
||||
}
|
||||
|
||||
await withStreamingLifecycle(
|
||||
async () => {
|
||||
await tspmIpcClient.subscribe(id, (log: any) => {
|
||||
if (log.seq !== undefined && log.seq <= lastSeq) return;
|
||||
if (log.seq !== undefined && log.seq > lastSeq + 1) {
|
||||
console.log(`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`);
|
||||
}
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
if (log.seq !== undefined) lastSeq = log.seq;
|
||||
});
|
||||
},
|
||||
async () => {
|
||||
console.log('\n\nStopping log stream...');
|
||||
try { await tspmIpcClient.unsubscribe(id); } catch {}
|
||||
try { await tspmIpcClient.disconnect(); } catch {}
|
||||
}
|
||||
);
|
||||
}, {
|
||||
actionLabel: 'get logs',
|
||||
keepAlive: (argv) => getBool(argv, 'follow', 'f')
|
||||
});
|
||||
}
|
23
ts/cli/commands/process/restart.ts
Normal file
23
ts/cli/commands/process/restart.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'restart', async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm restart <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Restarting process: ${id}`);
|
||||
const response = await tspmIpcClient.request('restart', { id });
|
||||
|
||||
console.log(`✓ Process restarted successfully`);
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
}, { actionLabel: 'restart process' });
|
||||
}
|
83
ts/cli/commands/process/start.ts
Normal file
83
ts/cli/commands/process/start.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { IProcessConfig } from '../../../classes.tspm.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'start', async (argvArg: CliArguments) => {
|
||||
const script = argvArg._[1];
|
||||
if (!script) {
|
||||
console.error('Error: Please provide a script to run');
|
||||
console.log('Usage: tspm start <script> [options]');
|
||||
console.log('\nOptions:');
|
||||
console.log(' --name <name> Name for the process');
|
||||
console.log(' --memory <size> Memory limit (e.g., "512MB", "2GB")');
|
||||
console.log(' --cwd <path> Working directory');
|
||||
console.log(' --watch Watch for file changes and restart');
|
||||
console.log(' --watch-paths <paths> Comma-separated paths to watch');
|
||||
console.log(' --autorestart Auto-restart on crash');
|
||||
return;
|
||||
}
|
||||
|
||||
const memoryLimit = argvArg.memory ? parseMemoryString(argvArg.memory) : 512 * 1024 * 1024;
|
||||
const projectDir = argvArg.cwd || process.cwd();
|
||||
|
||||
// Direct .ts support via tsx (bundled with TSPM)
|
||||
let actualCommand = script;
|
||||
let commandArgs: string[] | undefined = undefined;
|
||||
|
||||
if (script.endsWith('.ts')) {
|
||||
try {
|
||||
const tsxPath = await (async () => {
|
||||
const { createRequire } = await import('module');
|
||||
const require = createRequire(import.meta.url);
|
||||
return require.resolve('tsx/dist/cli.mjs');
|
||||
})();
|
||||
|
||||
const scriptPath = plugins.path.isAbsolute(script) ? script : plugins.path.join(projectDir, script);
|
||||
actualCommand = tsxPath;
|
||||
commandArgs = [scriptPath];
|
||||
} catch {
|
||||
actualCommand = 'tsx';
|
||||
commandArgs = [script];
|
||||
}
|
||||
}
|
||||
|
||||
const name = argvArg.name || script;
|
||||
const watch = argvArg.watch || false;
|
||||
const autorestart = argvArg.autorestart !== false; // default true
|
||||
const watchPaths = argvArg.watchPaths
|
||||
? (typeof argvArg.watchPaths === 'string' ? (argvArg.watchPaths as string).split(',') : argvArg.watchPaths)
|
||||
: undefined;
|
||||
|
||||
const processConfig: IProcessConfig = {
|
||||
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
|
||||
name,
|
||||
command: actualCommand,
|
||||
args: commandArgs,
|
||||
projectDir,
|
||||
memoryLimitBytes: memoryLimit,
|
||||
autorestart,
|
||||
watch,
|
||||
watchPaths,
|
||||
};
|
||||
|
||||
console.log(`Starting process: ${name}`);
|
||||
console.log(` Command: ${script}${script.endsWith('.ts') ? ' (via tsx)' : ''}`);
|
||||
console.log(` Directory: ${projectDir}`);
|
||||
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
||||
console.log(` Auto-restart: ${autorestart}`);
|
||||
if (watch) {
|
||||
console.log(` Watch mode: enabled`);
|
||||
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(', ')}`);
|
||||
}
|
||||
|
||||
const response = await tspmIpcClient.request('start', { config: processConfig });
|
||||
console.log(`✓ Process started successfully`);
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
}, { actionLabel: 'start process' });
|
||||
}
|
24
ts/cli/commands/process/stop.ts
Normal file
24
ts/cli/commands/process/stop.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../classes.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(smartcli, 'stop', async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm stop <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Stopping process: ${id}`);
|
||||
const response = await tspmIpcClient.request('stop', { id });
|
||||
|
||||
if (response.success) {
|
||||
console.log(`✓ ${response.message}`);
|
||||
} else {
|
||||
console.error(`✗ Failed to stop process: ${response.message}`);
|
||||
}
|
||||
}, { actionLabel: 'stop process' });
|
||||
}
|
33
ts/cli/commands/service/disable.ts
Normal file
33
ts/cli/commands/service/disable.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { TspmServiceManager } from '../../../classes.servicemanager.js';
|
||||
import { Logger } from '../../../utils.errorhandler.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
|
||||
export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
const cliLogger = new Logger('CLI');
|
||||
|
||||
smartcli.addCommand('disable').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const serviceManager = new TspmServiceManager();
|
||||
console.log('Disabling TSPM daemon service...');
|
||||
|
||||
await serviceManager.disableService();
|
||||
|
||||
console.log('✓ TSPM daemon service disabled');
|
||||
console.log(' The daemon will no longer start on system boot');
|
||||
console.log(' Use "tspm enable" to re-enable the service');
|
||||
} catch (error) {
|
||||
console.error('Error disabling service:', error.message);
|
||||
if (error.message.includes('permission') || error.message.includes('denied')) {
|
||||
console.log('\nNote: You may need to run this command with sudo');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
33
ts/cli/commands/service/enable.ts
Normal file
33
ts/cli/commands/service/enable.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import * as plugins from '../../../plugins.js';
|
||||
import { TspmServiceManager } from '../../../classes.servicemanager.js';
|
||||
import { Logger } from '../../../utils.errorhandler.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
|
||||
export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
const cliLogger = new Logger('CLI');
|
||||
|
||||
smartcli.addCommand('enable').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const serviceManager = new TspmServiceManager();
|
||||
console.log('Enabling TSPM daemon as system service...');
|
||||
|
||||
await serviceManager.enableService();
|
||||
|
||||
console.log('✓ TSPM daemon enabled and started as system service');
|
||||
console.log(' The daemon will now start automatically on system boot');
|
||||
console.log(' Use "tspm disable" to remove the service');
|
||||
} catch (error) {
|
||||
console.error('Error enabling service:', error.message);
|
||||
if (error.message.includes('permission') || error.message.includes('denied')) {
|
||||
console.log('\nNote: You may need to run this command with sudo');
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
16
ts/cli/helpers/argv.ts
Normal file
16
ts/cli/helpers/argv.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { CliArguments } from '../types.js';
|
||||
|
||||
// Argument parsing helpers
|
||||
export const getBool = (argv: CliArguments, ...keys: string[]) =>
|
||||
keys.some(k => Boolean((argv as any)[k]));
|
||||
|
||||
export const getNumber = (argv: CliArguments, key: string, fallback: number) => {
|
||||
const v = (argv as any)[key];
|
||||
const n = typeof v === 'string' ? Number(v) : v;
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
};
|
||||
|
||||
export const getString = (argv: CliArguments, key: string, fallback?: string) => {
|
||||
const v = (argv as any)[key];
|
||||
return typeof v === 'string' ? v : fallback;
|
||||
};
|
14
ts/cli/helpers/errors.ts
Normal file
14
ts/cli/helpers/errors.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// Helper function to handle daemon connection errors
|
||||
export function handleDaemonError(error: any, action: string): void {
|
||||
if (error.message?.includes('daemon is not running') ||
|
||||
error.message?.includes('Not connected') ||
|
||||
error.message?.includes('ECONNREFUSED')) {
|
||||
console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
|
||||
console.log('\nTo start the daemon, run one of:');
|
||||
console.log(' tspm daemon start - Start for this session only');
|
||||
console.log(' tspm enable - Enable as system service (recommended)');
|
||||
} else {
|
||||
console.error(`Error ${action}:`, error.message);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
17
ts/cli/helpers/formatting.ts
Normal file
17
ts/cli/helpers/formatting.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Helper function for padding strings
|
||||
export function pad(str: string, length: number): string {
|
||||
return str.length > length
|
||||
? str.substring(0, length - 3) + '...'
|
||||
: str.padEnd(length);
|
||||
}
|
||||
|
||||
// Helper for unknown errors
|
||||
export const unknownError = (err: any) =>
|
||||
(err?.message && typeof err.message === 'string') ? err.message : String(err);
|
||||
|
||||
// Helper function to format log entries
|
||||
export function formatLog(log: any): string {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
return `${timestamp} ${prefix} ${log.message}`;
|
||||
}
|
22
ts/cli/helpers/lifecycle.ts
Normal file
22
ts/cli/helpers/lifecycle.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Streaming lifecycle helper
|
||||
export function withStreamingLifecycle(
|
||||
setup: () => Promise<void>,
|
||||
teardown: () => Promise<void>,
|
||||
) {
|
||||
let isCleaningUp = false;
|
||||
const cleanup = async () => {
|
||||
if (isCleaningUp) return;
|
||||
isCleaningUp = true;
|
||||
try {
|
||||
await teardown();
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
process.once('SIGINT', cleanup);
|
||||
process.once('SIGTERM', cleanup);
|
||||
return (async () => {
|
||||
await setup();
|
||||
await new Promise(() => {}); // keep alive
|
||||
})();
|
||||
}
|
33
ts/cli/helpers/memory.ts
Normal file
33
ts/cli/helpers/memory.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Helper function to parse memory strings (e.g., "512MB", "2GB")
|
||||
export function parseMemoryString(memStr: string): number {
|
||||
const units = {
|
||||
KB: 1024,
|
||||
MB: 1024 * 1024,
|
||||
GB: 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
|
||||
);
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = (match[2] || 'MB') as keyof typeof units;
|
||||
|
||||
return Math.floor(value * units[unit]);
|
||||
}
|
||||
|
||||
// Helper function to format memory for display
|
||||
export function formatMemory(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
} else if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
} else if (bytes >= 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
} else {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
}
|
68
ts/cli/index.ts
Normal file
68
ts/cli/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import { Logger, LogLevel } from '../utils.errorhandler.js';
|
||||
|
||||
// Import command registration functions
|
||||
import { registerDefaultCommand } from './commands/default.js';
|
||||
import { registerStartCommand } from './commands/process/start.js';
|
||||
import { registerStopCommand } from './commands/process/stop.js';
|
||||
import { registerRestartCommand } from './commands/process/restart.js';
|
||||
import { registerDeleteCommand } from './commands/process/delete.js';
|
||||
import { registerListCommand } from './commands/process/list.js';
|
||||
import { registerDescribeCommand } from './commands/process/describe.js';
|
||||
import { registerLogsCommand } from './commands/process/logs.js';
|
||||
import { registerStartAllCommand } from './commands/batch/start-all.js';
|
||||
import { registerStopAllCommand } from './commands/batch/stop-all.js';
|
||||
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
|
||||
import { registerDaemonCommand } from './commands/daemon/index.js';
|
||||
import { registerEnableCommand } from './commands/service/enable.js';
|
||||
import { registerDisableCommand } from './commands/service/disable.js';
|
||||
|
||||
// Export types for external use
|
||||
export type { CliArguments } from './types.js';
|
||||
|
||||
/**
|
||||
* Main CLI entry point
|
||||
*/
|
||||
export const run = async (): Promise<void> => {
|
||||
const cliLogger = new Logger('CLI');
|
||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
|
||||
// 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);
|
||||
|
||||
// Register all commands
|
||||
// Default command (help + list)
|
||||
registerDefaultCommand(smartcliInstance);
|
||||
|
||||
// Process commands
|
||||
registerStartCommand(smartcliInstance);
|
||||
registerStopCommand(smartcliInstance);
|
||||
registerRestartCommand(smartcliInstance);
|
||||
registerDeleteCommand(smartcliInstance);
|
||||
registerListCommand(smartcliInstance);
|
||||
registerDescribeCommand(smartcliInstance);
|
||||
registerLogsCommand(smartcliInstance);
|
||||
|
||||
// Batch commands
|
||||
registerStartAllCommand(smartcliInstance);
|
||||
registerStopAllCommand(smartcliInstance);
|
||||
registerRestartAllCommand(smartcliInstance);
|
||||
|
||||
// Daemon commands
|
||||
registerDaemonCommand(smartcliInstance);
|
||||
|
||||
// Service commands
|
||||
registerEnableCommand(smartcliInstance);
|
||||
registerDisableCommand(smartcliInstance);
|
||||
|
||||
// Start parsing commands
|
||||
smartcliInstance.startParse();
|
||||
};
|
19
ts/cli/registration/daemon-check.ts
Normal file
19
ts/cli/registration/daemon-check.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { tspmIpcClient } from '../../classes.ipcclient.js';
|
||||
|
||||
/**
|
||||
* Preflight the daemon if required. Uses getDaemonStatus() which is safe and cheap:
|
||||
* it only connects if the PID file is valid.
|
||||
*/
|
||||
export async function ensureDaemonOrHint(requireDaemon: boolean | undefined, actionLabel?: string): Promise<boolean> {
|
||||
if (requireDaemon === false) return true; // command does not require daemon
|
||||
const status = await tspmIpcClient.getDaemonStatus();
|
||||
if (!status) {
|
||||
// Same hint as handleDaemonError, but early and consistent
|
||||
console.error(`Error: Cannot ${actionLabel || 'perform action'} - TSPM daemon is not running.`);
|
||||
console.log('\nTo start the daemon, run one of:');
|
||||
console.log(' tspm daemon start - Start for this session only');
|
||||
console.log(' tspm enable - Enable as system service (recommended)');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
87
ts/cli/registration/index.ts
Normal file
87
ts/cli/registration/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { CliArguments, CommandAction, IpcCommandOptions } from '../types.js';
|
||||
import { handleDaemonError } from '../helpers/errors.js';
|
||||
import { unknownError } from '../helpers/formatting.js';
|
||||
import { runIpcCommand } from '../utils/ipc.js';
|
||||
import { ensureDaemonOrHint } from './daemon-check.js';
|
||||
|
||||
/**
|
||||
* Add an IPC-based CLI command with:
|
||||
* - optional daemon preflight
|
||||
* - standard error handling
|
||||
* - automatic disconnect via runIpcCommand unless keepAlive is true
|
||||
*/
|
||||
export function registerIpcCommand(
|
||||
smartcli: plugins.smartcli.Smartcli,
|
||||
name: string,
|
||||
action: CommandAction,
|
||||
opts: IpcCommandOptions = {}
|
||||
) {
|
||||
const { actionLabel = name, keepAlive = false, requireDaemon = true } = opts;
|
||||
|
||||
smartcli.addCommand(name).subscribe({
|
||||
next: async (argv: CliArguments) => {
|
||||
// Early preflight for better UX
|
||||
const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
|
||||
if (!ok) {
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate keepAlive - can be boolean or function
|
||||
const shouldKeepAlive = typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
|
||||
|
||||
if (shouldKeepAlive) {
|
||||
// Let action manage its own connection/cleanup lifecycle
|
||||
try {
|
||||
await action(argv);
|
||||
} catch (error) {
|
||||
handleDaemonError(error, actionLabel);
|
||||
}
|
||||
} else {
|
||||
// Auto-disconnect pattern for one-shot IPC commands
|
||||
await runIpcCommand(async () => {
|
||||
try {
|
||||
await action(argv);
|
||||
} catch (error) {
|
||||
handleDaemonError(error, actionLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
// Fallback error path (should be rare with try/catch in next)
|
||||
console.error(`Unexpected error in command "${name}":`, unknownError(err));
|
||||
process.exit(1);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register local commands that don't require IPC/daemon connection
|
||||
* Used for daemon lifecycle, service management, and other local operations
|
||||
*/
|
||||
export function registerLocalCommand(
|
||||
smartcli: plugins.smartcli.Smartcli,
|
||||
name: string,
|
||||
action: (argv: CliArguments) => Promise<void>,
|
||||
opts: { actionLabel?: string } = {}
|
||||
) {
|
||||
const { actionLabel = name } = opts;
|
||||
smartcli.addCommand(name).subscribe({
|
||||
next: async (argv: CliArguments) => {
|
||||
try {
|
||||
await action(argv);
|
||||
} catch (error: any) {
|
||||
console.error(`Error ${actionLabel}:`, error?.message || String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(`Unexpected error in command "${name}":`, unknownError(err));
|
||||
process.exit(1);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
20
ts/cli/types.ts
Normal file
20
ts/cli/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface CliArguments {
|
||||
verbose?: boolean;
|
||||
watch?: boolean;
|
||||
memory?: string;
|
||||
cwd?: string;
|
||||
daemon?: boolean;
|
||||
test?: boolean;
|
||||
name?: string;
|
||||
autorestart?: boolean;
|
||||
watchPaths?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type CommandAction = (argv: CliArguments) => Promise<void>;
|
||||
|
||||
export interface IpcCommandOptions {
|
||||
actionLabel?: string; // used in error message, e.g. "start process"
|
||||
keepAlive?: boolean | ((argv: CliArguments) => boolean); // true for streaming commands (don't auto-disconnect), or function to determine at runtime
|
||||
requireDaemon?: boolean; // default true for IPC-bound commands
|
||||
}
|
14
ts/cli/utils/ipc.ts
Normal file
14
ts/cli/utils/ipc.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { tspmIpcClient } from '../../classes.ipcclient.js';
|
||||
|
||||
// Helper function to run IPC commands with automatic disconnect
|
||||
export async function runIpcCommand<T>(body: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await body();
|
||||
} finally {
|
||||
try {
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch {
|
||||
// Ignore disconnect errors
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user