@smthrs/std 1.0.0-rc.6 → 1.0.0-rc.7
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 +36 -36
- package/dist/cjs/Bash.d.ts +10 -0
- package/dist/cjs/Bash.d.ts.map +1 -1
- package/dist/cjs/Bash.js +13 -12
- package/dist/cjs/Bash.js.map +3 -3
- package/dist/cjs/internal/Exec.d.ts +14 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +65 -27
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/cjs/internal/Text.d.ts +11 -0
- package/dist/cjs/internal/Text.d.ts.map +1 -1
- package/dist/cjs/internal/Text.js +3 -0
- package/dist/cjs/internal/Text.js.map +2 -2
- package/dist/esm/Bash.d.ts +10 -0
- package/dist/esm/Bash.d.ts.map +1 -1
- package/dist/esm/Bash.js +22 -13
- package/dist/esm/Bash.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +14 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +86 -21
- package/dist/esm/internal/Exec.js.map +1 -1
- package/dist/esm/internal/Text.d.ts +11 -0
- package/dist/esm/internal/Text.d.ts.map +1 -1
- package/dist/esm/internal/Text.js +11 -0
- package/dist/esm/internal/Text.js.map +1 -1
- package/docs/api.md +1 -1
- package/docs/concepts/limits-and-disclosure.md +8 -5
- package/docs/guides/run-a-shell-command.md +16 -6
- package/docs/reference/flows.md +2 -2
- package/docs/troubleshooting.md +3 -3
- package/package.json +6 -6
- package/src/Bash.ts +25 -13
- package/src/internal/Exec.ts +125 -43
- package/src/internal/Text.ts +15 -0
|
@@ -20,6 +20,7 @@ import * as ChildProcess from "effect/process/ChildProcess";
|
|
|
20
20
|
import * as Schema from "effect/Schema";
|
|
21
21
|
import * as Stream from "effect/Stream";
|
|
22
22
|
import * as StdError from "../StdError.js";
|
|
23
|
+
import { elideMiddle } from "./Text.js";
|
|
23
24
|
/**
|
|
24
25
|
* Failure codes a buffered execution can report, matching the codes the
|
|
25
26
|
* removed kernel `Shell` used so handler-level mapping is unchanged.
|
|
@@ -46,32 +47,22 @@ export class ExecError extends Schema.TaggedError()("@smthrs/std/ExecError", {
|
|
|
46
47
|
}) {
|
|
47
48
|
}
|
|
48
49
|
const decoder = new TextDecoder("utf-8");
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
* bytes, so at most three leading continuation bytes can precede the first
|
|
55
|
-
* whole character. Skipping them is counted as dropped rather than decoded to a
|
|
56
|
-
* replacement character the process never printed.
|
|
57
|
-
*/
|
|
58
|
-
const decodeTail = (state) => {
|
|
59
|
-
const bytes = new Uint8Array(state.bytes);
|
|
50
|
+
const emptyTail = () => ({ chunks: [], bytes: 0, dropped: 0 });
|
|
51
|
+
/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */
|
|
52
|
+
const isContinuation = (byte) => (byte & 0b1100_0000) === 0b1000_0000;
|
|
53
|
+
const join = (chunks, size) => {
|
|
54
|
+
const bytes = new Uint8Array(size);
|
|
60
55
|
let offset = 0;
|
|
61
|
-
for (const chunk of
|
|
56
|
+
for (const chunk of chunks) {
|
|
62
57
|
bytes.set(chunk, offset);
|
|
63
58
|
offset += chunk.byteLength;
|
|
64
59
|
}
|
|
65
|
-
|
|
66
|
-
return { text: decoder.decode(bytes), droppedBytes: 0 };
|
|
67
|
-
let start = 0;
|
|
68
|
-
while (start < bytes.byteLength && start < 3 && (bytes[start] & 0b1100_0000) === 0b1000_0000)
|
|
69
|
-
start++;
|
|
70
|
-
return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start };
|
|
60
|
+
return bytes;
|
|
71
61
|
};
|
|
72
|
-
|
|
62
|
+
/** Appends one chunk and drops from the front whatever no longer fits. */
|
|
63
|
+
const keepTail = (state, chunk, maxBytes) => {
|
|
73
64
|
if (chunk.byteLength === 0)
|
|
74
|
-
return
|
|
65
|
+
return;
|
|
75
66
|
state.chunks.push(chunk);
|
|
76
67
|
state.bytes += chunk.byteLength;
|
|
77
68
|
while (state.bytes > maxBytes && state.chunks.length > 0) {
|
|
@@ -88,13 +79,87 @@ const boundedTail = (stream, maxBytes, overflow) => Stream.runFold(stream, () =>
|
|
|
88
79
|
state.dropped += excess;
|
|
89
80
|
}
|
|
90
81
|
}
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Joins the retained chunks and decodes them, skipping the partial code point a
|
|
85
|
+
* dropped head can leave behind.
|
|
86
|
+
*
|
|
87
|
+
* A code point is at most four bytes, so at most three leading continuation
|
|
88
|
+
* bytes can precede the first whole character. Skipping them is counted as
|
|
89
|
+
* dropped rather than decoded to a replacement character the process never
|
|
90
|
+
* printed.
|
|
91
|
+
*/
|
|
92
|
+
const decodeTail = (state) => {
|
|
93
|
+
const bytes = join(state.chunks, state.bytes);
|
|
94
|
+
if (state.dropped === 0)
|
|
95
|
+
return { text: decoder.decode(bytes), droppedBytes: 0 };
|
|
96
|
+
let start = 0;
|
|
97
|
+
while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]))
|
|
98
|
+
start++;
|
|
99
|
+
return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start };
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* The length of a cut head without the character the cut left unfinished.
|
|
103
|
+
*
|
|
104
|
+
* Its last character starts at the last byte that is not a continuation byte,
|
|
105
|
+
* and that byte says how many bytes the character needs. One that needs more
|
|
106
|
+
* than the head holds is dropped, for the same reason {@link decodeTail} skips
|
|
107
|
+
* a partial code point.
|
|
108
|
+
*/
|
|
109
|
+
const wholeLength = (bytes) => {
|
|
110
|
+
for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {
|
|
111
|
+
const byte = bytes[bytes.byteLength - back];
|
|
112
|
+
if (isContinuation(byte))
|
|
113
|
+
continue;
|
|
114
|
+
const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1;
|
|
115
|
+
return width > back ? bytes.byteLength - back : bytes.byteLength;
|
|
116
|
+
}
|
|
117
|
+
return bytes.byteLength;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Decodes a stream kept by its ends. A stream that fit is decoded whole;
|
|
121
|
+
* otherwise each end is cut on a character boundary and one line naming every
|
|
122
|
+
* byte between them stands where the middle was.
|
|
123
|
+
*/
|
|
124
|
+
const decodeEnds = (state) => {
|
|
125
|
+
if (state.tail.dropped === 0) {
|
|
126
|
+
const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes);
|
|
127
|
+
return { text: decoder.decode(whole), droppedBytes: 0 };
|
|
128
|
+
}
|
|
129
|
+
const head = join(state.head, state.headBytes);
|
|
130
|
+
const kept = wholeLength(head);
|
|
131
|
+
const tail = decodeTail(state.tail);
|
|
132
|
+
const droppedBytes = head.byteLength - kept + tail.droppedBytes;
|
|
133
|
+
return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes };
|
|
134
|
+
};
|
|
135
|
+
const boundedTail = (stream, maxBytes, overflow) => Stream.runFold(stream, emptyTail, (state, chunk) => {
|
|
136
|
+
keepTail(state, chunk, maxBytes);
|
|
91
137
|
return state;
|
|
92
138
|
}).pipe(Effect.map((state) => overflow === "refuse" && state.dropped > 0
|
|
93
139
|
? { text: "", droppedBytes: state.dropped }
|
|
94
140
|
: decodeTail(state)));
|
|
141
|
+
/**
|
|
142
|
+
* Keeps the first half of the bound and the last half. The head fills first,
|
|
143
|
+
* and every byte after it passes through the tail, which drops from its front:
|
|
144
|
+
* what falls out of the tail is the middle.
|
|
145
|
+
*/
|
|
146
|
+
const boundedEnds = (stream, maxBytes) => {
|
|
147
|
+
const headBytes = Math.floor(maxBytes / 2);
|
|
148
|
+
return Stream.runFold(stream, () => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {
|
|
149
|
+
const taken = Math.min(headBytes - state.headBytes, chunk.byteLength);
|
|
150
|
+
if (taken > 0) {
|
|
151
|
+
state.head.push(chunk.subarray(0, taken));
|
|
152
|
+
state.headBytes += taken;
|
|
153
|
+
}
|
|
154
|
+
keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes);
|
|
155
|
+
return state;
|
|
156
|
+
}).pipe(Effect.map(decodeEnds));
|
|
157
|
+
};
|
|
95
158
|
const capture = (stream, maxBytes, overflow) => maxBytes === undefined
|
|
96
159
|
? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))
|
|
97
|
-
:
|
|
160
|
+
: overflow === "ends"
|
|
161
|
+
? boundedEnds(stream, maxBytes)
|
|
162
|
+
: boundedTail(stream, maxBytes, overflow);
|
|
98
163
|
const unbounded = (command, options) => Effect.gen(function* () {
|
|
99
164
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
100
165
|
const settings = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Exec.js","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Buffered shell execution over the permission-aware process spawner.\n *\n * `@smthrs/kernel` used to expose a `Shell` service whose `exec` returned a\n * buffered `{ stdout, stderr, exitCode }`. The kernel replaced it with\n * `ChildProcessSpawner`, Effect's own process boundary, which streams and has\n * no buffered form. This module is the buffered form the shell flows still\n * want, expressed in terms of the spawner so the `proc:spawn` capability check\n * and the command-line resource stay exactly where the kernel put them.\n *\n * Commands run through the platform shell (`shell: true`), matching the old\n * service: the flows take a command *line*, not an argv.\n *\n * @since 1.0.0\n */\n\nimport * as ChildProcessEnvironment from \"@smthrs/kernel/ChildProcessEnvironment\"\nimport * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Effect from \"effect/Effect\"\nimport * as ChildProcess from \"effect/process/ChildProcess\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport * as StdError from \"../StdError.ts\"\n\n/**\n * Failure codes a buffered execution can report, matching the codes the\n * removed kernel `Shell` used so handler-level mapping is unchanged.\n *\n * @category models\n * @since 1.0.0\n */\nexport const ExecErrorCode = Schema.Literals([\"timeout\", \"spawn_error\", \"capture_overflow\"])\n\n/**\n * Failure codes a buffered execution can report.\n *\n * @category models\n * @since 1.0.0\n */\nexport type ExecErrorCode = typeof ExecErrorCode.Type\n\n/**\n * A command that never produced an exit code.\n *\n * A non-zero exit is a successful {@link ExecResult}, not a failure; this\n * error is reserved for a command that could not start or was cut off by its\n * timeout, or whose captured output exceeded a refusing capture bound.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class ExecError extends Schema.TaggedError<ExecError>()(\"@smthrs/std/ExecError\", {\n code: ExecErrorCode,\n message: Schema.String,\n /** The limit a `timeout` ran past, in milliseconds. */\n limitMillis: Schema.optional(Schema.Number)\n}) {}\n\n/**\n * The buffered result of one command.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecResult {\n readonly stdout: string\n readonly stderr: string\n readonly exitCode: number\n /** UTF-8 bytes dropped from the head of stdout to stay inside the capture bound. */\n readonly stdoutDroppedBytes: number\n /** UTF-8 bytes dropped from the head of stderr to stay inside the capture bound. */\n readonly stderrDroppedBytes: number\n}\n\n/**\n * Options accepted by {@link exec}.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecOptions {\n readonly cwd?: string | undefined\n readonly env?: Readonly<Record<string, string>> | undefined\n readonly timeoutMs?: number | undefined\n /**\n * The program's arguments. Present means an argv, absent means a command\n * line for the platform shell. An argv is how a payload reaches a program\n * without being quoted into a string: nothing between here and `execve`\n * re-parses it.\n */\n readonly args?: ReadonlyArray<string> | undefined\n /**\n * Text written to the program's standard input, as data. A script delivered\n * this way is never quoted, escaped, or heredoc-terminated, which is the\n * whole point: quoting corruption cost the measured SWE-bench program twelve\n * probe failures and one instance's most expensive frame.\n */\n readonly stdin?: string | undefined\n /**\n * Bytes retained per captured stream. A stream that prints more keeps its\n * tail and reports what it dropped, so a command printing gigabytes costs a\n * bounded amount of memory rather than the whole of what it printed.\n *\n * Absent means unbounded, which is what a caller wants when it needs every\n * byte a tool produced (`rg --files`, `git worktree list`).\n */\n readonly maxCaptureBytes?: number | undefined\n /** Defaults to `tail`; `refuse` fails if either stream exceeds the bound. */\n readonly overflow?: \"tail\" | \"refuse\" | undefined\n}\n\n/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */\ninterface Capture {\n readonly text: string\n readonly droppedBytes: number\n}\n\ninterface Tail {\n chunks: Array<Uint8Array>\n bytes: number\n dropped: number\n}\n\nconst decoder = new TextDecoder(\"utf-8\")\n\n/**\n * Joins the retained chunks and decodes them, skipping the partial code point a\n * dropped head can leave behind.\n *\n * A UTF-8 continuation byte is `10xxxxxx`, and a code point is at most four\n * bytes, so at most three leading continuation bytes can precede the first\n * whole character. Skipping them is counted as dropped rather than decoded to a\n * replacement character the process never printed.\n */\nconst decodeTail = (state: Tail): Capture => {\n const bytes = new Uint8Array(state.bytes)\n let offset = 0\n for (const chunk of state.chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }\n let start = 0\n while (start < bytes.byteLength && start < 3 && (bytes[start]! & 0b1100_0000) === 0b1000_0000) start++\n return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }\n}\n\nconst boundedTail = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number,\n overflow: \"tail\" | \"refuse\" | undefined\n): Effect.Effect<Capture, E> =>\n Stream.runFold(\n stream,\n (): Tail => ({ chunks: [], bytes: 0, dropped: 0 }),\n (state, chunk) => {\n if (chunk.byteLength === 0) return state\n state.chunks.push(chunk)\n state.bytes += chunk.byteLength\n while (state.bytes > maxBytes && state.chunks.length > 0) {\n const excess = state.bytes - maxBytes\n const first = state.chunks[0]!\n if (first.byteLength <= excess) {\n state.chunks.shift()\n state.bytes -= first.byteLength\n state.dropped += first.byteLength\n } else {\n state.chunks[0] = first.subarray(excess)\n state.bytes -= excess\n state.dropped += excess\n }\n }\n return state\n }\n ).pipe(Effect.map((state) =>\n overflow === \"refuse\" && state.dropped > 0\n ? { text: \"\", droppedBytes: state.dropped }\n : decodeTail(state)\n ))\n\nconst capture = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number | undefined,\n overflow: \"tail\" | \"refuse\" | undefined\n): Effect.Effect<Capture, E> =>\n maxBytes === undefined\n ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))\n : boundedTail(stream, maxBytes, overflow)\n\nconst unbounded = (\n command: string,\n options: ExecOptions\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n const settings: ChildProcess.CommandOptions = {\n ...(options.args === undefined ? { shell: true } : {}),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n env: ChildProcessEnvironment.make(process.env, options.env),\n extendEnv: false,\n ...(options.stdin === undefined\n ? {}\n : { stdin: Stream.make(new TextEncoder().encode(options.stdin)) })\n }\n const handle = yield* spawner.spawn(\n options.args === undefined\n ? ChildProcess.make(command, settings)\n : ChildProcess.make(command, [...options.args], settings)\n )\n // stdout, stderr, and the exit code have to be consumed concurrently: a\n // command that fills a pipe blocks until the pipe is drained, so waiting\n // for exit first would deadlock on any output larger than the buffer.\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n capture(handle.stdout, options.maxCaptureBytes, options.overflow),\n capture(handle.stderr, options.maxCaptureBytes, options.overflow),\n handle.exitCode\n ],\n { concurrency: \"unbounded\" }\n )\n if (options.overflow === \"refuse\") {\n const overflowed = [\n stdout.droppedBytes > 0 ? \"stdout\" : undefined,\n stderr.droppedBytes > 0 ? \"stderr\" : undefined\n ]\n .filter((stream): stream is string => stream !== undefined)\n if (overflowed.length > 0) {\n return yield* Effect.fail(\n new ExecError({\n code: \"capture_overflow\",\n message: `${command} ${\n overflowed.join(\" and \")\n } exceeded the ${options.maxCaptureBytes}-byte capture cap; refusing partial output`\n })\n )\n }\n }\n return {\n stdout: stdout.text,\n stderr: stderr.text,\n exitCode,\n stdoutDroppedBytes: stdout.droppedBytes,\n stderrDroppedBytes: stderr.droppedBytes\n }\n }).pipe(\n Effect.scoped,\n Effect.mapError((error) =>\n error instanceof ExecError ? error : new ExecError({\n code: \"spawn_error\",\n message: `exec: ${command}: ${error.message}`\n })\n )\n )\n\n/**\n * Runs a command line through the platform shell and buffers its output.\n *\n * Cancellation is scope closure: the spawn is scoped, so an interrupted fiber\n * — including the one a `timeoutMs` interrupts — terminates the process rather\n * than leaking it.\n *\n * @category execution\n * @since 1.0.0\n */\nexport const exec = (\n command: string,\n options: ExecOptions = {}\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n options.timeoutMs === undefined\n ? unbounded(command, options)\n : Effect.timeoutOrElse(unbounded(command, options), {\n duration: options.timeoutMs,\n orElse: () =>\n Effect.fail(\n new ExecError({\n code: \"timeout\",\n message: `exec: \\`${command}\\` exceeded ${options.timeoutMs}ms`,\n limitMillis: options.timeoutMs\n })\n )\n })\n\n/**\n * The model-facing failure code a command that never produced an exit code\n * carries.\n *\n * A timeout keeps its own code, because \"the command never finished\" and \"the\n * command never started\" are different things to a caller deciding whether to\n * retry. Every other way of producing no exit code is a host failure.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdErrorCode = (error: ExecError): StdError.Code =>\n error.code === \"timeout\" ? \"timeout\" : \"command_failed\"\n\n/**\n * The model-facing failure one execution error becomes.\n *\n * A timeout names the command, which is what the caller asked for and all a\n * timeout has to say. Any other failure carries the message the execution\n * produced, which already names the command.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdError = (command: string, error: ExecError): StdError.StdError =>\n new StdError.StdError({\n code: toStdErrorCode(error),\n message: error.code === \"timeout\"\n ? `Command timed out: ${command}`\n : `Command failed to start: ${error.message}`,\n ...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })\n })\n"]}
|
|
1
|
+
{"version":3,"file":"Exec.js","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Buffered shell execution over the permission-aware process spawner.\n *\n * `@smthrs/kernel` used to expose a `Shell` service whose `exec` returned a\n * buffered `{ stdout, stderr, exitCode }`. The kernel replaced it with\n * `ChildProcessSpawner`, Effect's own process boundary, which streams and has\n * no buffered form. This module is the buffered form the shell flows still\n * want, expressed in terms of the spawner so the `proc:spawn` capability check\n * and the command-line resource stay exactly where the kernel put them.\n *\n * Commands run through the platform shell (`shell: true`), matching the old\n * service: the flows take a command *line*, not an argv.\n *\n * @since 1.0.0\n */\n\nimport * as ChildProcessEnvironment from \"@smthrs/kernel/ChildProcessEnvironment\"\nimport * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Effect from \"effect/Effect\"\nimport * as ChildProcess from \"effect/process/ChildProcess\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport * as StdError from \"../StdError.ts\"\nimport { elideMiddle } from \"./Text.ts\"\n\n/**\n * Failure codes a buffered execution can report, matching the codes the\n * removed kernel `Shell` used so handler-level mapping is unchanged.\n *\n * @category models\n * @since 1.0.0\n */\nexport const ExecErrorCode = Schema.Literals([\"timeout\", \"spawn_error\", \"capture_overflow\"])\n\n/**\n * Failure codes a buffered execution can report.\n *\n * @category models\n * @since 1.0.0\n */\nexport type ExecErrorCode = typeof ExecErrorCode.Type\n\n/**\n * A command that never produced an exit code.\n *\n * A non-zero exit is a successful {@link ExecResult}, not a failure; this\n * error is reserved for a command that could not start or was cut off by its\n * timeout, or whose captured output exceeded a refusing capture bound.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class ExecError extends Schema.TaggedError<ExecError>()(\"@smthrs/std/ExecError\", {\n code: ExecErrorCode,\n message: Schema.String,\n /** The limit a `timeout` ran past, in milliseconds. */\n limitMillis: Schema.optional(Schema.Number)\n}) {}\n\n/**\n * The buffered result of one command.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecResult {\n readonly stdout: string\n readonly stderr: string\n readonly exitCode: number\n /** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */\n readonly stdoutDroppedBytes: number\n /** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */\n readonly stderrDroppedBytes: number\n}\n\n/**\n * Options accepted by {@link exec}.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecOptions {\n readonly cwd?: string | undefined\n readonly env?: Readonly<Record<string, string>> | undefined\n readonly timeoutMs?: number | undefined\n /**\n * The program's arguments. Present means an argv, absent means a command\n * line for the platform shell. An argv is how a payload reaches a program\n * without being quoted into a string: nothing between here and `execve`\n * re-parses it.\n */\n readonly args?: ReadonlyArray<string> | undefined\n /**\n * Text written to the program's standard input, as data. A script delivered\n * this way is never quoted, escaped, or heredoc-terminated, which is the\n * whole point: quoting corruption cost the measured SWE-bench program twelve\n * probe failures and one instance's most expensive frame.\n */\n readonly stdin?: string | undefined\n /**\n * Bytes retained per captured stream. A stream that prints more keeps what\n * `overflow` names and reports what it dropped, so a command printing\n * gigabytes costs a bounded amount of memory rather than the whole of what it\n * printed.\n *\n * Absent means unbounded, which is what a caller wants when it needs every\n * byte a tool produced (`rg --files`, `git worktree list`).\n */\n readonly maxCaptureBytes?: number | undefined\n /**\n * What a stream past the bound keeps. `tail`, the default, keeps its last\n * bytes. `ends` keeps its first and last halves and puts one line naming the\n * dropped bytes where the middle was. `refuse` fails if either stream exceeds\n * the bound.\n */\n readonly overflow?: Overflow | undefined\n}\n\ntype Overflow = \"tail\" | \"ends\" | \"refuse\"\n\n/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */\ninterface Capture {\n readonly text: string\n readonly droppedBytes: number\n}\n\ninterface Tail {\n chunks: Array<Uint8Array>\n bytes: number\n dropped: number\n}\n\n/** A stream's first bytes and its last ones: what `overflow: \"ends\"` keeps. */\ninterface Ends {\n readonly head: Array<Uint8Array>\n headBytes: number\n readonly tail: Tail\n}\n\nconst decoder = new TextDecoder(\"utf-8\")\n\nconst emptyTail = (): Tail => ({ chunks: [], bytes: 0, dropped: 0 })\n\n/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */\nconst isContinuation = (byte: number): boolean => (byte & 0b1100_0000) === 0b1000_0000\n\nconst join = (chunks: ReadonlyArray<Uint8Array>, size: number): Uint8Array => {\n const bytes = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n return bytes\n}\n\n/** Appends one chunk and drops from the front whatever no longer fits. */\nconst keepTail = (state: Tail, chunk: Uint8Array, maxBytes: number): void => {\n if (chunk.byteLength === 0) return\n state.chunks.push(chunk)\n state.bytes += chunk.byteLength\n while (state.bytes > maxBytes && state.chunks.length > 0) {\n const excess = state.bytes - maxBytes\n const first = state.chunks[0]!\n if (first.byteLength <= excess) {\n state.chunks.shift()\n state.bytes -= first.byteLength\n state.dropped += first.byteLength\n } else {\n state.chunks[0] = first.subarray(excess)\n state.bytes -= excess\n state.dropped += excess\n }\n }\n}\n\n/**\n * Joins the retained chunks and decodes them, skipping the partial code point a\n * dropped head can leave behind.\n *\n * A code point is at most four bytes, so at most three leading continuation\n * bytes can precede the first whole character. Skipping them is counted as\n * dropped rather than decoded to a replacement character the process never\n * printed.\n */\nconst decodeTail = (state: Tail): Capture => {\n const bytes = join(state.chunks, state.bytes)\n if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }\n let start = 0\n while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]!)) start++\n return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }\n}\n\n/**\n * The length of a cut head without the character the cut left unfinished.\n *\n * Its last character starts at the last byte that is not a continuation byte,\n * and that byte says how many bytes the character needs. One that needs more\n * than the head holds is dropped, for the same reason {@link decodeTail} skips\n * a partial code point.\n */\nconst wholeLength = (bytes: Uint8Array): number => {\n for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {\n const byte = bytes[bytes.byteLength - back]!\n if (isContinuation(byte)) continue\n const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1\n return width > back ? bytes.byteLength - back : bytes.byteLength\n }\n return bytes.byteLength\n}\n\n/**\n * Decodes a stream kept by its ends. A stream that fit is decoded whole;\n * otherwise each end is cut on a character boundary and one line naming every\n * byte between them stands where the middle was.\n */\nconst decodeEnds = (state: Ends): Capture => {\n if (state.tail.dropped === 0) {\n const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes)\n return { text: decoder.decode(whole), droppedBytes: 0 }\n }\n const head = join(state.head, state.headBytes)\n const kept = wholeLength(head)\n const tail = decodeTail(state.tail)\n const droppedBytes = head.byteLength - kept + tail.droppedBytes\n return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes }\n}\n\nconst boundedTail = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n Stream.runFold(stream, emptyTail, (state, chunk) => {\n keepTail(state, chunk, maxBytes)\n return state\n }).pipe(Effect.map((state) =>\n overflow === \"refuse\" && state.dropped > 0\n ? { text: \"\", droppedBytes: state.dropped }\n : decodeTail(state)\n ))\n\n/**\n * Keeps the first half of the bound and the last half. The head fills first,\n * and every byte after it passes through the tail, which drops from its front:\n * what falls out of the tail is the middle.\n */\nconst boundedEnds = <E>(stream: Stream.Stream<Uint8Array, E>, maxBytes: number): Effect.Effect<Capture, E> => {\n const headBytes = Math.floor(maxBytes / 2)\n return Stream.runFold(stream, (): Ends => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {\n const taken = Math.min(headBytes - state.headBytes, chunk.byteLength)\n if (taken > 0) {\n state.head.push(chunk.subarray(0, taken))\n state.headBytes += taken\n }\n keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes)\n return state\n }).pipe(Effect.map(decodeEnds))\n}\n\nconst capture = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number | undefined,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n maxBytes === undefined\n ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))\n : overflow === \"ends\"\n ? boundedEnds(stream, maxBytes)\n : boundedTail(stream, maxBytes, overflow)\n\nconst unbounded = (\n command: string,\n options: ExecOptions\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n const settings: ChildProcess.CommandOptions = {\n ...(options.args === undefined ? { shell: true } : {}),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n env: ChildProcessEnvironment.make(process.env, options.env),\n extendEnv: false,\n ...(options.stdin === undefined\n ? {}\n : { stdin: Stream.make(new TextEncoder().encode(options.stdin)) })\n }\n const handle = yield* spawner.spawn(\n options.args === undefined\n ? ChildProcess.make(command, settings)\n : ChildProcess.make(command, [...options.args], settings)\n )\n // stdout, stderr, and the exit code have to be consumed concurrently: a\n // command that fills a pipe blocks until the pipe is drained, so waiting\n // for exit first would deadlock on any output larger than the buffer.\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n capture(handle.stdout, options.maxCaptureBytes, options.overflow),\n capture(handle.stderr, options.maxCaptureBytes, options.overflow),\n handle.exitCode\n ],\n { concurrency: \"unbounded\" }\n )\n if (options.overflow === \"refuse\") {\n const overflowed = [\n stdout.droppedBytes > 0 ? \"stdout\" : undefined,\n stderr.droppedBytes > 0 ? \"stderr\" : undefined\n ]\n .filter((stream): stream is string => stream !== undefined)\n if (overflowed.length > 0) {\n return yield* Effect.fail(\n new ExecError({\n code: \"capture_overflow\",\n message: `${command} ${\n overflowed.join(\" and \")\n } exceeded the ${options.maxCaptureBytes}-byte capture cap; refusing partial output`\n })\n )\n }\n }\n return {\n stdout: stdout.text,\n stderr: stderr.text,\n exitCode,\n stdoutDroppedBytes: stdout.droppedBytes,\n stderrDroppedBytes: stderr.droppedBytes\n }\n }).pipe(\n Effect.scoped,\n Effect.mapError((error) =>\n error instanceof ExecError ? error : new ExecError({\n code: \"spawn_error\",\n message: `exec: ${command}: ${error.message}`\n })\n )\n )\n\n/**\n * Runs a command line through the platform shell and buffers its output.\n *\n * Cancellation is scope closure: the spawn is scoped, so an interrupted fiber\n * — including the one a `timeoutMs` interrupts — terminates the process rather\n * than leaking it.\n *\n * @category execution\n * @since 1.0.0\n */\nexport const exec = (\n command: string,\n options: ExecOptions = {}\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n options.timeoutMs === undefined\n ? unbounded(command, options)\n : Effect.timeoutOrElse(unbounded(command, options), {\n duration: options.timeoutMs,\n orElse: () =>\n Effect.fail(\n new ExecError({\n code: \"timeout\",\n message: `exec: \\`${command}\\` exceeded ${options.timeoutMs}ms`,\n limitMillis: options.timeoutMs\n })\n )\n })\n\n/**\n * The model-facing failure code a command that never produced an exit code\n * carries.\n *\n * A timeout keeps its own code, because \"the command never finished\" and \"the\n * command never started\" are different things to a caller deciding whether to\n * retry. Every other way of producing no exit code is a host failure.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdErrorCode = (error: ExecError): StdError.Code =>\n error.code === \"timeout\" ? \"timeout\" : \"command_failed\"\n\n/**\n * The model-facing failure one execution error becomes.\n *\n * A timeout names the command, which is what the caller asked for and all a\n * timeout has to say. Any other failure carries the message the execution\n * produced, which already names the command.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdError = (command: string, error: ExecError): StdError.StdError =>\n new StdError.StdError({\n code: toStdErrorCode(error),\n message: error.code === \"timeout\"\n ? `Command timed out: ${command}`\n : `Command failed to start: ${error.message}`,\n ...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })\n })\n"]}
|
|
@@ -85,6 +85,17 @@ export interface Page {
|
|
|
85
85
|
export declare const truncateBytes: (text: string, maxBytes: number, options: {
|
|
86
86
|
readonly keep: "head" | "tail";
|
|
87
87
|
}) => Truncation;
|
|
88
|
+
/**
|
|
89
|
+
* Joins the start and the end of a stream cut in the middle around one line
|
|
90
|
+
* naming the bytes cut between them.
|
|
91
|
+
*
|
|
92
|
+
* The line stands alone, so a start that stops mid-line is ended before it.
|
|
93
|
+
* Only the line's own newlines are added; `droppedBytes` counts printed bytes.
|
|
94
|
+
*
|
|
95
|
+
* @category rendering
|
|
96
|
+
* @since 1.0.0-rc.5
|
|
97
|
+
*/
|
|
98
|
+
export declare const elideMiddle: (start: string, droppedBytes: number, end: string) => string;
|
|
88
99
|
/**
|
|
89
100
|
* Splits on LF without counting a terminal LF as a line.
|
|
90
101
|
* Carriage returns remain part of each line, including the CR in CRLF, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Text.d.ts","sourceRoot":"","sources":["../../../src/internal/Text.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,OAAQ,CAAA;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,WAAW,OAAQ,CAAA;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,OAAQ,CAAA;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAKD;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,UAAU,MAAM,EAChB,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,KAC1C,UAwBF,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,aAAa,CAAC,MAAM,CAI9D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,MAAM,EACZ,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KAC3D,IAWF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE,OAAO,MAAM,KAAG,MACL,CAAA"}
|
|
1
|
+
{"version":3,"file":"Text.d.ts","sourceRoot":"","sources":["../../../src/internal/Text.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,OAAQ,CAAA;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,WAAW,OAAQ,CAAA;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,cAAc,OAAQ,CAAA;AAEnC;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAS,CAAA;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B;AAKD;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,EACZ,UAAU,MAAM,EAChB,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,KAC1C,UAwBF,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,EAAE,cAAc,MAAM,EAAE,KAAK,MAAM,KAAG,MAG1D,CAAA;AAErB;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,aAAa,CAAC,MAAM,CAI9D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,KAAK,GAChB,MAAM,MAAM,EACZ,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KAC3D,IAWF,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,MAAM,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE,OAAO,MAAM,KAAG,MACL,CAAA"}
|
|
@@ -88,6 +88,17 @@ export const truncateBytes = (text, maxBytes, options) => {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Joins the start and the end of a stream cut in the middle around one line
|
|
93
|
+
* naming the bytes cut between them.
|
|
94
|
+
*
|
|
95
|
+
* The line stands alone, so a start that stops mid-line is ended before it.
|
|
96
|
+
* Only the line's own newlines are added; `droppedBytes` counts printed bytes.
|
|
97
|
+
*
|
|
98
|
+
* @category rendering
|
|
99
|
+
* @since 1.0.0-rc.5
|
|
100
|
+
*/
|
|
101
|
+
export const elideMiddle = (start, droppedBytes, end) => `${start}${start === "" || start.endsWith("\n") ? "" : "\n"}[${droppedBytes} ${droppedBytes === 1 ? "byte" : "bytes"} omitted]\n${end}`;
|
|
91
102
|
/**
|
|
92
103
|
* Splits on LF without counting a terminal LF as a line.
|
|
93
104
|
* Carriage returns remain part of each line, including the CR in CRLF, so
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Text.js","sourceRoot":"","sources":["../../../src/internal/Text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared text paging, numbering, and byte-budget helpers.\n *\n * Every limit here is a display budget, not a policy limit: exceeding one is\n * always disclosed to the model rather than silently applied.\n *\n * @since 1.0.0\n */\n\n/**\n * Default number of lines or entries returned by a paged flow.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const DEFAULT_READ_LIMIT = 2_000\n\n/**\n * Maximum number of directory entries or glob paths returned in one call.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_ENTRIES = 1_000\n\n/**\n * Maximum number of grep matches returned in one call.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_GREP_MATCHES = 200\n\n/**\n * Maximum number of Unicode scalar values displayed for a single line.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_LINE_CHARS = 2_000\n\n/**\n * Maximum UTF-8 byte budget for one rendered text payload.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_OUTPUT_BYTES = 60_000\n\n/**\n * Maximum UTF-8 byte budget captured per shell output stream.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_SHELL_OUTPUT_BYTES = 30_000\n\n/**\n * A byte-budgeted rendering of a text value.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Truncation {\n readonly text: string\n readonly truncated: boolean\n readonly keptBytes: number\n readonly droppedBytes: number\n}\n\n/**\n * One page of source lines.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Page {\n readonly lines: ReadonlyArray<string>\n readonly startLine: number\n readonly endLine: number\n readonly totalLines: number\n}\n\nconst encoder = new TextEncoder()\nconst fatalDecoder = new TextDecoder(\"utf-8\", { fatal: true })\n\n/**\n * Trims a string to a UTF-8 byte budget without splitting a code point.\n *\n * `keep: \"head\"` retains the beginning of the value; `keep: \"tail\"` retains\n * the end, which is what shell output needs because the interesting part of an\n * overflowing stream is almost always its last lines.\n *\n * @category rendering\n * @since 1.0.0\n */\nexport const truncateBytes = (\n text: string,\n maxBytes: number,\n options: { readonly keep: \"head\" | \"tail\" }\n): Truncation => {\n const bytes = encoder.encode(text)\n if (bytes.byteLength <= maxBytes) {\n return { text, truncated: false, keptBytes: bytes.byteLength, droppedBytes: 0 }\n }\n let start = options.keep === \"head\" ? 0 : bytes.byteLength - maxBytes\n let end = options.keep === \"head\" ? maxBytes : bytes.byteLength\n // TextEncoder produces valid UTF-8. Moving a cut inward reaches a scalar\n // boundary after at most three continuation bytes, so decoding must succeed.\n while (true) {\n try {\n const decoded = fatalDecoder.decode(bytes.subarray(start, end))\n const keptBytes = end - start\n return {\n text: decoded,\n truncated: true,\n keptBytes,\n droppedBytes: bytes.byteLength - keptBytes\n }\n } catch {\n if (options.keep === \"head\") end--\n else start++\n }\n }\n}\n\n/**\n * Splits on LF without counting a terminal LF as a line.\n * Carriage returns remain part of each line, including the CR in CRLF, so\n * joining a page with LF preserves its bytes except for the final LF.\n *\n * @category parsing\n * @since 1.0.0\n */\nexport const sourceLines = (text: string): ReadonlyArray<string> => {\n const lines = text.length === 0 ? [] : text.split(\"\\n\")\n if (text.endsWith(\"\\n\")) lines.pop()\n return lines\n}\n\n/**\n * Selects a 1-based page of lines from a text value.\n *\n * @category paging\n * @since 1.0.0\n */\nexport const slice = (\n text: string,\n options: { readonly offset: number; readonly limit: number }\n): Page => {\n const lines = sourceLines(text)\n const totalLines = lines.length\n const startLine = Math.max(1, options.offset)\n const endLine = Math.min(totalLines, startLine + Math.max(0, options.limit) - 1)\n return {\n lines: lines.slice(startLine - 1, endLine),\n startLine,\n endLine,\n totalLines\n }\n}\n\n/**\n * Renders the disclosure appended whenever output was capped.\n *\n * @category rendering\n * @since 1.0.0\n */\nexport const notice = (unit: string, shown: number, total: number): string =>\n `Showing ${shown} of ${total} ${unit}; output was truncated.`\n"]}
|
|
1
|
+
{"version":3,"file":"Text.js","sourceRoot":"","sources":["../../../src/internal/Text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared text paging, numbering, and byte-budget helpers.\n *\n * Every limit here is a display budget, not a policy limit: exceeding one is\n * always disclosed to the model rather than silently applied.\n *\n * @since 1.0.0\n */\n\n/**\n * Default number of lines or entries returned by a paged flow.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const DEFAULT_READ_LIMIT = 2_000\n\n/**\n * Maximum number of directory entries or glob paths returned in one call.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_ENTRIES = 1_000\n\n/**\n * Maximum number of grep matches returned in one call.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_GREP_MATCHES = 200\n\n/**\n * Maximum number of Unicode scalar values displayed for a single line.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_LINE_CHARS = 2_000\n\n/**\n * Maximum UTF-8 byte budget for one rendered text payload.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_OUTPUT_BYTES = 60_000\n\n/**\n * Maximum UTF-8 byte budget captured per shell output stream.\n *\n * @category constants\n * @since 1.0.0\n */\nexport const MAX_SHELL_OUTPUT_BYTES = 30_000\n\n/**\n * A byte-budgeted rendering of a text value.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Truncation {\n readonly text: string\n readonly truncated: boolean\n readonly keptBytes: number\n readonly droppedBytes: number\n}\n\n/**\n * One page of source lines.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface Page {\n readonly lines: ReadonlyArray<string>\n readonly startLine: number\n readonly endLine: number\n readonly totalLines: number\n}\n\nconst encoder = new TextEncoder()\nconst fatalDecoder = new TextDecoder(\"utf-8\", { fatal: true })\n\n/**\n * Trims a string to a UTF-8 byte budget without splitting a code point.\n *\n * `keep: \"head\"` retains the beginning of the value; `keep: \"tail\"` retains\n * the end, which is what shell output needs because the interesting part of an\n * overflowing stream is almost always its last lines.\n *\n * @category rendering\n * @since 1.0.0\n */\nexport const truncateBytes = (\n text: string,\n maxBytes: number,\n options: { readonly keep: \"head\" | \"tail\" }\n): Truncation => {\n const bytes = encoder.encode(text)\n if (bytes.byteLength <= maxBytes) {\n return { text, truncated: false, keptBytes: bytes.byteLength, droppedBytes: 0 }\n }\n let start = options.keep === \"head\" ? 0 : bytes.byteLength - maxBytes\n let end = options.keep === \"head\" ? maxBytes : bytes.byteLength\n // TextEncoder produces valid UTF-8. Moving a cut inward reaches a scalar\n // boundary after at most three continuation bytes, so decoding must succeed.\n while (true) {\n try {\n const decoded = fatalDecoder.decode(bytes.subarray(start, end))\n const keptBytes = end - start\n return {\n text: decoded,\n truncated: true,\n keptBytes,\n droppedBytes: bytes.byteLength - keptBytes\n }\n } catch {\n if (options.keep === \"head\") end--\n else start++\n }\n }\n}\n\n/**\n * Joins the start and the end of a stream cut in the middle around one line\n * naming the bytes cut between them.\n *\n * The line stands alone, so a start that stops mid-line is ended before it.\n * Only the line's own newlines are added; `droppedBytes` counts printed bytes.\n *\n * @category rendering\n * @since 1.0.0-rc.5\n */\nexport const elideMiddle = (start: string, droppedBytes: number, end: string): string =>\n `${start}${start === \"\" || start.endsWith(\"\\n\") ? \"\" : \"\\n\"}[${droppedBytes} ${\n droppedBytes === 1 ? \"byte\" : \"bytes\"\n } omitted]\\n${end}`\n\n/**\n * Splits on LF without counting a terminal LF as a line.\n * Carriage returns remain part of each line, including the CR in CRLF, so\n * joining a page with LF preserves its bytes except for the final LF.\n *\n * @category parsing\n * @since 1.0.0\n */\nexport const sourceLines = (text: string): ReadonlyArray<string> => {\n const lines = text.length === 0 ? [] : text.split(\"\\n\")\n if (text.endsWith(\"\\n\")) lines.pop()\n return lines\n}\n\n/**\n * Selects a 1-based page of lines from a text value.\n *\n * @category paging\n * @since 1.0.0\n */\nexport const slice = (\n text: string,\n options: { readonly offset: number; readonly limit: number }\n): Page => {\n const lines = sourceLines(text)\n const totalLines = lines.length\n const startLine = Math.max(1, options.offset)\n const endLine = Math.min(totalLines, startLine + Math.max(0, options.limit) - 1)\n return {\n lines: lines.slice(startLine - 1, endLine),\n startLine,\n endLine,\n totalLines\n }\n}\n\n/**\n * Renders the disclosure appended whenever output was capped.\n *\n * @category rendering\n * @since 1.0.0\n */\nexport const notice = (unit: string, shown: number, total: number): string =>\n `Showing ${shown} of ${total} ${unit}; output was truncated.`\n"]}
|
package/docs/api.md
CHANGED
|
@@ -60,7 +60,7 @@ Some of them export more than the common nine:
|
|
|
60
60
|
|
|
61
61
|
| Module | Additional exports |
|
|
62
62
|
| -------------- | ------------------------------------------------------------------------------------------- |
|
|
63
|
-
| `Bash` | `DEFAULT_TIMEOUT_MS`
|
|
63
|
+
| `Bash` | `DEFAULT_TIMEOUT_MS`, `elideMiddle(start, droppedBytes, end)` |
|
|
64
64
|
| `Explore` | `make(options: { model?: string })`, and no `run` |
|
|
65
65
|
| `Grep` | `ContextLine`, `Symbol`, `Match` schemas |
|
|
66
66
|
| `Read` | `MAX_READ_FILE_BYTES`, the 64 MiB bound past which `read` refuses a file |
|
|
@@ -84,17 +84,20 @@ caller paging a large directory could see one entry twice and another never.
|
|
|
84
84
|
`stdoutTruncated`, `stderrTruncated`, and `tailTruncated` are not display
|
|
85
85
|
details. A truncated capture is a fragment of what the process printed, and
|
|
86
86
|
[`@smthrs/harness`](/api/harness) reads these flags to refuse a later write of
|
|
87
|
-
those exact bytes: writing a captured
|
|
88
|
-
|
|
87
|
+
those exact bytes: writing a captured fragment over a file replaces the file
|
|
88
|
+
with a piece of a log. Read the flag before writing captured output anywhere, and set
|
|
89
89
|
it in your own handler whenever you cut a capture: a consumer that cannot tell a
|
|
90
90
|
fragment from the whole writes the fragment.
|
|
91
91
|
|
|
92
92
|
`stdoutDroppedBytes`, `stderrDroppedBytes`, and `tailDroppedBytes` count what
|
|
93
93
|
the process actually produced beyond the capture. Capture is bounded where the
|
|
94
94
|
stream is read rather than after it is read, so a command that prints gigabytes
|
|
95
|
-
costs the bound instead of the whole of what it printed. `bash` keeps the
|
|
96
|
-
|
|
97
|
-
`
|
|
95
|
+
costs the bound instead of the whole of what it printed. `bash` keeps the
|
|
96
|
+
first and last 15,000 bytes of each stream and puts one line such as
|
|
97
|
+
`[10025 bytes omitted]` where the middle was, because a search prints its
|
|
98
|
+
matches first and a failing command prints its verdict last. Its dropped-bytes
|
|
99
|
+
count is the size of that middle. `test` keeps the tail; `read`, `fetch`, and
|
|
100
|
+
`http-post` keep the head.
|
|
98
101
|
|
|
99
102
|
Every caller-supplied command (`bash`, `test`, `shell_command`) passes a capture
|
|
100
103
|
bound. The internal `git` plumbing behind `Checkpoints` and `TestRun`'s baseline
|
|
@@ -159,15 +159,25 @@ channel.
|
|
|
159
159
|
| Field | Meaning |
|
|
160
160
|
| ------------------------------------------ | ----------------------------------------------------------- |
|
|
161
161
|
| `exitCode` | The command's exit code. |
|
|
162
|
-
| `stdout`, `stderr` | The captured streams
|
|
162
|
+
| `stdout`, `stderr` | The captured streams; start and end when truncated. |
|
|
163
163
|
| `stdoutTruncated`, `stderrTruncated` | Whether that stream is a fragment. |
|
|
164
|
-
| `stdoutDroppedBytes`, `stderrDroppedBytes` | Bytes omitted from the
|
|
164
|
+
| `stdoutDroppedBytes`, `stderrDroppedBytes` | Bytes omitted from the middle. |
|
|
165
165
|
| `invalidProbe` | Present when the shell refused to start the command at all. |
|
|
166
166
|
|
|
167
|
-
Each stream is bounded at 30,000 bytes
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
Each stream is bounded at 30,000 bytes: its first 15,000 and its last 15,000.
|
|
168
|
+
A search prints its matches first and a failing command prints its verdict
|
|
169
|
+
last, so both survive. One line stands where the middle was:
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
MARKER-HEAD
|
|
173
|
+
xxxxxxxxxxxx
|
|
174
|
+
[10025 bytes omitted]
|
|
175
|
+
xxxxxxxxxxxx
|
|
176
|
+
MARKER-TAIL
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Never write a truncated stream to a file: it is a fragment, not the content it
|
|
180
|
+
came from. `@smthrs/harness` reads the `Truncated` flags to refuse exactly that.
|
|
171
181
|
|
|
172
182
|
`timeoutMs` defaults to `Bash.DEFAULT_TIMEOUT_MS`, ten minutes.
|
|
173
183
|
|
package/docs/reference/flows.md
CHANGED
|
@@ -241,9 +241,9 @@ envelope `bash` declares for every call.
|
|
|
241
241
|
| Output | Type | Meaning |
|
|
242
242
|
| ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------- |
|
|
243
243
|
| `exitCode` | number | The command's exit code, including non-zero. |
|
|
244
|
-
| `stdout`, `stderr` | string | Captured streams
|
|
244
|
+
| `stdout`, `stderr` | string | Captured streams; start and end kept when truncated. |
|
|
245
245
|
| `stdoutTruncated`, `stderrTruncated` | boolean | Whether that stream is a fragment. |
|
|
246
|
-
| `stdoutDroppedBytes`, `stderrDroppedBytes` | number | Bytes omitted from the
|
|
246
|
+
| `stdoutDroppedBytes`, `stderrDroppedBytes` | number | Bytes omitted from the middle. |
|
|
247
247
|
| `invalidProbe` | `Probe.InvalidProbe`, optional | Reserved for independently confirmed launch failures; not inferred from exit code. |
|
|
248
248
|
|
|
249
249
|
`Bash.Input` and `Bash.Output` are also exported as TypeScript types alongside
|
package/docs/troubleshooting.md
CHANGED
|
@@ -260,11 +260,11 @@ runs. The server would have run workspace-written code on the host.
|
|
|
260
260
|
absolute path, not a launcher. For typescript-language-server, also pin
|
|
261
261
|
`initializationOptions: { tsserver: { path } }` to a host copy.
|
|
262
262
|
|
|
263
|
-
## A write of captured output replaced a file with a
|
|
263
|
+
## A write of captured output replaced a file with a fragment
|
|
264
264
|
|
|
265
265
|
**What happened.** A truncated `stdout`, `stderr`, or `tail` was written to a
|
|
266
|
-
file. Those fields are fragments: the
|
|
267
|
-
printed,
|
|
266
|
+
file. Those fields are fragments: `bash` keeps the start and the end of what the
|
|
267
|
+
process printed and `test` keeps the tail, never the whole of it.
|
|
268
268
|
|
|
269
269
|
**What to change.** Check `stdoutTruncated`, `stderrTruncated`, or
|
|
270
270
|
`tailTruncated` before writing captured output anywhere.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/std",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "agent"
|
|
@@ -401,11 +401,11 @@
|
|
|
401
401
|
"tag": "next"
|
|
402
402
|
},
|
|
403
403
|
"dependencies": {
|
|
404
|
-
"@smthrs/capability": "1.0.0-rc.
|
|
405
|
-
"@smthrs/control": "1.0.0-rc.
|
|
406
|
-
"@smthrs/core": "1.0.0-rc.
|
|
407
|
-
"@smthrs/kernel": "1.0.0-rc.
|
|
408
|
-
"@smthrs/model": "1.0.0-rc.
|
|
404
|
+
"@smthrs/capability": "1.0.0-rc.7",
|
|
405
|
+
"@smthrs/control": "1.0.0-rc.7",
|
|
406
|
+
"@smthrs/core": "1.0.0-rc.7",
|
|
407
|
+
"@smthrs/kernel": "1.0.0-rc.7",
|
|
408
|
+
"@smthrs/model": "1.0.0-rc.7"
|
|
409
409
|
},
|
|
410
410
|
"devDependencies": {
|
|
411
411
|
"@effect/language-service": "0.87.2",
|
package/src/Bash.ts
CHANGED
|
@@ -27,7 +27,7 @@ import { capability, envelope } from "./internal/Declaration.ts"
|
|
|
27
27
|
import * as Diagnostics from "./internal/Diagnostics.ts"
|
|
28
28
|
import { outsideEnvelope } from "./internal/EnvelopePrecheck.ts"
|
|
29
29
|
import * as Exec from "./internal/Exec.ts"
|
|
30
|
-
import {
|
|
30
|
+
import { elideMiddle as elide, MAX_SHELL_OUTPUT_BYTES } from "./internal/Text.ts"
|
|
31
31
|
import * as Probe from "./Probe.ts"
|
|
32
32
|
import * as StdError from "./StdError.ts"
|
|
33
33
|
import * as TreeFingerprint from "./TreeFingerprint.ts"
|
|
@@ -173,10 +173,12 @@ export type Input = typeof Input.Type
|
|
|
173
173
|
export const Output = Schema.Struct({
|
|
174
174
|
exitCode: Schema.Number.annotate({ description: "Command exit code, including non-zero codes" }),
|
|
175
175
|
stdout: Schema.String.annotate({
|
|
176
|
-
description:
|
|
176
|
+
description:
|
|
177
|
+
"Captured stdout; when stdoutTruncated is true, only its start and end, with one line naming the bytes omitted between them"
|
|
177
178
|
}),
|
|
178
179
|
stderr: Schema.String.annotate({
|
|
179
|
-
description:
|
|
180
|
+
description:
|
|
181
|
+
"Captured stderr; when stderrTruncated is true, only its start and end, with one line naming the bytes omitted between them"
|
|
180
182
|
}),
|
|
181
183
|
stdoutTruncated: Schema.Boolean.annotate({
|
|
182
184
|
description:
|
|
@@ -186,8 +188,8 @@ export const Output = Schema.Struct({
|
|
|
186
188
|
description:
|
|
187
189
|
"Whether stderr exceeded the capture limit, leaving stderr a fragment that must not be written to a file"
|
|
188
190
|
}),
|
|
189
|
-
stdoutDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the
|
|
190
|
-
stderrDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the
|
|
191
|
+
stdoutDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the middle of stdout" }),
|
|
192
|
+
stderrDroppedBytes: Schema.Number.annotate({ description: "UTF-8 bytes omitted from the middle of stderr" }),
|
|
191
193
|
invalidProbe: Schema.optional(
|
|
192
194
|
Probe.InvalidProbe.annotate({
|
|
193
195
|
description: "Reserved for a confirmed launch failure; an exit code alone does not establish one"
|
|
@@ -209,6 +211,17 @@ export const Output = Schema.Struct({
|
|
|
209
211
|
*/
|
|
210
212
|
export type Output = typeof Output.Type
|
|
211
213
|
|
|
214
|
+
/**
|
|
215
|
+
* The text of a stream cut to its start and end: the start, one line naming
|
|
216
|
+
* the bytes omitted between them, and the end. A host that captures output
|
|
217
|
+
* itself, such as a terminal behind {@link inTerminal}, cuts with this so every
|
|
218
|
+
* {@link Output} reads the same.
|
|
219
|
+
*
|
|
220
|
+
* @category rendering
|
|
221
|
+
* @since 1.0.0-rc.5
|
|
222
|
+
*/
|
|
223
|
+
export const elideMiddle: (start: string, droppedBytes: number, end: string) => string = elide
|
|
224
|
+
|
|
212
225
|
/**
|
|
213
226
|
* Static conservative effect envelope for the bash flow.
|
|
214
227
|
*
|
|
@@ -500,6 +513,7 @@ export const run = Effect.fn("Bash.run")(function*(
|
|
|
500
513
|
...(spawned.env === undefined ? {} : { env: spawned.env }),
|
|
501
514
|
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
502
515
|
maxCaptureBytes: MAX_SHELL_OUTPUT_BYTES,
|
|
516
|
+
overflow: "ends",
|
|
503
517
|
...(spawned.args === undefined ? {} : { args: spawned.args }),
|
|
504
518
|
...(spawned.stdin === undefined ? {} : { stdin: spawned.stdin })
|
|
505
519
|
}).pipe(Effect.mapError((error) => Exec.toStdError(spawned.quoted, error)))
|
|
@@ -507,18 +521,16 @@ export const run = Effect.fn("Bash.run")(function*(
|
|
|
507
521
|
// A command on this host may have changed files a language server holds
|
|
508
522
|
// open; a container's files are not the ones it was sent.
|
|
509
523
|
if (!contained) yield* Diagnostics.refresh
|
|
510
|
-
const stdout = truncateBytes(result.stdout, MAX_SHELL_OUTPUT_BYTES, { keep: "tail" })
|
|
511
|
-
const stderr = truncateBytes(result.stderr, MAX_SHELL_OUTPUT_BYTES, { keep: "tail" })
|
|
512
524
|
// Exit codes alone cannot establish whether the shell refused to launch a
|
|
513
525
|
// command or a launched runner chose that status after executing tests.
|
|
514
526
|
return {
|
|
515
527
|
exitCode: result.exitCode,
|
|
516
|
-
stdout: stdout
|
|
517
|
-
stderr: stderr
|
|
518
|
-
stdoutTruncated: result.stdoutDroppedBytes > 0
|
|
519
|
-
stderrTruncated: result.stderrDroppedBytes > 0
|
|
520
|
-
stdoutDroppedBytes: result.stdoutDroppedBytes
|
|
521
|
-
stderrDroppedBytes: result.stderrDroppedBytes
|
|
528
|
+
stdout: result.stdout,
|
|
529
|
+
stderr: result.stderr,
|
|
530
|
+
stdoutTruncated: result.stdoutDroppedBytes > 0,
|
|
531
|
+
stderrTruncated: result.stderrDroppedBytes > 0,
|
|
532
|
+
stdoutDroppedBytes: result.stdoutDroppedBytes,
|
|
533
|
+
stderrDroppedBytes: result.stderrDroppedBytes,
|
|
522
534
|
...(mutated === undefined ? {} : { mutated })
|
|
523
535
|
}
|
|
524
536
|
})
|