@nimbus-sh/core 0.4.0 → 0.5.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 +35 -7
- package/dist/_shared/error-text.d.ts +15 -0
- package/dist/_shared/error-text.d.ts.map +1 -0
- package/dist/_shared/error-text.js +21 -0
- package/dist/_shared/exports-resolver.d.ts +19 -7
- package/dist/_shared/exports-resolver.d.ts.map +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/retry.d.ts.map +1 -1
- package/dist/_shared/retry.js +3 -1
- package/dist/_shared/w7-frame.js +2 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +30 -16
- package/dist/observability/oom-classify.d.ts +1 -1
- package/dist/observability/oom-classify.d.ts.map +1 -1
- package/dist/observability/oom-classify.js +7 -0
- package/dist/observability/oom-discriminator.d.ts +1 -1
- package/dist/runtime/esbuild-service.d.ts +4 -3
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +9 -4
- package/dist/runtime/installed-runtimes.d.ts +4 -3
- package/dist/runtime/installed-runtimes.d.ts.map +1 -1
- package/dist/runtime/long-running-handle.d.ts +3 -3
- package/dist/runtime/long-running-handle.d.ts.map +1 -1
- package/dist/runtime/os-contracts.d.ts +7 -5
- package/dist/runtime/os-contracts.d.ts.map +1 -1
- package/dist/runtime/port-registry.d.ts +28 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +79 -9
- package/dist/runtime/process-logs.d.ts +9 -1
- package/dist/runtime/process-logs.d.ts.map +1 -1
- package/dist/runtime/project-detect.d.ts +6 -1
- package/dist/runtime/project-detect.d.ts.map +1 -1
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +3 -4
- package/dist/runtime/runtime-registry.d.ts +4 -3
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +2 -1
- package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
- package/dist/runtime/virtual-socket-kernel.js +10 -5
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +22 -14
- package/dist/shell/features.d.ts.map +1 -1
- package/dist/shell/features.js +5 -4
- package/dist/shell/unix-commands.d.ts +14 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +115 -69
- package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
- package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/index.js +3 -2
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
- package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/process.js +6 -3
- package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
- package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/Shell.js +86 -75
- package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
- package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/lexer.js +40 -0
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +8 -2
- package/dist/vfs/sqlite-vfs.d.ts +61 -1
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +199 -35
- package/package.json +1 -1
- package/src/_shared/error-text.ts +21 -0
- package/src/_shared/exports-resolver.ts +24 -4
- package/src/_shared/retry.ts +4 -2
- package/src/_shared/w7-frame.ts +2 -1
- package/src/constants.ts +30 -16
- package/src/observability/oom-classify.ts +7 -0
- package/src/observability/oom-discriminator.ts +1 -1
- package/src/runtime/esbuild-service.ts +16 -11
- package/src/runtime/installed-runtimes.ts +4 -3
- package/src/runtime/long-running-handle.ts +3 -3
- package/src/runtime/os-contracts.ts +7 -5
- package/src/runtime/port-registry.ts +97 -9
- package/src/runtime/process-logs.ts +12 -3
- package/src/runtime/project-detect.ts +7 -1
- package/src/runtime/require-resolver.ts +7 -7
- package/src/runtime/runtime-registry.ts +9 -7
- package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
- package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
- package/src/runtime/virtual-socket-kernel.ts +10 -5
- package/src/runtime/wasm-runner.ts +55 -29
- package/src/shell/features.ts +5 -4
- package/src/shell/unix-commands.ts +273 -128
- package/src/substrate/lifo/commands/net/ip.ts +15 -15
- package/src/substrate/lifo/commands/system/ps.ts +2 -1
- package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
- package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
- package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
- package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
- package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
- package/src/substrate/lifo/node-compat/index.ts +3 -2
- package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
- package/src/substrate/lifo/node-compat/process.ts +5 -2
- package/src/substrate/lifo/shell/Shell.ts +92 -70
- package/src/substrate/lifo/shell/lexer.ts +38 -0
- package/src/vfs/seed-project.ts +10 -3
- package/src/vfs/sqlite-vfs.ts +209 -36
|
@@ -12,12 +12,19 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
|
|
15
|
-
import { getSymlinkRegistry } from '../vfs/symlink-registry.js';
|
|
15
|
+
import { getSymlinkRegistry, type SymlinkRegistry } from '../vfs/symlink-registry.js';
|
|
16
16
|
import { requireVfsCred, type VfsCred } from '../runtime/os-contracts.js';
|
|
17
17
|
import { dec, enc } from '../_shared/bytes.js';
|
|
18
|
+
import { errorText } from '../_shared/error-text.js';
|
|
18
19
|
import { NIMBUS_VERSION } from '../constants.js';
|
|
19
20
|
import { SinkWriter, streamRange } from '../_shared/byte-stream.js';
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
fileTypeChar,
|
|
23
|
+
isCharacterDevice,
|
|
24
|
+
type FileType,
|
|
25
|
+
type VFS,
|
|
26
|
+
} from '../substrate/lifo/kernel/vfs/index.js';
|
|
27
|
+
import type { Command, CommandInputStream } from '../substrate/lifo/commands/types.js';
|
|
21
28
|
import { runSed } from '../substrate/lifo/commands/text/sed.js';
|
|
22
29
|
import { parseArgs } from '../substrate/lifo/utils/args.js';
|
|
23
30
|
import {
|
|
@@ -27,6 +34,41 @@ import {
|
|
|
27
34
|
} from './unix-accounts.js';
|
|
28
35
|
import { createSuCommand, createSudoCommand, createUmaskCommand } from './elevation-commands.js';
|
|
29
36
|
|
|
37
|
+
/**
|
|
38
|
+
* stdin as the shell hands it over: a pipe reader, whose `readAll` resolves
|
|
39
|
+
* once upstream closes, or the terminal's own stream, which stays open past
|
|
40
|
+
* the command and so is taken from `buffer` instead of awaited.
|
|
41
|
+
*/
|
|
42
|
+
type ShellStdin = CommandInputStream & {
|
|
43
|
+
feed?(text: string): void;
|
|
44
|
+
buffer?: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The VFS the caller supplies. The shell hands the kernel's mount-aware tree,
|
|
49
|
+
* so `/dev` and the other mounts resolve; an embedder that invokes a command
|
|
50
|
+
* directly hands a credentialed durable view. `readdirStat` belongs to the
|
|
51
|
+
* first and `isDirectory` to the second, which is why the paths that want
|
|
52
|
+
* either one probe for it.
|
|
53
|
+
*/
|
|
54
|
+
type CtxVfs = CredentialedVfs | VFS;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A stat as either layer reports it. The kernel's tree leaves out what a mount
|
|
58
|
+
* cannot know — ownership, access time — and that is what the fallbacks at the
|
|
59
|
+
* use sites stand in for.
|
|
60
|
+
*/
|
|
61
|
+
type CtxStat = {
|
|
62
|
+
type: FileType;
|
|
63
|
+
size: number;
|
|
64
|
+
mode: number;
|
|
65
|
+
mtime: number;
|
|
66
|
+
ctime: number;
|
|
67
|
+
atime?: number;
|
|
68
|
+
uid?: number;
|
|
69
|
+
gid?: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
30
72
|
type Ctx = {
|
|
31
73
|
pid: number;
|
|
32
74
|
args: string[];
|
|
@@ -39,9 +81,13 @@ type Ctx = {
|
|
|
39
81
|
stderr: { write(s: string): void };
|
|
40
82
|
cwd: string;
|
|
41
83
|
env: Record<string, string>;
|
|
42
|
-
|
|
84
|
+
/**
|
|
85
|
+
* The shell's stream until `wrap` drains it to a string in place. Command
|
|
86
|
+
* bodies read the string; `head`, wrapped for streaming, reads the stream.
|
|
87
|
+
*/
|
|
88
|
+
stdin?: string | ShellStdin;
|
|
43
89
|
cred: VfsCred;
|
|
44
|
-
vfs:
|
|
90
|
+
vfs: CtxVfs;
|
|
45
91
|
signal: AbortSignal;
|
|
46
92
|
setUmask(mask: number): void;
|
|
47
93
|
runAs(cred: VfsCred, argv: string[]): Promise<number>;
|
|
@@ -49,10 +95,50 @@ type Ctx = {
|
|
|
49
95
|
};
|
|
50
96
|
|
|
51
97
|
type CmdFn = (ctx: Ctx) => number | Promise<number>;
|
|
98
|
+
|
|
52
99
|
type UnixVfs = CredentialedVfs & {
|
|
53
|
-
readonly symlinks:
|
|
100
|
+
readonly symlinks: SymlinkRegistry;
|
|
54
101
|
};
|
|
55
102
|
|
|
103
|
+
/**
|
|
104
|
+
* A command the registry resolved. A runtime that is known but not installed
|
|
105
|
+
* is stored as a stub carrying `__nimbusRuntimeInstallHint` (the worker's
|
|
106
|
+
* `shell/npm-bin-entrypoints.ts` sets it), which `which` and `type` must not
|
|
107
|
+
* report as a builtin.
|
|
108
|
+
*/
|
|
109
|
+
type ResolvedCommand = CmdFn & { __nimbusRuntimeInstallHint?: boolean };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The registry these commands dispatch through: registration, and name
|
|
113
|
+
* resolution for `which`, `type`, `command`, `find -exec` and `xargs`.
|
|
114
|
+
* `resolve` answers `unknown` because the registry holds whatever any module
|
|
115
|
+
* registered — and the worker's npm-bin fallback replaces the method outright
|
|
116
|
+
* — so what comes back is a command only once it has been checked.
|
|
117
|
+
*/
|
|
118
|
+
type UnixCommandRegistry = {
|
|
119
|
+
register(name: string, handler: Command): void;
|
|
120
|
+
resolve(name: string): unknown;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A resolved entry as a command this module can run. Every handler in the
|
|
125
|
+
* registry takes a command context; the ones registered below read the string
|
|
126
|
+
* `wrap` leaves in `stdin`, and a command dispatched from here is handed that
|
|
127
|
+
* string rather than a reader.
|
|
128
|
+
*/
|
|
129
|
+
function asResolvedCommand(resolved: unknown): ResolvedCommand | null {
|
|
130
|
+
return typeof resolved === 'function' ? resolved as ResolvedCommand : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* stdin as text. `wrap` drains the shell's stream into `ctx.stdin` before a
|
|
135
|
+
* command body runs, so a command that does not read a stream itself sees the
|
|
136
|
+
* string it left there, and nothing at all when there was no stdin.
|
|
137
|
+
*/
|
|
138
|
+
function stdinText(ctx: Ctx): string | undefined {
|
|
139
|
+
return typeof ctx.stdin === 'string' ? ctx.stdin : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
56
142
|
function unixVfsFor(sqliteVfs: SqliteVFS, cred: VfsCred): UnixVfs {
|
|
57
143
|
return {
|
|
58
144
|
...sqliteVfs.as(cred),
|
|
@@ -114,8 +200,8 @@ function unixGroupLabel(vfs: UnixVfs, gid: number): string {
|
|
|
114
200
|
}
|
|
115
201
|
}
|
|
116
202
|
|
|
117
|
-
function isRuntimeInstallHintHandler(handler:
|
|
118
|
-
return !!handler && !!
|
|
203
|
+
function isRuntimeInstallHintHandler(handler: ResolvedCommand | null): boolean {
|
|
204
|
+
return !!handler && !!handler.__nimbusRuntimeInstallHint;
|
|
119
205
|
}
|
|
120
206
|
|
|
121
207
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
@@ -270,13 +356,13 @@ function _pathLookup(
|
|
|
270
356
|
}
|
|
271
357
|
|
|
272
358
|
async function _registryResolved(
|
|
273
|
-
registry:
|
|
359
|
+
registry: UnixCommandRegistry,
|
|
274
360
|
name: string,
|
|
275
361
|
options: { includeInstallHints?: boolean } = {},
|
|
276
|
-
): Promise<
|
|
362
|
+
): Promise<ResolvedCommand | null> {
|
|
277
363
|
try {
|
|
278
364
|
const resolved = typeof registry.resolve === 'function'
|
|
279
|
-
? await registry.resolve(name)
|
|
365
|
+
? asResolvedCommand(await registry.resolve(name))
|
|
280
366
|
: null;
|
|
281
367
|
if (resolved && (options.includeInstallHints || !isRuntimeInstallHintHandler(resolved))) {
|
|
282
368
|
return resolved;
|
|
@@ -292,7 +378,7 @@ async function _registryResolved(
|
|
|
292
378
|
* caller knows the command is a shell builtin (no fallback). */
|
|
293
379
|
async function _whichLookup(
|
|
294
380
|
vfs: UnixVfs,
|
|
295
|
-
registry:
|
|
381
|
+
registry: UnixCommandRegistry,
|
|
296
382
|
name: string,
|
|
297
383
|
envPath: string,
|
|
298
384
|
): Promise<string | null> {
|
|
@@ -305,7 +391,7 @@ async function _whichLookup(
|
|
|
305
391
|
: null;
|
|
306
392
|
}
|
|
307
393
|
|
|
308
|
-
function mkWhich(vfs: UnixVfs, registry:
|
|
394
|
+
function mkWhich(vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
|
|
309
395
|
return async (ctx) => {
|
|
310
396
|
// Parse flags. Supports -a (show all matches), -s (silent — no
|
|
311
397
|
// stdout, only exit code). Default behaviour matches GNU which.
|
|
@@ -368,7 +454,7 @@ function mkWhich(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
368
454
|
* binaries on the virtual VFS; print just the binary path. With no
|
|
369
455
|
* match, print just the name (matches `whereis` behavior on missing).
|
|
370
456
|
*/
|
|
371
|
-
function mkWhereis(vfs: UnixVfs, registry:
|
|
457
|
+
function mkWhereis(vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
|
|
372
458
|
return async (ctx) => {
|
|
373
459
|
const names = ctx.args.filter(a => !a.startsWith('-'));
|
|
374
460
|
if (names.length === 0) {
|
|
@@ -402,7 +488,7 @@ function mkWhereis(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
402
488
|
* (because the interpreter only consults aliases
|
|
403
489
|
* for the head word).
|
|
404
490
|
*/
|
|
405
|
-
function mkCommand(vfs: UnixVfs, registry:
|
|
491
|
+
function mkCommand(vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
|
|
406
492
|
return async (ctx) => {
|
|
407
493
|
const args = [...ctx.args];
|
|
408
494
|
let mode: '-v' | '-V' | 'invoke' = 'invoke';
|
|
@@ -433,7 +519,7 @@ function mkCommand(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
433
519
|
// because we're calling the resolved cmd not the alias name.
|
|
434
520
|
const name = args[0];
|
|
435
521
|
try {
|
|
436
|
-
const resolved = await registry.resolve(name);
|
|
522
|
+
const resolved = asResolvedCommand(await registry.resolve(name));
|
|
437
523
|
if (!resolved) {
|
|
438
524
|
ctx.stderr.write(`command: ${name}: not found\n`);
|
|
439
525
|
return 127;
|
|
@@ -441,8 +527,8 @@ function mkCommand(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
441
527
|
const subCtx = { ...ctx, args: args.slice(1) };
|
|
442
528
|
const code = await resolved(subCtx);
|
|
443
529
|
return typeof code === 'number' ? code : 0;
|
|
444
|
-
} catch (e
|
|
445
|
-
ctx.stderr.write(`command: ${name}: ${e
|
|
530
|
+
} catch (e) {
|
|
531
|
+
ctx.stderr.write(`command: ${name}: ${errorText(e)}\n`);
|
|
446
532
|
return 1;
|
|
447
533
|
}
|
|
448
534
|
};
|
|
@@ -466,13 +552,15 @@ function mkCommand(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
466
552
|
* commands module already has access to; treat any registry resolve
|
|
467
553
|
* as "shell builtin" classification.
|
|
468
554
|
*/
|
|
469
|
-
function mkType(_vfs: UnixVfs, registry:
|
|
555
|
+
function mkType(_vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
|
|
470
556
|
return async (ctx) => {
|
|
471
557
|
if (ctx.args.length === 0) return 0;
|
|
472
558
|
let exit = 0;
|
|
473
559
|
for (const name of ctx.args) {
|
|
474
560
|
try {
|
|
475
|
-
const resolved = typeof registry.resolve === 'function'
|
|
561
|
+
const resolved = typeof registry.resolve === 'function'
|
|
562
|
+
? asResolvedCommand(await registry.resolve(name))
|
|
563
|
+
: null;
|
|
476
564
|
if (resolved && !isRuntimeInstallHintHandler(resolved)) {
|
|
477
565
|
ctx.stdout.write(`${name} is a shell builtin\n`);
|
|
478
566
|
} else {
|
|
@@ -577,7 +665,7 @@ const _DAYS_FULL = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday',
|
|
|
577
665
|
const _DAYS_ABBR = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
|
578
666
|
|
|
579
667
|
function strftime(d: Date, fmt: string, utc: boolean): string {
|
|
580
|
-
const get = (m: string):
|
|
668
|
+
const get = (m: string): number => {
|
|
581
669
|
switch (m) {
|
|
582
670
|
case 'FullYear': return utc ? d.getUTCFullYear() : d.getFullYear();
|
|
583
671
|
case 'Month': return utc ? d.getUTCMonth() : d.getMonth();
|
|
@@ -782,7 +870,7 @@ interface FindExecBatch {
|
|
|
782
870
|
}
|
|
783
871
|
|
|
784
872
|
type FindNode = (entry: FindEntry, st: FindState) => Promise<boolean>;
|
|
785
|
-
function mkFind(vfs: UnixVfs, registry:
|
|
873
|
+
function mkFind(vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
|
|
786
874
|
return async (ctx) => {
|
|
787
875
|
const state: FindState = {
|
|
788
876
|
minDepth: 0,
|
|
@@ -803,8 +891,8 @@ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
803
891
|
const runExec = async (argv: string[]): Promise<boolean> => {
|
|
804
892
|
const [name, ...rest] = argv;
|
|
805
893
|
if (!name) return false;
|
|
806
|
-
let target;
|
|
807
|
-
try { target = await registry.resolve(name); } catch { target = null; }
|
|
894
|
+
let target: ResolvedCommand | null;
|
|
895
|
+
try { target = asResolvedCommand(await registry.resolve(name)); } catch { target = null; }
|
|
808
896
|
if (!target) {
|
|
809
897
|
ctx.stderr.write(`find: ${name}: No such file or directory\n`);
|
|
810
898
|
return false;
|
|
@@ -826,8 +914,8 @@ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
826
914
|
execInterpreterDepth: ctx.execInterpreterDepth,
|
|
827
915
|
});
|
|
828
916
|
return code === 0;
|
|
829
|
-
} catch (e
|
|
830
|
-
ctx.stderr.write(`find: ${name}: ${e
|
|
917
|
+
} catch (e) {
|
|
918
|
+
ctx.stderr.write(`find: ${name}: ${errorText(e)}\n`);
|
|
831
919
|
return false;
|
|
832
920
|
}
|
|
833
921
|
};
|
|
@@ -978,8 +1066,8 @@ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
978
1066
|
if (e.type === 'directory') vfs.rmdir(e.vfsPath);
|
|
979
1067
|
else vfs.unlink(e.vfsPath);
|
|
980
1068
|
return true;
|
|
981
|
-
} catch (err
|
|
982
|
-
ctx.stderr.write(`find: cannot delete '${e.display}': ${err
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
ctx.stderr.write(`find: cannot delete '${e.display}': ${errorText(err)}\n`);
|
|
983
1071
|
return false;
|
|
984
1072
|
}
|
|
985
1073
|
};
|
|
@@ -1070,7 +1158,7 @@ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
1070
1158
|
if (pos < args.length) {
|
|
1071
1159
|
throw new FindUsageError(`paths must precede expression: \`${args[pos]}'`);
|
|
1072
1160
|
}
|
|
1073
|
-
} catch (e
|
|
1161
|
+
} catch (e) {
|
|
1074
1162
|
if (e instanceof FindUsageError) {
|
|
1075
1163
|
ctx.stderr.write(`find: ${e.message}\n`);
|
|
1076
1164
|
return 1;
|
|
@@ -1152,6 +1240,13 @@ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
1152
1240
|
};
|
|
1153
1241
|
}
|
|
1154
1242
|
|
|
1243
|
+
/**
|
|
1244
|
+
* `grep`'s argv, carrying `-F` alongside it. Both spellings of the flag — its
|
|
1245
|
+
* own word and a letter inside a cluster — land on the parsed argv, which is
|
|
1246
|
+
* what the pattern escape below reads.
|
|
1247
|
+
*/
|
|
1248
|
+
type GrepArgv = string[] & { __fixedStrings?: boolean };
|
|
1249
|
+
|
|
1155
1250
|
/**
|
|
1156
1251
|
* shell compatibility (2026-05-11): grep flag handling.
|
|
1157
1252
|
*
|
|
@@ -1169,7 +1264,7 @@ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
1169
1264
|
*/
|
|
1170
1265
|
function mkGrep(vfs: UnixVfs): CmdFn {
|
|
1171
1266
|
return (ctx) => {
|
|
1172
|
-
const args = [...ctx.args];
|
|
1267
|
+
const args: GrepArgv = [...ctx.args];
|
|
1173
1268
|
// Parse flags. Support combined `-rni` form (single dash + chars).
|
|
1174
1269
|
let recursive = false, ignoreCase = false, lineNum = false;
|
|
1175
1270
|
let countOnly = false, invertMatch = false, wordMatch = false;
|
|
@@ -1190,7 +1285,7 @@ function mkGrep(vfs: UnixVfs): CmdFn {
|
|
|
1190
1285
|
if (a === '-E' || a === '--extended-regexp') { /* JS regex is ERE-ish */ continue; }
|
|
1191
1286
|
if (a === '-F' || a === '--fixed-strings') {
|
|
1192
1287
|
// Mark as literal — handled below via escape.
|
|
1193
|
-
|
|
1288
|
+
args.__fixedStrings = true;
|
|
1194
1289
|
continue;
|
|
1195
1290
|
}
|
|
1196
1291
|
if (a.startsWith('-') && a.length > 1 && !a.startsWith('--')) {
|
|
@@ -1205,7 +1300,7 @@ function mkGrep(vfs: UnixVfs): CmdFn {
|
|
|
1205
1300
|
else if (ch === 'l') filesOnly = true;
|
|
1206
1301
|
else if (ch === 'q') quiet = true;
|
|
1207
1302
|
else if (ch === 'E') { /* ERE noop */ }
|
|
1208
|
-
else if (ch === 'F')
|
|
1303
|
+
else if (ch === 'F') args.__fixedStrings = true;
|
|
1209
1304
|
}
|
|
1210
1305
|
continue;
|
|
1211
1306
|
}
|
|
@@ -1214,7 +1309,7 @@ function mkGrep(vfs: UnixVfs): CmdFn {
|
|
|
1214
1309
|
if (positional.length < 1) { ctx.stderr.write('Usage: grep [-rnicvlqEFw] PATTERN [FILE...]\n'); return 1; }
|
|
1215
1310
|
let pattern = positional[0];
|
|
1216
1311
|
const targets = positional.slice(1);
|
|
1217
|
-
if (
|
|
1312
|
+
if (args.__fixedStrings) {
|
|
1218
1313
|
// -F: escape regex metacharacters for literal match.
|
|
1219
1314
|
pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1220
1315
|
}
|
|
@@ -1281,8 +1376,9 @@ function mkGrep(vfs: UnixVfs): CmdFn {
|
|
|
1281
1376
|
walkDir((ctx.cwd || '/home/user').replace(/^\/+/, ''));
|
|
1282
1377
|
} else if (targets.length === 0) {
|
|
1283
1378
|
// Read from stdin (if piped) — single virtual "file" with no label.
|
|
1284
|
-
|
|
1285
|
-
|
|
1379
|
+
const piped = stdinText(ctx);
|
|
1380
|
+
if (piped) {
|
|
1381
|
+
processLines(piped.split('\n'), '');
|
|
1286
1382
|
}
|
|
1287
1383
|
} else {
|
|
1288
1384
|
for (const target of targets) {
|
|
@@ -1321,7 +1417,7 @@ function mkGrep(vfs: UnixVfs): CmdFn {
|
|
|
1321
1417
|
* (when wrap already coalesced) still work via the same code path.
|
|
1322
1418
|
*/
|
|
1323
1419
|
function mkHead(_vfs: UnixVfs): CmdFn {
|
|
1324
|
-
return async (ctx
|
|
1420
|
+
return async (ctx) => {
|
|
1325
1421
|
const parsed = parseHeadArgs(ctx.args);
|
|
1326
1422
|
if (parsed.error) { ctx.stderr.write(`head: ${parsed.error}\n`); return 1; }
|
|
1327
1423
|
const { lines: n, bytes, files } = parsed;
|
|
@@ -1342,8 +1438,9 @@ function mkHead(_vfs: UnixVfs): CmdFn {
|
|
|
1342
1438
|
if (chunk === null) break;
|
|
1343
1439
|
buffered += chunk;
|
|
1344
1440
|
// Process complete lines while we have them.
|
|
1345
|
-
|
|
1346
|
-
|
|
1441
|
+
while (emitted < n) {
|
|
1442
|
+
const nlIdx = buffered.indexOf('\n');
|
|
1443
|
+
if (nlIdx === -1) break;
|
|
1347
1444
|
out.push(buffered.substring(0, nlIdx));
|
|
1348
1445
|
buffered = buffered.substring(nlIdx + 1);
|
|
1349
1446
|
emitted++;
|
|
@@ -1546,7 +1643,8 @@ function mkTail(vfs: UnixVfs): CmdFn {
|
|
|
1546
1643
|
};
|
|
1547
1644
|
|
|
1548
1645
|
if (files.length === 0) {
|
|
1549
|
-
|
|
1646
|
+
const piped = stdinText(ctx);
|
|
1647
|
+
if (piped) emit(piped);
|
|
1550
1648
|
return 0;
|
|
1551
1649
|
}
|
|
1552
1650
|
const label = verbose || files.length > 1;
|
|
@@ -1668,7 +1766,7 @@ function mkWc(vfs: UnixVfs): CmdFn {
|
|
|
1668
1766
|
};
|
|
1669
1767
|
|
|
1670
1768
|
if (files.length === 0) {
|
|
1671
|
-
const bytes = enc.encode(ctx
|
|
1769
|
+
const bytes = enc.encode(stdinText(ctx) ?? '');
|
|
1672
1770
|
// Nothing bounds a stream's counts ahead of time, so a multi-column
|
|
1673
1771
|
// report over standard input uses the fixed width GNU falls back to.
|
|
1674
1772
|
emit(measure(bytes), columns === 1 ? 0 : 7, '');
|
|
@@ -1717,7 +1815,7 @@ function mkSort(vfs: UnixVfs): CmdFn {
|
|
|
1717
1815
|
ctx.stderr.write(`sort: invalid option -- '${unknown[0].replace(/^-+/, '')}'\n`);
|
|
1718
1816
|
return 1;
|
|
1719
1817
|
}
|
|
1720
|
-
let input = ctx
|
|
1818
|
+
let input = stdinText(ctx) || '';
|
|
1721
1819
|
if (positional.length > 0 && !input) {
|
|
1722
1820
|
try { input = vfs.readFileString(resolvePath(ctx.cwd, positional[0])); }
|
|
1723
1821
|
catch { ctx.stderr.write(`sort: ${positional[0]}: No such file\n`); return 1; }
|
|
@@ -1762,7 +1860,7 @@ function mkUniq(vfs: UnixVfs): CmdFn {
|
|
|
1762
1860
|
}
|
|
1763
1861
|
// File operands were ignored outright, so `uniq file` read stdin and
|
|
1764
1862
|
// printed nothing at all.
|
|
1765
|
-
let input = ctx
|
|
1863
|
+
let input = stdinText(ctx) || '';
|
|
1766
1864
|
if (positional.length > 0 && positional[0] !== '-') {
|
|
1767
1865
|
try { input = vfs.readFileString(resolvePath(ctx.cwd, positional[0])); }
|
|
1768
1866
|
catch { ctx.stderr.write(`uniq: ${positional[0]}: No such file\n`); return 1; }
|
|
@@ -1805,7 +1903,7 @@ function mkSed(vfs: UnixVfs): CmdFn {
|
|
|
1805
1903
|
vfs,
|
|
1806
1904
|
stdout: ctx.stdout,
|
|
1807
1905
|
stderr: ctx.stderr,
|
|
1808
|
-
stdin: ctx.stdin ===
|
|
1906
|
+
stdin: typeof ctx.stdin === 'string' ? stringInput(ctx.stdin) : undefined,
|
|
1809
1907
|
});
|
|
1810
1908
|
}
|
|
1811
1909
|
|
|
@@ -1815,6 +1913,13 @@ function stringInput(text: string): { readAll(): Promise<string> } {
|
|
|
1815
1913
|
};
|
|
1816
1914
|
}
|
|
1817
1915
|
|
|
1916
|
+
/**
|
|
1917
|
+
* What an awk expression evaluates to. The subset below has neither arrays nor
|
|
1918
|
+
* a match operator, so every value is one of the two scalars awk itself has,
|
|
1919
|
+
* and `print` decides which spelling to use.
|
|
1920
|
+
*/
|
|
1921
|
+
type AwkValue = string | number;
|
|
1922
|
+
|
|
1818
1923
|
/**
|
|
1819
1924
|
* shell compatibility (2026-05-11): expanded awk subset.
|
|
1820
1925
|
*
|
|
@@ -1877,7 +1982,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
1877
1982
|
}
|
|
1878
1983
|
}
|
|
1879
1984
|
const program = programArgs[0] || '';
|
|
1880
|
-
let input = ctx
|
|
1985
|
+
let input = stdinText(ctx) || '';
|
|
1881
1986
|
if (fileArgs.length > 0 && !input) {
|
|
1882
1987
|
try { input = vfs.readFileString(resolvePath(ctx.cwd, fileArgs[0])); }
|
|
1883
1988
|
catch { ctx.stderr.write(`awk: ${fileArgs[0]}: No such file\n`); return 1; }
|
|
@@ -1950,7 +2055,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
1950
2055
|
if (cursor < src.length && src[cursor] === '{') body = parseBraced();
|
|
1951
2056
|
let re: RegExp;
|
|
1952
2057
|
try { re = new RegExp(patSrc); }
|
|
1953
|
-
catch (e
|
|
2058
|
+
catch (e) { ctx.stderr.write(`awk: bad regex /${patSrc}/: ${errorText(e)}\n`); return 1; }
|
|
1954
2059
|
blocks.push({ kind: 'PATTERN', pattern: re, body });
|
|
1955
2060
|
continue;
|
|
1956
2061
|
}
|
|
@@ -1972,7 +2077,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
1972
2077
|
// IDENT (+|-|*|/|%)= EXPR
|
|
1973
2078
|
// next (skip rest of body for this line — rare)
|
|
1974
2079
|
interface State {
|
|
1975
|
-
vars: Record<string,
|
|
2080
|
+
vars: Record<string, AwkValue>;
|
|
1976
2081
|
fields: string[]; // [$0, $1, $2, ...]
|
|
1977
2082
|
NR: number;
|
|
1978
2083
|
NF: number;
|
|
@@ -1995,14 +2100,14 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
1995
2100
|
* term := factor (('*'|'/'|'%') factor)*
|
|
1996
2101
|
* factor := number | string | '$' (number | 'NF') | ident | '(' expr ')'
|
|
1997
2102
|
*/
|
|
1998
|
-
function evalExpr(expr: string, st: State):
|
|
2103
|
+
function evalExpr(expr: string, st: State): AwkValue {
|
|
1999
2104
|
const text = expr.trim();
|
|
2000
2105
|
let pos = 0;
|
|
2001
2106
|
function skipWs() { while (pos < text.length && /\s/.test(text[pos])) pos++; }
|
|
2002
2107
|
function peek(): string { return text[pos]; }
|
|
2003
2108
|
function consume(ch: string): boolean { skipWs(); if (text[pos] === ch) { pos++; return true; } return false; }
|
|
2004
2109
|
function expect(ch: string): void { if (!consume(ch)) throw new Error(`expected '${ch}' at "${text.slice(pos, pos + 20)}"`); }
|
|
2005
|
-
function parseExpr():
|
|
2110
|
+
function parseExpr(): AwkValue {
|
|
2006
2111
|
let left = parseTerm();
|
|
2007
2112
|
for (;;) {
|
|
2008
2113
|
skipWs();
|
|
@@ -2016,7 +2121,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2016
2121
|
}
|
|
2017
2122
|
return left;
|
|
2018
2123
|
}
|
|
2019
|
-
function parseTerm():
|
|
2124
|
+
function parseTerm(): AwkValue {
|
|
2020
2125
|
let left = parseFactor();
|
|
2021
2126
|
for (;;) {
|
|
2022
2127
|
skipWs();
|
|
@@ -2030,7 +2135,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2030
2135
|
}
|
|
2031
2136
|
return left;
|
|
2032
2137
|
}
|
|
2033
|
-
function parseFactor():
|
|
2138
|
+
function parseFactor(): AwkValue {
|
|
2034
2139
|
skipWs();
|
|
2035
2140
|
if (pos >= text.length) throw new Error(`unexpected end of expression`);
|
|
2036
2141
|
const ch = text[pos];
|
|
@@ -2101,7 +2206,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2101
2206
|
}
|
|
2102
2207
|
throw new Error(`unexpected '${ch}' at "${text.slice(pos, pos + 20)}"`);
|
|
2103
2208
|
}
|
|
2104
|
-
function toNum(v:
|
|
2209
|
+
function toNum(v: AwkValue): number {
|
|
2105
2210
|
if (typeof v === 'number') return v;
|
|
2106
2211
|
const n = parseFloat(v);
|
|
2107
2212
|
return Number.isFinite(n) ? n : 0;
|
|
@@ -2114,8 +2219,8 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2114
2219
|
// separator). Be permissive — return what we have.
|
|
2115
2220
|
}
|
|
2116
2221
|
return v;
|
|
2117
|
-
} catch (e
|
|
2118
|
-
throw new Error(`expr error: ${e
|
|
2222
|
+
} catch (e) {
|
|
2223
|
+
throw new Error(`expr error: ${errorText(e)} in "${expr}"`);
|
|
2119
2224
|
}
|
|
2120
2225
|
}
|
|
2121
2226
|
function stripStringsForScan(s: string): string {
|
|
@@ -2327,7 +2432,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2327
2432
|
if (cur.trim()) out.push(cur.trim());
|
|
2328
2433
|
return out;
|
|
2329
2434
|
}
|
|
2330
|
-
function stringify(v:
|
|
2435
|
+
function stringify(v: AwkValue | null | undefined): string {
|
|
2331
2436
|
if (v === undefined || v === null) return '';
|
|
2332
2437
|
if (typeof v === 'number') {
|
|
2333
2438
|
if (Number.isInteger(v)) return String(v);
|
|
@@ -2336,7 +2441,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2336
2441
|
}
|
|
2337
2442
|
return String(v);
|
|
2338
2443
|
}
|
|
2339
|
-
function printfFormat(fmt: string, fargs:
|
|
2444
|
+
function printfFormat(fmt: string, fargs: AwkValue[]): string {
|
|
2340
2445
|
let out = '';
|
|
2341
2446
|
let i = 0;
|
|
2342
2447
|
let argIdx = 0;
|
|
@@ -2367,7 +2472,7 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2367
2472
|
}
|
|
2368
2473
|
return out;
|
|
2369
2474
|
}
|
|
2370
|
-
function formatOne(spec: string, arg:
|
|
2475
|
+
function formatOne(spec: string, arg: AwkValue): string {
|
|
2371
2476
|
const conv = spec[spec.length - 1];
|
|
2372
2477
|
const flagsAndWidth = spec.slice(1, -1);
|
|
2373
2478
|
const dotIdx = flagsAndWidth.indexOf('.');
|
|
@@ -2467,8 +2572,8 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2467
2572
|
}
|
|
2468
2573
|
// END blocks last.
|
|
2469
2574
|
for (const b of blocks) if (b.kind === 'END') runBlock(b);
|
|
2470
|
-
} catch (e
|
|
2471
|
-
ctx.stderr.write(`awk: ${e
|
|
2575
|
+
} catch (e) {
|
|
2576
|
+
ctx.stderr.write(`awk: ${errorText(e)}\n`);
|
|
2472
2577
|
return 1;
|
|
2473
2578
|
}
|
|
2474
2579
|
|
|
@@ -2498,9 +2603,12 @@ function mkAwk(vfs: UnixVfs): CmdFn {
|
|
|
2498
2603
|
*
|
|
2499
2604
|
* Unsupported (document as gap): -P (parallel), -L (per-line), -p (prompt).
|
|
2500
2605
|
*/
|
|
2501
|
-
function mkXargs(vfs: UnixVfs, registry:
|
|
2606
|
+
function mkXargs(vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
|
|
2502
2607
|
return async (ctx) => {
|
|
2503
|
-
|
|
2608
|
+
// NOT trimmed: `-0` exists so a name may carry the whitespace a split
|
|
2609
|
+
// would eat, and trimming the stream rewrites its first and last item.
|
|
2610
|
+
// The default split already drops the empties a trim would have removed.
|
|
2611
|
+
const input = stdinText(ctx) || '';
|
|
2504
2612
|
if (!input) return 0;
|
|
2505
2613
|
|
|
2506
2614
|
// Parse flags first
|
|
@@ -2540,10 +2648,10 @@ function mkXargs(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
2540
2648
|
: input.split(/\s+/).filter(Boolean);
|
|
2541
2649
|
|
|
2542
2650
|
// Resolve target command from registry (handles both eager + lazy maps).
|
|
2543
|
-
let target;
|
|
2651
|
+
let target: ResolvedCommand | null;
|
|
2544
2652
|
try {
|
|
2545
|
-
target = await registry.resolve(cmdName);
|
|
2546
|
-
} catch
|
|
2653
|
+
target = asResolvedCommand(await registry.resolve(cmdName));
|
|
2654
|
+
} catch { target = null; }
|
|
2547
2655
|
if (!target) {
|
|
2548
2656
|
// Defer to write-to-stderr; mimic real xargs which would exec(2) and fail.
|
|
2549
2657
|
ctx.stderr.write(`xargs: ${cmdName}: command not found\n`);
|
|
@@ -2575,8 +2683,8 @@ function mkXargs(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
2575
2683
|
try {
|
|
2576
2684
|
const code = await target(newCtx(subbed));
|
|
2577
2685
|
if (typeof code === 'number' && code !== 0) exit = code;
|
|
2578
|
-
} catch (e
|
|
2579
|
-
ctx.stderr.write(`xargs: ${cmdName}: ${e
|
|
2686
|
+
} catch (e) {
|
|
2687
|
+
ctx.stderr.write(`xargs: ${cmdName}: ${errorText(e)}\n`);
|
|
2580
2688
|
exit = 1;
|
|
2581
2689
|
}
|
|
2582
2690
|
}
|
|
@@ -2588,8 +2696,8 @@ function mkXargs(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
2588
2696
|
try {
|
|
2589
2697
|
const code = await target(newCtx([...cmdArgsInitial, ...batch]));
|
|
2590
2698
|
if (typeof code === 'number' && code !== 0) exit = code;
|
|
2591
|
-
} catch (e
|
|
2592
|
-
ctx.stderr.write(`xargs: ${cmdName}: ${e
|
|
2699
|
+
} catch (e) {
|
|
2700
|
+
ctx.stderr.write(`xargs: ${cmdName}: ${errorText(e)}\n`);
|
|
2593
2701
|
exit = 1;
|
|
2594
2702
|
}
|
|
2595
2703
|
if (!Number.isFinite(batchSize)) break; // single batch when no -n
|
|
@@ -2601,7 +2709,7 @@ function mkXargs(vfs: UnixVfs, registry: any): CmdFn {
|
|
|
2601
2709
|
|
|
2602
2710
|
function mkTee(vfs: UnixVfs): CmdFn {
|
|
2603
2711
|
return (ctx) => {
|
|
2604
|
-
const input = ctx
|
|
2712
|
+
const input = stdinText(ctx) || '';
|
|
2605
2713
|
const append = ctx.args.includes('-a');
|
|
2606
2714
|
const files = ctx.args.filter(a => !a.startsWith('-'));
|
|
2607
2715
|
ctx.stdout.write(input);
|
|
@@ -2694,7 +2802,13 @@ function mkDiff(vfs: UnixVfs): CmdFn {
|
|
|
2694
2802
|
}
|
|
2695
2803
|
}
|
|
2696
2804
|
return hasDiff ? 1 : 0;
|
|
2697
|
-
} catch (e
|
|
2805
|
+
} catch (e) {
|
|
2806
|
+
// `diff` reports the thrown value's `message`, whatever it holds, rather
|
|
2807
|
+
// than the value: a throw carrying none has always printed `undefined`.
|
|
2808
|
+
const message = typeof e === 'object' && e !== null && 'message' in e ? e.message : undefined;
|
|
2809
|
+
ctx.stderr.write(`diff: ${String(message)}\n`);
|
|
2810
|
+
return 2;
|
|
2811
|
+
}
|
|
2698
2812
|
};
|
|
2699
2813
|
}
|
|
2700
2814
|
|
|
@@ -2715,6 +2829,39 @@ function mkDiff(vfs: UnixVfs): CmdFn {
|
|
|
2715
2829
|
* (rmdir recursive when -r). Translate raw errors so the unix-command
|
|
2716
2830
|
* contract is honoured.
|
|
2717
2831
|
*/
|
|
2832
|
+
/** The single-character backslash escapes `echo -e` and `printf` both expand. */
|
|
2833
|
+
const BACKSLASH_ESCAPES: Readonly<Record<string, string>> = {
|
|
2834
|
+
'\\': '\\',
|
|
2835
|
+
n: '\n',
|
|
2836
|
+
t: '\t',
|
|
2837
|
+
r: '\r',
|
|
2838
|
+
a: '\x07',
|
|
2839
|
+
b: '\b',
|
|
2840
|
+
f: '\f',
|
|
2841
|
+
v: '\v',
|
|
2842
|
+
};
|
|
2843
|
+
|
|
2844
|
+
/**
|
|
2845
|
+
* Expand POSIX backslash escapes in one pass.
|
|
2846
|
+
*
|
|
2847
|
+
* A pass per escape needs somewhere to park a literal `\` so the later passes
|
|
2848
|
+
* cannot read it as the start of an escape, and whatever character that is, the
|
|
2849
|
+
* text may hold one already — or an earlier escape may have just produced one.
|
|
2850
|
+
* NUL was the parking spot, so `printf 'a\0b'` and `echo -e 'a\x00b'` both came
|
|
2851
|
+
* back as `a\b`: the NUL they had just produced was restored as a backslash.
|
|
2852
|
+
* One left-to-right pass consumes `\\` as a unit and needs no parking spot.
|
|
2853
|
+
*/
|
|
2854
|
+
function expandBackslashEscapes(text: string): string {
|
|
2855
|
+
return text.replace(
|
|
2856
|
+
/\\(?:([\\ntrabfv])|0([0-7]{1,3})?|x([0-9a-fA-F]{1,2}))/g,
|
|
2857
|
+
(_match, simple: string | undefined, octal: string | undefined, hex: string | undefined) => {
|
|
2858
|
+
if (simple !== undefined) return BACKSLASH_ESCAPES[simple];
|
|
2859
|
+
if (hex !== undefined) return String.fromCharCode(parseInt(hex, 16));
|
|
2860
|
+
return String.fromCharCode(octal ? parseInt(octal, 8) : 0);
|
|
2861
|
+
},
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2718
2865
|
/**
|
|
2719
2866
|
* shell compatibilityb (2026-05-11): registry-level echo so `X | xargs echo`
|
|
2720
2867
|
* resolves. `echo` is a Shell.builtins entry, NOT in the
|
|
@@ -2750,18 +2897,7 @@ function mkEcho(): CmdFn {
|
|
|
2750
2897
|
}
|
|
2751
2898
|
let out = args.slice(i).join(' ');
|
|
2752
2899
|
if (interpretEscapes) {
|
|
2753
|
-
out = out
|
|
2754
|
-
.replace(/\\\\/g, '\u0000')
|
|
2755
|
-
.replace(/\\n/g, '\n')
|
|
2756
|
-
.replace(/\\t/g, '\t')
|
|
2757
|
-
.replace(/\\r/g, '\r')
|
|
2758
|
-
.replace(/\\b/g, '\b')
|
|
2759
|
-
.replace(/\\f/g, '\f')
|
|
2760
|
-
.replace(/\\v/g, '\v')
|
|
2761
|
-
.replace(/\\a/g, '\x07')
|
|
2762
|
-
.replace(/\\0([0-7]{1,3})?/g, (_m, oct) => String.fromCharCode(oct ? parseInt(oct, 8) : 0))
|
|
2763
|
-
.replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
2764
|
-
.replace(/\u0000/g, '\\');
|
|
2900
|
+
out = expandBackslashEscapes(out);
|
|
2765
2901
|
}
|
|
2766
2902
|
ctx.stdout.write(suppressNewline ? out : out + '\n');
|
|
2767
2903
|
return 0;
|
|
@@ -2803,7 +2939,7 @@ function mkLs(vfs: UnixVfs): CmdFn {
|
|
|
2803
2939
|
const targets = positionals.length > 0 ? positionals : [ctx.cwd];
|
|
2804
2940
|
|
|
2805
2941
|
const reg = vfs.symlinks;
|
|
2806
|
-
const kvfs
|
|
2942
|
+
const kvfs = ctx.vfs;
|
|
2807
2943
|
|
|
2808
2944
|
function fmtTime(mtime: number): string {
|
|
2809
2945
|
const d = new Date(mtime);
|
|
@@ -2825,6 +2961,17 @@ function mkLs(vfs: UnixVfs): CmdFn {
|
|
|
2825
2961
|
linkTarget?: string;
|
|
2826
2962
|
};
|
|
2827
2963
|
|
|
2964
|
+
/** An entry as either VFS layer lists it, before the row is rendered. */
|
|
2965
|
+
type ListedEntry = {
|
|
2966
|
+
name: string;
|
|
2967
|
+
type: FileType;
|
|
2968
|
+
size: number;
|
|
2969
|
+
mtime: number;
|
|
2970
|
+
mode: number;
|
|
2971
|
+
uid?: number;
|
|
2972
|
+
gid?: number;
|
|
2973
|
+
};
|
|
2974
|
+
|
|
2828
2975
|
let exit = 0;
|
|
2829
2976
|
|
|
2830
2977
|
function listDir(dirPath: string): Entry[] {
|
|
@@ -2832,9 +2979,9 @@ function mkLs(vfs: UnixVfs): CmdFn {
|
|
|
2832
2979
|
const out: Entry[] = [];
|
|
2833
2980
|
// Real entries via ctx.vfs.readdirStat (Kernel.VFS — handles
|
|
2834
2981
|
// mounts like /dev) with fallback to closure-captured SqliteVFS.
|
|
2835
|
-
let real:
|
|
2982
|
+
let real: ListedEntry[] = [];
|
|
2836
2983
|
try {
|
|
2837
|
-
if (kvfs && typeof kvfs.readdirStat === 'function') {
|
|
2984
|
+
if (kvfs && 'readdirStat' in kvfs && typeof kvfs.readdirStat === 'function') {
|
|
2838
2985
|
real = kvfs.readdirStat(fp);
|
|
2839
2986
|
} else {
|
|
2840
2987
|
const names = vfs.readdir(fp);
|
|
@@ -2843,16 +2990,16 @@ function mkLs(vfs: UnixVfs): CmdFn {
|
|
|
2843
2990
|
+ '/' + n.name;
|
|
2844
2991
|
try {
|
|
2845
2992
|
const s = vfs.stat(childPath);
|
|
2846
|
-
return { name: n.name, type: n.type, size:
|
|
2847
|
-
mtime:
|
|
2848
|
-
uid:
|
|
2993
|
+
return { name: n.name, type: n.type, size: s.size ?? 0,
|
|
2994
|
+
mtime: s.mtime ?? Date.now(), mode: s.mode ?? 0o644,
|
|
2995
|
+
uid: s.uid ?? ctx.cred.uid, gid: s.gid ?? ctx.cred.gid };
|
|
2849
2996
|
} catch {
|
|
2850
2997
|
return { name: n.name, type: n.type, size: 0, mtime: Date.now(), mode: 0o644,
|
|
2851
2998
|
uid: ctx.cred.uid, gid: ctx.cred.gid };
|
|
2852
2999
|
}
|
|
2853
3000
|
});
|
|
2854
3001
|
}
|
|
2855
|
-
} catch (e
|
|
3002
|
+
} catch (e) {
|
|
2856
3003
|
ctx.stderr.write(`ls: cannot access '${dirPath}': ${fsErrorMessage(e)}\n`);
|
|
2857
3004
|
exit = 2;
|
|
2858
3005
|
return [];
|
|
@@ -2942,7 +3089,7 @@ function mkLs(vfs: UnixVfs): CmdFn {
|
|
|
2942
3089
|
continue;
|
|
2943
3090
|
}
|
|
2944
3091
|
try {
|
|
2945
|
-
const s:
|
|
3092
|
+
const s: CtxStat = kvfs && typeof kvfs.stat === 'function' ? kvfs.stat(fp) : vfs.stat(fp);
|
|
2946
3093
|
if (s.type === 'directory' && !flagDirectory) {
|
|
2947
3094
|
dirArgs.push(arg);
|
|
2948
3095
|
} else {
|
|
@@ -2956,8 +3103,8 @@ function mkLs(vfs: UnixVfs): CmdFn {
|
|
|
2956
3103
|
gid: s.gid ?? ctx.cred.gid,
|
|
2957
3104
|
});
|
|
2958
3105
|
}
|
|
2959
|
-
} catch (e
|
|
2960
|
-
ctx.stderr.write(`ls: cannot access '${arg}': ${e
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
ctx.stderr.write(`ls: cannot access '${arg}': ${errorText(e)}\n`);
|
|
2961
3108
|
exit = 1;
|
|
2962
3109
|
}
|
|
2963
3110
|
}
|
|
@@ -3004,7 +3151,8 @@ function mkCat(vfs: UnixVfs): CmdFn {
|
|
|
3004
3151
|
return (ctx) => {
|
|
3005
3152
|
const files = ctx.args.filter(a => !a.startsWith('-'));
|
|
3006
3153
|
if (files.length === 0) {
|
|
3007
|
-
|
|
3154
|
+
const piped = stdinText(ctx);
|
|
3155
|
+
if (piped) ctx.stdout.write(piped);
|
|
3008
3156
|
return 0;
|
|
3009
3157
|
}
|
|
3010
3158
|
// Resolve both native VFS symlinks and the legacy registry before reads.
|
|
@@ -3093,13 +3241,13 @@ function mkRm(vfs: UnixVfs): CmdFn {
|
|
|
3093
3241
|
} else {
|
|
3094
3242
|
vfs.unlink(fp);
|
|
3095
3243
|
}
|
|
3096
|
-
} catch (e
|
|
3244
|
+
} catch (e) {
|
|
3097
3245
|
// -f suppresses ENOENT only (file disappeared mid-loop); other
|
|
3098
3246
|
// errors (ENOTEMPTY because of a logic bug, ENOTDIR mismatches,
|
|
3099
3247
|
// permission errors) must still surface. Pre-fix the broad
|
|
3100
3248
|
// `if (force) continue` masked the readdir-iteration bug that
|
|
3101
3249
|
// left directories undeleted.
|
|
3102
|
-
const msg =
|
|
3250
|
+
const msg = errorText(e);
|
|
3103
3251
|
if (force && /ENOENT/.test(msg)) continue;
|
|
3104
3252
|
ctx.stderr.write(`rm: cannot remove '${t}': ${fsErrorMessage(e)}\n`);
|
|
3105
3253
|
exit = 1;
|
|
@@ -3120,12 +3268,20 @@ function mkTouch(vfs: UnixVfs): CmdFn {
|
|
|
3120
3268
|
const dir = parts.slice(0, i).join('/');
|
|
3121
3269
|
if (dir && !targetVfs.exists(dir)) targetVfs.mkdir(dir, { recursive: true });
|
|
3122
3270
|
}
|
|
3123
|
-
if (targetVfs.exists(fp)
|
|
3271
|
+
if (!targetVfs.exists(fp)) {
|
|
3272
|
+
targetVfs.writeFile(fp, '');
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
// Every view reaching here implements `isDirectory`: the lifo VFS gained
|
|
3276
|
+
// it alongside `isFile`, which is what stopped `touch` failing on an
|
|
3277
|
+
// existing file. The `stat` fallback this replaced narrowed to `never`
|
|
3278
|
+
// once the surface was typed — the type system reporting that the guard
|
|
3279
|
+
// it sat behind can no longer be false.
|
|
3280
|
+
const isDirectory = targetVfs.isDirectory(fp);
|
|
3281
|
+
if (!isDirectory) {
|
|
3124
3282
|
// Update mtime by re-writing the same content
|
|
3125
3283
|
const content = targetVfs.readFile(fp);
|
|
3126
3284
|
targetVfs.writeFile(fp, content);
|
|
3127
|
-
} else if (!targetVfs.exists(fp)) {
|
|
3128
|
-
targetVfs.writeFile(fp, '');
|
|
3129
3285
|
}
|
|
3130
3286
|
}
|
|
3131
3287
|
return 0;
|
|
@@ -3264,8 +3420,8 @@ function expandStatFormat(
|
|
|
3264
3420
|
for (let i = 0; i < format.length; i++) {
|
|
3265
3421
|
const ch = format[i];
|
|
3266
3422
|
if (ch === '\\' && i + 1 < format.length) {
|
|
3267
|
-
const
|
|
3268
|
-
out +=
|
|
3423
|
+
const esc = format[++i];
|
|
3424
|
+
out += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc === '0' ? '\0' : esc;
|
|
3269
3425
|
continue;
|
|
3270
3426
|
}
|
|
3271
3427
|
if (ch !== '%') {
|
|
@@ -3401,9 +3557,9 @@ function mkStat(vfs: UnixVfs, sqliteVfs: SqliteVFS): CmdFn {
|
|
|
3401
3557
|
const activeFormat = format ?? (terse ? STAT_TERSE_FORMAT : null);
|
|
3402
3558
|
// shell compatibility follow-up: try Kernel.VFS (ctx.vfs) first so /dev
|
|
3403
3559
|
// mount paths resolve. Same pattern as mkCat.
|
|
3404
|
-
const kvfs
|
|
3560
|
+
const kvfs = ctx.vfs;
|
|
3405
3561
|
for (const f of files) {
|
|
3406
|
-
let st:
|
|
3562
|
+
let st: CtxStat | null = null;
|
|
3407
3563
|
let displayPath = f;
|
|
3408
3564
|
// Try Kernel.VFS first (sees mounts).
|
|
3409
3565
|
if (kvfs && typeof kvfs.stat === 'function') {
|
|
@@ -3493,7 +3649,7 @@ function mkBase64(vfs: UnixVfs): CmdFn {
|
|
|
3493
3649
|
try { bytes = vfs.readFile(resolvePath(ctx.cwd, file)); }
|
|
3494
3650
|
catch (error) { ctx.stderr.write(`base64: ${file}: ${fsErrorMessage(error)}\n`); return 1; }
|
|
3495
3651
|
} else {
|
|
3496
|
-
bytes = enc.encode(ctx
|
|
3652
|
+
bytes = enc.encode(stdinText(ctx) ?? '');
|
|
3497
3653
|
}
|
|
3498
3654
|
|
|
3499
3655
|
if (flags.decode) {
|
|
@@ -3663,18 +3819,7 @@ function mkPrintf(): CmdFn {
|
|
|
3663
3819
|
const rawFmt = ctx.args[0];
|
|
3664
3820
|
const vals = ctx.args.slice(1);
|
|
3665
3821
|
// Process backslash escapes in the format string first.
|
|
3666
|
-
const fmt = rawFmt
|
|
3667
|
-
.replace(/\\\\/g, '\u0000') // protect literal \\\\
|
|
3668
|
-
.replace(/\\n/g, '\n')
|
|
3669
|
-
.replace(/\\t/g, '\t')
|
|
3670
|
-
.replace(/\\r/g, '\r')
|
|
3671
|
-
.replace(/\\a/g, '\x07')
|
|
3672
|
-
.replace(/\\b/g, '\b')
|
|
3673
|
-
.replace(/\\f/g, '\f')
|
|
3674
|
-
.replace(/\\v/g, '\v')
|
|
3675
|
-
.replace(/\\0([0-7]{1,3})?/g, (_m, oct) => String.fromCharCode(oct ? parseInt(oct, 8) : 0))
|
|
3676
|
-
.replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))
|
|
3677
|
-
.replace(/\u0000/g, '\\');
|
|
3822
|
+
const fmt = expandBackslashEscapes(rawFmt);
|
|
3678
3823
|
|
|
3679
3824
|
let out = '';
|
|
3680
3825
|
let argIdx = 0;
|
|
@@ -3718,7 +3863,7 @@ function mkPrintf(): CmdFn {
|
|
|
3718
3863
|
};
|
|
3719
3864
|
}
|
|
3720
3865
|
|
|
3721
|
-
function formatOneArg(spec: string, arg:
|
|
3866
|
+
function formatOneArg(spec: string, arg: string | undefined): string {
|
|
3722
3867
|
const conv = spec[spec.length - 1];
|
|
3723
3868
|
const flagsAndWidth = spec.slice(1, -1);
|
|
3724
3869
|
const dotIdx = flagsAndWidth.indexOf('.');
|
|
@@ -3937,7 +4082,7 @@ function mkSha256sum(vfs: UnixVfs): CmdFn {
|
|
|
3937
4082
|
if (flags.check) return verifySha256Sums(ctx, vfs, positional, digest, flags.status === true);
|
|
3938
4083
|
|
|
3939
4084
|
if (positional.length === 0 || (positional.length === 1 && positional[0] === '-')) {
|
|
3940
|
-
ctx.stdout.write(`${await digest(enc.encode(ctx
|
|
4085
|
+
ctx.stdout.write(`${await digest(enc.encode(stdinText(ctx) ?? ''))} -\n`);
|
|
3941
4086
|
return 0;
|
|
3942
4087
|
}
|
|
3943
4088
|
|
|
@@ -4095,20 +4240,20 @@ function wrapStreaming(fn: CmdFn): (ctx: Ctx) => Promise<number> {
|
|
|
4095
4240
|
return async (ctx: Ctx) => {
|
|
4096
4241
|
try {
|
|
4097
4242
|
if (ctx.stdin && typeof ctx.stdin !== 'string') {
|
|
4098
|
-
const stdinObj = ctx.stdin
|
|
4243
|
+
const stdinObj = ctx.stdin;
|
|
4099
4244
|
const isTerminalStdin = typeof stdinObj.feed === 'function';
|
|
4100
4245
|
if (isTerminalStdin) {
|
|
4101
4246
|
const buf: string[] = Array.isArray(stdinObj.buffer)
|
|
4102
4247
|
? stdinObj.buffer.splice(0)
|
|
4103
4248
|
: [];
|
|
4104
|
-
|
|
4249
|
+
ctx.stdin = buf.join('');
|
|
4105
4250
|
}
|
|
4106
4251
|
// else: leave as pipe reader for the command to handle.
|
|
4107
4252
|
}
|
|
4108
4253
|
const result = fn(ctx);
|
|
4109
4254
|
return await result;
|
|
4110
|
-
} catch (e
|
|
4111
|
-
ctx.stderr.write(`${e
|
|
4255
|
+
} catch (e) {
|
|
4256
|
+
ctx.stderr.write(`${errorText(e)}\n`);
|
|
4112
4257
|
return 1;
|
|
4113
4258
|
}
|
|
4114
4259
|
};
|
|
@@ -4137,7 +4282,7 @@ function wrap(fn: CmdFn): (ctx: Ctx) => Promise<number> {
|
|
|
4137
4282
|
// terminal stdin and drain its already-buffered bytes synchronously
|
|
4138
4283
|
// without awaiting EOF. Pipe readers (no `feed`) await readAll().
|
|
4139
4284
|
if (ctx.stdin && typeof ctx.stdin !== 'string') {
|
|
4140
|
-
const stdinObj = ctx.stdin
|
|
4285
|
+
const stdinObj = ctx.stdin;
|
|
4141
4286
|
const isTerminalStdin = typeof stdinObj.feed === 'function';
|
|
4142
4287
|
if (isTerminalStdin) {
|
|
4143
4288
|
// Drain any already-queued bytes (typically empty for the
|
|
@@ -4147,26 +4292,26 @@ function wrap(fn: CmdFn): (ctx: Ctx) => Promise<number> {
|
|
|
4147
4292
|
const buf: string[] = Array.isArray(stdinObj.buffer)
|
|
4148
4293
|
? stdinObj.buffer.splice(0)
|
|
4149
4294
|
: [];
|
|
4150
|
-
|
|
4295
|
+
ctx.stdin = buf.join('');
|
|
4151
4296
|
} else if (typeof stdinObj.readAll === 'function') {
|
|
4152
4297
|
// Pipe reader — upstream will close() after writing, so
|
|
4153
4298
|
// readAll() resolves bounded.
|
|
4154
|
-
|
|
4299
|
+
ctx.stdin = await stdinObj.readAll();
|
|
4155
4300
|
} else if (typeof stdinObj.toString === 'function') {
|
|
4156
|
-
|
|
4301
|
+
ctx.stdin = stdinObj.toString();
|
|
4157
4302
|
}
|
|
4158
4303
|
}
|
|
4159
4304
|
const result = fn(ctx);
|
|
4160
4305
|
return await result;
|
|
4161
|
-
} catch (e
|
|
4162
|
-
ctx.stderr.write(`${e
|
|
4306
|
+
} catch (e) {
|
|
4307
|
+
ctx.stderr.write(`${errorText(e)}\n`);
|
|
4163
4308
|
return 1;
|
|
4164
4309
|
}
|
|
4165
4310
|
};
|
|
4166
4311
|
}
|
|
4167
4312
|
|
|
4168
4313
|
export function registerUnixCommands(
|
|
4169
|
-
registry:
|
|
4314
|
+
registry: UnixCommandRegistry,
|
|
4170
4315
|
sqliteVfs: SqliteVFS,
|
|
4171
4316
|
): void {
|
|
4172
4317
|
registry.register('which', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkWhich(vfs, registry))));
|
|
@@ -4279,8 +4424,8 @@ export function registerUnixCommands(
|
|
|
4279
4424
|
try {
|
|
4280
4425
|
const content = vfs.readFileString(srcFp);
|
|
4281
4426
|
vfs.writeFile(linkFp, content);
|
|
4282
|
-
} catch (e
|
|
4283
|
-
ctx.stderr.write(`ln: ${target}: ${e
|
|
4427
|
+
} catch (e) {
|
|
4428
|
+
ctx.stderr.write(`ln: ${target}: ${errorText(e)}\n`);
|
|
4284
4429
|
return 1;
|
|
4285
4430
|
}
|
|
4286
4431
|
return 0;
|