@xynogen/pix-ssh 0.5.2 → 0.5.6
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/README.md +9 -3
- package/package.json +5 -5
- package/src/index.ts +67 -26
- package/src/lib.ts +28 -28
package/README.md
CHANGED
|
@@ -49,16 +49,22 @@ Completed calls collapse after the configured Pix delay into a status row such a
|
|
|
49
49
|
pi install npm:@xynogen/pix-ssh
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
> Standalone
|
|
52
|
+
> Standalone and opt-in. [`@xynogen/pix-core`](https://www.npmjs.com/package/@xynogen/pix-core) does not bundle it. Remote execution over SSH is a networked, privileged capability.
|
|
53
53
|
|
|
54
54
|
## Full distro
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
This package is part of [Pix](https://github.com/xynogen/pix-mono). The installer sets up Pi and the full distro. See [Install](https://github.com/xynogen/pix-mono#install) for the notes for each OS.
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
+
# Linux / macOS
|
|
59
60
|
curl -fsSL https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.sh | sh
|
|
60
61
|
```
|
|
61
62
|
|
|
63
|
+
```powershell
|
|
64
|
+
# Windows
|
|
65
|
+
irm https://raw.githubusercontent.com/xynogen/pix-mono/main/scripts/install.ps1 | iex
|
|
66
|
+
```
|
|
67
|
+
|
|
62
68
|
## License
|
|
63
69
|
|
|
64
|
-
MIT
|
|
70
|
+
MIT. See [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xynogen/pix-ssh",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Pi tool
|
|
3
|
+
"version": "0.5.6",
|
|
4
|
+
"description": "Pi tool \u2014 ssh_run: run remote commands over SSH with password/key auth and remote sudo",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"
|
|
41
|
-
"@xynogen/pix-
|
|
42
|
-
"@xynogen/pix-runtime": "^0.8.0"
|
|
40
|
+
"@xynogen/pix-pretty": "^1.30.0",
|
|
41
|
+
"@xynogen/pix-runtime": "^0.13.0"
|
|
43
42
|
},
|
|
44
43
|
"peerDependencies": {
|
|
44
|
+
"typebox": "*",
|
|
45
45
|
"@earendil-works/pi-coding-agent": "*",
|
|
46
46
|
"@earendil-works/pi-tui": "*"
|
|
47
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -27,28 +27,34 @@
|
|
|
27
27
|
|
|
28
28
|
import type { AgentToolUpdateCallback, ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
29
29
|
import { Text } from "@earendil-works/pi-tui";
|
|
30
|
-
import {
|
|
30
|
+
import { resolveBaseBackground } from "@xynogen/pix-pretty/ansi";
|
|
31
|
+
import { collapsedCommandRow, commandPreview } from "@xynogen/pix-pretty/command-preview";
|
|
31
32
|
import { MAX_PREVIEW_LINES } from "@xynogen/pix-pretty/config";
|
|
32
33
|
import { type OverlayResult, showOverlay } from "@xynogen/pix-pretty/gate-overlay";
|
|
33
34
|
import { icon } from "@xynogen/pix-pretty/icon-catalog";
|
|
34
35
|
import { renderBashOutput } from "@xynogen/pix-pretty/renderers";
|
|
35
36
|
import type { RenderContextLike, ThemeLike, ToolResultLike } from "@xynogen/pix-pretty/types";
|
|
36
37
|
import {
|
|
38
|
+
BODY_PAD,
|
|
39
|
+
bodyLine,
|
|
37
40
|
dotJoin,
|
|
38
41
|
fillToolBackground,
|
|
42
|
+
formatCollapsedToolRow,
|
|
43
|
+
formatToolCallTitle,
|
|
39
44
|
frameToolResult,
|
|
45
|
+
getErrorMessage,
|
|
40
46
|
getTextContent,
|
|
41
47
|
hideCollapsedToolCall,
|
|
48
|
+
moreLines,
|
|
42
49
|
normalizeLineEndings,
|
|
43
|
-
renderCollapsedToolRow,
|
|
44
50
|
renderToolError,
|
|
45
51
|
ruleFrame,
|
|
46
|
-
sectionRule,
|
|
47
52
|
termW,
|
|
48
53
|
unframeToolResult,
|
|
49
54
|
} from "@xynogen/pix-pretty/utils";
|
|
50
55
|
import { SPINNER } from "@xynogen/pix-pretty/widget-format";
|
|
51
56
|
import { getUnattendedMode, withAgentBlock } from "@xynogen/pix-runtime";
|
|
57
|
+
import { requireTool } from "@xynogen/pix-runtime/binaries";
|
|
52
58
|
import { type CollapseState, tickCollapse } from "@xynogen/pix-runtime/collapse";
|
|
53
59
|
import { Type } from "typebox";
|
|
54
60
|
import {
|
|
@@ -351,6 +357,22 @@ function formatAliasList(aliases: HostAlias[]): string {
|
|
|
351
357
|
* given, otherwise the alias inventory from ~/.ssh/config. Read-only.
|
|
352
358
|
* Details carry `_type: "sshInfo"` so renderResult falls to its generic
|
|
353
359
|
* plain-text branch (no collapse, no exit-code framing). */
|
|
360
|
+
/**
|
|
361
|
+
* The pix-runtime lookup message for the first missing binary this action
|
|
362
|
+
* needs (ssh, plus scp for file transfers), or undefined when all resolve.
|
|
363
|
+
* sshpass is checked lazily: it only matters when a login password is used.
|
|
364
|
+
*/
|
|
365
|
+
export function missingSshTools(action: "command" | "file"): string | undefined {
|
|
366
|
+
for (const name of action === "file" ? ["ssh", "scp"] : ["ssh"]) {
|
|
367
|
+
try {
|
|
368
|
+
requireTool(name);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
return getErrorMessage(err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
|
|
354
376
|
async function infoResult(host: string | undefined, sig?: AbortSignal) {
|
|
355
377
|
const details = { _type: "sshInfo" as const };
|
|
356
378
|
if (host?.trim()) {
|
|
@@ -358,7 +380,7 @@ async function infoResult(host: string | undefined, sig?: AbortSignal) {
|
|
|
358
380
|
try {
|
|
359
381
|
spec = parseHost(host);
|
|
360
382
|
} catch (err) {
|
|
361
|
-
const msg =
|
|
383
|
+
const msg = getErrorMessage(err);
|
|
362
384
|
return {
|
|
363
385
|
content: [{ type: "text" as const, text: `ssh_run failed: ${msg}` }],
|
|
364
386
|
details,
|
|
@@ -379,6 +401,7 @@ async function infoResult(host: string | undefined, sig?: AbortSignal) {
|
|
|
379
401
|
export default function (pi: ExtensionAPI): void {
|
|
380
402
|
pi.registerTool({
|
|
381
403
|
name: "ssh_run",
|
|
404
|
+
...({ exposure: "deferred" } as const),
|
|
382
405
|
label: "Run over SSH",
|
|
383
406
|
description:
|
|
384
407
|
"Run a command or transfer files/directories on a REMOTE host over SSH. " +
|
|
@@ -496,11 +519,25 @@ export default function (pi: ExtensionAPI): void {
|
|
|
496
519
|
};
|
|
497
520
|
}
|
|
498
521
|
|
|
522
|
+
// Fail up front with pix's install hint: the probes below treat a spawn
|
|
523
|
+
// error as "unreachable", which would hide a missing ssh/scp.
|
|
524
|
+
const missingTool = missingSshTools(action);
|
|
525
|
+
if (missingTool) {
|
|
526
|
+
return {
|
|
527
|
+
content: [{ type: "text", text: `ssh_run failed: ${missingTool}` }],
|
|
528
|
+
details: makeDetails(command, params.host, sudo, reason, {
|
|
529
|
+
outcome: "error",
|
|
530
|
+
errorKind: "execution",
|
|
531
|
+
}),
|
|
532
|
+
isError: true,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
499
536
|
let spec: HostSpec;
|
|
500
537
|
try {
|
|
501
538
|
spec = parseHost(params.host);
|
|
502
539
|
} catch (err) {
|
|
503
|
-
const msg =
|
|
540
|
+
const msg = getErrorMessage(err);
|
|
504
541
|
return {
|
|
505
542
|
content: [{ type: "text", text: `ssh_run failed: ${msg}` }],
|
|
506
543
|
details: makeDetails(command, params.host, sudo, reason, {
|
|
@@ -750,7 +787,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
750
787
|
...(sig ? { signal: sig } : {}),
|
|
751
788
|
});
|
|
752
789
|
} catch (err) {
|
|
753
|
-
const msg =
|
|
790
|
+
const msg = getErrorMessage(err);
|
|
754
791
|
return {
|
|
755
792
|
content: [{ type: "text", text: `ssh_run failed: ${msg}` }],
|
|
756
793
|
details: makeDetails(
|
|
@@ -851,20 +888,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
851
888
|
if (args.action === "info") {
|
|
852
889
|
text.setText(
|
|
853
890
|
fillToolBackground(
|
|
854
|
-
`${theme
|
|
891
|
+
`${formatToolCallTitle(theme, "ssh info", renderCtx)} ${theme.fg("dim", host || "list aliases")}`,
|
|
855
892
|
),
|
|
856
893
|
);
|
|
857
894
|
return text;
|
|
858
895
|
}
|
|
859
896
|
const operation = normalizeOperation(args);
|
|
860
|
-
const command =
|
|
897
|
+
const command = operation.command || "(empty command)";
|
|
861
898
|
const prefix = operation.sudo ? "sudo " : "";
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
899
|
+
return commandPreview(
|
|
900
|
+
`${formatToolCallTitle(theme, operation.action === "file" ? "ssh file" : "ssh", renderCtx)} ${theme.fg("dim", host)}`,
|
|
901
|
+
prefix + command,
|
|
902
|
+
"bash",
|
|
903
|
+
theme,
|
|
904
|
+
renderCtx.state,
|
|
905
|
+
renderCtx.invalidate,
|
|
906
|
+
renderCtx.expanded,
|
|
866
907
|
);
|
|
867
|
-
return text;
|
|
868
908
|
}) as never,
|
|
869
909
|
|
|
870
910
|
renderResult: ((
|
|
@@ -884,7 +924,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
884
924
|
text.setText(renderToolError(getTextContent(result) || "Error", theme));
|
|
885
925
|
} else {
|
|
886
926
|
text.setText(
|
|
887
|
-
fillToolBackground(
|
|
927
|
+
fillToolBackground(`${BODY_PAD}${theme.fg("muted", getTextContent(result) || "done")}`),
|
|
888
928
|
);
|
|
889
929
|
}
|
|
890
930
|
return isPartial ? text : completed(renderCtx.isError);
|
|
@@ -901,21 +941,24 @@ export default function (pi: ExtensionAPI): void {
|
|
|
901
941
|
)
|
|
902
942
|
) {
|
|
903
943
|
const status = details.outcome === "success" ? "success" : "error";
|
|
904
|
-
|
|
905
|
-
|
|
944
|
+
return collapsedCommandRow(
|
|
945
|
+
formatCollapsedToolRow(
|
|
906
946
|
theme,
|
|
907
947
|
"ssh",
|
|
908
|
-
|
|
948
|
+
details.host,
|
|
909
949
|
terminalMeta(details),
|
|
910
950
|
status,
|
|
911
|
-
),
|
|
951
|
+
).trimEnd(),
|
|
952
|
+
safeOneLine(details.command),
|
|
953
|
+
theme,
|
|
912
954
|
);
|
|
913
|
-
return text;
|
|
914
955
|
}
|
|
915
956
|
|
|
916
957
|
if (details.outcome === "awaiting-approval" || details.outcome === "running") {
|
|
917
958
|
text.setText(
|
|
918
|
-
fillToolBackground(
|
|
959
|
+
fillToolBackground(
|
|
960
|
+
`${BODY_PAD}${theme.fg("muted", getTextContent(result) || "working")}`,
|
|
961
|
+
),
|
|
919
962
|
);
|
|
920
963
|
return text;
|
|
921
964
|
}
|
|
@@ -925,7 +968,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
925
968
|
text.setText(
|
|
926
969
|
details.outcome === "error"
|
|
927
970
|
? renderToolError(diagnostic, theme)
|
|
928
|
-
: fillToolBackground(
|
|
971
|
+
: fillToolBackground(`${BODY_PAD}${theme.fg("warning", diagnostic)}`),
|
|
929
972
|
);
|
|
930
973
|
return isPartial ? text : completed(true);
|
|
931
974
|
}
|
|
@@ -937,18 +980,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
937
980
|
const lineCount = lines.length;
|
|
938
981
|
|
|
939
982
|
if (!rendered) {
|
|
940
|
-
text.setText(fillToolBackground(
|
|
983
|
+
text.setText(fillToolBackground(`${BODY_PAD}${summary}`));
|
|
941
984
|
return isPartial ? text : completed(details.outcome !== "success");
|
|
942
985
|
}
|
|
943
986
|
|
|
944
987
|
const maxShow = renderCtx.expanded ? lineCount : MAX_PREVIEW_LINES;
|
|
945
988
|
const show = lines.slice(0, maxShow);
|
|
946
|
-
const footer =
|
|
947
|
-
lineCount > maxShow ? [`${FG_DIM} … ${lineCount - maxShow} more lines${RST}`] : [];
|
|
989
|
+
const footer = lineCount > maxShow ? [moreLines(lineCount - maxShow, theme)] : [];
|
|
948
990
|
const statusKey = details.outcome === "success" ? "success" : "error";
|
|
949
991
|
const paint = (s: string) => theme.fg(statusKey, s);
|
|
950
|
-
const
|
|
951
|
-
const body = show.map((line) => ` ${sectionRule(line, theme, sw) ?? line}`);
|
|
992
|
+
const body = show.map((line) => bodyLine(line, theme));
|
|
952
993
|
const out = isPartial ? [...body, ...footer] : ruleFrame(body, footer, termW(), paint);
|
|
953
994
|
text.setText(fillToolBackground(out.join("\n")));
|
|
954
995
|
return text;
|
package/src/lib.ts
CHANGED
|
@@ -13,14 +13,21 @@
|
|
|
13
13
|
* entered once per session per host.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { spawn } from "node:child_process";
|
|
16
|
+
import type { spawn } from "node:child_process";
|
|
17
17
|
import { createHash } from "node:crypto";
|
|
18
18
|
import { globSync, readFileSync } from "node:fs";
|
|
19
|
-
import { homedir, tmpdir } from "node:os";
|
|
20
19
|
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_MAX_BYTES,
|
|
22
|
+
DEFAULT_MAX_LINES,
|
|
23
|
+
truncateHead,
|
|
24
|
+
} from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import { spawnTool } from "@xynogen/pix-runtime/exec";
|
|
26
|
+
import { expandHome, homeDir, tempDir } from "@xynogen/pix-runtime/paths";
|
|
27
|
+
|
|
28
|
+
// Pi's own tool-output limits (50KB / 2000 lines), so every tool caps the same way.
|
|
29
|
+
export const MAX_OUTPUT_BYTES = DEFAULT_MAX_BYTES;
|
|
30
|
+
export const MAX_OUTPUT_LINES = DEFAULT_MAX_LINES;
|
|
24
31
|
|
|
25
32
|
/** Per-host approval TTL (ms) — mirrors sudo's PAM ticket window (~15 min). */
|
|
26
33
|
export const APPROVAL_TTL_MS = 15 * 60_000;
|
|
@@ -144,7 +151,7 @@ export function resolveSshHost(spec: HostSpec, signal?: AbortSignal): Promise<Ho
|
|
|
144
151
|
|
|
145
152
|
return new Promise((resolve) => {
|
|
146
153
|
let stdout = "";
|
|
147
|
-
const proc =
|
|
154
|
+
const proc = spawnTool("ssh", args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
148
155
|
proc.stdout.on("data", (chunk: Buffer) => {
|
|
149
156
|
stdout += chunk.toString();
|
|
150
157
|
});
|
|
@@ -216,9 +223,7 @@ export function parseHostAliases(text: string): HostAlias[] {
|
|
|
216
223
|
/** Expand a Path with a leading `~` and resolve relative Includes against
|
|
217
224
|
* the containing config's directory (OpenSSH semantics). */
|
|
218
225
|
function expandConfigPath(pattern: string, baseDir: string): string {
|
|
219
|
-
|
|
220
|
-
if (p.startsWith("~/")) p = join(homedir(), p.slice(2));
|
|
221
|
-
else if (p === "~") p = homedir();
|
|
226
|
+
const p = expandHome(pattern);
|
|
222
227
|
return isAbsolute(p) ? p : resolvePath(baseDir, p);
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -228,7 +233,7 @@ function expandConfigPath(pattern: string, baseDir: string): string {
|
|
|
228
233
|
* (OpenSSH tolerates them). `seen` guards against Include cycles.
|
|
229
234
|
*/
|
|
230
235
|
export function readSshConfigAliases(
|
|
231
|
-
path = join(
|
|
236
|
+
path = join(homeDir(), ".ssh", "config"),
|
|
232
237
|
seen = new Set<string>(),
|
|
233
238
|
): HostAlias[] {
|
|
234
239
|
if (seen.has(path)) return [];
|
|
@@ -239,7 +244,7 @@ export function readSshConfigAliases(
|
|
|
239
244
|
} catch {
|
|
240
245
|
return [];
|
|
241
246
|
}
|
|
242
|
-
const baseDir = join(
|
|
247
|
+
const baseDir = join(homeDir(), ".ssh");
|
|
243
248
|
const out: HostAlias[] = [];
|
|
244
249
|
for (const raw of text.split("\n")) {
|
|
245
250
|
const line = raw.replace(/#.*$/, "").trim();
|
|
@@ -280,7 +285,7 @@ export function resolveHostInfo(spec: HostSpec, signal?: AbortSignal): Promise<H
|
|
|
280
285
|
args.push(hostTarget(spec));
|
|
281
286
|
return new Promise((resolve) => {
|
|
282
287
|
let stdout = "";
|
|
283
|
-
const proc =
|
|
288
|
+
const proc = spawnTool("ssh", args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
284
289
|
proc.stdout.on("data", (c: Buffer) => {
|
|
285
290
|
stdout += c.toString();
|
|
286
291
|
});
|
|
@@ -300,7 +305,7 @@ export function hostTarget(spec: HostSpec): string {
|
|
|
300
305
|
export function controlPathFor(spec: HostSpec): string {
|
|
301
306
|
const key = `${spec.user ?? ""}@${spec.host}:${spec.port ?? 22}`;
|
|
302
307
|
const hash = createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
303
|
-
return join(
|
|
308
|
+
return join(tempDir(), `pix-ssh-${hash}.sock`);
|
|
304
309
|
}
|
|
305
310
|
|
|
306
311
|
// ── ssh argv construction ────────────────────────────────────────────────────
|
|
@@ -405,17 +410,12 @@ export function truncate(
|
|
|
405
410
|
maxLines = MAX_OUTPUT_LINES,
|
|
406
411
|
maxBytes = MAX_OUTPUT_BYTES,
|
|
407
412
|
): { text: string; truncated: boolean } {
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let result = kept.join("\n");
|
|
415
|
-
if (Buffer.byteLength(result, "utf8") > maxBytes) {
|
|
416
|
-
result = Buffer.from(result, "utf8").subarray(0, maxBytes).toString("utf8");
|
|
417
|
-
}
|
|
418
|
-
return { text: result, truncated: true };
|
|
413
|
+
const r = truncateHead(text, { maxLines, maxBytes });
|
|
414
|
+
if (!r.firstLineExceedsLimit) return { text: r.content, truncated: r.truncated };
|
|
415
|
+
// One line over the byte cap (minified JSON, a base64 blob): truncateHead keeps
|
|
416
|
+
// nothing. Keep its first maxBytes instead, cut on a UTF-8 boundary.
|
|
417
|
+
const head = Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
|
|
418
|
+
return { text: head.replace(/\uFFFD$/, ""), truncated: true };
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
// ── Runners ──────────────────────────────────────────────────────────────────
|
|
@@ -440,7 +440,7 @@ export function probeKeyAuth(
|
|
|
440
440
|
const args = [...baseSshArgs(spec, controlPath), "-o", "BatchMode=yes", hostTarget(spec), "true"];
|
|
441
441
|
return new Promise((resolve) => {
|
|
442
442
|
let stderr = "";
|
|
443
|
-
const proc =
|
|
443
|
+
const proc = spawnTool("ssh", args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
444
444
|
proc.stderr.on("data", (c: Buffer) => {
|
|
445
445
|
stderr += c.toString();
|
|
446
446
|
});
|
|
@@ -488,7 +488,7 @@ export function probePasswordAuth(
|
|
|
488
488
|
];
|
|
489
489
|
return new Promise((resolve) => {
|
|
490
490
|
let stderr = "";
|
|
491
|
-
const proc =
|
|
491
|
+
const proc = spawnTool("sshpass", args, {
|
|
492
492
|
stdio: ["ignore", "ignore", "pipe"],
|
|
493
493
|
env: { ...process.env, SSHPASS: password },
|
|
494
494
|
});
|
|
@@ -525,7 +525,7 @@ export function probeSudoNoPassword(
|
|
|
525
525
|
"sudo -n true",
|
|
526
526
|
];
|
|
527
527
|
return new Promise((resolve) => {
|
|
528
|
-
const proc =
|
|
528
|
+
const proc = spawnTool("ssh", args, { stdio: ["ignore", "ignore", "ignore"] });
|
|
529
529
|
proc.on("error", () => resolve(false));
|
|
530
530
|
proc.on("close", (code) => resolve(code === 0));
|
|
531
531
|
signal?.addEventListener("abort", () => proc.kill("SIGTERM"), { once: true });
|
|
@@ -621,7 +621,7 @@ function spawnResult(
|
|
|
621
621
|
stdin?: string,
|
|
622
622
|
): Promise<SshResult> {
|
|
623
623
|
return new Promise((resolve, reject) => {
|
|
624
|
-
const proc =
|
|
624
|
+
const proc = spawnTool(bin, args, { stdio: ["pipe", "pipe", "pipe"], env });
|
|
625
625
|
let stdout = "";
|
|
626
626
|
let stderr = "";
|
|
627
627
|
proc.stdout.on("data", (c: Buffer) => {
|