@crewhaus/tool-media 0.7.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/README.md +156 -0
- package/dist/fixtures.d.ts +81 -0
- package/dist/fixtures.js +385 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +1132 -0
- package/dist/lib/barcode.d.ts +43 -0
- package/dist/lib/barcode.js +310 -0
- package/dist/lib/bytes.d.ts +45 -0
- package/dist/lib/bytes.js +162 -0
- package/dist/lib/chart.d.ts +32 -0
- package/dist/lib/chart.js +290 -0
- package/dist/lib/color.d.ts +53 -0
- package/dist/lib/color.js +267 -0
- package/dist/lib/diagram.d.ts +48 -0
- package/dist/lib/diagram.js +256 -0
- package/dist/lib/ffprobe.d.ts +36 -0
- package/dist/lib/ffprobe.js +154 -0
- package/dist/lib/identify.d.ts +39 -0
- package/dist/lib/identify.js +519 -0
- package/dist/lib/imageops.d.ts +86 -0
- package/dist/lib/imageops.js +236 -0
- package/dist/lib/jpeg.d.ts +81 -0
- package/dist/lib/jpeg.js +388 -0
- package/dist/lib/png.d.ts +59 -0
- package/dist/lib/png.js +466 -0
- package/dist/lib/qr.d.ts +70 -0
- package/dist/lib/qr.js +544 -0
- package/dist/lib/raster.d.ts +18 -0
- package/dist/lib/raster.js +82 -0
- package/dist/lib/sparkline.d.ts +21 -0
- package/dist/lib/sparkline.js +93 -0
- package/dist/lib/subtitle.d.ts +62 -0
- package/dist/lib/subtitle.js +233 -0
- package/dist/lib/svg.d.ts +76 -0
- package/dist/lib/svg.js +168 -0
- package/dist/paths.d.ts +53 -0
- package/dist/paths.js +161 -0
- package/dist/proc.d.ts +39 -0
- package/dist/proc.js +158 -0
- package/package.json +45 -0
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
2
|
+
/** Raised when a caller-supplied path resolves outside the workspace root. */
|
|
3
|
+
export declare class ToolPermissionError extends CrewhausError {
|
|
4
|
+
readonly name = "ToolPermissionError";
|
|
5
|
+
readonly toolName: string;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
constructor(toolName: string, attemptedPath: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A validated path, in both forms the tools need.
|
|
11
|
+
*
|
|
12
|
+
* `real` has every in-workspace symlink already resolved and is what I/O
|
|
13
|
+
* should use — operating on it closes most of the check-to-use window.
|
|
14
|
+
* `abs` is the lexical resolution, which is what a tool must use when the
|
|
15
|
+
* question is about the link ITSELF (`Stat` reporting a symlink target,
|
|
16
|
+
* `RemovePath` deleting a link rather than its destination).
|
|
17
|
+
*/
|
|
18
|
+
export type SafePath = {
|
|
19
|
+
readonly abs: string;
|
|
20
|
+
readonly real: string;
|
|
21
|
+
/** The path as the caller wrote it, for echoing back in results. */
|
|
22
|
+
readonly given: string;
|
|
23
|
+
/** Slash-separated path relative to the workspace root. */
|
|
24
|
+
readonly rel: string;
|
|
25
|
+
};
|
|
26
|
+
/** The workspace root, resolved. Every path is contained within it. */
|
|
27
|
+
export declare function workspaceRoot(): string;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve `rel` inside the workspace, or throw `ToolPermissionError`.
|
|
30
|
+
*
|
|
31
|
+
* The leaf is allowed not to exist — every tool here that creates something
|
|
32
|
+
* needs to validate a destination before it is made — so the deepest
|
|
33
|
+
* ancestor that EXISTS AS A NAME is resolved and the missing tail
|
|
34
|
+
* re-appended. "Exists as a name" rather than "exists": a symlink pointing
|
|
35
|
+
* at a file that is not there yet is still followed by `open(…, "w")`, so it
|
|
36
|
+
* is resolved here too rather than treated as a plain missing leaf. Any
|
|
37
|
+
* failure other than that walk fails closed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveSafe(toolName: string, rel: string, root?: string): SafePath;
|
|
40
|
+
/** A path relative to the workspace root, with `/` separators on every OS. */
|
|
41
|
+
export declare function toPosix(p: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* True when `candidate` is `parent` or lives underneath it. Both must already
|
|
44
|
+
* be absolute. Used to keep an archive entry, or a symlink inside an
|
|
45
|
+
* extracted tree, from reaching outside its destination.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isInside(parent: string, candidate: string): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Reject an archive member name that would escape its destination: an
|
|
50
|
+
* absolute path, a Windows drive prefix, or any `..` segment. Checked on the
|
|
51
|
+
* NAME, before extraction, because after extraction the damage is done.
|
|
52
|
+
*/
|
|
53
|
+
export declare function archiveEntryEscapes(name: string): boolean;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path containment — the gate every caller-supplied path in this package
|
|
3
|
+
* passes through before any syscall touches it.
|
|
4
|
+
*
|
|
5
|
+
* The approach is the one `@crewhaus/tool-fs` uses, and for the same reasons:
|
|
6
|
+
* a lexical check alone is fooled by a symlink that lives inside the
|
|
7
|
+
* workspace and points outside it (CWE-59), so the real path is checked too.
|
|
8
|
+
* The workspace root is `process.cwd()`, matching tool-fs, so a harness that
|
|
9
|
+
* trusts one tool's boundary gets the same boundary here.
|
|
10
|
+
*/
|
|
11
|
+
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { CrewhausError } from "@crewhaus/errors";
|
|
14
|
+
/** Raised when a caller-supplied path resolves outside the workspace root. */
|
|
15
|
+
export class ToolPermissionError extends CrewhausError {
|
|
16
|
+
name = "ToolPermissionError";
|
|
17
|
+
toolName;
|
|
18
|
+
path;
|
|
19
|
+
constructor(toolName, attemptedPath) {
|
|
20
|
+
super("tool", `tool "${toolName}" rejected path "${attemptedPath}": resolved location escapes the workspace root`);
|
|
21
|
+
this.toolName = toolName;
|
|
22
|
+
this.path = attemptedPath;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** The workspace root, resolved. Every path is contained within it. */
|
|
26
|
+
export function workspaceRoot() {
|
|
27
|
+
return path.resolve(process.cwd());
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* True when the NAME exists, whether or not it leads anywhere.
|
|
31
|
+
*
|
|
32
|
+
* `existsSync` follows symlinks, so it answers false for a link whose target
|
|
33
|
+
* is missing — and a missing target is exactly the case that matters here: a
|
|
34
|
+
* dangling link is still a door. `open(…, "w")` through `evil -> /etc/x`
|
|
35
|
+
* CREATES `/etc/x`. Probing with `lstat` keeps that name in the part of the
|
|
36
|
+
* path that gets resolved rather than in the "does not exist yet" tail that
|
|
37
|
+
* is appended to the root verbatim.
|
|
38
|
+
*/
|
|
39
|
+
function nameExists(p) {
|
|
40
|
+
try {
|
|
41
|
+
lstatSync(p);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Where `target` would actually land, with every symlink on the way already
|
|
50
|
+
* followed — including one whose own target does not exist yet.
|
|
51
|
+
*
|
|
52
|
+
* `realpathSync` gives up with ENOENT on a dangling link, which would leave
|
|
53
|
+
* that link unresolved and let it stand in for a plain missing file. So the
|
|
54
|
+
* deepest ancestor that exists as a NAME is resolved, a dangling one is
|
|
55
|
+
* followed a hop by hand, and the components that do not exist are appended.
|
|
56
|
+
* The result is the path an `open` would create, which is the only path
|
|
57
|
+
* worth checking containment against.
|
|
58
|
+
*/
|
|
59
|
+
function resolveLocation(target, depth = 0) {
|
|
60
|
+
if (depth > 40)
|
|
61
|
+
throw new Error(`symlink chain at "${target}" is too long to resolve`);
|
|
62
|
+
let probe = target;
|
|
63
|
+
const tail = [];
|
|
64
|
+
while (!nameExists(probe)) {
|
|
65
|
+
tail.unshift(path.basename(probe));
|
|
66
|
+
const parent = path.dirname(probe);
|
|
67
|
+
if (parent === probe)
|
|
68
|
+
break; // reached the filesystem root
|
|
69
|
+
probe = parent;
|
|
70
|
+
}
|
|
71
|
+
let probeReal;
|
|
72
|
+
try {
|
|
73
|
+
probeReal = realpathSync(probe);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
if (err.code !== "ENOENT")
|
|
77
|
+
throw err;
|
|
78
|
+
// The name is there but `realpath` cannot finish it: a symlink with a
|
|
79
|
+
// missing target. `readlinkSync` throws EINVAL on anything else, which
|
|
80
|
+
// fails closed.
|
|
81
|
+
const link = readlinkSync(probe);
|
|
82
|
+
// A RELATIVE target resolves against the directory that actually CONTAINS
|
|
83
|
+
// the link, which is not the link's lexical parent when that parent is
|
|
84
|
+
// itself reached through a symlink. `<root>/dirlink/x -> ../y` with
|
|
85
|
+
// `dirlink` pointing out of the root really lands at `<elsewhere>/y`, but
|
|
86
|
+
// measured from the lexical parent it reads as `<root>/y` — an in-root
|
|
87
|
+
// path the caller's path does not lead to. So the parent is made real
|
|
88
|
+
// first. An absolute target ignores the base.
|
|
89
|
+
const base = realpathSync(path.dirname(probe));
|
|
90
|
+
probeReal = resolveLocation(path.resolve(base, link), depth + 1);
|
|
91
|
+
}
|
|
92
|
+
return tail.length > 0 ? path.join(probeReal, ...tail) : probeReal;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve `rel` inside the workspace, or throw `ToolPermissionError`.
|
|
96
|
+
*
|
|
97
|
+
* The leaf is allowed not to exist — every tool here that creates something
|
|
98
|
+
* needs to validate a destination before it is made — so the deepest
|
|
99
|
+
* ancestor that EXISTS AS A NAME is resolved and the missing tail
|
|
100
|
+
* re-appended. "Exists as a name" rather than "exists": a symlink pointing
|
|
101
|
+
* at a file that is not there yet is still followed by `open(…, "w")`, so it
|
|
102
|
+
* is resolved here too rather than treated as a plain missing leaf. Any
|
|
103
|
+
* failure other than that walk fails closed.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
106
|
+
const rootResolved = path.resolve(root);
|
|
107
|
+
const abs = path.resolve(rootResolved, rel);
|
|
108
|
+
// 1) Lexical containment — rejects `..` and absolute escapes. The trailing
|
|
109
|
+
// separator avoids the `/root` vs `/root-sibling` prefix pitfall.
|
|
110
|
+
if (abs !== rootResolved && !abs.startsWith(`${rootResolved}${path.sep}`)) {
|
|
111
|
+
throw new ToolPermissionError(toolName, rel);
|
|
112
|
+
}
|
|
113
|
+
let real;
|
|
114
|
+
try {
|
|
115
|
+
const rootReal = realpathSync(rootResolved);
|
|
116
|
+
real = resolveLocation(abs);
|
|
117
|
+
// 2) Symlink-aware containment. The real path must land inside the real
|
|
118
|
+
// root, so an in-workspace symlink pointing at /etc is refused here.
|
|
119
|
+
if (real !== rootReal && !real.startsWith(`${rootReal}${path.sep}`)) {
|
|
120
|
+
throw new ToolPermissionError(toolName, rel);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err instanceof ToolPermissionError)
|
|
125
|
+
throw err;
|
|
126
|
+
throw new ToolPermissionError(toolName, rel);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
abs,
|
|
130
|
+
real,
|
|
131
|
+
given: rel,
|
|
132
|
+
rel: toPosix(path.relative(rootResolved, abs)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** A path relative to the workspace root, with `/` separators on every OS. */
|
|
136
|
+
export function toPosix(p) {
|
|
137
|
+
return path.sep === "/" ? p : p.split(path.sep).join("/");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* True when `candidate` is `parent` or lives underneath it. Both must already
|
|
141
|
+
* be absolute. Used to keep an archive entry, or a symlink inside an
|
|
142
|
+
* extracted tree, from reaching outside its destination.
|
|
143
|
+
*/
|
|
144
|
+
export function isInside(parent, candidate) {
|
|
145
|
+
const p = path.resolve(parent);
|
|
146
|
+
const c = path.resolve(candidate);
|
|
147
|
+
return c === p || c.startsWith(`${p}${path.sep}`);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Reject an archive member name that would escape its destination: an
|
|
151
|
+
* absolute path, a Windows drive prefix, or any `..` segment. Checked on the
|
|
152
|
+
* NAME, before extraction, because after extraction the damage is done.
|
|
153
|
+
*/
|
|
154
|
+
export function archiveEntryEscapes(name) {
|
|
155
|
+
const normalized = name.replace(/\\/g, "/");
|
|
156
|
+
if (normalized.startsWith("/"))
|
|
157
|
+
return true;
|
|
158
|
+
if (/^[A-Za-z]:/.test(normalized))
|
|
159
|
+
return true;
|
|
160
|
+
return normalized.split("/").some((segment) => segment === "..");
|
|
161
|
+
}
|
package/dist/proc.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawning, with the three properties a process-launching tool in this repo
|
|
3
|
+
* owes its caller: a deadline it cannot outlive, cooperation with the turn's
|
|
4
|
+
* `AbortSignal`, and an output cap that bounds MEMORY rather than being
|
|
5
|
+
* applied after the fact.
|
|
6
|
+
*
|
|
7
|
+
* That last one is why this reads the pipes chunk by chunk instead of
|
|
8
|
+
* `new Response(proc.stdout).text()`: a process that prints a gigabyte must
|
|
9
|
+
* cost a bounded number of bytes here, and be killed, not buffered and then
|
|
10
|
+
* measured.
|
|
11
|
+
*
|
|
12
|
+
* Arguments are always an argv array, never a shell string, so a file name
|
|
13
|
+
* containing a space, a quote or a `;` is an argument rather than a command.
|
|
14
|
+
* Nothing in this package builds a shell line.
|
|
15
|
+
*/
|
|
16
|
+
export type RunResult = {
|
|
17
|
+
readonly code: number;
|
|
18
|
+
readonly stdout: string;
|
|
19
|
+
readonly stderr: string;
|
|
20
|
+
readonly timedOut: boolean;
|
|
21
|
+
/** True when the executable itself could not be found. */
|
|
22
|
+
readonly missing: boolean;
|
|
23
|
+
/** True when either stream was cut off at the byte cap. */
|
|
24
|
+
readonly truncated: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare const DEFAULT_PROCESS_TIMEOUT_MS = 20000;
|
|
27
|
+
export declare const MAX_PROCESS_TIMEOUT_MS = 120000;
|
|
28
|
+
/** Plenty for structured probe output; a process past it is misbehaving. */
|
|
29
|
+
export declare const DEFAULT_MAX_OUTPUT_BYTES: number;
|
|
30
|
+
export type RunOptions = {
|
|
31
|
+
readonly cwd: string;
|
|
32
|
+
readonly timeoutMs: number;
|
|
33
|
+
readonly maxOutputBytes?: number;
|
|
34
|
+
readonly signal?: AbortSignal;
|
|
35
|
+
};
|
|
36
|
+
/** Run `argv` to completion, or kill it when the deadline passes. */
|
|
37
|
+
export declare function runProcess(argv: ReadonlyArray<string>, options: RunOptions): Promise<RunResult>;
|
|
38
|
+
/** A one-line description of a failed run, safe to hand back to a caller. */
|
|
39
|
+
export declare function describeFailure(argv: ReadonlyArray<string>, result: RunResult): string;
|
package/dist/proc.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawning, with the three properties a process-launching tool in this repo
|
|
3
|
+
* owes its caller: a deadline it cannot outlive, cooperation with the turn's
|
|
4
|
+
* `AbortSignal`, and an output cap that bounds MEMORY rather than being
|
|
5
|
+
* applied after the fact.
|
|
6
|
+
*
|
|
7
|
+
* That last one is why this reads the pipes chunk by chunk instead of
|
|
8
|
+
* `new Response(proc.stdout).text()`: a process that prints a gigabyte must
|
|
9
|
+
* cost a bounded number of bytes here, and be killed, not buffered and then
|
|
10
|
+
* measured.
|
|
11
|
+
*
|
|
12
|
+
* Arguments are always an argv array, never a shell string, so a file name
|
|
13
|
+
* containing a space, a quote or a `;` is an argument rather than a command.
|
|
14
|
+
* Nothing in this package builds a shell line.
|
|
15
|
+
*/
|
|
16
|
+
export const DEFAULT_PROCESS_TIMEOUT_MS = 20_000;
|
|
17
|
+
export const MAX_PROCESS_TIMEOUT_MS = 120_000;
|
|
18
|
+
/** Plenty for structured probe output; a process past it is misbehaving. */
|
|
19
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
/** How long a killed process gets to let its pipes reach EOF. */
|
|
21
|
+
const DRAIN_GRACE_MS = 500;
|
|
22
|
+
/**
|
|
23
|
+
* Read a pipe until it ends, `limit` bytes have arrived, or `stop` resolves.
|
|
24
|
+
*
|
|
25
|
+
* That third condition is not decoration. A shell script killed mid-`sleep`
|
|
26
|
+
* leaves the `sleep` holding the write end of the pipe, so waiting for EOF
|
|
27
|
+
* waits for a process nobody is going to kill. `stop` fires a short grace
|
|
28
|
+
* period after the child exits, and the reader is cancelled either way.
|
|
29
|
+
*/
|
|
30
|
+
async function readCapped(stream, limit, stop) {
|
|
31
|
+
if (stream === null)
|
|
32
|
+
return { text: "", truncated: false };
|
|
33
|
+
const reader = stream.getReader();
|
|
34
|
+
const parts = [];
|
|
35
|
+
let total = 0;
|
|
36
|
+
let truncated = false;
|
|
37
|
+
try {
|
|
38
|
+
while (true) {
|
|
39
|
+
const next = await Promise.race([reader.read(), stop]);
|
|
40
|
+
if (next === "stop") {
|
|
41
|
+
truncated = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
const { done, value } = next;
|
|
45
|
+
if (done)
|
|
46
|
+
break;
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
continue;
|
|
49
|
+
if (total + value.length > limit) {
|
|
50
|
+
parts.push(value.subarray(0, Math.max(0, limit - total)));
|
|
51
|
+
total = limit;
|
|
52
|
+
truncated = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
parts.push(value);
|
|
56
|
+
total += value.length;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
// Cancelling releases the pipe, so the child sees EPIPE and stops
|
|
61
|
+
// writing instead of blocking on a reader that has gone away.
|
|
62
|
+
try {
|
|
63
|
+
await reader.cancel();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Already closed.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const joined = new Uint8Array(total);
|
|
70
|
+
let at = 0;
|
|
71
|
+
for (const part of parts) {
|
|
72
|
+
joined.set(part, at);
|
|
73
|
+
at += part.length;
|
|
74
|
+
}
|
|
75
|
+
return { text: new TextDecoder().decode(joined), truncated };
|
|
76
|
+
}
|
|
77
|
+
/** Run `argv` to completion, or kill it when the deadline passes. */
|
|
78
|
+
export async function runProcess(argv, options) {
|
|
79
|
+
const limit = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
|
|
80
|
+
let proc;
|
|
81
|
+
try {
|
|
82
|
+
proc = Bun.spawn([...argv], {
|
|
83
|
+
cwd: options.cwd,
|
|
84
|
+
stdout: "pipe",
|
|
85
|
+
stderr: "pipe",
|
|
86
|
+
stdin: "ignore",
|
|
87
|
+
// Passed explicitly rather than inherited: Bun resolves the
|
|
88
|
+
// executable against the env it is HANDED, and a caller that has
|
|
89
|
+
// adjusted PATH since the process started expects the new one.
|
|
90
|
+
env: { ...process.env },
|
|
91
|
+
// The runtime aborts the turn by firing this signal; Bun forwards it
|
|
92
|
+
// to the child, so a cancelled turn does not leave a process behind.
|
|
93
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
98
|
+
return {
|
|
99
|
+
code: 127,
|
|
100
|
+
stdout: "",
|
|
101
|
+
stderr: message,
|
|
102
|
+
timedOut: false,
|
|
103
|
+
missing: true,
|
|
104
|
+
truncated: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let timedOut = false;
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
timedOut = true;
|
|
110
|
+
try {
|
|
111
|
+
proc.kill("SIGKILL");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Already exited between the timer firing and the kill.
|
|
115
|
+
}
|
|
116
|
+
}, options.timeoutMs);
|
|
117
|
+
let stopReading = () => { };
|
|
118
|
+
const stop = new Promise((resolve) => {
|
|
119
|
+
stopReading = resolve;
|
|
120
|
+
});
|
|
121
|
+
// Reading starts BEFORE the wait on exit: a child that fills its pipe
|
|
122
|
+
// blocks until somebody drains it, and a deadlock is not a deadline.
|
|
123
|
+
const stdout = readCapped(proc.stdout, limit, stop);
|
|
124
|
+
const stderr = readCapped(proc.stderr, limit, stop);
|
|
125
|
+
let drainTimer;
|
|
126
|
+
try {
|
|
127
|
+
const code = await proc.exited;
|
|
128
|
+
drainTimer = setTimeout(() => stopReading("stop"), DRAIN_GRACE_MS);
|
|
129
|
+
const [out, err] = await Promise.all([stdout, stderr]);
|
|
130
|
+
return {
|
|
131
|
+
code,
|
|
132
|
+
stdout: out.text,
|
|
133
|
+
stderr: err.text,
|
|
134
|
+
timedOut,
|
|
135
|
+
missing: code === 127 && out.text === "" && err.text === "",
|
|
136
|
+
truncated: out.truncated || err.truncated,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
if (drainTimer !== undefined)
|
|
142
|
+
clearTimeout(drainTimer);
|
|
143
|
+
stopReading("stop");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** A one-line description of a failed run, safe to hand back to a caller. */
|
|
147
|
+
export function describeFailure(argv, result) {
|
|
148
|
+
const command = argv[0] ?? "";
|
|
149
|
+
if (result.missing) {
|
|
150
|
+
return `\`${command}\` is not installed on this machine, so this cannot be done here`;
|
|
151
|
+
}
|
|
152
|
+
if (result.timedOut) {
|
|
153
|
+
return `\`${command}\` was killed for exceeding its timeout`;
|
|
154
|
+
}
|
|
155
|
+
const detail = (result.stderr.trim() !== "" ? result.stderr : result.stdout).trim();
|
|
156
|
+
const clipped = detail.length > 2000 ? `${detail.slice(0, 2000)}…` : detail;
|
|
157
|
+
return `\`${command}\` exited ${result.code}${clipped === "" ? "" : `: ${clipped}`}`;
|
|
158
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crewhaus/tool-media",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Deterministic media tools: image inspection and transformation, barcode encoding and decoding, chart and diagram rendering, and audio and video probing",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "bun test src"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@crewhaus/errors": "0.7.0",
|
|
19
|
+
"@crewhaus/tool-builder": "0.7.0",
|
|
20
|
+
"@crewhaus/tool-catalog": "0.7.0",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@crewhaus/tool-executor": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"license": "Apache-2.0",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "Max Meier",
|
|
29
|
+
"email": "max@crewhaus.ai",
|
|
30
|
+
"url": "https://crewhaus.ai"
|
|
31
|
+
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/crewhaus/factory.git",
|
|
35
|
+
"directory": "packages/tool-media"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/crewhaus/factory/tree/main/packages/tool-media#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/crewhaus/factory/issues"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
45
|
+
}
|