feat(terminal): add live terminal task rendering with interactive and non-interactive output modes

This commit is contained in:
2026-05-13 14:52:18 +00:00
parent 8852bd5c86
commit a5ec2717c5
11 changed files with 3535 additions and 4593 deletions
+79
View File
@@ -3,6 +3,25 @@ import * as smartrx from '@push.rocks/smartrx';
import * as smartcli from '../ts/index.js';
class TestWritable implements smartcli.ISmartcliWritable {
public chunks: string[] = [];
public isTTY: boolean;
public columns = 80;
constructor(isTTYArg: boolean) {
this.isTTY = isTTYArg;
}
public write(chunkArg: string): boolean {
this.chunks.push(chunkArg);
return true;
}
public toString(): string {
return this.chunks.join('');
}
}
tap.test('should create a new Smartcli', async () => {
const smartCliTestObject = new smartcli.Smartcli();
expect(smartCliTestObject).toBeInstanceOf(smartcli.Smartcli);
@@ -36,4 +55,64 @@ tap.test('should accept a command', async () => {
expect(hasExecuted).toBeTrue();
});
tap.test('should render terminal tasks in non-interactive mode', async () => {
const stream = new TestWritable(false);
const terminal = new smartcli.SmartcliTerminal({ stream, interactive: false, colors: false });
const task = terminal.createTask({ job: 'build package', rows: 2 });
task.update('running tsbuild');
task.complete('done');
const output = stream.toString();
expect(output).toInclude('[start] build package');
expect(output).toInclude('[build package] running tsbuild');
expect(output).toInclude('[ok] build package - done');
expect(terminal.getTasks()).toHaveLength(0);
});
tap.test('should render fixed rows in interactive mode', async () => {
const stream = new TestWritable(true);
const terminal = new smartcli.SmartcliTerminal({ stream, interactive: true, colors: false });
const task = terminal.createTask({ job: 'install dependencies', rows: 2 });
task.update('fetching packages');
const renderedRows = task.renderPlainRows(80);
expect(renderedRows).toHaveLength(2);
expect(renderedRows[0]).toInclude('[run] install dependencies');
expect(stream.toString()).toInclude('\u001B[2K');
task.complete('installed');
expect(stream.toString()).toInclude('[ok] install dependencies - installed');
expect(terminal.getTasks()).toHaveLength(0);
});
tap.test('should attach persistent terminal task errors', async () => {
const stream = new TestWritable(true);
const terminal = new smartcli.SmartcliTerminal({ stream, interactive: true, colors: false });
const task = terminal.createTask({ job: 'deploy release', rows: 3 });
task.attachError('deployment failed', { keepOpen: true });
expect(task.status).toEqual('failed');
expect(task.getErrorLines()).toContain('deployment failed');
expect(task.renderPlainRows(80)[0]).toInclude('[err] deploy release');
expect(terminal.getTasks()).toHaveLength(1);
terminal.clear();
});
tap.test('should collapse failed terminal tasks into permanent output', async () => {
const stream = new TestWritable(false);
const terminal = new smartcli.SmartcliTerminal({ stream, interactive: false, colors: false });
const task = terminal.createTask({ job: 'publish package' });
task.attachError('registry rejected package');
const output = stream.toString();
expect(output).toInclude('[err] publish package - registry rejected package');
expect(terminal.getTasks()).toHaveLength(0);
});
export default tap.start();