@crewhaus/tool-proc 0.7.0 → 0.8.1
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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +11 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +202 -90
- package/dist/lib/addr.d.ts +129 -0
- package/dist/lib/addr.js +391 -0
- package/dist/lib/config.d.ts +34 -0
- package/dist/lib/config.js +133 -0
- package/dist/lib/env.d.ts +18 -5
- package/dist/lib/env.js +18 -4
- package/dist/lib/format.d.ts +12 -7
- package/dist/lib/format.js +31 -81
- package/dist/lib/which.d.ts +17 -0
- package/dist/lib/which.js +80 -0
- package/dist/registry.d.ts +39 -0
- package/dist/registry.js +66 -2
- package/dist/safe-path.d.ts +11 -0
- package/dist/safe-path.js +62 -10
- package/dist/spawn.d.ts +39 -0
- package/dist/spawn.js +85 -121
- package/package.json +11 -6
package/dist/spawn.js
CHANGED
|
@@ -1,57 +1,75 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* each stream a bounded drain window and move on with what arrived.
|
|
25
|
-
*/
|
|
26
|
-
const DRAIN_GRACE_MS = 500;
|
|
1
|
+
import { constants } from "node:os";
|
|
2
|
+
import { spawnBounded } from "@crewhaus/tool-safety/streams";
|
|
3
|
+
/** No UTF-16 code unit takes more than three bytes of UTF-8. */
|
|
4
|
+
const BYTES_PER_CHAR = 3;
|
|
5
|
+
const utf8Bytes = (text) => Buffer.byteLength(text, "utf8");
|
|
6
|
+
/** `text` cut to at most `chars` UTF-16 units from the front, never mid-pair. */
|
|
7
|
+
function headChars(text, chars) {
|
|
8
|
+
if (text.length <= chars)
|
|
9
|
+
return text;
|
|
10
|
+
const cut = text.slice(0, chars);
|
|
11
|
+
const last = cut.charCodeAt(cut.length - 1);
|
|
12
|
+
return last >= 0xd800 && last <= 0xdbff ? cut.slice(0, -1) : cut;
|
|
13
|
+
}
|
|
14
|
+
/** `text` cut to at most `chars` UTF-16 units from the end, never mid-pair. */
|
|
15
|
+
function tailChars(text, chars) {
|
|
16
|
+
if (chars <= 0)
|
|
17
|
+
return "";
|
|
18
|
+
if (text.length <= chars)
|
|
19
|
+
return text;
|
|
20
|
+
const cut = text.slice(text.length - chars);
|
|
21
|
+
const first = cut.charCodeAt(0);
|
|
22
|
+
return first >= 0xdc00 && first <= 0xdfff ? cut.slice(1) : cut;
|
|
23
|
+
}
|
|
27
24
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* uninterruptible kernel wait either — a stalled NFS read, a wayward driver.
|
|
33
|
-
* `proc.exited` then never resolves, and every other bound in this file is
|
|
34
|
-
* downstream of that await, so the whole call hangs with no deadline left to
|
|
35
|
-
* save it. This is the last bound: past it the tool reports what it has and
|
|
36
|
-
* lets go. (There is no portable way to put a process into that state, so
|
|
37
|
-
* this is the one guarantee here with no test behind it.)
|
|
25
|
+
* One stream, capped to `maxChars`: its head and its tail — a command's head
|
|
26
|
+
* says what it started doing, its tail how it ended — with a marker stating
|
|
27
|
+
* exactly how many BYTES were dropped between them. Bytes, because the part
|
|
28
|
+
* past the byte budget was counted as it streamed past and never decoded.
|
|
38
29
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
30
|
+
function capStream(head, tail, truncated, omittedBytes, maxChars) {
|
|
31
|
+
const headRoom = Math.floor(maxChars / 2);
|
|
32
|
+
const tailRoom = maxChars - headRoom;
|
|
33
|
+
if (!truncated) {
|
|
34
|
+
if (head.length <= maxChars)
|
|
35
|
+
return { text: head, truncated: false };
|
|
36
|
+
const h = headChars(head, headRoom);
|
|
37
|
+
const t = tailChars(head, tailRoom);
|
|
38
|
+
const dropped = utf8Bytes(head) - utf8Bytes(h) - utf8Bytes(t);
|
|
39
|
+
return { text: `${h}\n...[${dropped} bytes dropped]...\n${t}`, truncated: true };
|
|
40
|
+
}
|
|
41
|
+
const h = headChars(head, headRoom);
|
|
42
|
+
const whole = tail ?? "";
|
|
43
|
+
const t = tailChars(whole, tailRoom);
|
|
44
|
+
const dropped = omittedBytes + (utf8Bytes(head) - utf8Bytes(h)) + (utf8Bytes(whole) - utf8Bytes(t));
|
|
45
|
+
return { text: `${h}\n...[${dropped} bytes dropped]...\n${t}`, truncated: true };
|
|
46
|
+
}
|
|
47
|
+
/** The exit status a shell would report: 128 + the signal's number for a signalled child. */
|
|
48
|
+
function exitStatus(r) {
|
|
49
|
+
if (r.exitCode !== null)
|
|
50
|
+
return r.exitCode;
|
|
51
|
+
if (r.signal !== null) {
|
|
52
|
+
const n = constants.signals[r.signal];
|
|
53
|
+
if (n !== undefined)
|
|
54
|
+
return 128 + n;
|
|
53
55
|
}
|
|
54
|
-
|
|
56
|
+
return -1;
|
|
57
|
+
}
|
|
58
|
+
export async function runOnce(argv, options) {
|
|
59
|
+
const max = Math.max(0, options.maxOutputChars);
|
|
60
|
+
const tailRoom = max - Math.floor(max / 2);
|
|
61
|
+
const r = await spawnBounded({
|
|
62
|
+
cmd: argv,
|
|
63
|
+
cwd: options.cwd,
|
|
64
|
+
env: options.env,
|
|
65
|
+
...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
|
|
66
|
+
timeoutMs: options.timeoutMs,
|
|
67
|
+
maxStdoutBytes: max * BYTES_PER_CHAR,
|
|
68
|
+
maxStderrBytes: max * BYTES_PER_CHAR,
|
|
69
|
+
tailBytes: tailRoom * BYTES_PER_CHAR,
|
|
70
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
71
|
+
});
|
|
72
|
+
if (r.spawnError !== undefined) {
|
|
55
73
|
return {
|
|
56
74
|
argv,
|
|
57
75
|
exitCode: -1,
|
|
@@ -60,78 +78,24 @@ export async function runOnce(argv, options) {
|
|
|
60
78
|
stdoutTruncated: false,
|
|
61
79
|
stderrTruncated: false,
|
|
62
80
|
timedOut: false,
|
|
63
|
-
durationMs:
|
|
64
|
-
spawnError:
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
let timedOut = false;
|
|
68
|
-
let reapGiveUp;
|
|
69
|
-
const term = setTimeout(() => {
|
|
70
|
-
timedOut = true;
|
|
71
|
-
try {
|
|
72
|
-
proc.kill("SIGTERM");
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
// Already gone between the timer firing and the signal.
|
|
76
|
-
}
|
|
77
|
-
}, options.timeoutMs);
|
|
78
|
-
const hardKill = setTimeout(() => {
|
|
79
|
-
try {
|
|
80
|
-
proc.kill("SIGKILL");
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
// Already gone.
|
|
84
|
-
}
|
|
85
|
-
}, options.timeoutMs + KILL_GRACE_MS);
|
|
86
|
-
try {
|
|
87
|
-
// The spread options above widen Bun's stdio inference, so name the
|
|
88
|
-
// piped shape explicitly rather than let the union leak outward.
|
|
89
|
-
// `.catch` is attached HERE, not after the race: the loser of the race
|
|
90
|
-
// below stays pending, and a stream torn down with the killed process
|
|
91
|
-
// would otherwise reject with nobody listening — an unhandled rejection
|
|
92
|
-
// that takes down the harness rather than the command that caused it.
|
|
93
|
-
const stdoutText = new Response(proc.stdout)
|
|
94
|
-
.text()
|
|
95
|
-
.catch(() => "");
|
|
96
|
-
const stderrText = new Response(proc.stderr)
|
|
97
|
-
.text()
|
|
98
|
-
.catch(() => "");
|
|
99
|
-
const abandonedMarker = Symbol("abandoned");
|
|
100
|
-
const reaped = await Promise.race([
|
|
101
|
-
proc.exited,
|
|
102
|
-
new Promise((resolve) => {
|
|
103
|
-
reapGiveUp = setTimeout(() => resolve(abandonedMarker), options.timeoutMs + KILL_GRACE_MS + REAP_GRACE_MS);
|
|
104
|
-
}),
|
|
105
|
-
]);
|
|
106
|
-
const abandoned = reaped === abandonedMarker;
|
|
107
|
-
const exitCode = abandoned ? -1 : reaped;
|
|
108
|
-
const drainFallback = () => new Promise((resolve) => setTimeout(() => resolve(""), DRAIN_GRACE_MS));
|
|
109
|
-
const [rawOut, rawErr] = await Promise.all([
|
|
110
|
-
Promise.race([stdoutText, drainFallback()]),
|
|
111
|
-
Promise.race([stderrText, drainFallback()]),
|
|
112
|
-
]);
|
|
113
|
-
const out = capText(rawOut, options.maxOutputChars);
|
|
114
|
-
const err = capText(rawErr, options.maxOutputChars);
|
|
115
|
-
return {
|
|
116
|
-
argv,
|
|
117
|
-
exitCode,
|
|
118
|
-
stdout: out.text,
|
|
119
|
-
stderr: err.text,
|
|
120
|
-
stdoutTruncated: out.truncated,
|
|
121
|
-
stderrTruncated: err.truncated,
|
|
122
|
-
timedOut: timedOut || abandoned,
|
|
123
|
-
durationMs: elapsed(),
|
|
124
|
-
...(abandoned ? { abandoned: true } : {}),
|
|
81
|
+
durationMs: r.durationMs,
|
|
82
|
+
spawnError: r.spawnError,
|
|
125
83
|
};
|
|
126
84
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
85
|
+
const out = capStream(r.stdout, r.stdoutTail, r.stdoutTruncated, r.stdoutOmittedBytes, max);
|
|
86
|
+
const err = capStream(r.stderr, r.stderrTail, r.stderrTruncated, r.stderrOmittedBytes, max);
|
|
87
|
+
return {
|
|
88
|
+
argv,
|
|
89
|
+
exitCode: r.abandoned ? -1 : exitStatus(r),
|
|
90
|
+
stdout: out.text,
|
|
91
|
+
stderr: err.text,
|
|
92
|
+
stdoutTruncated: out.truncated,
|
|
93
|
+
stderrTruncated: err.truncated,
|
|
94
|
+
timedOut: r.timedOut || r.abandoned,
|
|
95
|
+
durationMs: r.durationMs,
|
|
96
|
+
...(r.abandoned ? { abandoned: true } : {}),
|
|
97
|
+
...(r.outputComplete ? {} : { outputIncomplete: true }),
|
|
98
|
+
};
|
|
135
99
|
}
|
|
136
100
|
/** A deadline-aware sleep: resolves early (and reports it) when aborted. */
|
|
137
101
|
export function sleep(ms, signal) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-proc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic process tools: background processes, log tailing, port and file waits, and retry with backoff",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
19
|
+
"@crewhaus/errors": "0.8.1",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.1",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.1",
|
|
22
|
+
"@crewhaus/tool-safety": "0.8.1",
|
|
21
23
|
"zod": "^3.23.8"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
@@ -41,5 +43,8 @@
|
|
|
41
43
|
"publishConfig": {
|
|
42
44
|
"access": "public"
|
|
43
45
|
},
|
|
44
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
46
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
47
|
+
"engines": {
|
|
48
|
+
"bun": ">=1.3.14"
|
|
49
|
+
}
|
|
45
50
|
}
|