@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.
Files changed (121) hide show
  1. package/README.md +35 -7
  2. package/dist/_shared/error-text.d.ts +15 -0
  3. package/dist/_shared/error-text.d.ts.map +1 -0
  4. package/dist/_shared/error-text.js +21 -0
  5. package/dist/_shared/exports-resolver.d.ts +19 -7
  6. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  7. package/dist/_shared/exports-resolver.js +1 -1
  8. package/dist/_shared/retry.d.ts.map +1 -1
  9. package/dist/_shared/retry.js +3 -1
  10. package/dist/_shared/w7-frame.js +2 -1
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +30 -16
  13. package/dist/observability/oom-classify.d.ts +1 -1
  14. package/dist/observability/oom-classify.d.ts.map +1 -1
  15. package/dist/observability/oom-classify.js +7 -0
  16. package/dist/observability/oom-discriminator.d.ts +1 -1
  17. package/dist/runtime/esbuild-service.d.ts +4 -3
  18. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  19. package/dist/runtime/esbuild-service.js +9 -4
  20. package/dist/runtime/installed-runtimes.d.ts +4 -3
  21. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  22. package/dist/runtime/long-running-handle.d.ts +3 -3
  23. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  24. package/dist/runtime/os-contracts.d.ts +7 -5
  25. package/dist/runtime/os-contracts.d.ts.map +1 -1
  26. package/dist/runtime/port-registry.d.ts +28 -0
  27. package/dist/runtime/port-registry.d.ts.map +1 -1
  28. package/dist/runtime/port-registry.js +79 -9
  29. package/dist/runtime/process-logs.d.ts +9 -1
  30. package/dist/runtime/process-logs.d.ts.map +1 -1
  31. package/dist/runtime/project-detect.d.ts +6 -1
  32. package/dist/runtime/project-detect.d.ts.map +1 -1
  33. package/dist/runtime/require-resolver.d.ts.map +1 -1
  34. package/dist/runtime/require-resolver.js +3 -4
  35. package/dist/runtime/runtime-registry.d.ts +4 -3
  36. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  37. package/dist/runtime/runtime-registry.js +2 -1
  38. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  39. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  40. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  41. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  42. package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
  43. package/dist/runtime/virtual-socket-kernel.js +10 -5
  44. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  45. package/dist/runtime/wasm-runner.js +22 -14
  46. package/dist/shell/features.d.ts.map +1 -1
  47. package/dist/shell/features.js +5 -4
  48. package/dist/shell/unix-commands.d.ts +14 -1
  49. package/dist/shell/unix-commands.d.ts.map +1 -1
  50. package/dist/shell/unix-commands.js +115 -69
  51. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  52. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  53. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  54. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  55. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  56. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  57. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  58. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  59. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  60. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  61. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  62. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  63. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  64. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  65. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  66. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  67. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  68. package/dist/substrate/lifo/node-compat/index.js +3 -2
  69. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  70. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  71. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  72. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  73. package/dist/substrate/lifo/node-compat/process.js +6 -3
  74. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  75. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  76. package/dist/substrate/lifo/shell/Shell.js +86 -75
  77. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  78. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  79. package/dist/substrate/lifo/shell/lexer.js +40 -0
  80. package/dist/vfs/seed-project.d.ts.map +1 -1
  81. package/dist/vfs/seed-project.js +8 -2
  82. package/dist/vfs/sqlite-vfs.d.ts +61 -1
  83. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  84. package/dist/vfs/sqlite-vfs.js +199 -35
  85. package/package.json +1 -1
  86. package/src/_shared/error-text.ts +21 -0
  87. package/src/_shared/exports-resolver.ts +24 -4
  88. package/src/_shared/retry.ts +4 -2
  89. package/src/_shared/w7-frame.ts +2 -1
  90. package/src/constants.ts +30 -16
  91. package/src/observability/oom-classify.ts +7 -0
  92. package/src/observability/oom-discriminator.ts +1 -1
  93. package/src/runtime/esbuild-service.ts +16 -11
  94. package/src/runtime/installed-runtimes.ts +4 -3
  95. package/src/runtime/long-running-handle.ts +3 -3
  96. package/src/runtime/os-contracts.ts +7 -5
  97. package/src/runtime/port-registry.ts +97 -9
  98. package/src/runtime/process-logs.ts +12 -3
  99. package/src/runtime/project-detect.ts +7 -1
  100. package/src/runtime/require-resolver.ts +7 -7
  101. package/src/runtime/runtime-registry.ts +9 -7
  102. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  103. package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
  104. package/src/runtime/virtual-socket-kernel.ts +10 -5
  105. package/src/runtime/wasm-runner.ts +55 -29
  106. package/src/shell/features.ts +5 -4
  107. package/src/shell/unix-commands.ts +273 -128
  108. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  109. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  110. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  111. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  112. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  113. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  114. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  115. package/src/substrate/lifo/node-compat/index.ts +3 -2
  116. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  117. package/src/substrate/lifo/node-compat/process.ts +5 -2
  118. package/src/substrate/lifo/shell/Shell.ts +92 -70
  119. package/src/substrate/lifo/shell/lexer.ts +38 -0
  120. package/src/vfs/seed-project.ts +10 -3
  121. 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 { fileTypeChar, isCharacterDevice } from '../substrate/lifo/kernel/vfs/index.js';
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
- stdin?: string;
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: CredentialedVfs;
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: ReturnType<typeof getSymlinkRegistry>;
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: unknown): boolean {
118
- return !!handler && !!(handler as any).__nimbusRuntimeInstallHint;
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: any,
359
+ registry: UnixCommandRegistry,
274
360
  name: string,
275
361
  options: { includeInstallHints?: boolean } = {},
276
- ): Promise<CmdFn | null> {
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: any,
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: any): CmdFn {
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: any): CmdFn {
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: any): CmdFn {
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: any) {
445
- ctx.stderr.write(`command: ${name}: ${e?.message || e}\n`);
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: any): CmdFn {
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' ? await registry.resolve(name) : null;
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): any => {
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: any): CmdFn {
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: any) {
830
- ctx.stderr.write(`find: ${name}: ${e?.message || e}\n`);
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: any) {
982
- ctx.stderr.write(`find: cannot delete '${e.display}': ${err?.message || err}\n`);
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: any) {
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
- (args as any).__fixedStrings = true;
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') (args as any).__fixedStrings = true;
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 ((args as any).__fixedStrings) {
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
- if (ctx.stdin) {
1285
- processLines(ctx.stdin.split('\n'), '');
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: any) => {
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
- let nlIdx;
1346
- while (emitted < n && (nlIdx = buffered.indexOf('\n')) !== -1) {
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
- if (ctx.stdin) emit(ctx.stdin);
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.stdin ?? '');
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.stdin || '';
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.stdin || '';
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 === undefined ? undefined : stringInput(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.stdin || '';
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: any) { ctx.stderr.write(`awk: bad regex /${patSrc}/: ${e?.message || e}\n`); return 1; }
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, any>;
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): any {
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(): any {
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(): any {
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(): any {
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: any): number {
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: any) {
2118
- throw new Error(`expr error: ${e?.message || e} in "${expr}"`);
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: any): string {
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: any[]): string {
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: any): string {
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: any) {
2471
- ctx.stderr.write(`awk: ${e?.message || e}\n`);
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: any): CmdFn {
2606
+ function mkXargs(vfs: UnixVfs, registry: UnixCommandRegistry): CmdFn {
2502
2607
  return async (ctx) => {
2503
- const input = (ctx.stdin || '').trim();
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 (_e) { target = null; }
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: any) {
2579
- ctx.stderr.write(`xargs: ${cmdName}: ${e?.message || e}\n`);
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: any) {
2592
- ctx.stderr.write(`xargs: ${cmdName}: ${e?.message || e}\n`);
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.stdin || '';
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: any) { ctx.stderr.write(`diff: ${e.message}\n`); return 2; }
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: any = (ctx as any).vfs;
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: any[] = [];
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: (s as any).size ?? 0,
2847
- mtime: (s as any).mtime ?? Date.now(), mode: (s as any).mode ?? 0o644,
2848
- uid: (s as any).uid ?? ctx.cred.uid, gid: (s as any).gid ?? ctx.cred.gid };
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: any) {
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: any = kvfs && typeof kvfs.stat === 'function' ? kvfs.stat(fp) : vfs.stat(fp);
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: any) {
2960
- ctx.stderr.write(`ls: cannot access '${arg}': ${e?.message || e}\n`);
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
- if (ctx.stdin) ctx.stdout.write(ctx.stdin);
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: any) {
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 = String(e?.message || e);
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) && !targetVfs.isDirectory(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 escape = format[++i];
3268
- out += escape === 'n' ? '\n' : escape === 't' ? '\t' : escape === '0' ? '\0' : escape;
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: any = (ctx as any).vfs;
3560
+ const kvfs = ctx.vfs;
3405
3561
  for (const f of files) {
3406
- let st: any = null;
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.stdin ?? '');
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: any): string {
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.stdin ?? ''))} -\n`);
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 as any;
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
- (ctx as any).stdin = buf.join('');
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: any) {
4111
- ctx.stderr.write(`${e?.message || e}\n`);
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 as any;
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
- (ctx as any).stdin = buf.join('');
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
- (ctx as any).stdin = await stdinObj.readAll();
4299
+ ctx.stdin = await stdinObj.readAll();
4155
4300
  } else if (typeof stdinObj.toString === 'function') {
4156
- (ctx as any).stdin = stdinObj.toString();
4301
+ ctx.stdin = stdinObj.toString();
4157
4302
  }
4158
4303
  }
4159
4304
  const result = fn(ctx);
4160
4305
  return await result;
4161
- } catch (e: any) {
4162
- ctx.stderr.write(`${e?.message || e}\n`);
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: any,
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: any) {
4283
- ctx.stderr.write(`ln: ${target}: ${e?.message || e}\n`);
4427
+ } catch (e) {
4428
+ ctx.stderr.write(`ln: ${target}: ${errorText(e)}\n`);
4284
4429
  return 1;
4285
4430
  }
4286
4431
  return 0;