Files
tspm/ts/cli/commands/process/logs.ts

102 lines
3.6 KiB
TypeScript

import * as plugins from '../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.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 target = argvArg._[1];
if (!target) {
console.error('Error: Please provide a process target');
console.log('Usage: tspm logs <id | id:N | name:LABEL> [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 resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
const id = resolved.id;
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: ${resolved.name || 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'),
},
);
}