visual-exec 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/visual-exec.d.ts +38 -4
- package/dist/visual-exec.js +107 -22
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { VisualExec, VisualExecOptions, ExecOutput, ExecErrorContext, VisualExecError, parsers, jsonLinesParser, keyValueParser } from "./visual-exec.js";
|
|
1
|
+
export { VisualExec, type VisualExecOptions, type ExecOutput, type ExecErrorContext, type VisualExecError, parsers, jsonLinesParser, keyValueParser } from "./visual-exec.js";
|
|
2
2
|
export type { OnOutputCallback, OnCompleteCallback, OutputFileOptions, ProgressExtractor, OutputMatcher } from "./visual-exec.js";
|
|
3
3
|
export { getDefaultLogger } from "./get-default-logger.js";
|
|
4
4
|
export { VisualExec as default } from "./visual-exec.js";
|
package/dist/visual-exec.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import VisualLogger from "visual-logger";
|
|
1
|
+
import VisualLogger, { type ItemOptions } from "visual-logger";
|
|
2
2
|
export type OutputStream = "stdout" | "stderr";
|
|
3
3
|
/** Called on each chunk of stdout/stderr during execution */
|
|
4
4
|
export type OnOutputCallback = (data: string, stream: OutputStream) => void;
|
|
@@ -40,7 +40,7 @@ export interface VisualExecOptions {
|
|
|
40
40
|
/** Visual logger instance to use */
|
|
41
41
|
visualLogger?: VisualLogger;
|
|
42
42
|
/** Spinner style to use */
|
|
43
|
-
spinner?:
|
|
43
|
+
spinner?: ItemOptions["spinner"];
|
|
44
44
|
/** Title displayed during execution */
|
|
45
45
|
displayTitle?: string;
|
|
46
46
|
/** Label used in log messages */
|
|
@@ -126,6 +126,27 @@ export declare const parsers: {
|
|
|
126
126
|
jsonLines: typeof jsonLinesParser;
|
|
127
127
|
keyValue: typeof keyValueParser;
|
|
128
128
|
};
|
|
129
|
+
export interface SanitizeOptions {
|
|
130
|
+
/** "space" collapses tabs (single-line contexts), "keep" preserves them (dumps) */
|
|
131
|
+
tabs?: "space" | "keep";
|
|
132
|
+
/** what to do with a lone \r - "drop", or "newline" to keep progress redraws readable */
|
|
133
|
+
cr?: "drop" | "newline";
|
|
134
|
+
/** append an SGR reset when the text emitted colour, so it cannot bleed into later output */
|
|
135
|
+
resetAtEnd?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Strip terminal control sequences from child output, keeping colour. Never use this for
|
|
139
|
+
* output written to a file or handed to a callback - those should get the bytes untouched.
|
|
140
|
+
*/
|
|
141
|
+
export declare function sanitizeText(text: string, options?: SanitizeOptions): string;
|
|
142
|
+
/** Profile for text squeezed into a single progress line. */
|
|
143
|
+
export declare function sanitizeForDisplay(text: string): string;
|
|
144
|
+
/**
|
|
145
|
+
* Profile for a multi-line transcript written into scrollback: tabs are real formatting,
|
|
146
|
+
* carriage returns become line breaks so progress redraws stay readable, and a trailing
|
|
147
|
+
* reset stops an unterminated child colour bleeding into later output.
|
|
148
|
+
*/
|
|
149
|
+
export declare function sanitizeForOutput(text: string): string;
|
|
129
150
|
export declare class VisualExec {
|
|
130
151
|
private _title;
|
|
131
152
|
private _logLabel;
|
|
@@ -171,7 +192,14 @@ export declare class VisualExec {
|
|
|
171
192
|
abort(signal?: string): void;
|
|
172
193
|
/** Alias for abort() */
|
|
173
194
|
kill(signal?: string): void;
|
|
174
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Attach to an already-started child and render its output.
|
|
197
|
+
*
|
|
198
|
+
* Resolves with the command's `ExecOutput`, unless an `onComplete` callback returned a
|
|
199
|
+
* value - that value is passed through instead. `T` names what the caller expects back;
|
|
200
|
+
* it defaults to `ExecOutput`, which is what a run without an `onComplete` produces.
|
|
201
|
+
*/
|
|
202
|
+
show<T = ExecOutput>(child: ChildProcess): Promise<T>;
|
|
175
203
|
logResult(err: VisualExecError | null, output?: ExecOutput): void;
|
|
176
204
|
checkForErrors(text: string): RegExpMatchArray | null;
|
|
177
205
|
/**
|
|
@@ -179,6 +207,12 @@ export declare class VisualExec {
|
|
|
179
207
|
* Set to a no-op function to suppress output logging.
|
|
180
208
|
*/
|
|
181
209
|
logFinalOutput(err: VisualExecError | null, output: ExecOutput): void;
|
|
182
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Run the command and render its output.
|
|
212
|
+
*
|
|
213
|
+
* Resolves with `ExecOutput`, or with whatever an `onComplete` callback returned in its
|
|
214
|
+
* place. `T` defaults to `ExecOutput` so the common case needs no cast at the call site.
|
|
215
|
+
*/
|
|
216
|
+
execute<T = ExecOutput>(command?: string): Promise<T>;
|
|
183
217
|
}
|
|
184
218
|
export default VisualExec;
|
package/dist/visual-exec.js
CHANGED
|
@@ -3,7 +3,7 @@ import chalk from "chalk";
|
|
|
3
3
|
import * as fs from "fs";
|
|
4
4
|
import * as path from "path";
|
|
5
5
|
import { getDefaultLogger } from "./get-default-logger.js";
|
|
6
|
-
import VisualLogger from "visual-logger";
|
|
6
|
+
import VisualLogger, {} from "visual-logger";
|
|
7
7
|
import hasAnsi from "has-ansi";
|
|
8
8
|
import stripAnsi from "strip-ansi";
|
|
9
9
|
// Set xsh to use native Promise
|
|
@@ -70,6 +70,70 @@ export const parsers = {
|
|
|
70
70
|
jsonLines: jsonLinesParser,
|
|
71
71
|
keyValue: keyValueParser
|
|
72
72
|
};
|
|
73
|
+
//
|
|
74
|
+
// Child output is captured from a pipe and rendered back into this process's terminal. It
|
|
75
|
+
// can carry the child's own control sequences - a nested fyn renders its own visual logger
|
|
76
|
+
// and emits eraseLine/cursorUp runs. Those are zero-width to string-width, so line
|
|
77
|
+
// accounting stays correct while the terminal still *executes* them, walking the cursor up
|
|
78
|
+
// and erasing committed output.
|
|
79
|
+
//
|
|
80
|
+
// Keep SGR (colour); drop anything that can move the cursor, erase, scroll, switch charset,
|
|
81
|
+
// set the window title, or write the clipboard.
|
|
82
|
+
//
|
|
83
|
+
// The escape grammar, in the order the alternation must test it:
|
|
84
|
+
// CSI ESC [ params intermediates final
|
|
85
|
+
// OSC/DCS/PM/APC/SOS ESC ] P ^ _ X ... terminated by BEL or ST (or truncated at end)
|
|
86
|
+
// nF ESC intermediates(0x20-0x2F)+ final e.g. ESC(0, ESC#8, ESC%G
|
|
87
|
+
// Fp/Fe/Fs ESC single(0x30-0x7E) e.g. ESC7, ESCc, ESC M
|
|
88
|
+
//
|
|
89
|
+
const ESCAPE_SEQUENCE = new RegExp("\u001B(?:" +
|
|
90
|
+
"\\[[0-?]*[ -/]*[@-~]" +
|
|
91
|
+
"|[\\]P^_X][\\s\\S]*?(?:\\u0007|\u001B\\\\|$)" +
|
|
92
|
+
"|[ -/]+[0-~]" +
|
|
93
|
+
"|[0-~]" +
|
|
94
|
+
")", "g");
|
|
95
|
+
const SGR_SEQUENCE = new RegExp("^\u001B\\[[0-?]*[ -/]*m$");
|
|
96
|
+
const HAS_SGR = new RegExp("\u001B\\[[0-?]*[ -/]*m");
|
|
97
|
+
const SGR_RESET = "\u001B[0m";
|
|
98
|
+
// 8-bit C1 controls: escapes without the ESC byte. 0x9B is an 8-bit CSI and 0x9D/0x90 are
|
|
99
|
+
// 8-bit OSC/DCS, so consume their payload too - dropping only the introducer would leave the
|
|
100
|
+
// parameters behind as inert but visible junk. Anything left is stripped bare.
|
|
101
|
+
const C1_SEQUENCE = /[\u009B][0-?]*[ -/]*[@-~]|[\u009D\u0090\u009E\u009F][\s\S]*?(?:\u0007|\u009C|$)/g;
|
|
102
|
+
const C1_CONTROL = /[\u0080-\u009F]/g;
|
|
103
|
+
// C0 controls that move the cursor or clear the screen. \n and \t are left to the profile,
|
|
104
|
+
// and \u001B is left alone - the only ones remaining are inside kept SGR sequences.
|
|
105
|
+
// eslint-disable-next-line no-control-regex
|
|
106
|
+
const UNSAFE_CONTROL = /[\u0000-\u0008\u000B-\u001A\u001C-\u001F\u007F]/g;
|
|
107
|
+
/**
|
|
108
|
+
* Strip terminal control sequences from child output, keeping colour. Never use this for
|
|
109
|
+
* output written to a file or handed to a callback - those should get the bytes untouched.
|
|
110
|
+
*/
|
|
111
|
+
export function sanitizeText(text, options = {}) {
|
|
112
|
+
const { tabs = "space", cr = "drop", resetAtEnd = false } = options;
|
|
113
|
+
let out = text.replace(ESCAPE_SEQUENCE, seq => (SGR_SEQUENCE.test(seq) ? seq : ""));
|
|
114
|
+
// must run before UNSAFE_CONTROL, which would otherwise swallow the \r
|
|
115
|
+
if (cr === "newline") {
|
|
116
|
+
out = out.replace(/\r\n?/g, "\n");
|
|
117
|
+
}
|
|
118
|
+
out = out
|
|
119
|
+
.replace(/\t/g, tabs === "keep" ? "\t" : " ")
|
|
120
|
+
.replace(C1_SEQUENCE, "")
|
|
121
|
+
.replace(C1_CONTROL, "")
|
|
122
|
+
.replace(UNSAFE_CONTROL, "");
|
|
123
|
+
return resetAtEnd && HAS_SGR.test(out) ? out + SGR_RESET : out;
|
|
124
|
+
}
|
|
125
|
+
/** Profile for text squeezed into a single progress line. */
|
|
126
|
+
export function sanitizeForDisplay(text) {
|
|
127
|
+
return sanitizeText(text, { tabs: "space", cr: "drop" });
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Profile for a multi-line transcript written into scrollback: tabs are real formatting,
|
|
131
|
+
* carriage returns become line breaks so progress redraws stay readable, and a trailing
|
|
132
|
+
* reset stops an unterminated child colour bleeding into later output.
|
|
133
|
+
*/
|
|
134
|
+
export function sanitizeForOutput(text) {
|
|
135
|
+
return sanitizeText(text, { tabs: "keep", cr: "newline", resetAtEnd: true });
|
|
136
|
+
}
|
|
73
137
|
export class VisualExec {
|
|
74
138
|
constructor(options) {
|
|
75
139
|
this._aborted = false;
|
|
@@ -157,7 +221,9 @@ export class VisualExec {
|
|
|
157
221
|
this._onProgress(progress);
|
|
158
222
|
}
|
|
159
223
|
_updateDigest(item, buf) {
|
|
160
|
-
|
|
224
|
+
// sanitize at the boundary so every downstream use - lines, item.buf, the item message -
|
|
225
|
+
// is already inert. Raw output still reaches onOutput/outputFile via _createDataHandler.
|
|
226
|
+
const newBuf = item.buf + sanitizeForDisplay(buf);
|
|
161
227
|
const lines = newBuf
|
|
162
228
|
.split("\n")
|
|
163
229
|
.map(x => x && x.trim())
|
|
@@ -228,6 +294,13 @@ export class VisualExec {
|
|
|
228
294
|
kill(signal) {
|
|
229
295
|
this.abort(signal);
|
|
230
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* Attach to an already-started child and render its output.
|
|
299
|
+
*
|
|
300
|
+
* Resolves with the command's `ExecOutput`, unless an `onComplete` callback returned a
|
|
301
|
+
* value - that value is passed through instead. `T` names what the caller expects back;
|
|
302
|
+
* it defaults to `ExecOutput`, which is what a run without an `onComplete` produces.
|
|
303
|
+
*/
|
|
231
304
|
show(child) {
|
|
232
305
|
this._stdoutKey = Symbol("visual-exec-stdout");
|
|
233
306
|
this._stderrKey = Symbol("visual-exec-stderr");
|
|
@@ -270,10 +343,11 @@ export class VisualExec {
|
|
|
270
343
|
const startTime = Date.now();
|
|
271
344
|
let execPromise = child.promise
|
|
272
345
|
.catch((err) => {
|
|
273
|
-
var _a
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
(_d =
|
|
346
|
+
var _a;
|
|
347
|
+
var _b, _c, _d;
|
|
348
|
+
const output = (_b = err.output) !== null && _b !== void 0 ? _b : { stdout: "", stderr: "" };
|
|
349
|
+
const exitCode = (_d = (_c = err.exitCode) !== null && _c !== void 0 ? _c : err.code) !== null && _d !== void 0 ? _d : 1;
|
|
350
|
+
(_a = this._onComplete) === null || _a === void 0 ? void 0 : _a.call(this, output, exitCode);
|
|
277
351
|
this.logResult(err);
|
|
278
352
|
throw err;
|
|
279
353
|
})
|
|
@@ -352,6 +426,8 @@ export class VisualExec {
|
|
|
352
426
|
});
|
|
353
427
|
execPromise = Promise.race([execPromise, abortPromise]);
|
|
354
428
|
}
|
|
429
|
+
// The runtime value is `ExecOutput` unless `onComplete` substituted its own; only the
|
|
430
|
+
// caller knows which, so the promise is handed back as the `T` they asked for.
|
|
355
431
|
return execPromise.finally(cleanup);
|
|
356
432
|
}
|
|
357
433
|
logResult(err, output) {
|
|
@@ -393,7 +469,12 @@ export class VisualExec {
|
|
|
393
469
|
this._logger[level](`${chalk.green("No output")} from ${this._outputLabel}`);
|
|
394
470
|
return;
|
|
395
471
|
}
|
|
396
|
-
|
|
472
|
+
//
|
|
473
|
+
// The child's bytes end up on this terminal and in the saved log data, so scrub the
|
|
474
|
+
// control sequences out first - an error report that erases the lines above it is worse
|
|
475
|
+
// than useless. sanitizeForOutput runs before colorize, which only adds SGR.
|
|
476
|
+
//
|
|
477
|
+
const colorize = (t) => sanitizeForOutput(t).replace(/ERR!/g, chalk.red("ERR!"));
|
|
397
478
|
const logs = [chalk.green(">>>"), `Start of output from ${this._outputLabel} ===`];
|
|
398
479
|
if (output.stdout) {
|
|
399
480
|
logs.push(`\n${colorize(output.stdout)}`);
|
|
@@ -404,6 +485,12 @@ export class VisualExec {
|
|
|
404
485
|
logs.push(chalk.blue("\n<<<"), `End of output from ${this._outputLabel} ---`);
|
|
405
486
|
this._logger.prefix(false)[level](...logs);
|
|
406
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* Run the command and render its output.
|
|
490
|
+
*
|
|
491
|
+
* Resolves with `ExecOutput`, or with whatever an `onComplete` callback returned in its
|
|
492
|
+
* place. `T` defaults to `ExecOutput` so the common case needs no cast at the call site.
|
|
493
|
+
*/
|
|
407
494
|
execute(command) {
|
|
408
495
|
this._startTime = Date.now();
|
|
409
496
|
this._aborted = false;
|
|
@@ -421,22 +508,20 @@ export class VisualExec {
|
|
|
421
508
|
child: result.child
|
|
422
509
|
};
|
|
423
510
|
const duration = () => Date.now() - this._startTime;
|
|
424
|
-
const baseContext = (output) => {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
lastLines: lastLines([...((_c = output === null || output === void 0 ? void 0 : output.stdout) !== null && _c !== void 0 ? _c : "").split("\n"), ...((_d = output === null || output === void 0 ? void 0 : output.stderr) !== null && _d !== void 0 ? _d : "").split("\n")].join("\n"), DEFAULT_LAST_LINES)
|
|
433
|
-
});
|
|
434
|
-
};
|
|
511
|
+
const baseContext = (output) => { var _a, _b, _c, _d; return ({
|
|
512
|
+
command: cmd,
|
|
513
|
+
cwd: this._cwd,
|
|
514
|
+
duration: duration(),
|
|
515
|
+
stdout: (_a = output === null || output === void 0 ? void 0 : output.stdout) !== null && _a !== void 0 ? _a : "",
|
|
516
|
+
stderr: (_b = output === null || output === void 0 ? void 0 : output.stderr) !== null && _b !== void 0 ? _b : "",
|
|
517
|
+
lastLines: lastLines([...((_c = output === null || output === void 0 ? void 0 : output.stdout) !== null && _c !== void 0 ? _c : "").split("\n"), ...((_d = output === null || output === void 0 ? void 0 : output.stderr) !== null && _d !== void 0 ? _d : "").split("\n")].join("\n"), DEFAULT_LAST_LINES)
|
|
518
|
+
}); };
|
|
435
519
|
child.promise = child.promise.catch((err) => {
|
|
436
|
-
var _a
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
const
|
|
520
|
+
var _a;
|
|
521
|
+
var _b, _c, _d, _e;
|
|
522
|
+
const output = (_b = err.output) !== null && _b !== void 0 ? _b : { stdout: "", stderr: "" };
|
|
523
|
+
const exitCode = (_c = err.code) !== null && _c !== void 0 ? _c : 1;
|
|
524
|
+
const signal = (_e = (_d = err.signal) !== null && _d !== void 0 ? _d : (((_a = result.child) === null || _a === void 0 ? void 0 : _a.killed) ? "SIGTERM" : null)) !== null && _e !== void 0 ? _e : null;
|
|
440
525
|
const context = {
|
|
441
526
|
...baseContext(output),
|
|
442
527
|
exitCode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "visual-exec",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"homepage": "https://github.com/jchip/fynjs/tree/main/packages/visual-exec",
|
|
5
5
|
"description": "Visual shell exec with spinners and progress display",
|
|
6
6
|
"type": "module",
|
|
@@ -14,12 +14,12 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=22.
|
|
17
|
+
"node": ">=22.18.0"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
21
21
|
"test": "vitest run",
|
|
22
|
-
"ci:check": "tsc --noEmit -p tsconfig.json && npm test",
|
|
22
|
+
"ci:check": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json && npm test",
|
|
23
23
|
"test:watch": "vitest",
|
|
24
24
|
"prepublishOnly": "xrun --serial build test",
|
|
25
25
|
"postpack": "publish-util-postpack"
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"ci-info": "^4.4.0",
|
|
50
50
|
"has-ansi": "^6.0.2",
|
|
51
51
|
"strip-ansi": "^7.2.0",
|
|
52
|
-
"visual-logger": "^2.
|
|
53
|
-
"xsh": "^1.0
|
|
52
|
+
"visual-logger": "^2.1.0",
|
|
53
|
+
"xsh": "^1.1.0"
|
|
54
54
|
},
|
|
55
55
|
"sideEffects": false
|
|
56
56
|
}
|