@nimbus-sh/core 0.5.0 → 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 +47 -26
- package/dist/_shared/byte-stream.d.ts +5 -4
- package/dist/_shared/byte-stream.d.ts.map +1 -1
- package/dist/_shared/cache-stats.d.ts +23 -1
- package/dist/_shared/cache-stats.d.ts.map +1 -1
- package/dist/_shared/cache-stats.js +13 -1
- package/dist/_shared/exports-resolver.d.ts +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/real-node-imports.d.ts +7 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -1
- package/dist/_shared/real-node-imports.js +8 -0
- package/dist/_shared/retry.js +1 -1
- package/dist/_shared/tarball.d.ts +23 -5
- package/dist/_shared/tarball.d.ts.map +1 -1
- package/dist/_shared/tarball.js +38 -5
- package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
- package/dist/_shared/vfs-write-ledger.js +22 -16
- package/dist/constants.d.ts +5 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -56
- package/dist/runtime/assets-loader.js +1 -1
- package/dist/runtime/comment-strip.d.ts +0 -35
- package/dist/runtime/comment-strip.d.ts.map +1 -1
- package/dist/runtime/comment-strip.js +48 -20
- package/dist/runtime/esbuild-service.d.ts +38 -42
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +494 -197
- package/dist/runtime/javascript-ast.d.ts.map +1 -1
- package/dist/runtime/javascript-ast.js +45 -7
- package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-backend.js +18 -16
- package/dist/runtime/port-registry.d.ts +4 -2
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +60 -8
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
- package/dist/shell/stdin-adapter.d.ts +11 -0
- package/dist/shell/stdin-adapter.d.ts.map +1 -0
- package/dist/shell/stdin-adapter.js +59 -0
- package/dist/shell/unix-commands.d.ts +2 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +1078 -50
- package/dist/substrate/lifo/commands/io/dd.js +1 -1
- package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/net/curl.js +270 -54
- package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
- package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/npm.js +26 -43
- package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/head.js +2 -1
- package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/sed.js +499 -117
- package/dist/substrate/lifo/commands/types.d.ts +8 -4
- package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
- package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.js +122 -23
- package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
- package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/interpreter.js +79 -29
- package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
- package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/pipe.js +62 -18
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +1 -1
- package/dist/vfs/sqlite-vfs.d.ts +21 -30
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +113 -29
- package/dist/workspace/nimbus-workspace.d.ts +15 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +20 -3
- package/dist/workspace/supervisor-op.d.ts +23 -0
- package/dist/workspace/supervisor-op.d.ts.map +1 -0
- package/dist/workspace/supervisor-op.js +92 -0
- package/package.json +3 -2
- package/src/_shared/byte-stream.ts +5 -4
- package/src/_shared/cache-stats.ts +27 -1
- package/src/_shared/exports-resolver.ts +1 -1
- package/src/_shared/real-node-imports.ts +8 -0
- package/src/_shared/retry.ts +1 -1
- package/src/_shared/tarball.ts +56 -5
- package/src/_shared/vfs-write-ledger.ts +22 -16
- package/src/constants.ts +30 -60
- package/src/runtime/assets-loader.ts +1 -1
- package/src/runtime/comment-strip.ts +42 -23
- package/src/runtime/esbuild-service.ts +566 -217
- package/src/runtime/javascript-ast.ts +40 -9
- package/src/runtime/opentui-wasm-backend.ts +19 -17
- package/src/runtime/port-registry.ts +66 -8
- package/src/runtime/virtual-socket-kernel.generated.ts +1 -1
- package/src/shell/stdin-adapter.ts +58 -0
- package/src/shell/unix-commands.ts +1054 -50
- package/src/substrate/lifo/commands/io/dd.ts +2 -2
- package/src/substrate/lifo/commands/net/curl.ts +310 -58
- package/src/substrate/lifo/commands/system/npm.ts +58 -38
- package/src/substrate/lifo/commands/text/head.ts +2 -1
- package/src/substrate/lifo/commands/text/sed.ts +519 -109
- package/src/substrate/lifo/commands/types.ts +8 -5
- package/src/substrate/lifo/node-compat/zlib.ts +155 -26
- package/src/substrate/lifo/shell/interpreter.ts +74 -27
- package/src/substrate/lifo/shell/pipe.ts +65 -24
- package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
- package/src/vfs/seed-project.ts +3 -3
- package/src/vfs/sqlite-vfs.ts +128 -64
- package/src/workspace/nimbus-workspace.ts +26 -2
- package/src/workspace/supervisor-op.ts +117 -0
- package/dist/_shared/install-phase.d.ts +0 -47
- package/dist/_shared/install-phase.d.ts.map +0 -1
- package/dist/_shared/install-phase.js +0 -1
- package/dist/_shared/rpc-dispose.d.ts +0 -4
- package/dist/_shared/rpc-dispose.d.ts.map +0 -1
- package/dist/_shared/rpc-dispose.js +0 -30
- package/dist/_shared/w7-frame.d.ts +0 -79
- package/dist/_shared/w7-frame.d.ts.map +0 -1
- package/dist/_shared/w7-frame.js +0 -840
- package/dist/_shared/weighted-credit-pool.d.ts +0 -61
- package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
- package/dist/_shared/weighted-credit-pool.js +0 -196
- package/dist/observability/diag-counters.d.ts +0 -236
- package/dist/observability/diag-counters.d.ts.map +0 -1
- package/dist/observability/diag-counters.js +0 -195
- package/dist/observability/heap-estimate.d.ts +0 -225
- package/dist/observability/heap-estimate.d.ts.map +0 -1
- package/dist/observability/heap-estimate.js +0 -185
- package/dist/observability/heavy-alloc-coord.d.ts +0 -93
- package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
- package/dist/observability/heavy-alloc-coord.js +0 -181
- package/dist/observability/oom-classify.d.ts +0 -138
- package/dist/observability/oom-classify.d.ts.map +0 -1
- package/dist/observability/oom-classify.js +0 -268
- package/dist/observability/oom-discriminator.d.ts +0 -176
- package/dist/observability/oom-discriminator.d.ts.map +0 -1
- package/dist/observability/oom-discriminator.js +0 -272
- package/src/_shared/install-phase.ts +0 -56
- package/src/_shared/rpc-dispose.ts +0 -31
- package/src/_shared/w7-frame.ts +0 -1013
- package/src/_shared/weighted-credit-pool.ts +0 -244
- package/src/observability/diag-counters.ts +0 -343
- package/src/observability/heap-estimate.ts +0 -331
- package/src/observability/heavy-alloc-coord.ts +0 -241
- package/src/observability/oom-classify.ts +0 -260
- package/src/observability/oom-discriminator.ts +0 -405
|
@@ -4,9 +4,8 @@ import type { VfsCred } from '../../../runtime/os-contracts.js';
|
|
|
4
4
|
export interface CommandOutputStream {
|
|
5
5
|
write(text: string): void;
|
|
6
6
|
/**
|
|
7
|
-
* Present on sinks that store bytes verbatim
|
|
8
|
-
*
|
|
9
|
-
* they have no way to carry a byte that is not text.
|
|
7
|
+
* Present on sinks that store bytes verbatim — files, `/dev/null`, and
|
|
8
|
+
* byte-capable shell pipes. Sinks without it take decoded text instead.
|
|
10
9
|
*/
|
|
11
10
|
writeBytes?(bytes: Uint8Array): void;
|
|
12
11
|
/**
|
|
@@ -22,9 +21,13 @@ export interface CommandInputStream {
|
|
|
22
21
|
read(): Promise<string | null>; // null = EOF
|
|
23
22
|
readAll(): Promise<string>;
|
|
24
23
|
readLine?(): Promise<string | null>;
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Bounded raw-byte read. Byte-capable sources (shell pipes, dumps) return
|
|
26
|
+
* the original bytes; text-only sources encode what they hold. Null means
|
|
27
|
+
* EOF with nothing returned.
|
|
28
|
+
*/
|
|
29
|
+
readBytes?(maxLength: number): Promise<Uint8Array | null>;
|
|
26
30
|
}
|
|
27
|
-
|
|
28
31
|
export interface TerminalInputStream extends CommandInputStream {
|
|
29
32
|
rawMode: boolean;
|
|
30
33
|
}
|
|
@@ -2,6 +2,79 @@ import { Buffer } from './buffer.js';
|
|
|
2
2
|
|
|
3
3
|
type ZlibCallback = (err: Error | null, result?: Buffer) => void;
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Node's async surface: the callback is required, with options optional in
|
|
7
|
+
* front of it. There is no promise return to fall back on.
|
|
8
|
+
*/
|
|
9
|
+
interface ZlibAsync {
|
|
10
|
+
(data: ZlibInput, callback: ZlibCallback): void;
|
|
11
|
+
(data: ZlibInput, options: object, callback: ZlibCallback): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Everything a zlib entry point accepts: strings encode to UTF-8;
|
|
16
|
+
* ArrayBuffers and every ArrayBufferView carry their own bytes.
|
|
17
|
+
*/
|
|
18
|
+
type ZlibInput = string | ArrayBufferView | ArrayBuffer;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One funnel for every entry point. The result views the caller's exact
|
|
22
|
+
* byte region — byteOffset and byteLength preserved, element values never
|
|
23
|
+
* reinterpreted — so sniffing and the codec see identical bytes for a
|
|
24
|
+
* Uint8Array, a Uint16Array, an offset DataView, or a bare ArrayBuffer.
|
|
25
|
+
*/
|
|
26
|
+
function toBytes(data: ZlibInput): Uint8Array {
|
|
27
|
+
if (typeof data === 'string') return new TextEncoder().encode(data);
|
|
28
|
+
if (data instanceof Uint8Array) return data;
|
|
29
|
+
if (ArrayBuffer.isView(data)) {
|
|
30
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
31
|
+
}
|
|
32
|
+
return new Uint8Array(data);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Node splits decompression failures two ways, and the engine's own failure
|
|
37
|
+
* is the only thing that can tell them apart: an input that ran out is
|
|
38
|
+
* Z_BUF_ERROR / errno -5, while everything else — bad header, bad block, and
|
|
39
|
+
* a checksum or length trailer that can only fail once the member is
|
|
40
|
+
* complete — is Z_DATA_ERROR / errno -3 carrying the engine's message. Which
|
|
41
|
+
* side of a write or a close the failure lands on says nothing about that,
|
|
42
|
+
* so it is not consulted.
|
|
43
|
+
*/
|
|
44
|
+
function isUnexpectedEnd(cause: unknown): boolean {
|
|
45
|
+
const message = cause instanceof Error && typeof cause.message === 'string' ? cause.message : String(cause);
|
|
46
|
+
return /unexpected end|end of (?:file|input|stream)|premature|truncated|\bEOF\b/i.test(message);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function zDataError(cause: unknown): Error {
|
|
50
|
+
const message = cause instanceof Error && cause.message ? cause.message : String(cause);
|
|
51
|
+
return Object.assign(new Error(message), { code: 'Z_DATA_ERROR', errno: -3, cause });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function zBufError(cause: unknown): Error {
|
|
55
|
+
return Object.assign(new Error('unexpected end of file'), {
|
|
56
|
+
code: 'Z_BUF_ERROR',
|
|
57
|
+
errno: -5,
|
|
58
|
+
cause,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Node throws this synchronously rather than deferring a callback that does
|
|
64
|
+
* not exist.
|
|
65
|
+
*/
|
|
66
|
+
function invalidCallback(value: unknown): TypeError {
|
|
67
|
+
const received =
|
|
68
|
+
value === undefined ? 'undefined'
|
|
69
|
+
: value === null ? 'null'
|
|
70
|
+
: typeof value === 'object' ? `an instance of ${value.constructor?.name ?? 'Object'}`
|
|
71
|
+
: `type ${typeof value} (${String(value)})`;
|
|
72
|
+
return Object.assign(
|
|
73
|
+
new TypeError(`The "callback" argument must be of type function. Received ${received}`),
|
|
74
|
+
{ code: 'ERR_INVALID_ARG_TYPE' },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
5
78
|
async function processStream(
|
|
6
79
|
data: Uint8Array,
|
|
7
80
|
format: CompressionFormat,
|
|
@@ -13,15 +86,35 @@ async function processStream(
|
|
|
13
86
|
: new DecompressionStream(format);
|
|
14
87
|
|
|
15
88
|
const writer = stream.writable.getWriter();
|
|
16
|
-
writer.write(data as unknown as ArrayBuffer);
|
|
17
|
-
writer.close();
|
|
18
|
-
|
|
19
89
|
const reader = stream.readable.getReader();
|
|
20
90
|
const chunks: Uint8Array[] = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
91
|
+
// Feeding and draining run together: a single write large enough to fill
|
|
92
|
+
// the codec's output queue only completes while somebody reads. Both sides
|
|
93
|
+
// are contained so neither rejection surfaces unhandled, and the first
|
|
94
|
+
// failure is the one reported.
|
|
95
|
+
const failures: unknown[] = [];
|
|
96
|
+
const record = (reason: unknown): void => {
|
|
97
|
+
if (failures.length === 0) failures.push(reason);
|
|
98
|
+
};
|
|
99
|
+
const drain = (async () => {
|
|
100
|
+
for (;;) {
|
|
101
|
+
const { done, value } = await reader.read();
|
|
102
|
+
if (done) return;
|
|
103
|
+
chunks.push(value);
|
|
104
|
+
}
|
|
105
|
+
})().catch(record);
|
|
106
|
+
const feed = (async () => {
|
|
107
|
+
await writer.write(data);
|
|
108
|
+
await writer.close();
|
|
109
|
+
})().catch(record);
|
|
110
|
+
await Promise.all([drain, feed]);
|
|
111
|
+
|
|
112
|
+
if (failures.length > 0) {
|
|
113
|
+
const reason = failures[0];
|
|
114
|
+
if (type === 'decompress') {
|
|
115
|
+
throw isUnexpectedEnd(reason) ? zBufError(reason) : zDataError(reason);
|
|
116
|
+
}
|
|
117
|
+
throw reason instanceof Error ? reason : new Error(String(reason));
|
|
25
118
|
}
|
|
26
119
|
|
|
27
120
|
let totalLength = 0;
|
|
@@ -35,14 +128,28 @@ async function processStream(
|
|
|
35
128
|
return result;
|
|
36
129
|
}
|
|
37
130
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Node's Unzip sniffs the wrapper itself: the gzip magic bytes mean gunzip,
|
|
133
|
+
* anything else is treated as zlib-wrapped deflate.
|
|
134
|
+
*/
|
|
135
|
+
function looksLikeGzip(data: Uint8Array): boolean {
|
|
136
|
+
return data.length >= 2 && data[0] === 0x1f && data[1] === 0x8b;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function wrapAsync(
|
|
140
|
+
format: CompressionFormat | ((data: Uint8Array) => CompressionFormat),
|
|
141
|
+
type: 'compress' | 'decompress',
|
|
142
|
+
): ZlibAsync {
|
|
143
|
+
return function (data: ZlibInput, optionsOrCb: object | ZlibCallback, cb?: ZlibCallback): void {
|
|
144
|
+
const callback = typeof optionsOrCb === 'function' ? optionsOrCb : cb;
|
|
145
|
+
if (typeof callback !== 'function') throw invalidCallback(callback);
|
|
146
|
+
const bytes = toBytes(data);
|
|
147
|
+
// Two arms, not .then().catch(): a callback that throws on success must
|
|
148
|
+
// not be handed its own exception as a second, failed invocation.
|
|
149
|
+
processStream(bytes, typeof format === 'function' ? format(bytes) : format, type).then(
|
|
150
|
+
(result) => callback(null, result),
|
|
151
|
+
(err) => callback(err instanceof Error ? err : new Error(String(err))),
|
|
152
|
+
);
|
|
46
153
|
};
|
|
47
154
|
}
|
|
48
155
|
|
|
@@ -52,20 +159,40 @@ export const deflate = wrapAsync('deflate', 'compress');
|
|
|
52
159
|
export const inflate = wrapAsync('deflate', 'decompress');
|
|
53
160
|
export const deflateRaw = wrapAsync('deflate-raw', 'compress');
|
|
54
161
|
export const inflateRaw = wrapAsync('deflate-raw', 'decompress');
|
|
55
|
-
export const unzip = wrapAsync('gzip', 'decompress');
|
|
162
|
+
export const unzip = wrapAsync((data) => (looksLikeGzip(data) ? 'gzip' : 'deflate'), 'decompress');
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The Sync surface needs a synchronous compress primitive; browser-class
|
|
166
|
+
* runtimes only offer the async CompressionStream. Each Sync name refuses
|
|
167
|
+
* with the working async replacement spelled out instead of failing as a
|
|
168
|
+
* missing export or a vague "not supported".
|
|
169
|
+
*/
|
|
170
|
+
function syncUnavailable(name: string, asyncName: string): never {
|
|
171
|
+
throw Object.assign(new Error(
|
|
172
|
+
`zlib.${name}: synchronous compression is not available on this runtime. Use the async zlib.${asyncName}(data, callback) form instead.`,
|
|
173
|
+
), { code: 'ERR_ZLIB_SYNC_UNAVAILABLE' });
|
|
174
|
+
}
|
|
56
175
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
176
|
+
export function gzipSync(buffer?: ZlibInput, options?: object): never {
|
|
177
|
+
syncUnavailable('gzipSync', 'gzip');
|
|
178
|
+
}
|
|
179
|
+
export function gunzipSync(buffer?: ZlibInput, options?: object): never {
|
|
180
|
+
syncUnavailable('gunzipSync', 'gunzip');
|
|
181
|
+
}
|
|
182
|
+
export function deflateSync(buffer?: ZlibInput, options?: object): never {
|
|
183
|
+
syncUnavailable('deflateSync', 'deflate');
|
|
184
|
+
}
|
|
185
|
+
export function inflateSync(buffer?: ZlibInput, options?: object): never {
|
|
186
|
+
syncUnavailable('inflateSync', 'inflate');
|
|
60
187
|
}
|
|
61
|
-
export function
|
|
62
|
-
|
|
188
|
+
export function deflateRawSync(buffer?: ZlibInput, options?: object): never {
|
|
189
|
+
syncUnavailable('deflateRawSync', 'deflateRaw');
|
|
63
190
|
}
|
|
64
|
-
export function
|
|
65
|
-
|
|
191
|
+
export function inflateRawSync(buffer?: ZlibInput, options?: object): never {
|
|
192
|
+
syncUnavailable('inflateRawSync', 'inflateRaw');
|
|
66
193
|
}
|
|
67
|
-
export function
|
|
68
|
-
|
|
194
|
+
export function unzipSync(buffer?: ZlibInput, options?: object): never {
|
|
195
|
+
syncUnavailable('unzipSync', 'unzip');
|
|
69
196
|
}
|
|
70
197
|
|
|
71
198
|
export const constants = {
|
|
@@ -90,5 +217,7 @@ export const constants = {
|
|
|
90
217
|
|
|
91
218
|
export default {
|
|
92
219
|
gzip, gunzip, deflate, inflate, deflateRaw, inflateRaw, unzip,
|
|
93
|
-
gzipSync, gunzipSync, deflateSync, inflateSync,
|
|
220
|
+
gzipSync, gunzipSync, deflateSync, inflateSync,
|
|
221
|
+
deflateRawSync, inflateRawSync, unzipSync,
|
|
222
|
+
constants,
|
|
94
223
|
};
|
|
@@ -40,6 +40,7 @@ import { exitCodeForAbortSignal } from './signals.js';
|
|
|
40
40
|
import { resolve } from '../utils/path.js';
|
|
41
41
|
import { encode } from '../utils/encoding.js';
|
|
42
42
|
import { globMatch } from '../utils/glob.js';
|
|
43
|
+
import { staticStdinReader } from '../../../shell/stdin-adapter.js';
|
|
43
44
|
|
|
44
45
|
/**
|
|
45
46
|
* Bytes a file-backed descriptor holds before committing. Matches the stream
|
|
@@ -1391,7 +1392,7 @@ export class Interpreter {
|
|
|
1391
1392
|
const body = heredoc.quoted
|
|
1392
1393
|
? heredoc.body
|
|
1393
1394
|
: await expandWord([{ text: heredoc.body, quoted: 'double' }], expandCtx);
|
|
1394
|
-
this.setInputFd(fds, redir.fd ?? 0, { stream:
|
|
1395
|
+
this.setInputFd(fds, redir.fd ?? 0, { stream: staticStdinReader(body), terminal: false });
|
|
1395
1396
|
continue;
|
|
1396
1397
|
}
|
|
1397
1398
|
|
|
@@ -1533,11 +1534,17 @@ export class Interpreter {
|
|
|
1533
1534
|
return { stream: terminalStdin, terminal: true };
|
|
1534
1535
|
}
|
|
1535
1536
|
if (target === '/dev/stdin') return this.resolveInputFd('0', fds.inputFds, fds.terminalInputFds);
|
|
1537
|
+
const targetPath = resolve(this.config.getCwd(), target);
|
|
1538
|
+
const vfs = io.vfs ?? this.config.vfs;
|
|
1536
1539
|
try {
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1540
|
+
// Open-authorize before the command runs, the way open(2) would: a
|
|
1541
|
+
// missing, unreadable, or directory target fails the redirection even
|
|
1542
|
+
// when the command never reads a byte.
|
|
1543
|
+
if (vfs.stat(targetPath).type === 'directory') {
|
|
1544
|
+
throw Object.assign(new Error(`EISDIR: ${targetPath}`), { code: 'EISDIR' });
|
|
1545
|
+
}
|
|
1546
|
+
vfs.access(targetPath, 0o4);
|
|
1547
|
+
return { stream: this.createFileReader(vfs, targetPath), terminal: false };
|
|
1541
1548
|
} catch (error) {
|
|
1542
1549
|
throw new RedirectionOpenError(target, error);
|
|
1543
1550
|
}
|
|
@@ -1607,37 +1614,77 @@ export class Interpreter {
|
|
|
1607
1614
|
for (const stream of fds.outputFds.values()) stream.flush?.();
|
|
1608
1615
|
}
|
|
1609
1616
|
|
|
1617
|
+
/**
|
|
1618
|
+
* Byte-faithful redirected input: bounded range reads keep >64 KiB
|
|
1619
|
+
* redirections intact and preserve bytes that are not valid UTF-8, while
|
|
1620
|
+
* the text view still decodes progressively across chunk boundaries.
|
|
1621
|
+
*
|
|
1622
|
+
* Only a zero-length range means EOF. Devices and some mounts answer with
|
|
1623
|
+
* fewer bytes than asked whenever their internal bound is hit; those short
|
|
1624
|
+
* nonempty reads advance the offset and continue, exactly like read(2).
|
|
1625
|
+
*/
|
|
1610
1626
|
private createFileReader(vfs: VFS, path: string): CommandInputStream {
|
|
1611
|
-
const
|
|
1612
|
-
return this.createStringReader(content);
|
|
1613
|
-
}
|
|
1614
|
-
|
|
1615
|
-
private createStringReader(content: string): CommandInputStream {
|
|
1627
|
+
const decoder = new TextDecoder('utf-8');
|
|
1616
1628
|
let offset = 0;
|
|
1629
|
+
let eof = false;
|
|
1630
|
+
const pull = (max: number): Uint8Array | null => {
|
|
1631
|
+
if (eof) return null;
|
|
1632
|
+
const chunk = vfs.readRange(path, offset, max);
|
|
1633
|
+
if (chunk.length === 0) {
|
|
1634
|
+
eof = true;
|
|
1635
|
+
return null;
|
|
1636
|
+
}
|
|
1637
|
+
offset += chunk.length;
|
|
1638
|
+
return chunk;
|
|
1639
|
+
};
|
|
1617
1640
|
return {
|
|
1641
|
+
readBytes: async (maxLength: number) => {
|
|
1642
|
+
if (maxLength <= 0) return new Uint8Array(0);
|
|
1643
|
+
return pull(maxLength);
|
|
1644
|
+
},
|
|
1618
1645
|
read: async () => {
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1646
|
+
while (true) {
|
|
1647
|
+
const bytes = pull(65536);
|
|
1648
|
+
if (bytes === null) {
|
|
1649
|
+
const tail = decoder.decode();
|
|
1650
|
+
return tail.length > 0 ? tail : null;
|
|
1651
|
+
}
|
|
1652
|
+
const text = decoder.decode(bytes, { stream: true });
|
|
1653
|
+
if (text.length > 0) return text;
|
|
1654
|
+
}
|
|
1623
1655
|
},
|
|
1624
1656
|
readAll: async () => {
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1657
|
+
let out = '';
|
|
1658
|
+
while (true) {
|
|
1659
|
+
const bytes = pull(65536);
|
|
1660
|
+
if (bytes === null) break;
|
|
1661
|
+
out += decoder.decode(bytes, { stream: true });
|
|
1662
|
+
}
|
|
1663
|
+
out += decoder.decode();
|
|
1664
|
+
return out;
|
|
1629
1665
|
},
|
|
1630
1666
|
readLine: async () => {
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
const
|
|
1635
|
-
|
|
1636
|
-
|
|
1667
|
+
let line = '';
|
|
1668
|
+
let sawAny = false;
|
|
1669
|
+
while (true) {
|
|
1670
|
+
const bytes = pull(65536);
|
|
1671
|
+
if (bytes === null) break;
|
|
1672
|
+
sawAny = true;
|
|
1673
|
+
// Split on the raw 0x0A byte so pushback returns ORIGINAL bytes;
|
|
1674
|
+
// decoded-text pushback would corrupt multibyte sequences that
|
|
1675
|
+
// straddle the chunk boundary or mis-measure invalid UTF-8.
|
|
1676
|
+
const newline = bytes.indexOf(0x0a);
|
|
1677
|
+
if (newline >= 0) {
|
|
1678
|
+
offset -= bytes.length - (newline + 1);
|
|
1679
|
+
eof = false;
|
|
1680
|
+
line += decoder.decode(bytes.subarray(0, newline), { stream: true });
|
|
1681
|
+
const flushed = decoder.decode();
|
|
1682
|
+
return line + flushed;
|
|
1683
|
+
}
|
|
1684
|
+
line += decoder.decode(bytes, { stream: true });
|
|
1637
1685
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
return line;
|
|
1686
|
+
line += decoder.decode();
|
|
1687
|
+
return sawAny || line.length > 0 ? line : null;
|
|
1641
1688
|
},
|
|
1642
1689
|
};
|
|
1643
1690
|
}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import type { CommandOutputStream, CommandInputStream } from '../commands/types.js';
|
|
2
|
+
import { encode } from '../utils/encoding.js';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* A shell pipe that carries the producer's exact bytes. Text writes are
|
|
6
|
+
* encoded once at the write side, `writeBytes` stores bytes verbatim, and
|
|
7
|
+
* the text view (`read`/`readAll`/`readLine`) decodes progressively so a
|
|
8
|
+
* multi-byte UTF-8 sequence split across chunks survives intact.
|
|
9
|
+
*/
|
|
3
10
|
export class PipeChannel {
|
|
4
|
-
private buffer:
|
|
11
|
+
private buffer: Uint8Array[] = [];
|
|
5
12
|
private closed = false;
|
|
6
|
-
private waiting: Array<(value:
|
|
13
|
+
private waiting: Array<(value: Uint8Array | null) => void> = [];
|
|
14
|
+
private decoder = new TextDecoder('utf-8');
|
|
7
15
|
|
|
8
16
|
readonly writer: CommandOutputStream = {
|
|
9
17
|
write: (text: string) => {
|
|
10
18
|
if (this.closed) return;
|
|
11
|
-
this.deliver(text, 'back');
|
|
19
|
+
this.deliver(encode(text), 'back');
|
|
20
|
+
},
|
|
21
|
+
writeBytes: (bytes: Uint8Array) => {
|
|
22
|
+
if (this.closed) return;
|
|
23
|
+
this.deliver(bytes, 'back');
|
|
12
24
|
},
|
|
13
25
|
};
|
|
14
26
|
|
|
@@ -19,18 +31,31 @@ export class PipeChannel {
|
|
|
19
31
|
readBytes: (maxLength: number) => this.readBytes(maxLength),
|
|
20
32
|
};
|
|
21
33
|
|
|
22
|
-
|
|
34
|
+
/** Next queued chunk, a waiter's delivery, or null once closed and empty. */
|
|
35
|
+
private pull(): Promise<Uint8Array | null> {
|
|
23
36
|
if (this.buffer.length > 0) {
|
|
24
37
|
return Promise.resolve(this.buffer.shift() ?? null);
|
|
25
38
|
}
|
|
26
39
|
if (this.closed) {
|
|
27
40
|
return Promise.resolve(null);
|
|
28
41
|
}
|
|
29
|
-
return new Promise<
|
|
42
|
+
return new Promise<Uint8Array | null>((resolve) => {
|
|
30
43
|
this.waiting.push(resolve);
|
|
31
44
|
});
|
|
32
45
|
}
|
|
33
46
|
|
|
47
|
+
private async read(): Promise<string | null> {
|
|
48
|
+
while (true) {
|
|
49
|
+
const bytes = await this.pull();
|
|
50
|
+
if (bytes === null) {
|
|
51
|
+
const tail = this.decoder.decode();
|
|
52
|
+
return tail.length > 0 ? tail : null;
|
|
53
|
+
}
|
|
54
|
+
const text = this.decoder.decode(bytes, { stream: true });
|
|
55
|
+
if (text.length > 0) return text;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
private async readAll(): Promise<string> {
|
|
35
60
|
const parts: string[] = [];
|
|
36
61
|
while (true) {
|
|
@@ -43,30 +68,45 @@ export class PipeChannel {
|
|
|
43
68
|
|
|
44
69
|
private async readLine(): Promise<string | null> {
|
|
45
70
|
let line = '';
|
|
71
|
+
let sawAny = false;
|
|
46
72
|
while (true) {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
const bytes = await this.pull();
|
|
74
|
+
if (bytes === null) break;
|
|
75
|
+
sawAny = true;
|
|
76
|
+
// Split on the raw 0x0A byte so pushback returns ORIGINAL bytes; a
|
|
77
|
+
// multibyte sequence straddling the chunk boundary then survives as
|
|
78
|
+
// é instead of collapsing into a replacement character.
|
|
79
|
+
const newline = bytes.indexOf(0x0a);
|
|
51
80
|
if (newline >= 0) {
|
|
52
|
-
const rest =
|
|
81
|
+
const rest = bytes.subarray(newline + 1);
|
|
53
82
|
if (rest.length > 0) this.deliver(rest, 'front');
|
|
54
|
-
|
|
83
|
+
line += this.decoder.decode(bytes.subarray(0, newline), { stream: true });
|
|
84
|
+
const flushed = this.decoder.decode();
|
|
85
|
+
return line + flushed;
|
|
55
86
|
}
|
|
56
|
-
|
|
57
|
-
line += chunk;
|
|
87
|
+
line += this.decoder.decode(bytes, { stream: true });
|
|
58
88
|
}
|
|
89
|
+
// A trailing incomplete sequence still surfaces as U+FFFD at EOF.
|
|
90
|
+
const tail = this.decoder.decode();
|
|
91
|
+
line += tail;
|
|
92
|
+
return sawAny || tail.length > 0 ? line : null;
|
|
59
93
|
}
|
|
60
94
|
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Bounded byte read: returns whatever the producer has already delivered,
|
|
97
|
+
* capped at maxLength. maxLength bounds the result, it is never a fill
|
|
98
|
+
* target — waiting to complete it would stall every consumer downstream of
|
|
99
|
+
* a live open producer. A larger chunk keeps only its first maxLength
|
|
100
|
+
* bytes; the remainder stays queued in original order.
|
|
101
|
+
*/
|
|
102
|
+
private async readBytes(maxLength: number): Promise<Uint8Array | null> {
|
|
103
|
+
if (maxLength <= 0) return new Uint8Array(0);
|
|
63
104
|
|
|
64
|
-
const chunk = await this.
|
|
105
|
+
const chunk = await this.pull();
|
|
65
106
|
if (chunk === null) return null;
|
|
66
107
|
if (chunk.length <= maxLength) return chunk;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return chunk.slice(0, maxLength);
|
|
108
|
+
this.buffer.unshift(chunk.subarray(maxLength));
|
|
109
|
+
return chunk.subarray(0, maxLength);
|
|
70
110
|
}
|
|
71
111
|
|
|
72
112
|
close(): void {
|
|
@@ -77,16 +117,17 @@ export class PipeChannel {
|
|
|
77
117
|
}
|
|
78
118
|
}
|
|
79
119
|
|
|
80
|
-
private deliver(
|
|
81
|
-
if (
|
|
120
|
+
private deliver(bytes: Uint8Array, position: 'front' | 'back'): void {
|
|
121
|
+
if (bytes.length === 0) return;
|
|
82
122
|
|
|
83
123
|
if (this.waiting.length > 0) {
|
|
84
124
|
const resolve = this.waiting.shift()!;
|
|
85
|
-
resolve(
|
|
125
|
+
resolve(bytes);
|
|
86
126
|
return;
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
if (position === 'front') this.buffer.unshift(
|
|
90
|
-
else this.buffer.push(
|
|
129
|
+
if (position === 'front') this.buffer.unshift(bytes);
|
|
130
|
+
else this.buffer.push(bytes);
|
|
91
131
|
}
|
|
132
|
+
|
|
92
133
|
}
|