feat(terminal): add live terminal task rendering with interactive and non-interactive output modes
This commit is contained in:
+13
-9
@@ -11,18 +11,22 @@
|
||||
"projectDomain": "push.rocks"
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
"targets": {
|
||||
"npm": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemaVersion": 2
|
||||
},
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [LICENSE](LICENSE) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmRegistryUrl": "registry.npmjs.org"
|
||||
"npmGlobalTools": []
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"npmci": {
|
||||
"type": "object",
|
||||
"description": "settings for npmci"
|
||||
},
|
||||
"gitzone": {
|
||||
"type": "object",
|
||||
"description": "settings for gitzone",
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+11
-1
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## Pending
|
||||
|
||||
### Features
|
||||
|
||||
- add live terminal task rendering with interactive and non-interactive output modes (terminal)
|
||||
- introduces SmartcliTerminal and SmartcliTerminalTask exports for fixed-row task rendering
|
||||
- supports task updates, completion, failure handling, and persistent error output
|
||||
- detects interactive terminals and falls back to append-only logs in CI or non-TTY environments
|
||||
- adds tests covering interactive rendering, non-interactive logging, and error attachment behavior
|
||||
|
||||
## 2026-04-30 - 4.0.21 - fix(smartcli)
|
||||
tighten command parsing and error handling while updating build and package configuration
|
||||
|
||||
@@ -408,4 +418,4 @@ Initial commit
|
||||
---
|
||||
|
||||
## Summary of Omitted Versions
|
||||
The following versions contained no additional user‐facing changes beyond version bumps and are summarized here: 1.0.12, 0.0.6, and 0.0.3.
|
||||
The following versions contained no additional user‐facing changes beyond version bumps and are summarized here: 1.0.12, 0.0.6, and 0.0.3.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartcli",
|
||||
"description": "A library that simplifies building reactive command-line applications using observables, with robust support for commands, arguments, options, aliases, and asynchronous operation management.",
|
||||
"npmPackagename": "@push.rocks/smartcli",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"CLI",
|
||||
"command line",
|
||||
"observable",
|
||||
"reactive",
|
||||
"asynchronous",
|
||||
"commands",
|
||||
"arguments",
|
||||
"options",
|
||||
"alias",
|
||||
"typescript",
|
||||
"node.js",
|
||||
"development tool"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
}
|
||||
}
|
||||
+5
-6
@@ -39,15 +39,15 @@
|
||||
"@push.rocks/lik": "^6.4.1",
|
||||
"@push.rocks/smartlog": "^3.2.2",
|
||||
"@push.rocks/smartobject": "^1.0.12",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"@push.rocks/smartpromise": "^4.2.4",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
"yargs-parser": "22.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsbuild": "^4.4.0",
|
||||
"@git.zone/tsrun": "^2.0.3",
|
||||
"@git.zone/tstest": "^3.6.3",
|
||||
"@types/node": "^25.6.0",
|
||||
"@git.zone/tsbuild": "^4.4.1",
|
||||
"@git.zone/tsrun": "^2.0.4",
|
||||
"@git.zone/tstest": "^3.6.6",
|
||||
"@types/node": "^25.7.0",
|
||||
"@types/yargs-parser": "^21.0.3"
|
||||
},
|
||||
"files": [
|
||||
@@ -61,7 +61,6 @@
|
||||
"cli.js",
|
||||
".smartconfig.json",
|
||||
"LICENSE",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
Generated
+1805
-2480
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -1 +1,9 @@
|
||||
export { Smartcli } from './smartcli.classes.smartcli.js';
|
||||
export { SmartcliTerminal, SmartcliTerminalTask } from './smartcli.classes.terminal.js';
|
||||
export type {
|
||||
ISmartcliTerminalAttachErrorOptions,
|
||||
ISmartcliTerminalOptions,
|
||||
ISmartcliTerminalTaskOptions,
|
||||
ISmartcliWritable,
|
||||
TSmartcliTerminalTaskStatus,
|
||||
} from './smartcli.classes.terminal.js';
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
export type TSmartcliTerminalTaskStatus = 'running' | 'completed' | 'failed';
|
||||
|
||||
export interface ISmartcliWritable {
|
||||
isTTY?: boolean;
|
||||
columns?: number;
|
||||
write(chunk: string): void | boolean;
|
||||
}
|
||||
|
||||
export interface ISmartcliTerminalOptions {
|
||||
stream?: ISmartcliWritable;
|
||||
interactive?: boolean;
|
||||
colors?: boolean;
|
||||
}
|
||||
|
||||
export interface ISmartcliTerminalTaskOptions {
|
||||
job: string;
|
||||
rows?: number;
|
||||
logLimit?: number;
|
||||
}
|
||||
|
||||
export interface ISmartcliTerminalAttachErrorOptions {
|
||||
keepOpen?: boolean;
|
||||
}
|
||||
|
||||
const ansiCodes = {
|
||||
reset: '\u001B[0m',
|
||||
red: '\u001B[31m',
|
||||
green: '\u001B[32m',
|
||||
cyan: '\u001B[36m',
|
||||
gray: '\u001B[90m',
|
||||
};
|
||||
|
||||
/**
|
||||
* A live terminal renderer for multiple fixed-row tasks.
|
||||
* It automatically falls back to append-only logs in non-interactive environments.
|
||||
*/
|
||||
export class SmartcliTerminal {
|
||||
private stream: ISmartcliWritable;
|
||||
private interactive: boolean;
|
||||
private colors: boolean;
|
||||
private tasks: SmartcliTerminalTask[] = [];
|
||||
private renderedLineCount = 0;
|
||||
|
||||
constructor(optionsArg: ISmartcliTerminalOptions = {}) {
|
||||
this.stream = optionsArg.stream || getDefaultStream();
|
||||
this.interactive = getInteractiveState(this.stream, optionsArg.interactive);
|
||||
this.colors = optionsArg.colors ?? (this.interactive && !hasEnvFlag('NO_COLOR'));
|
||||
}
|
||||
|
||||
public createTask(optionsArg: ISmartcliTerminalTaskOptions): SmartcliTerminalTask {
|
||||
const task = new SmartcliTerminalTask(this, optionsArg);
|
||||
this.tasks.push(task);
|
||||
|
||||
if (this.interactive) {
|
||||
this.render();
|
||||
} else {
|
||||
this.writePermanentLine(`[start] ${task.job}`);
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
public createProcess(optionsArg: ISmartcliTerminalTaskOptions): SmartcliTerminalTask {
|
||||
return this.createTask(optionsArg);
|
||||
}
|
||||
|
||||
public isInteractive(): boolean {
|
||||
return this.interactive;
|
||||
}
|
||||
|
||||
public getTasks(): SmartcliTerminalTask[] {
|
||||
return [...this.tasks];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public updateTask(taskArg: SmartcliTerminalTask, messageArg?: string): void {
|
||||
if (!this.tasks.includes(taskArg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.interactive) {
|
||||
this.render();
|
||||
} else if (messageArg) {
|
||||
this.writePermanentLine(`[${taskArg.job}] ${messageArg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public completeTask(taskArg: SmartcliTerminalTask, messageArg?: string): void {
|
||||
const message = messageArg || taskArg.getLastLogLine();
|
||||
const summary = `[ok] ${taskArg.job}${message ? ` - ${message}` : ''}`;
|
||||
this.finalizeTask(taskArg, [summary]);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public failTask(taskArg: SmartcliTerminalTask, errorLinesArg: string[]): void {
|
||||
const summary = `[err] ${taskArg.job}${errorLinesArg[0] ? ` - ${errorLinesArg[0]}` : ''}`;
|
||||
const detailLines = errorLinesArg.slice(1).map((lineArg) => ` ${lineArg}`);
|
||||
this.finalizeTask(taskArg, [summary, ...detailLines]);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
if (this.interactive) {
|
||||
this.clearRenderedBlock();
|
||||
}
|
||||
this.tasks = [];
|
||||
}
|
||||
|
||||
private finalizeTask(taskArg: SmartcliTerminalTask, linesArg: string[]): void {
|
||||
this.tasks = this.tasks.filter((task) => task !== taskArg);
|
||||
|
||||
if (this.interactive) {
|
||||
this.clearRenderedBlock();
|
||||
this.writePermanentLines(linesArg);
|
||||
this.render();
|
||||
} else {
|
||||
this.writePermanentLines(linesArg);
|
||||
}
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
if (!this.interactive) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = this.getLineWidth();
|
||||
const lines = this.tasks.flatMap((taskArg) => {
|
||||
return taskArg.renderPlainRows(width).map((lineArg, indexArg) => {
|
||||
const truncatedLine = truncateLine(lineArg, width);
|
||||
return indexArg === 0 ? this.colorizeStatusLabel(truncatedLine) : truncatedLine;
|
||||
});
|
||||
});
|
||||
|
||||
if (this.renderedLineCount > 0) {
|
||||
this.stream.write(`\u001B[${this.renderedLineCount}F`);
|
||||
}
|
||||
|
||||
const lineCount = Math.max(this.renderedLineCount, lines.length);
|
||||
for (let index = 0; index < lineCount; index++) {
|
||||
this.stream.write('\u001B[2K\r');
|
||||
if (index < lines.length) {
|
||||
this.stream.write(lines[index]);
|
||||
}
|
||||
this.stream.write('\n');
|
||||
}
|
||||
|
||||
this.renderedLineCount = lines.length;
|
||||
}
|
||||
|
||||
private clearRenderedBlock(): void {
|
||||
if (this.renderedLineCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stream.write(`\u001B[${this.renderedLineCount}F`);
|
||||
this.stream.write(`\u001B[${this.renderedLineCount}M`);
|
||||
this.renderedLineCount = 0;
|
||||
}
|
||||
|
||||
private writePermanentLines(linesArg: string[]): void {
|
||||
for (const line of linesArg) {
|
||||
this.writePermanentLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private writePermanentLine(lineArg: string): void {
|
||||
const line = this.colors ? this.colorizeStatusLabel(lineArg) : lineArg;
|
||||
this.stream.write(`${line}\n`);
|
||||
}
|
||||
|
||||
private colorizeStatusLabel(lineArg: string): string {
|
||||
if (!this.colors) {
|
||||
return lineArg;
|
||||
}
|
||||
|
||||
if (lineArg.startsWith('[ok]')) {
|
||||
return `${ansiCodes.green}[ok]${ansiCodes.reset}${lineArg.slice(4)}`;
|
||||
}
|
||||
if (lineArg.startsWith('[err]')) {
|
||||
return `${ansiCodes.red}[err]${ansiCodes.reset}${lineArg.slice(5)}`;
|
||||
}
|
||||
if (lineArg.startsWith('[run]')) {
|
||||
return `${ansiCodes.cyan}[run]${ansiCodes.reset}${lineArg.slice(5)}`;
|
||||
}
|
||||
if (lineArg.startsWith('[start]')) {
|
||||
return `${ansiCodes.gray}[start]${ansiCodes.reset}${lineArg.slice(7)}`;
|
||||
}
|
||||
return lineArg;
|
||||
}
|
||||
|
||||
private getLineWidth(): number {
|
||||
return Math.max(20, (this.stream.columns || 80) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class SmartcliTerminalTask {
|
||||
public readonly job: string;
|
||||
public readonly rows: number;
|
||||
public status: TSmartcliTerminalTaskStatus = 'running';
|
||||
private terminal: SmartcliTerminal;
|
||||
private logLimit: number;
|
||||
private logLines: string[] = [];
|
||||
private errorLines: string[] = [];
|
||||
|
||||
constructor(terminalArg: SmartcliTerminal, optionsArg: ISmartcliTerminalTaskOptions) {
|
||||
this.terminal = terminalArg;
|
||||
this.job = optionsArg.job;
|
||||
this.rows = Math.max(1, Math.floor(optionsArg.rows || 3));
|
||||
this.logLimit = Math.max(this.rows, Math.floor(optionsArg.logLimit || 100));
|
||||
}
|
||||
|
||||
public log(messageArg: string): this {
|
||||
if (this.status !== 'running') {
|
||||
return this;
|
||||
}
|
||||
|
||||
const newLines = normalizeLines(messageArg);
|
||||
this.logLines.push(...newLines);
|
||||
if (this.logLines.length > this.logLimit) {
|
||||
this.logLines.splice(0, this.logLines.length - this.logLimit);
|
||||
}
|
||||
|
||||
this.terminal.updateTask(this, newLines.join('\n'));
|
||||
return this;
|
||||
}
|
||||
|
||||
public update(messageArg: string): this {
|
||||
return this.log(messageArg);
|
||||
}
|
||||
|
||||
public complete(messageArg?: string): this {
|
||||
if (this.status !== 'running') {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.status = 'completed';
|
||||
this.terminal.completeTask(this, messageArg);
|
||||
return this;
|
||||
}
|
||||
|
||||
public attachError(
|
||||
errorArg: unknown,
|
||||
optionsArg: ISmartcliTerminalAttachErrorOptions = {}
|
||||
): this {
|
||||
if (this.status !== 'running') {
|
||||
return this;
|
||||
}
|
||||
|
||||
this.status = 'failed';
|
||||
this.errorLines = formatError(errorArg);
|
||||
|
||||
if (optionsArg.keepOpen) {
|
||||
this.terminal.updateTask(this, this.errorLines.join('\n'));
|
||||
} else {
|
||||
this.terminal.failTask(this, this.errorLines);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public fail(errorArg: unknown): this {
|
||||
return this.attachError(errorArg);
|
||||
}
|
||||
|
||||
public getLastLogLine(): string | undefined {
|
||||
return this.logLines[this.logLines.length - 1];
|
||||
}
|
||||
|
||||
public getLogLines(): string[] {
|
||||
return [...this.logLines];
|
||||
}
|
||||
|
||||
public getErrorLines(): string[] {
|
||||
return [...this.errorLines];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
public renderPlainRows(widthArg: number): string[] {
|
||||
const statusLabel = this.status === 'failed' ? '[err]' : '[run]';
|
||||
const lines: string[] = [];
|
||||
const detailLines = this.errorLines.length > 0 ? this.errorLines : this.logLines;
|
||||
|
||||
if (this.rows === 1) {
|
||||
const lastDetail = detailLines[detailLines.length - 1];
|
||||
lines.push(`${statusLabel} ${this.job}${lastDetail ? ` - ${lastDetail}` : ''}`);
|
||||
return lines;
|
||||
}
|
||||
|
||||
lines.push(`${statusLabel} ${this.job}`);
|
||||
const visibleDetailLineCount = this.rows - 1;
|
||||
const visibleDetailLines = detailLines.slice(-visibleDetailLineCount);
|
||||
for (const line of visibleDetailLines) {
|
||||
lines.push(` ${line}`);
|
||||
}
|
||||
|
||||
while (lines.length < this.rows) {
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.map((lineArg) => truncateLine(lineArg, widthArg));
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultStream(): ISmartcliWritable {
|
||||
const globalObject: any = globalThis as any;
|
||||
if (globalObject.process?.stdout?.write) {
|
||||
return globalObject.process.stdout;
|
||||
}
|
||||
|
||||
return {
|
||||
isTTY: false,
|
||||
write: (chunkArg: string) => {
|
||||
if (typeof console !== 'undefined') {
|
||||
console.log(chunkArg.replace(/\n$/, ''));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getInteractiveState(streamArg: ISmartcliWritable, overrideArg?: boolean): boolean {
|
||||
if (typeof overrideArg === 'boolean') {
|
||||
return overrideArg;
|
||||
}
|
||||
|
||||
if (!streamArg.isTTY) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(
|
||||
hasEnvFlag('CI') ||
|
||||
hasEnvFlag('GITHUB_ACTIONS') ||
|
||||
hasEnvFlag('JENKINS_URL') ||
|
||||
hasEnvFlag('GITLAB_CI') ||
|
||||
hasEnvFlag('TRAVIS') ||
|
||||
hasEnvFlag('CIRCLECI') ||
|
||||
getEnvValue('TERM') === 'dumb'
|
||||
);
|
||||
}
|
||||
|
||||
function hasEnvFlag(nameArg: string): boolean {
|
||||
const value = getEnvValue(nameArg);
|
||||
return Boolean(value && value !== '0' && value.toLowerCase() !== 'false');
|
||||
}
|
||||
|
||||
function getEnvValue(nameArg: string): string | undefined {
|
||||
const globalObject: any = globalThis as any;
|
||||
return globalObject.process?.env?.[nameArg];
|
||||
}
|
||||
|
||||
function normalizeLines(messageArg: string): string[] {
|
||||
return String(messageArg)
|
||||
.split(/\r?\n/)
|
||||
.map((lineArg) => lineArg.trimEnd())
|
||||
.filter((lineArg) => lineArg.length > 0);
|
||||
}
|
||||
|
||||
function formatError(errorArg: unknown): string[] {
|
||||
if (errorArg instanceof Error) {
|
||||
return normalizeLines(errorArg.stack || errorArg.message || errorArg.name);
|
||||
}
|
||||
|
||||
if (typeof errorArg === 'string') {
|
||||
return normalizeLines(errorArg);
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonString = JSON.stringify(errorArg);
|
||||
return normalizeLines(jsonString === undefined ? String(errorArg) : jsonString);
|
||||
} catch {
|
||||
return [String(errorArg)];
|
||||
}
|
||||
}
|
||||
|
||||
function truncateLine(lineArg: string, widthArg: number): string {
|
||||
if (lineArg.length <= widthArg) {
|
||||
return lineArg;
|
||||
}
|
||||
|
||||
if (widthArg <= 3) {
|
||||
return lineArg.slice(0, widthArg);
|
||||
}
|
||||
|
||||
return `${lineArg.slice(0, widthArg - 3)}...`;
|
||||
}
|
||||
Reference in New Issue
Block a user