@nimbus-sh/core 0.1.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 (1056) hide show
  1. package/dist/_shared/ai-egress.d.ts +73 -0
  2. package/dist/_shared/ai-egress.d.ts.map +1 -0
  3. package/dist/_shared/ai-egress.js +88 -0
  4. package/dist/_shared/byte-stream.d.ts +59 -0
  5. package/dist/_shared/byte-stream.d.ts.map +1 -0
  6. package/dist/_shared/byte-stream.js +77 -0
  7. package/dist/_shared/bytes.d.ts +18 -0
  8. package/dist/_shared/bytes.d.ts.map +1 -0
  9. package/dist/_shared/bytes.js +17 -0
  10. package/dist/_shared/cache-stats.d.ts +145 -0
  11. package/dist/_shared/cache-stats.d.ts.map +1 -0
  12. package/dist/_shared/cache-stats.js +161 -0
  13. package/dist/_shared/crypto.d.ts +17 -0
  14. package/dist/_shared/crypto.d.ts.map +1 -0
  15. package/dist/_shared/crypto.js +131 -0
  16. package/dist/_shared/esbuild-facet-shim.d.ts +21 -0
  17. package/dist/_shared/esbuild-facet-shim.d.ts.map +1 -0
  18. package/dist/_shared/esbuild-facet-shim.js +22 -0
  19. package/dist/_shared/exports-resolver.d.ts +74 -0
  20. package/dist/_shared/exports-resolver.d.ts.map +1 -0
  21. package/dist/_shared/exports-resolver.js +284 -0
  22. package/dist/_shared/facet-vfs-cursor.d.ts +48 -0
  23. package/dist/_shared/facet-vfs-cursor.d.ts.map +1 -0
  24. package/dist/_shared/facet-vfs-cursor.js +51 -0
  25. package/dist/_shared/install-phase.d.ts +47 -0
  26. package/dist/_shared/install-phase.d.ts.map +1 -0
  27. package/dist/_shared/install-phase.js +1 -0
  28. package/dist/_shared/lru-map.d.ts +25 -0
  29. package/dist/_shared/lru-map.d.ts.map +1 -0
  30. package/dist/_shared/lru-map.js +57 -0
  31. package/dist/_shared/real-node-imports.d.ts +43 -0
  32. package/dist/_shared/real-node-imports.d.ts.map +1 -0
  33. package/dist/_shared/real-node-imports.js +52 -0
  34. package/dist/_shared/retry.d.ts +92 -0
  35. package/dist/_shared/retry.d.ts.map +1 -0
  36. package/dist/_shared/retry.js +155 -0
  37. package/dist/_shared/rpc-dispose.d.ts +4 -0
  38. package/dist/_shared/rpc-dispose.d.ts.map +1 -0
  39. package/dist/_shared/rpc-dispose.js +30 -0
  40. package/dist/_shared/terminal.d.ts +2 -0
  41. package/dist/_shared/terminal.d.ts.map +1 -0
  42. package/dist/_shared/terminal.js +3 -0
  43. package/dist/_shared/typescript-specifiers.d.ts +58 -0
  44. package/dist/_shared/typescript-specifiers.d.ts.map +1 -0
  45. package/dist/_shared/typescript-specifiers.js +84 -0
  46. package/dist/_shared/untrusted-request.d.ts +34 -0
  47. package/dist/_shared/untrusted-request.d.ts.map +1 -0
  48. package/dist/_shared/untrusted-request.js +74 -0
  49. package/dist/_shared/vfs-write-ledger.d.ts +3 -0
  50. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -0
  51. package/dist/_shared/vfs-write-ledger.js +439 -0
  52. package/dist/_shared/w7-frame.d.ts +79 -0
  53. package/dist/_shared/w7-frame.d.ts.map +1 -0
  54. package/dist/_shared/w7-frame.js +839 -0
  55. package/dist/_shared/weighted-credit-pool.d.ts +61 -0
  56. package/dist/_shared/weighted-credit-pool.d.ts.map +1 -0
  57. package/dist/_shared/weighted-credit-pool.js +196 -0
  58. package/dist/constants.d.ts +64 -0
  59. package/dist/constants.d.ts.map +1 -0
  60. package/dist/constants.js +264 -0
  61. package/dist/index.d.ts +16 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +13 -0
  64. package/dist/observability/diag-counters.d.ts +236 -0
  65. package/dist/observability/diag-counters.d.ts.map +1 -0
  66. package/dist/observability/diag-counters.js +195 -0
  67. package/dist/observability/heap-estimate.d.ts +225 -0
  68. package/dist/observability/heap-estimate.d.ts.map +1 -0
  69. package/dist/observability/heap-estimate.js +185 -0
  70. package/dist/observability/heavy-alloc-coord.d.ts +93 -0
  71. package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
  72. package/dist/observability/heavy-alloc-coord.js +181 -0
  73. package/dist/observability/oom-classify.d.ts +138 -0
  74. package/dist/observability/oom-classify.d.ts.map +1 -0
  75. package/dist/observability/oom-classify.js +261 -0
  76. package/dist/observability/oom-discriminator.d.ts +176 -0
  77. package/dist/observability/oom-discriminator.d.ts.map +1 -0
  78. package/dist/observability/oom-discriminator.js +272 -0
  79. package/dist/runtime/assets-loader.d.ts +71 -0
  80. package/dist/runtime/assets-loader.d.ts.map +1 -0
  81. package/dist/runtime/assets-loader.js +139 -0
  82. package/dist/runtime/barrel-detect.d.ts +46 -0
  83. package/dist/runtime/barrel-detect.d.ts.map +1 -0
  84. package/dist/runtime/barrel-detect.js +73 -0
  85. package/dist/runtime/bash/preamble.d.ts +2 -0
  86. package/dist/runtime/bash/preamble.d.ts.map +1 -0
  87. package/dist/runtime/bash/preamble.js +1849 -0
  88. package/dist/runtime/bash/types.d.ts +433 -0
  89. package/dist/runtime/bash/types.d.ts.map +1 -0
  90. package/dist/runtime/bash/types.js +1 -0
  91. package/dist/runtime/bundle-profile.d.ts +14 -0
  92. package/dist/runtime/bundle-profile.d.ts.map +1 -0
  93. package/dist/runtime/bundle-profile.js +20 -0
  94. package/dist/runtime/cirrus-vite-config-rewriter.d.ts +2 -0
  95. package/dist/runtime/cirrus-vite-config-rewriter.d.ts.map +1 -0
  96. package/dist/runtime/cirrus-vite-config-rewriter.js +28 -0
  97. package/dist/runtime/cli-flags.d.ts +2 -0
  98. package/dist/runtime/cli-flags.d.ts.map +1 -0
  99. package/dist/runtime/cli-flags.js +11 -0
  100. package/dist/runtime/comment-strip.d.ts +37 -0
  101. package/dist/runtime/comment-strip.d.ts.map +1 -0
  102. package/dist/runtime/comment-strip.js +74 -0
  103. package/dist/runtime/cpython-preamble.d.ts +32 -0
  104. package/dist/runtime/cpython-preamble.d.ts.map +1 -0
  105. package/dist/runtime/cpython-preamble.js +296 -0
  106. package/dist/runtime/esbuild-service.d.ts +242 -0
  107. package/dist/runtime/esbuild-service.d.ts.map +1 -0
  108. package/dist/runtime/esbuild-service.js +1452 -0
  109. package/dist/runtime/framework-detect.d.ts +70 -0
  110. package/dist/runtime/framework-detect.d.ts.map +1 -0
  111. package/dist/runtime/framework-detect.js +177 -0
  112. package/dist/runtime/import-meta-transform.d.ts +3 -0
  113. package/dist/runtime/import-meta-transform.d.ts.map +1 -0
  114. package/dist/runtime/import-meta-transform.js +15 -0
  115. package/dist/runtime/javascript-ast.d.ts +13 -0
  116. package/dist/runtime/javascript-ast.d.ts.map +1 -0
  117. package/dist/runtime/javascript-ast.js +57 -0
  118. package/dist/runtime/long-running-handle.d.ts +119 -0
  119. package/dist/runtime/long-running-handle.d.ts.map +1 -0
  120. package/dist/runtime/long-running-handle.js +169 -0
  121. package/dist/runtime/module-source-rewriter.d.ts +11 -0
  122. package/dist/runtime/module-source-rewriter.d.ts.map +1 -0
  123. package/dist/runtime/module-source-rewriter.js +61 -0
  124. package/dist/runtime/opentui-wasm-backend.d.ts +160 -0
  125. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -0
  126. package/dist/runtime/opentui-wasm-backend.js +416 -0
  127. package/dist/runtime/os-contracts.d.ts +436 -0
  128. package/dist/runtime/os-contracts.d.ts.map +1 -0
  129. package/dist/runtime/os-contracts.js +101 -0
  130. package/dist/runtime/port-registry.d.ts +100 -0
  131. package/dist/runtime/port-registry.d.ts.map +1 -0
  132. package/dist/runtime/port-registry.js +265 -0
  133. package/dist/runtime/process-input-routing.d.ts +33 -0
  134. package/dist/runtime/process-input-routing.d.ts.map +1 -0
  135. package/dist/runtime/process-input-routing.js +59 -0
  136. package/dist/runtime/process-input.d.ts +39 -0
  137. package/dist/runtime/process-input.d.ts.map +1 -0
  138. package/dist/runtime/process-input.js +117 -0
  139. package/dist/runtime/process-io-protocol.d.ts +90 -0
  140. package/dist/runtime/process-io-protocol.d.ts.map +1 -0
  141. package/dist/runtime/process-io-protocol.js +81 -0
  142. package/dist/runtime/process-logs.d.ts +352 -0
  143. package/dist/runtime/process-logs.d.ts.map +1 -0
  144. package/dist/runtime/process-logs.js +750 -0
  145. package/dist/runtime/process-table.d.ts +101 -0
  146. package/dist/runtime/process-table.d.ts.map +1 -0
  147. package/dist/runtime/process-table.js +175 -0
  148. package/dist/runtime/project-detect.d.ts +21 -0
  149. package/dist/runtime/project-detect.d.ts.map +1 -0
  150. package/dist/runtime/project-detect.js +46 -0
  151. package/dist/runtime/python-server-adapter.d.ts +33 -0
  152. package/dist/runtime/python-server-adapter.d.ts.map +1 -0
  153. package/dist/runtime/python-server-adapter.js +132 -0
  154. package/dist/runtime/require-resolver.d.ts +44 -0
  155. package/dist/runtime/require-resolver.d.ts.map +1 -0
  156. package/dist/runtime/require-resolver.js +698 -0
  157. package/dist/runtime/router-basename.d.ts +36 -0
  158. package/dist/runtime/router-basename.d.ts.map +1 -0
  159. package/dist/runtime/router-basename.js +310 -0
  160. package/dist/runtime/ruby-green-threads.d.ts +136 -0
  161. package/dist/runtime/ruby-green-threads.d.ts.map +1 -0
  162. package/dist/runtime/ruby-green-threads.js +904 -0
  163. package/dist/runtime/ruby-socket-shim.d.ts +2 -0
  164. package/dist/runtime/ruby-socket-shim.d.ts.map +1 -0
  165. package/dist/runtime/ruby-socket-shim.js +548 -0
  166. package/dist/runtime/session-process-supervisor.d.ts +156 -0
  167. package/dist/runtime/session-process-supervisor.d.ts.map +1 -0
  168. package/dist/runtime/session-process-supervisor.js +238 -0
  169. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +70 -0
  170. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
  171. package/dist/runtime/sqlite-runtime-fs-bridge.js +425 -0
  172. package/dist/runtime/streams.d.ts +13 -0
  173. package/dist/runtime/streams.d.ts.map +1 -0
  174. package/dist/runtime/streams.js +546 -0
  175. package/dist/runtime/undici-shim.d.ts +62 -0
  176. package/dist/runtime/undici-shim.d.ts.map +1 -0
  177. package/dist/runtime/undici-shim.js +516 -0
  178. package/dist/runtime/vfs-manifest.d.ts +63 -0
  179. package/dist/runtime/vfs-manifest.d.ts.map +1 -0
  180. package/dist/runtime/vfs-manifest.js +141 -0
  181. package/dist/runtime/vfs-snapshot.d.ts +75 -0
  182. package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
  183. package/dist/runtime/vfs-snapshot.js +220 -0
  184. package/dist/runtime/virtual-socket-kernel.d.ts +252 -0
  185. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
  186. package/dist/runtime/virtual-socket-kernel.js +1143 -0
  187. package/dist/runtime/vite-config-parser.d.ts +13 -0
  188. package/dist/runtime/vite-config-parser.d.ts.map +1 -0
  189. package/dist/runtime/vite-config-parser.js +258 -0
  190. package/dist/runtime/wasi/types.d.ts +347 -0
  191. package/dist/runtime/wasi/types.d.ts.map +1 -0
  192. package/dist/runtime/wasi/types.js +1 -0
  193. package/dist/runtime/wasi-instance.d.ts +215 -0
  194. package/dist/runtime/wasi-instance.d.ts.map +1 -0
  195. package/dist/runtime/wasi-instance.generated.d.ts +14 -0
  196. package/dist/runtime/wasi-instance.generated.d.ts.map +1 -0
  197. package/dist/runtime/wasi-instance.generated.js +13 -0
  198. package/dist/runtime/wasi-instance.js +179 -0
  199. package/dist/runtime/wasi-threads.d.ts +169 -0
  200. package/dist/runtime/wasi-threads.d.ts.map +1 -0
  201. package/dist/runtime/wasi-threads.js +669 -0
  202. package/dist/runtime/wasm-memory.d.ts +84 -0
  203. package/dist/runtime/wasm-memory.d.ts.map +1 -0
  204. package/dist/runtime/wasm-memory.js +235 -0
  205. package/dist/shell/elevation-commands.d.ts +5 -0
  206. package/dist/shell/elevation-commands.d.ts.map +1 -0
  207. package/dist/shell/elevation-commands.js +109 -0
  208. package/dist/shell/exec-dispatch.d.ts +57 -0
  209. package/dist/shell/exec-dispatch.d.ts.map +1 -0
  210. package/dist/shell/exec-dispatch.js +181 -0
  211. package/dist/shell/features.d.ts +154 -0
  212. package/dist/shell/features.d.ts.map +1 -0
  213. package/dist/shell/features.js +792 -0
  214. package/dist/shell/shell-entrypoints.d.ts +55 -0
  215. package/dist/shell/shell-entrypoints.d.ts.map +1 -0
  216. package/dist/shell/shell-entrypoints.js +230 -0
  217. package/dist/shell/shell-invocation.d.ts +36 -0
  218. package/dist/shell/shell-invocation.d.ts.map +1 -0
  219. package/dist/shell/shell-invocation.js +148 -0
  220. package/dist/shell/unix-accounts.d.ts +27 -0
  221. package/dist/shell/unix-accounts.d.ts.map +1 -0
  222. package/dist/shell/unix-accounts.js +105 -0
  223. package/dist/shell/unix-commands.d.ts +15 -0
  224. package/dist/shell/unix-commands.d.ts.map +1 -0
  225. package/dist/shell/unix-commands.js +4702 -0
  226. package/dist/substrate/lifo/commands/archive/gunzip.d.ts +4 -0
  227. package/dist/substrate/lifo/commands/archive/gunzip.d.ts.map +1 -0
  228. package/dist/substrate/lifo/commands/archive/gunzip.js +58 -0
  229. package/dist/substrate/lifo/commands/archive/gzip.d.ts +4 -0
  230. package/dist/substrate/lifo/commands/archive/gzip.d.ts.map +1 -0
  231. package/dist/substrate/lifo/commands/archive/gzip.js +73 -0
  232. package/dist/substrate/lifo/commands/archive/tar.d.ts +4 -0
  233. package/dist/substrate/lifo/commands/archive/tar.d.ts.map +1 -0
  234. package/dist/substrate/lifo/commands/archive/tar.js +158 -0
  235. package/dist/substrate/lifo/commands/archive/unzip.d.ts +4 -0
  236. package/dist/substrate/lifo/commands/archive/unzip.d.ts.map +1 -0
  237. package/dist/substrate/lifo/commands/archive/unzip.js +114 -0
  238. package/dist/substrate/lifo/commands/archive/zip.d.ts +4 -0
  239. package/dist/substrate/lifo/commands/archive/zip.d.ts.map +1 -0
  240. package/dist/substrate/lifo/commands/archive/zip.js +58 -0
  241. package/dist/substrate/lifo/commands/fs/basename.d.ts +4 -0
  242. package/dist/substrate/lifo/commands/fs/basename.d.ts.map +1 -0
  243. package/dist/substrate/lifo/commands/fs/basename.js +12 -0
  244. package/dist/substrate/lifo/commands/fs/cat.d.ts +4 -0
  245. package/dist/substrate/lifo/commands/fs/cat.d.ts.map +1 -0
  246. package/dist/substrate/lifo/commands/fs/cat.js +52 -0
  247. package/dist/substrate/lifo/commands/fs/chmod.d.ts +25 -0
  248. package/dist/substrate/lifo/commands/fs/chmod.d.ts.map +1 -0
  249. package/dist/substrate/lifo/commands/fs/chmod.js +97 -0
  250. package/dist/substrate/lifo/commands/fs/chown.d.ts +4 -0
  251. package/dist/substrate/lifo/commands/fs/chown.d.ts.map +1 -0
  252. package/dist/substrate/lifo/commands/fs/chown.js +41 -0
  253. package/dist/substrate/lifo/commands/fs/cp.d.ts +4 -0
  254. package/dist/substrate/lifo/commands/fs/cp.d.ts.map +1 -0
  255. package/dist/substrate/lifo/commands/fs/cp.js +29 -0
  256. package/dist/substrate/lifo/commands/fs/df.d.ts +4 -0
  257. package/dist/substrate/lifo/commands/fs/df.d.ts.map +1 -0
  258. package/dist/substrate/lifo/commands/fs/df.js +52 -0
  259. package/dist/substrate/lifo/commands/fs/dirname.d.ts +4 -0
  260. package/dist/substrate/lifo/commands/fs/dirname.d.ts.map +1 -0
  261. package/dist/substrate/lifo/commands/fs/dirname.js +12 -0
  262. package/dist/substrate/lifo/commands/fs/du.d.ts +4 -0
  263. package/dist/substrate/lifo/commands/fs/du.d.ts.map +1 -0
  264. package/dist/substrate/lifo/commands/fs/du.js +104 -0
  265. package/dist/substrate/lifo/commands/fs/file.d.ts +4 -0
  266. package/dist/substrate/lifo/commands/fs/file.d.ts.map +1 -0
  267. package/dist/substrate/lifo/commands/fs/file.js +84 -0
  268. package/dist/substrate/lifo/commands/fs/find.d.ts +4 -0
  269. package/dist/substrate/lifo/commands/fs/find.d.ts.map +1 -0
  270. package/dist/substrate/lifo/commands/fs/find.js +72 -0
  271. package/dist/substrate/lifo/commands/fs/ln.d.ts +4 -0
  272. package/dist/substrate/lifo/commands/fs/ln.d.ts.map +1 -0
  273. package/dist/substrate/lifo/commands/fs/ln.js +15 -0
  274. package/dist/substrate/lifo/commands/fs/ls.d.ts +4 -0
  275. package/dist/substrate/lifo/commands/fs/ls.d.ts.map +1 -0
  276. package/dist/substrate/lifo/commands/fs/ls.js +146 -0
  277. package/dist/substrate/lifo/commands/fs/mkdir.d.ts +4 -0
  278. package/dist/substrate/lifo/commands/fs/mkdir.d.ts.map +1 -0
  279. package/dist/substrate/lifo/commands/fs/mkdir.js +31 -0
  280. package/dist/substrate/lifo/commands/fs/mktemp.d.ts +4 -0
  281. package/dist/substrate/lifo/commands/fs/mktemp.d.ts.map +1 -0
  282. package/dist/substrate/lifo/commands/fs/mktemp.js +48 -0
  283. package/dist/substrate/lifo/commands/fs/mv.d.ts +4 -0
  284. package/dist/substrate/lifo/commands/fs/mv.d.ts.map +1 -0
  285. package/dist/substrate/lifo/commands/fs/mv.js +113 -0
  286. package/dist/substrate/lifo/commands/fs/realpath.d.ts +4 -0
  287. package/dist/substrate/lifo/commands/fs/realpath.d.ts.map +1 -0
  288. package/dist/substrate/lifo/commands/fs/realpath.js +27 -0
  289. package/dist/substrate/lifo/commands/fs/rm.d.ts +4 -0
  290. package/dist/substrate/lifo/commands/fs/rm.d.ts.map +1 -0
  291. package/dist/substrate/lifo/commands/fs/rm.js +48 -0
  292. package/dist/substrate/lifo/commands/fs/rmdir.d.ts +4 -0
  293. package/dist/substrate/lifo/commands/fs/rmdir.d.ts.map +1 -0
  294. package/dist/substrate/lifo/commands/fs/rmdir.js +44 -0
  295. package/dist/substrate/lifo/commands/fs/stat.d.ts +4 -0
  296. package/dist/substrate/lifo/commands/fs/stat.d.ts.map +1 -0
  297. package/dist/substrate/lifo/commands/fs/stat.js +35 -0
  298. package/dist/substrate/lifo/commands/fs/touch.d.ts +4 -0
  299. package/dist/substrate/lifo/commands/fs/touch.d.ts.map +1 -0
  300. package/dist/substrate/lifo/commands/fs/touch.js +26 -0
  301. package/dist/substrate/lifo/commands/fs/tree.d.ts +4 -0
  302. package/dist/substrate/lifo/commands/fs/tree.d.ts.map +1 -0
  303. package/dist/substrate/lifo/commands/fs/tree.js +69 -0
  304. package/dist/substrate/lifo/commands/index.d.ts +3 -0
  305. package/dist/substrate/lifo/commands/index.d.ts.map +1 -0
  306. package/dist/substrate/lifo/commands/index.js +1 -0
  307. package/dist/substrate/lifo/commands/io/dd.d.ts +4 -0
  308. package/dist/substrate/lifo/commands/io/dd.d.ts.map +1 -0
  309. package/dist/substrate/lifo/commands/io/dd.js +256 -0
  310. package/dist/substrate/lifo/commands/io/printf.d.ts +4 -0
  311. package/dist/substrate/lifo/commands/io/printf.d.ts.map +1 -0
  312. package/dist/substrate/lifo/commands/io/printf.js +82 -0
  313. package/dist/substrate/lifo/commands/io/tee.d.ts +4 -0
  314. package/dist/substrate/lifo/commands/io/tee.d.ts.map +1 -0
  315. package/dist/substrate/lifo/commands/io/tee.js +31 -0
  316. package/dist/substrate/lifo/commands/io/xargs.d.ts +4 -0
  317. package/dist/substrate/lifo/commands/io/xargs.d.ts.map +1 -0
  318. package/dist/substrate/lifo/commands/io/xargs.js +44 -0
  319. package/dist/substrate/lifo/commands/io/yes.d.ts +4 -0
  320. package/dist/substrate/lifo/commands/io/yes.d.ts.map +1 -0
  321. package/dist/substrate/lifo/commands/io/yes.js +10 -0
  322. package/dist/substrate/lifo/commands/net/curl.d.ts +6 -0
  323. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -0
  324. package/dist/substrate/lifo/commands/net/curl.js +617 -0
  325. package/dist/substrate/lifo/commands/net/dig.d.ts +4 -0
  326. package/dist/substrate/lifo/commands/net/dig.d.ts.map +1 -0
  327. package/dist/substrate/lifo/commands/net/dig.js +77 -0
  328. package/dist/substrate/lifo/commands/net/forward.d.ts +14 -0
  329. package/dist/substrate/lifo/commands/net/forward.d.ts.map +1 -0
  330. package/dist/substrate/lifo/commands/net/forward.js +80 -0
  331. package/dist/substrate/lifo/commands/net/host.d.ts +14 -0
  332. package/dist/substrate/lifo/commands/net/host.d.ts.map +1 -0
  333. package/dist/substrate/lifo/commands/net/host.js +160 -0
  334. package/dist/substrate/lifo/commands/net/ifconfig.d.ts +8 -0
  335. package/dist/substrate/lifo/commands/net/ifconfig.d.ts.map +1 -0
  336. package/dist/substrate/lifo/commands/net/ifconfig.js +120 -0
  337. package/dist/substrate/lifo/commands/net/ip.d.ts +14 -0
  338. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -0
  339. package/dist/substrate/lifo/commands/net/ip.js +508 -0
  340. package/dist/substrate/lifo/commands/net/netstat.d.ts +8 -0
  341. package/dist/substrate/lifo/commands/net/netstat.d.ts.map +1 -0
  342. package/dist/substrate/lifo/commands/net/netstat.js +94 -0
  343. package/dist/substrate/lifo/commands/net/ping.d.ts +4 -0
  344. package/dist/substrate/lifo/commands/net/ping.d.ts.map +1 -0
  345. package/dist/substrate/lifo/commands/net/ping.js +64 -0
  346. package/dist/substrate/lifo/commands/net/ports.d.ts +8 -0
  347. package/dist/substrate/lifo/commands/net/ports.d.ts.map +1 -0
  348. package/dist/substrate/lifo/commands/net/ports.js +36 -0
  349. package/dist/substrate/lifo/commands/net/route.d.ts +8 -0
  350. package/dist/substrate/lifo/commands/net/route.d.ts.map +1 -0
  351. package/dist/substrate/lifo/commands/net/route.js +128 -0
  352. package/dist/substrate/lifo/commands/net/tunnel-v2.d.ts +4 -0
  353. package/dist/substrate/lifo/commands/net/tunnel-v2.d.ts.map +1 -0
  354. package/dist/substrate/lifo/commands/net/tunnel-v2.js +141 -0
  355. package/dist/substrate/lifo/commands/net/tunnel.d.ts +6 -0
  356. package/dist/substrate/lifo/commands/net/tunnel.d.ts.map +1 -0
  357. package/dist/substrate/lifo/commands/net/tunnel.js +232 -0
  358. package/dist/substrate/lifo/commands/net/wget.d.ts +4 -0
  359. package/dist/substrate/lifo/commands/net/wget.d.ts.map +1 -0
  360. package/dist/substrate/lifo/commands/net/wget.js +78 -0
  361. package/dist/substrate/lifo/commands/registry.d.ts +15 -0
  362. package/dist/substrate/lifo/commands/registry.d.ts.map +1 -0
  363. package/dist/substrate/lifo/commands/registry.js +120 -0
  364. package/dist/substrate/lifo/commands/signal.d.ts +12 -0
  365. package/dist/substrate/lifo/commands/signal.d.ts.map +1 -0
  366. package/dist/substrate/lifo/commands/signal.js +47 -0
  367. package/dist/substrate/lifo/commands/system/bc.d.ts +4 -0
  368. package/dist/substrate/lifo/commands/system/bc.d.ts.map +1 -0
  369. package/dist/substrate/lifo/commands/system/bc.js +243 -0
  370. package/dist/substrate/lifo/commands/system/cal.d.ts +4 -0
  371. package/dist/substrate/lifo/commands/system/cal.d.ts.map +1 -0
  372. package/dist/substrate/lifo/commands/system/cal.js +81 -0
  373. package/dist/substrate/lifo/commands/system/date.d.ts +4 -0
  374. package/dist/substrate/lifo/commands/system/date.d.ts.map +1 -0
  375. package/dist/substrate/lifo/commands/system/date.js +79 -0
  376. package/dist/substrate/lifo/commands/system/env.d.ts +4 -0
  377. package/dist/substrate/lifo/commands/system/env.d.ts.map +1 -0
  378. package/dist/substrate/lifo/commands/system/env.js +7 -0
  379. package/dist/substrate/lifo/commands/system/fastfetch.d.ts +4 -0
  380. package/dist/substrate/lifo/commands/system/fastfetch.d.ts.map +1 -0
  381. package/dist/substrate/lifo/commands/system/fastfetch.js +385 -0
  382. package/dist/substrate/lifo/commands/system/free.d.ts +4 -0
  383. package/dist/substrate/lifo/commands/system/free.d.ts.map +1 -0
  384. package/dist/substrate/lifo/commands/system/free.js +36 -0
  385. package/dist/substrate/lifo/commands/system/help.d.ts +4 -0
  386. package/dist/substrate/lifo/commands/system/help.d.ts.map +1 -0
  387. package/dist/substrate/lifo/commands/system/help.js +46 -0
  388. package/dist/substrate/lifo/commands/system/hostname.d.ts +4 -0
  389. package/dist/substrate/lifo/commands/system/hostname.d.ts.map +1 -0
  390. package/dist/substrate/lifo/commands/system/hostname.js +6 -0
  391. package/dist/substrate/lifo/commands/system/kill.d.ts +4 -0
  392. package/dist/substrate/lifo/commands/system/kill.d.ts.map +1 -0
  393. package/dist/substrate/lifo/commands/system/kill.js +76 -0
  394. package/dist/substrate/lifo/commands/system/lifo.d.ts +41 -0
  395. package/dist/substrate/lifo/commands/system/lifo.d.ts.map +1 -0
  396. package/dist/substrate/lifo/commands/system/lifo.js +446 -0
  397. package/dist/substrate/lifo/commands/system/logout.d.ts +5 -0
  398. package/dist/substrate/lifo/commands/system/logout.d.ts.map +1 -0
  399. package/dist/substrate/lifo/commands/system/logout.js +15 -0
  400. package/dist/substrate/lifo/commands/system/man.d.ts +4 -0
  401. package/dist/substrate/lifo/commands/system/man.d.ts.map +1 -0
  402. package/dist/substrate/lifo/commands/system/man.js +144 -0
  403. package/dist/substrate/lifo/commands/system/node.d.ts +6 -0
  404. package/dist/substrate/lifo/commands/system/node.d.ts.map +1 -0
  405. package/dist/substrate/lifo/commands/system/node.js +1241 -0
  406. package/dist/substrate/lifo/commands/system/npm.d.ts +26 -0
  407. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -0
  408. package/dist/substrate/lifo/commands/system/npm.js +934 -0
  409. package/dist/substrate/lifo/commands/system/pkg.d.ts +4 -0
  410. package/dist/substrate/lifo/commands/system/pkg.d.ts.map +1 -0
  411. package/dist/substrate/lifo/commands/system/pkg.js +97 -0
  412. package/dist/substrate/lifo/commands/system/ps.d.ts +5 -0
  413. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -0
  414. package/dist/substrate/lifo/commands/system/ps.js +35 -0
  415. package/dist/substrate/lifo/commands/system/registry-schemas.d.ts +44 -0
  416. package/dist/substrate/lifo/commands/system/registry-schemas.d.ts.map +1 -0
  417. package/dist/substrate/lifo/commands/system/registry-schemas.js +28 -0
  418. package/dist/substrate/lifo/commands/system/sha256sum.d.ts +4 -0
  419. package/dist/substrate/lifo/commands/system/sha256sum.d.ts.map +1 -0
  420. package/dist/substrate/lifo/commands/system/sha256sum.js +86 -0
  421. package/dist/substrate/lifo/commands/system/sl.d.ts +4 -0
  422. package/dist/substrate/lifo/commands/system/sl.d.ts.map +1 -0
  423. package/dist/substrate/lifo/commands/system/sl.js +204 -0
  424. package/dist/substrate/lifo/commands/system/sleep.d.ts +4 -0
  425. package/dist/substrate/lifo/commands/system/sleep.d.ts.map +1 -0
  426. package/dist/substrate/lifo/commands/system/sleep.js +27 -0
  427. package/dist/substrate/lifo/commands/system/stty.d.ts +4 -0
  428. package/dist/substrate/lifo/commands/system/stty.d.ts.map +1 -0
  429. package/dist/substrate/lifo/commands/system/stty.js +61 -0
  430. package/dist/substrate/lifo/commands/system/systemctl.d.ts +4 -0
  431. package/dist/substrate/lifo/commands/system/systemctl.d.ts.map +1 -0
  432. package/dist/substrate/lifo/commands/system/systemctl.js +164 -0
  433. package/dist/substrate/lifo/commands/system/top.d.ts +4 -0
  434. package/dist/substrate/lifo/commands/system/top.d.ts.map +1 -0
  435. package/dist/substrate/lifo/commands/system/top.js +38 -0
  436. package/dist/substrate/lifo/commands/system/uname.d.ts +4 -0
  437. package/dist/substrate/lifo/commands/system/uname.d.ts.map +1 -0
  438. package/dist/substrate/lifo/commands/system/uname.js +55 -0
  439. package/dist/substrate/lifo/commands/system/uptime.d.ts +4 -0
  440. package/dist/substrate/lifo/commands/system/uptime.d.ts.map +1 -0
  441. package/dist/substrate/lifo/commands/system/uptime.js +16 -0
  442. package/dist/substrate/lifo/commands/system/watch.d.ts +4 -0
  443. package/dist/substrate/lifo/commands/system/watch.d.ts.map +1 -0
  444. package/dist/substrate/lifo/commands/system/watch.js +53 -0
  445. package/dist/substrate/lifo/commands/system/which.d.ts +4 -0
  446. package/dist/substrate/lifo/commands/system/which.d.ts.map +1 -0
  447. package/dist/substrate/lifo/commands/system/which.js +45 -0
  448. package/dist/substrate/lifo/commands/system/whoami.d.ts +4 -0
  449. package/dist/substrate/lifo/commands/system/whoami.d.ts.map +1 -0
  450. package/dist/substrate/lifo/commands/system/whoami.js +6 -0
  451. package/dist/substrate/lifo/commands/text/awk.d.ts +4 -0
  452. package/dist/substrate/lifo/commands/text/awk.d.ts.map +1 -0
  453. package/dist/substrate/lifo/commands/text/awk.js +200 -0
  454. package/dist/substrate/lifo/commands/text/base64.d.ts +4 -0
  455. package/dist/substrate/lifo/commands/text/base64.d.ts.map +1 -0
  456. package/dist/substrate/lifo/commands/text/base64.js +102 -0
  457. package/dist/substrate/lifo/commands/text/cut.d.ts +4 -0
  458. package/dist/substrate/lifo/commands/text/cut.d.ts.map +1 -0
  459. package/dist/substrate/lifo/commands/text/cut.js +91 -0
  460. package/dist/substrate/lifo/commands/text/diff.d.ts +4 -0
  461. package/dist/substrate/lifo/commands/text/diff.d.ts.map +1 -0
  462. package/dist/substrate/lifo/commands/text/diff.js +292 -0
  463. package/dist/substrate/lifo/commands/text/grep.d.ts +4 -0
  464. package/dist/substrate/lifo/commands/text/grep.d.ts.map +1 -0
  465. package/dist/substrate/lifo/commands/text/grep.js +189 -0
  466. package/dist/substrate/lifo/commands/text/head.d.ts +4 -0
  467. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -0
  468. package/dist/substrate/lifo/commands/text/head.js +111 -0
  469. package/dist/substrate/lifo/commands/text/less.d.ts +4 -0
  470. package/dist/substrate/lifo/commands/text/less.d.ts.map +1 -0
  471. package/dist/substrate/lifo/commands/text/less.js +335 -0
  472. package/dist/substrate/lifo/commands/text/nano.d.ts +4 -0
  473. package/dist/substrate/lifo/commands/text/nano.d.ts.map +1 -0
  474. package/dist/substrate/lifo/commands/text/nano.js +510 -0
  475. package/dist/substrate/lifo/commands/text/nl.d.ts +4 -0
  476. package/dist/substrate/lifo/commands/text/nl.d.ts.map +1 -0
  477. package/dist/substrate/lifo/commands/text/nl.js +64 -0
  478. package/dist/substrate/lifo/commands/text/rev.d.ts +4 -0
  479. package/dist/substrate/lifo/commands/text/rev.d.ts.map +1 -0
  480. package/dist/substrate/lifo/commands/text/rev.js +41 -0
  481. package/dist/substrate/lifo/commands/text/sed.d.ts +21 -0
  482. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -0
  483. package/dist/substrate/lifo/commands/text/sed.js +248 -0
  484. package/dist/substrate/lifo/commands/text/seq.d.ts +4 -0
  485. package/dist/substrate/lifo/commands/text/seq.d.ts.map +1 -0
  486. package/dist/substrate/lifo/commands/text/seq.js +69 -0
  487. package/dist/substrate/lifo/commands/text/sort.d.ts +4 -0
  488. package/dist/substrate/lifo/commands/text/sort.d.ts.map +1 -0
  489. package/dist/substrate/lifo/commands/text/sort.js +90 -0
  490. package/dist/substrate/lifo/commands/text/strings.d.ts +4 -0
  491. package/dist/substrate/lifo/commands/text/strings.d.ts.map +1 -0
  492. package/dist/substrate/lifo/commands/text/strings.js +69 -0
  493. package/dist/substrate/lifo/commands/text/tac.d.ts +4 -0
  494. package/dist/substrate/lifo/commands/text/tac.d.ts.map +1 -0
  495. package/dist/substrate/lifo/commands/text/tac.js +32 -0
  496. package/dist/substrate/lifo/commands/text/tail.d.ts +4 -0
  497. package/dist/substrate/lifo/commands/text/tail.d.ts.map +1 -0
  498. package/dist/substrate/lifo/commands/text/tail.js +64 -0
  499. package/dist/substrate/lifo/commands/text/tr.d.ts +4 -0
  500. package/dist/substrate/lifo/commands/text/tr.d.ts.map +1 -0
  501. package/dist/substrate/lifo/commands/text/tr.js +180 -0
  502. package/dist/substrate/lifo/commands/text/uniq.d.ts +4 -0
  503. package/dist/substrate/lifo/commands/text/uniq.d.ts.map +1 -0
  504. package/dist/substrate/lifo/commands/text/uniq.js +82 -0
  505. package/dist/substrate/lifo/commands/text/wc.d.ts +4 -0
  506. package/dist/substrate/lifo/commands/text/wc.d.ts.map +1 -0
  507. package/dist/substrate/lifo/commands/text/wc.js +107 -0
  508. package/dist/substrate/lifo/commands/types.d.ts +49 -0
  509. package/dist/substrate/lifo/commands/types.d.ts.map +1 -0
  510. package/dist/substrate/lifo/commands/types.js +1 -0
  511. package/dist/substrate/lifo/index.d.ts +67 -0
  512. package/dist/substrate/lifo/index.d.ts.map +1 -0
  513. package/dist/substrate/lifo/index.js +66 -0
  514. package/dist/substrate/lifo/kernel/ServiceManager.d.ts +51 -0
  515. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -0
  516. package/dist/substrate/lifo/kernel/ServiceManager.js +302 -0
  517. package/dist/substrate/lifo/kernel/index.d.ts +40 -0
  518. package/dist/substrate/lifo/kernel/index.d.ts.map +1 -0
  519. package/dist/substrate/lifo/kernel/index.js +170 -0
  520. package/dist/substrate/lifo/kernel/network/Bridge.d.ts +110 -0
  521. package/dist/substrate/lifo/kernel/network/Bridge.d.ts.map +1 -0
  522. package/dist/substrate/lifo/kernel/network/Bridge.js +278 -0
  523. package/dist/substrate/lifo/kernel/network/DNSResolver.d.ts +50 -0
  524. package/dist/substrate/lifo/kernel/network/DNSResolver.d.ts.map +1 -0
  525. package/dist/substrate/lifo/kernel/network/DNSResolver.js +135 -0
  526. package/dist/substrate/lifo/kernel/network/NetworkInterface.d.ts +83 -0
  527. package/dist/substrate/lifo/kernel/network/NetworkInterface.d.ts.map +1 -0
  528. package/dist/substrate/lifo/kernel/network/NetworkInterface.js +183 -0
  529. package/dist/substrate/lifo/kernel/network/NetworkNamespace.d.ts +98 -0
  530. package/dist/substrate/lifo/kernel/network/NetworkNamespace.d.ts.map +1 -0
  531. package/dist/substrate/lifo/kernel/network/NetworkNamespace.js +149 -0
  532. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +178 -0
  533. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -0
  534. package/dist/substrate/lifo/kernel/network/NetworkStack.js +460 -0
  535. package/dist/substrate/lifo/kernel/network/PortBridge.d.ts +44 -0
  536. package/dist/substrate/lifo/kernel/network/PortBridge.d.ts.map +1 -0
  537. package/dist/substrate/lifo/kernel/network/PortBridge.js +158 -0
  538. package/dist/substrate/lifo/kernel/network/RoutingTable.d.ts +55 -0
  539. package/dist/substrate/lifo/kernel/network/RoutingTable.d.ts.map +1 -0
  540. package/dist/substrate/lifo/kernel/network/RoutingTable.js +167 -0
  541. package/dist/substrate/lifo/kernel/network/Socket.d.ts +65 -0
  542. package/dist/substrate/lifo/kernel/network/Socket.d.ts.map +1 -0
  543. package/dist/substrate/lifo/kernel/network/Socket.js +201 -0
  544. package/dist/substrate/lifo/kernel/network/index.d.ts +11 -0
  545. package/dist/substrate/lifo/kernel/network/index.d.ts.map +1 -0
  546. package/dist/substrate/lifo/kernel/network/index.js +9 -0
  547. package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.d.ts +55 -0
  548. package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.d.ts.map +1 -0
  549. package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.js +88 -0
  550. package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.d.ts +71 -0
  551. package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.d.ts.map +1 -0
  552. package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.js +191 -0
  553. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +89 -0
  554. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -0
  555. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +324 -0
  556. package/dist/substrate/lifo/kernel/network/types.d.ts +159 -0
  557. package/dist/substrate/lifo/kernel/network/types.d.ts.map +1 -0
  558. package/dist/substrate/lifo/kernel/network/types.js +5 -0
  559. package/dist/substrate/lifo/kernel/persistence/PersistenceManager.d.ts +12 -0
  560. package/dist/substrate/lifo/kernel/persistence/PersistenceManager.d.ts.map +1 -0
  561. package/dist/substrate/lifo/kernel/persistence/PersistenceManager.js +47 -0
  562. package/dist/substrate/lifo/kernel/persistence/backends.d.ts +15 -0
  563. package/dist/substrate/lifo/kernel/persistence/backends.d.ts.map +1 -0
  564. package/dist/substrate/lifo/kernel/persistence/backends.js +18 -0
  565. package/dist/substrate/lifo/kernel/persistence/index.d.ts +6 -0
  566. package/dist/substrate/lifo/kernel/persistence/index.d.ts.map +1 -0
  567. package/dist/substrate/lifo/kernel/persistence/index.js +3 -0
  568. package/dist/substrate/lifo/kernel/persistence/serializer.d.ts +21 -0
  569. package/dist/substrate/lifo/kernel/persistence/serializer.d.ts.map +1 -0
  570. package/dist/substrate/lifo/kernel/persistence/serializer.js +99 -0
  571. package/dist/substrate/lifo/kernel/samples.d.ts +3 -0
  572. package/dist/substrate/lifo/kernel/samples.d.ts.map +1 -0
  573. package/dist/substrate/lifo/kernel/samples.js +930 -0
  574. package/dist/substrate/lifo/kernel/storage/BlobStore.d.ts +22 -0
  575. package/dist/substrate/lifo/kernel/storage/BlobStore.d.ts.map +1 -0
  576. package/dist/substrate/lifo/kernel/storage/BlobStore.js +84 -0
  577. package/dist/substrate/lifo/kernel/storage/ContentStore.d.ts +54 -0
  578. package/dist/substrate/lifo/kernel/storage/ContentStore.d.ts.map +1 -0
  579. package/dist/substrate/lifo/kernel/storage/ContentStore.js +120 -0
  580. package/dist/substrate/lifo/kernel/storage/index.d.ts +6 -0
  581. package/dist/substrate/lifo/kernel/storage/index.d.ts.map +1 -0
  582. package/dist/substrate/lifo/kernel/storage/index.js +3 -0
  583. package/dist/substrate/lifo/kernel/unit-parser.d.ts +22 -0
  584. package/dist/substrate/lifo/kernel/unit-parser.d.ts.map +1 -0
  585. package/dist/substrate/lifo/kernel/unit-parser.js +80 -0
  586. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +99 -0
  587. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -0
  588. package/dist/substrate/lifo/kernel/vfs/VFS.js +679 -0
  589. package/dist/substrate/lifo/kernel/vfs/index.d.ts +8 -0
  590. package/dist/substrate/lifo/kernel/vfs/index.d.ts.map +1 -0
  591. package/dist/substrate/lifo/kernel/vfs/index.js +5 -0
  592. package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.d.ts +21 -0
  593. package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.d.ts.map +1 -0
  594. package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.js +137 -0
  595. package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.d.ts +63 -0
  596. package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.d.ts.map +1 -0
  597. package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.js +175 -0
  598. package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.d.ts +14 -0
  599. package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.d.ts.map +1 -0
  600. package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.js +126 -0
  601. package/dist/substrate/lifo/kernel/vfs/types.d.ts +117 -0
  602. package/dist/substrate/lifo/kernel/vfs/types.d.ts.map +1 -0
  603. package/dist/substrate/lifo/kernel/vfs/types.js +42 -0
  604. package/dist/substrate/lifo/node-compat/buffer.d.ts +44 -0
  605. package/dist/substrate/lifo/node-compat/buffer.d.ts.map +1 -0
  606. package/dist/substrate/lifo/node-compat/buffer.js +331 -0
  607. package/dist/substrate/lifo/node-compat/child_process.d.ts +10 -0
  608. package/dist/substrate/lifo/node-compat/child_process.d.ts.map +1 -0
  609. package/dist/substrate/lifo/node-compat/child_process.js +42 -0
  610. package/dist/substrate/lifo/node-compat/console.d.ts +21 -0
  611. package/dist/substrate/lifo/node-compat/console.d.ts.map +1 -0
  612. package/dist/substrate/lifo/node-compat/console.js +68 -0
  613. package/dist/substrate/lifo/node-compat/crypto.d.ts +24 -0
  614. package/dist/substrate/lifo/node-compat/crypto.d.ts.map +1 -0
  615. package/dist/substrate/lifo/node-compat/crypto.js +216 -0
  616. package/dist/substrate/lifo/node-compat/dns.d.ts +117 -0
  617. package/dist/substrate/lifo/node-compat/dns.d.ts.map +1 -0
  618. package/dist/substrate/lifo/node-compat/dns.js +144 -0
  619. package/dist/substrate/lifo/node-compat/esbuild.d.ts +10 -0
  620. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -0
  621. package/dist/substrate/lifo/node-compat/esbuild.js +70 -0
  622. package/dist/substrate/lifo/node-compat/events.d.ts +19 -0
  623. package/dist/substrate/lifo/node-compat/events.d.ts.map +1 -0
  624. package/dist/substrate/lifo/node-compat/events.js +83 -0
  625. package/dist/substrate/lifo/node-compat/fs.d.ts +195 -0
  626. package/dist/substrate/lifo/node-compat/fs.d.ts.map +1 -0
  627. package/dist/substrate/lifo/node-compat/fs.js +624 -0
  628. package/dist/substrate/lifo/node-compat/http.d.ts +133 -0
  629. package/dist/substrate/lifo/node-compat/http.d.ts.map +1 -0
  630. package/dist/substrate/lifo/node-compat/http.js +452 -0
  631. package/dist/substrate/lifo/node-compat/index.d.ts +20 -0
  632. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -0
  633. package/dist/substrate/lifo/node-compat/index.js +291 -0
  634. package/dist/substrate/lifo/node-compat/module.d.ts +55 -0
  635. package/dist/substrate/lifo/node-compat/module.d.ts.map +1 -0
  636. package/dist/substrate/lifo/node-compat/module.js +124 -0
  637. package/dist/substrate/lifo/node-compat/os.d.ts +39 -0
  638. package/dist/substrate/lifo/node-compat/os.d.ts.map +1 -0
  639. package/dist/substrate/lifo/node-compat/os.js +45 -0
  640. package/dist/substrate/lifo/node-compat/path.d.ts +86 -0
  641. package/dist/substrate/lifo/node-compat/path.d.ts.map +1 -0
  642. package/dist/substrate/lifo/node-compat/path.js +49 -0
  643. package/dist/substrate/lifo/node-compat/process.d.ts +127 -0
  644. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -0
  645. package/dist/substrate/lifo/node-compat/process.js +173 -0
  646. package/dist/substrate/lifo/node-compat/readline.d.ts +85 -0
  647. package/dist/substrate/lifo/node-compat/readline.d.ts.map +1 -0
  648. package/dist/substrate/lifo/node-compat/readline.js +205 -0
  649. package/dist/substrate/lifo/node-compat/rimraf.d.ts +30 -0
  650. package/dist/substrate/lifo/node-compat/rimraf.d.ts.map +1 -0
  651. package/dist/substrate/lifo/node-compat/rimraf.js +63 -0
  652. package/dist/substrate/lifo/node-compat/stream.d.ts +36 -0
  653. package/dist/substrate/lifo/node-compat/stream.d.ts.map +1 -0
  654. package/dist/substrate/lifo/node-compat/stream.js +90 -0
  655. package/dist/substrate/lifo/node-compat/string_decoder.d.ts +11 -0
  656. package/dist/substrate/lifo/node-compat/string_decoder.d.ts.map +1 -0
  657. package/dist/substrate/lifo/node-compat/string_decoder.js +23 -0
  658. package/dist/substrate/lifo/node-compat/timers.d.ts +16 -0
  659. package/dist/substrate/lifo/node-compat/timers.d.ts.map +1 -0
  660. package/dist/substrate/lifo/node-compat/timers.js +18 -0
  661. package/dist/substrate/lifo/node-compat/tty.d.ts +32 -0
  662. package/dist/substrate/lifo/node-compat/tty.d.ts.map +1 -0
  663. package/dist/substrate/lifo/node-compat/tty.js +76 -0
  664. package/dist/substrate/lifo/node-compat/url.d.ts +37 -0
  665. package/dist/substrate/lifo/node-compat/url.d.ts.map +1 -0
  666. package/dist/substrate/lifo/node-compat/url.js +65 -0
  667. package/dist/substrate/lifo/node-compat/util.d.ts +42 -0
  668. package/dist/substrate/lifo/node-compat/util.d.ts.map +1 -0
  669. package/dist/substrate/lifo/node-compat/util.js +132 -0
  670. package/dist/substrate/lifo/node-compat/zlib.d.ts +66 -0
  671. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -0
  672. package/dist/substrate/lifo/node-compat/zlib.js +80 -0
  673. package/dist/substrate/lifo/pkg/PackageManager.d.ts +19 -0
  674. package/dist/substrate/lifo/pkg/PackageManager.d.ts.map +1 -0
  675. package/dist/substrate/lifo/pkg/PackageManager.js +97 -0
  676. package/dist/substrate/lifo/pkg/lifo-dev.d.ts +38 -0
  677. package/dist/substrate/lifo/pkg/lifo-dev.d.ts.map +1 -0
  678. package/dist/substrate/lifo/pkg/lifo-dev.js +98 -0
  679. package/dist/substrate/lifo/pkg/lifo-runtime.d.ts +44 -0
  680. package/dist/substrate/lifo/pkg/lifo-runtime.d.ts.map +1 -0
  681. package/dist/substrate/lifo/pkg/lifo-runtime.js +372 -0
  682. package/dist/substrate/lifo/pkg/loader.d.ts +4 -0
  683. package/dist/substrate/lifo/pkg/loader.d.ts.map +1 -0
  684. package/dist/substrate/lifo/pkg/loader.js +24 -0
  685. package/dist/substrate/lifo/sandbox/HeadlessTerminal.d.ts +18 -0
  686. package/dist/substrate/lifo/sandbox/HeadlessTerminal.d.ts.map +1 -0
  687. package/dist/substrate/lifo/sandbox/HeadlessTerminal.js +28 -0
  688. package/dist/substrate/lifo/sandbox/Sandbox.d.ts +59 -0
  689. package/dist/substrate/lifo/sandbox/Sandbox.d.ts.map +1 -0
  690. package/dist/substrate/lifo/sandbox/Sandbox.js +234 -0
  691. package/dist/substrate/lifo/sandbox/SandboxCommands.d.ts +18 -0
  692. package/dist/substrate/lifo/sandbox/SandboxCommands.d.ts.map +1 -0
  693. package/dist/substrate/lifo/sandbox/SandboxCommands.js +64 -0
  694. package/dist/substrate/lifo/sandbox/SandboxFs.d.ts +43 -0
  695. package/dist/substrate/lifo/sandbox/SandboxFs.d.ts.map +1 -0
  696. package/dist/substrate/lifo/sandbox/SandboxFs.js +136 -0
  697. package/dist/substrate/lifo/sandbox/index.d.ts +3 -0
  698. package/dist/substrate/lifo/sandbox/index.d.ts.map +1 -0
  699. package/dist/substrate/lifo/sandbox/index.js +1 -0
  700. package/dist/substrate/lifo/sandbox/types.d.ts +116 -0
  701. package/dist/substrate/lifo/sandbox/types.d.ts.map +1 -0
  702. package/dist/substrate/lifo/sandbox/types.js +1 -0
  703. package/dist/substrate/lifo/shell/ProcessRegistry.d.ts +142 -0
  704. package/dist/substrate/lifo/shell/ProcessRegistry.d.ts.map +1 -0
  705. package/dist/substrate/lifo/shell/ProcessRegistry.js +256 -0
  706. package/dist/substrate/lifo/shell/Shell.d.ts +170 -0
  707. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -0
  708. package/dist/substrate/lifo/shell/Shell.js +1863 -0
  709. package/dist/substrate/lifo/shell/completer.d.ts +19 -0
  710. package/dist/substrate/lifo/shell/completer.d.ts.map +1 -0
  711. package/dist/substrate/lifo/shell/completer.js +164 -0
  712. package/dist/substrate/lifo/shell/default-shell.d.ts +9 -0
  713. package/dist/substrate/lifo/shell/default-shell.d.ts.map +1 -0
  714. package/dist/substrate/lifo/shell/default-shell.js +53 -0
  715. package/dist/substrate/lifo/shell/expander.d.ts +48 -0
  716. package/dist/substrate/lifo/shell/expander.d.ts.map +1 -0
  717. package/dist/substrate/lifo/shell/expander.js +1204 -0
  718. package/dist/substrate/lifo/shell/history.d.ts +22 -0
  719. package/dist/substrate/lifo/shell/history.d.ts.map +1 -0
  720. package/dist/substrate/lifo/shell/history.js +121 -0
  721. package/dist/substrate/lifo/shell/interpreter.d.ts +257 -0
  722. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -0
  723. package/dist/substrate/lifo/shell/interpreter.js +1490 -0
  724. package/dist/substrate/lifo/shell/jobs.d.ts +21 -0
  725. package/dist/substrate/lifo/shell/jobs.d.ts.map +1 -0
  726. package/dist/substrate/lifo/shell/jobs.js +48 -0
  727. package/dist/substrate/lifo/shell/lexer.d.ts +13 -0
  728. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -0
  729. package/dist/substrate/lifo/shell/lexer.js +614 -0
  730. package/dist/substrate/lifo/shell/parser.d.ts +7 -0
  731. package/dist/substrate/lifo/shell/parser.d.ts.map +1 -0
  732. package/dist/substrate/lifo/shell/parser.js +573 -0
  733. package/dist/substrate/lifo/shell/pipe.d.ts +15 -0
  734. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -0
  735. package/dist/substrate/lifo/shell/pipe.js +86 -0
  736. package/dist/substrate/lifo/shell/signals.d.ts +11 -0
  737. package/dist/substrate/lifo/shell/signals.d.ts.map +1 -0
  738. package/dist/substrate/lifo/shell/signals.js +57 -0
  739. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +28 -0
  740. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -0
  741. package/dist/substrate/lifo/shell/terminal-stdin.js +100 -0
  742. package/dist/substrate/lifo/shell/test-builtin.d.ts +16 -0
  743. package/dist/substrate/lifo/shell/test-builtin.d.ts.map +1 -0
  744. package/dist/substrate/lifo/shell/test-builtin.js +237 -0
  745. package/dist/substrate/lifo/shell/types.d.ts +140 -0
  746. package/dist/substrate/lifo/shell/types.d.ts.map +1 -0
  747. package/dist/substrate/lifo/shell/types.js +25 -0
  748. package/dist/substrate/lifo/terminal/ITerminal.d.ts +10 -0
  749. package/dist/substrate/lifo/terminal/ITerminal.d.ts.map +1 -0
  750. package/dist/substrate/lifo/terminal/ITerminal.js +1 -0
  751. package/dist/substrate/lifo/utils/archive.d.ts +28 -0
  752. package/dist/substrate/lifo/utils/archive.d.ts.map +1 -0
  753. package/dist/substrate/lifo/utils/archive.js +323 -0
  754. package/dist/substrate/lifo/utils/args.d.ts +18 -0
  755. package/dist/substrate/lifo/utils/args.d.ts.map +1 -0
  756. package/dist/substrate/lifo/utils/args.js +73 -0
  757. package/dist/substrate/lifo/utils/colors.d.ts +27 -0
  758. package/dist/substrate/lifo/utils/colors.d.ts.map +1 -0
  759. package/dist/substrate/lifo/utils/colors.js +26 -0
  760. package/dist/substrate/lifo/utils/encoding.d.ts +4 -0
  761. package/dist/substrate/lifo/utils/encoding.d.ts.map +1 -0
  762. package/dist/substrate/lifo/utils/encoding.js +18 -0
  763. package/dist/substrate/lifo/utils/glob.d.ts +12 -0
  764. package/dist/substrate/lifo/utils/glob.d.ts.map +1 -0
  765. package/dist/substrate/lifo/utils/glob.js +150 -0
  766. package/dist/substrate/lifo/utils/mime.d.ts +4 -0
  767. package/dist/substrate/lifo/utils/mime.d.ts.map +1 -0
  768. package/dist/substrate/lifo/utils/mime.js +107 -0
  769. package/dist/substrate/lifo/utils/path.d.ts +12 -0
  770. package/dist/substrate/lifo/utils/path.d.ts.map +1 -0
  771. package/dist/substrate/lifo/utils/path.js +82 -0
  772. package/dist/vfs/events.d.ts +51 -0
  773. package/dist/vfs/events.d.ts.map +1 -0
  774. package/dist/vfs/events.js +127 -0
  775. package/dist/vfs/path.d.ts +46 -0
  776. package/dist/vfs/path.d.ts.map +1 -0
  777. package/dist/vfs/path.js +69 -0
  778. package/dist/vfs/seed-project.d.ts +62 -0
  779. package/dist/vfs/seed-project.d.ts.map +1 -0
  780. package/dist/vfs/seed-project.js +1054 -0
  781. package/dist/vfs/sqlite-vfs.d.ts +821 -0
  782. package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
  783. package/dist/vfs/sqlite-vfs.js +4135 -0
  784. package/dist/vfs/symlink-registry.d.ts +60 -0
  785. package/dist/vfs/symlink-registry.d.ts.map +1 -0
  786. package/dist/vfs/symlink-registry.js +162 -0
  787. package/dist/workspace/nimbus-workspace.d.ts +94 -0
  788. package/dist/workspace/nimbus-workspace.d.ts.map +1 -0
  789. package/dist/workspace/nimbus-workspace.js +167 -0
  790. package/package.json +69 -0
  791. package/src/_shared/ai-egress.ts +91 -0
  792. package/src/_shared/byte-stream.ts +97 -0
  793. package/src/_shared/bytes.ts +19 -0
  794. package/src/_shared/cache-stats.ts +231 -0
  795. package/src/_shared/crypto.ts +184 -0
  796. package/src/_shared/esbuild-facet-shim.ts +22 -0
  797. package/src/_shared/exports-resolver.ts +308 -0
  798. package/src/_shared/facet-vfs-cursor.ts +58 -0
  799. package/src/_shared/install-phase.ts +56 -0
  800. package/src/_shared/lru-map.ts +59 -0
  801. package/src/_shared/real-node-imports.ts +53 -0
  802. package/src/_shared/retry.ts +202 -0
  803. package/src/_shared/rpc-dispose.ts +31 -0
  804. package/src/_shared/terminal.ts +3 -0
  805. package/src/_shared/typescript-specifiers.ts +86 -0
  806. package/src/_shared/untrusted-request.ts +75 -0
  807. package/src/_shared/vfs-write-ledger.ts +440 -0
  808. package/src/_shared/w7-frame.ts +1012 -0
  809. package/src/_shared/weighted-credit-pool.ts +244 -0
  810. package/src/constants.ts +285 -0
  811. package/src/index.ts +22 -0
  812. package/src/observability/diag-counters.ts +343 -0
  813. package/src/observability/heap-estimate.ts +331 -0
  814. package/src/observability/heavy-alloc-coord.ts +241 -0
  815. package/src/observability/oom-classify.ts +253 -0
  816. package/src/observability/oom-discriminator.ts +405 -0
  817. package/src/runtime/assets-loader.ts +189 -0
  818. package/src/runtime/barrel-detect.ts +69 -0
  819. package/src/runtime/bash/preamble.ts +1519 -0
  820. package/src/runtime/bash/types.ts +497 -0
  821. package/src/runtime/bundle-profile.ts +32 -0
  822. package/src/runtime/cirrus-vite-config-rewriter.ts +28 -0
  823. package/src/runtime/cli-flags.ts +11 -0
  824. package/src/runtime/comment-strip.ts +73 -0
  825. package/src/runtime/cpython-preamble.ts +298 -0
  826. package/src/runtime/esbuild-service.ts +1470 -0
  827. package/src/runtime/framework-detect.ts +245 -0
  828. package/src/runtime/import-meta-transform.ts +16 -0
  829. package/src/runtime/javascript-ast.ts +66 -0
  830. package/src/runtime/long-running-handle.ts +185 -0
  831. package/src/runtime/module-source-rewriter.ts +95 -0
  832. package/src/runtime/opentui-wasm-backend.ts +608 -0
  833. package/src/runtime/os-contracts.ts +529 -0
  834. package/src/runtime/port-registry.ts +298 -0
  835. package/src/runtime/process-input-routing.ts +92 -0
  836. package/src/runtime/process-input.ts +148 -0
  837. package/src/runtime/process-io-protocol.ts +97 -0
  838. package/src/runtime/process-logs.ts +882 -0
  839. package/src/runtime/process-table.ts +225 -0
  840. package/src/runtime/project-detect.ts +42 -0
  841. package/src/runtime/python-server-adapter.ts +133 -0
  842. package/src/runtime/require-resolver.ts +760 -0
  843. package/src/runtime/router-basename.ts +279 -0
  844. package/src/runtime/ruby-green-threads.ts +904 -0
  845. package/src/runtime/ruby-socket-shim.ts +548 -0
  846. package/src/runtime/session-process-supervisor.ts +323 -0
  847. package/src/runtime/sqlite-runtime-fs-bridge.ts +468 -0
  848. package/src/runtime/streams.ts +547 -0
  849. package/src/runtime/undici-shim.ts +517 -0
  850. package/src/runtime/vfs-manifest.ts +170 -0
  851. package/src/runtime/vfs-snapshot.ts +273 -0
  852. package/src/runtime/virtual-socket-kernel.ts +1370 -0
  853. package/src/runtime/vite-config-parser.ts +273 -0
  854. package/src/runtime/wasi/types.ts +448 -0
  855. package/src/runtime/wasi-instance.generated.ts +14 -0
  856. package/src/runtime/wasi-instance.ts +259 -0
  857. package/src/runtime/wasi-threads.ts +682 -0
  858. package/src/runtime/wasm-memory.ts +261 -0
  859. package/src/shell/elevation-commands.ts +117 -0
  860. package/src/shell/exec-dispatch.ts +201 -0
  861. package/src/shell/features.ts +870 -0
  862. package/src/shell/shell-entrypoints.ts +323 -0
  863. package/src/shell/shell-invocation.ts +181 -0
  864. package/src/shell/unix-accounts.ts +128 -0
  865. package/src/shell/unix-commands.ts +4335 -0
  866. package/src/substrate/lifo/UPSTREAM.md +14 -0
  867. package/src/substrate/lifo/commands/archive/gunzip.ts +63 -0
  868. package/src/substrate/lifo/commands/archive/gzip.ts +76 -0
  869. package/src/substrate/lifo/commands/archive/tar.ts +157 -0
  870. package/src/substrate/lifo/commands/archive/unzip.ts +112 -0
  871. package/src/substrate/lifo/commands/archive/zip.ts +70 -0
  872. package/src/substrate/lifo/commands/fs/basename.ts +16 -0
  873. package/src/substrate/lifo/commands/fs/cat.ts +54 -0
  874. package/src/substrate/lifo/commands/fs/chmod.ts +110 -0
  875. package/src/substrate/lifo/commands/fs/chown.ts +45 -0
  876. package/src/substrate/lifo/commands/fs/cp.ts +33 -0
  877. package/src/substrate/lifo/commands/fs/df.ts +56 -0
  878. package/src/substrate/lifo/commands/fs/dirname.ts +16 -0
  879. package/src/substrate/lifo/commands/fs/du.ts +100 -0
  880. package/src/substrate/lifo/commands/fs/file.ts +90 -0
  881. package/src/substrate/lifo/commands/fs/find.ts +76 -0
  882. package/src/substrate/lifo/commands/fs/ln.ts +19 -0
  883. package/src/substrate/lifo/commands/fs/ls.ts +164 -0
  884. package/src/substrate/lifo/commands/fs/mkdir.ts +37 -0
  885. package/src/substrate/lifo/commands/fs/mktemp.ts +56 -0
  886. package/src/substrate/lifo/commands/fs/mv.ts +111 -0
  887. package/src/substrate/lifo/commands/fs/realpath.ts +31 -0
  888. package/src/substrate/lifo/commands/fs/rm.ts +53 -0
  889. package/src/substrate/lifo/commands/fs/rmdir.ts +50 -0
  890. package/src/substrate/lifo/commands/fs/stat.ts +39 -0
  891. package/src/substrate/lifo/commands/fs/touch.ts +30 -0
  892. package/src/substrate/lifo/commands/fs/tree.ts +76 -0
  893. package/src/substrate/lifo/commands/index.ts +2 -0
  894. package/src/substrate/lifo/commands/io/dd.ts +289 -0
  895. package/src/substrate/lifo/commands/io/printf.ts +78 -0
  896. package/src/substrate/lifo/commands/io/tee.ts +37 -0
  897. package/src/substrate/lifo/commands/io/xargs.ts +49 -0
  898. package/src/substrate/lifo/commands/io/yes.ts +15 -0
  899. package/src/substrate/lifo/commands/net/curl.ts +739 -0
  900. package/src/substrate/lifo/commands/net/dig.ts +99 -0
  901. package/src/substrate/lifo/commands/net/forward.ts +90 -0
  902. package/src/substrate/lifo/commands/net/host.ts +182 -0
  903. package/src/substrate/lifo/commands/net/ifconfig.ts +139 -0
  904. package/src/substrate/lifo/commands/net/ip.ts +633 -0
  905. package/src/substrate/lifo/commands/net/netstat.ts +106 -0
  906. package/src/substrate/lifo/commands/net/ping.ts +74 -0
  907. package/src/substrate/lifo/commands/net/ports.ts +45 -0
  908. package/src/substrate/lifo/commands/net/route.ts +153 -0
  909. package/src/substrate/lifo/commands/net/tunnel-v2.ts +183 -0
  910. package/src/substrate/lifo/commands/net/tunnel.ts +272 -0
  911. package/src/substrate/lifo/commands/net/wget.ts +88 -0
  912. package/src/substrate/lifo/commands/registry.ts +141 -0
  913. package/src/substrate/lifo/commands/signal.ts +65 -0
  914. package/src/substrate/lifo/commands/system/bc.ts +265 -0
  915. package/src/substrate/lifo/commands/system/cal.ts +90 -0
  916. package/src/substrate/lifo/commands/system/date.ts +59 -0
  917. package/src/substrate/lifo/commands/system/env.ts +10 -0
  918. package/src/substrate/lifo/commands/system/fastfetch.ts +428 -0
  919. package/src/substrate/lifo/commands/system/free.ts +38 -0
  920. package/src/substrate/lifo/commands/system/help.ts +53 -0
  921. package/src/substrate/lifo/commands/system/hostname.ts +9 -0
  922. package/src/substrate/lifo/commands/system/kill.ts +89 -0
  923. package/src/substrate/lifo/commands/system/lifo.ts +525 -0
  924. package/src/substrate/lifo/commands/system/logout.ts +17 -0
  925. package/src/substrate/lifo/commands/system/man.ts +167 -0
  926. package/src/substrate/lifo/commands/system/node.ts +1305 -0
  927. package/src/substrate/lifo/commands/system/npm.ts +1078 -0
  928. package/src/substrate/lifo/commands/system/pkg.ts +111 -0
  929. package/src/substrate/lifo/commands/system/ps.ts +46 -0
  930. package/src/substrate/lifo/commands/system/registry-schemas.ts +33 -0
  931. package/src/substrate/lifo/commands/system/sha256sum.ts +89 -0
  932. package/src/substrate/lifo/commands/system/sl.ts +249 -0
  933. package/src/substrate/lifo/commands/system/sleep.ts +36 -0
  934. package/src/substrate/lifo/commands/system/stty.ts +69 -0
  935. package/src/substrate/lifo/commands/system/systemctl.ts +184 -0
  936. package/src/substrate/lifo/commands/system/top.ts +53 -0
  937. package/src/substrate/lifo/commands/system/uname.ts +59 -0
  938. package/src/substrate/lifo/commands/system/uptime.ts +19 -0
  939. package/src/substrate/lifo/commands/system/watch.ts +67 -0
  940. package/src/substrate/lifo/commands/system/which.ts +51 -0
  941. package/src/substrate/lifo/commands/system/whoami.ts +9 -0
  942. package/src/substrate/lifo/commands/text/awk.ts +210 -0
  943. package/src/substrate/lifo/commands/text/base64.ts +102 -0
  944. package/src/substrate/lifo/commands/text/cut.ts +92 -0
  945. package/src/substrate/lifo/commands/text/diff.ts +314 -0
  946. package/src/substrate/lifo/commands/text/grep.ts +170 -0
  947. package/src/substrate/lifo/commands/text/head.ts +95 -0
  948. package/src/substrate/lifo/commands/text/less.ts +335 -0
  949. package/src/substrate/lifo/commands/text/nano.ts +513 -0
  950. package/src/substrate/lifo/commands/text/nl.ts +74 -0
  951. package/src/substrate/lifo/commands/text/rev.ts +46 -0
  952. package/src/substrate/lifo/commands/text/sed.ts +297 -0
  953. package/src/substrate/lifo/commands/text/seq.ts +76 -0
  954. package/src/substrate/lifo/commands/text/sort.ts +88 -0
  955. package/src/substrate/lifo/commands/text/strings.ts +73 -0
  956. package/src/substrate/lifo/commands/text/tac.ts +37 -0
  957. package/src/substrate/lifo/commands/text/tail.ts +63 -0
  958. package/src/substrate/lifo/commands/text/tr.ts +192 -0
  959. package/src/substrate/lifo/commands/text/uniq.ts +77 -0
  960. package/src/substrate/lifo/commands/text/wc.ts +104 -0
  961. package/src/substrate/lifo/commands/types.ts +58 -0
  962. package/src/substrate/lifo/index.ts +173 -0
  963. package/src/substrate/lifo/kernel/ServiceManager.ts +371 -0
  964. package/src/substrate/lifo/kernel/index.ts +209 -0
  965. package/src/substrate/lifo/kernel/network/Bridge.ts +336 -0
  966. package/src/substrate/lifo/kernel/network/DNSResolver.ts +158 -0
  967. package/src/substrate/lifo/kernel/network/NetworkInterface.ts +235 -0
  968. package/src/substrate/lifo/kernel/network/NetworkNamespace.ts +182 -0
  969. package/src/substrate/lifo/kernel/network/NetworkStack.ts +590 -0
  970. package/src/substrate/lifo/kernel/network/PortBridge.ts +182 -0
  971. package/src/substrate/lifo/kernel/network/RoutingTable.ts +209 -0
  972. package/src/substrate/lifo/kernel/network/Socket.ts +245 -0
  973. package/src/substrate/lifo/kernel/network/index.ts +29 -0
  974. package/src/substrate/lifo/kernel/network/tunnel/BaseTunnel.ts +124 -0
  975. package/src/substrate/lifo/kernel/network/tunnel/VETHPair.ts +227 -0
  976. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +387 -0
  977. package/src/substrate/lifo/kernel/network/types.ts +178 -0
  978. package/src/substrate/lifo/kernel/persistence/PersistenceManager.ts +50 -0
  979. package/src/substrate/lifo/kernel/persistence/backends.ts +36 -0
  980. package/src/substrate/lifo/kernel/persistence/index.ts +5 -0
  981. package/src/substrate/lifo/kernel/persistence/serializer.ts +127 -0
  982. package/src/substrate/lifo/kernel/samples.ts +966 -0
  983. package/src/substrate/lifo/kernel/storage/BlobStore.ts +114 -0
  984. package/src/substrate/lifo/kernel/storage/ContentStore.ts +150 -0
  985. package/src/substrate/lifo/kernel/storage/index.ts +5 -0
  986. package/src/substrate/lifo/kernel/unit-parser.ts +95 -0
  987. package/src/substrate/lifo/kernel/vfs/VFS.ts +768 -0
  988. package/src/substrate/lifo/kernel/vfs/index.ts +9 -0
  989. package/src/substrate/lifo/kernel/vfs/providers/DevProvider.ts +172 -0
  990. package/src/substrate/lifo/kernel/vfs/providers/NativeFsProvider.ts +198 -0
  991. package/src/substrate/lifo/kernel/vfs/providers/ProcProvider.ts +144 -0
  992. package/src/substrate/lifo/kernel/vfs/types.ts +145 -0
  993. package/src/substrate/lifo/node-compat/buffer.ts +357 -0
  994. package/src/substrate/lifo/node-compat/child_process.ts +52 -0
  995. package/src/substrate/lifo/node-compat/console.ts +70 -0
  996. package/src/substrate/lifo/node-compat/crypto.ts +186 -0
  997. package/src/substrate/lifo/node-compat/dns.ts +214 -0
  998. package/src/substrate/lifo/node-compat/esbuild.ts +85 -0
  999. package/src/substrate/lifo/node-compat/events.ts +94 -0
  1000. package/src/substrate/lifo/node-compat/fs.ts +762 -0
  1001. package/src/substrate/lifo/node-compat/http.ts +561 -0
  1002. package/src/substrate/lifo/node-compat/index.ts +300 -0
  1003. package/src/substrate/lifo/node-compat/module.ts +148 -0
  1004. package/src/substrate/lifo/node-compat/os.ts +44 -0
  1005. package/src/substrate/lifo/node-compat/path.ts +74 -0
  1006. package/src/substrate/lifo/node-compat/process.ts +178 -0
  1007. package/src/substrate/lifo/node-compat/readline.ts +244 -0
  1008. package/src/substrate/lifo/node-compat/rimraf.ts +80 -0
  1009. package/src/substrate/lifo/node-compat/stream.ts +98 -0
  1010. package/src/substrate/lifo/node-compat/string_decoder.ts +26 -0
  1011. package/src/substrate/lifo/node-compat/timers.ts +21 -0
  1012. package/src/substrate/lifo/node-compat/tty.ts +83 -0
  1013. package/src/substrate/lifo/node-compat/url.ts +76 -0
  1014. package/src/substrate/lifo/node-compat/util.ts +133 -0
  1015. package/src/substrate/lifo/node-compat/zlib.ts +94 -0
  1016. package/src/substrate/lifo/pkg/PackageManager.ts +105 -0
  1017. package/src/substrate/lifo/pkg/lifo-dev.ts +130 -0
  1018. package/src/substrate/lifo/pkg/lifo-runtime.ts +473 -0
  1019. package/src/substrate/lifo/pkg/loader.ts +33 -0
  1020. package/src/substrate/lifo/platform-globals.d.ts +41 -0
  1021. package/src/substrate/lifo/sandbox/HeadlessTerminal.ts +38 -0
  1022. package/src/substrate/lifo/sandbox/Sandbox.ts +279 -0
  1023. package/src/substrate/lifo/sandbox/SandboxCommands.ts +74 -0
  1024. package/src/substrate/lifo/sandbox/SandboxFs.ts +160 -0
  1025. package/src/substrate/lifo/sandbox/index.ts +8 -0
  1026. package/src/substrate/lifo/sandbox/types.ts +117 -0
  1027. package/src/substrate/lifo/shell/ProcessRegistry.ts +379 -0
  1028. package/src/substrate/lifo/shell/Shell.ts +2048 -0
  1029. package/src/substrate/lifo/shell/completer.ts +212 -0
  1030. package/src/substrate/lifo/shell/default-shell.ts +63 -0
  1031. package/src/substrate/lifo/shell/expander.ts +1349 -0
  1032. package/src/substrate/lifo/shell/history.ts +138 -0
  1033. package/src/substrate/lifo/shell/interpreter.ts +1754 -0
  1034. package/src/substrate/lifo/shell/jobs.ts +64 -0
  1035. package/src/substrate/lifo/shell/lexer.ts +698 -0
  1036. package/src/substrate/lifo/shell/parser.ts +711 -0
  1037. package/src/substrate/lifo/shell/pipe.ts +92 -0
  1038. package/src/substrate/lifo/shell/signals.ts +70 -0
  1039. package/src/substrate/lifo/shell/terminal-stdin.ts +109 -0
  1040. package/src/substrate/lifo/shell/test-builtin.ts +316 -0
  1041. package/src/substrate/lifo/shell/types.ts +166 -0
  1042. package/src/substrate/lifo/terminal/ITerminal.ts +9 -0
  1043. package/src/substrate/lifo/utils/archive.ts +389 -0
  1044. package/src/substrate/lifo/utils/args.ts +89 -0
  1045. package/src/substrate/lifo/utils/colors.ts +29 -0
  1046. package/src/substrate/lifo/utils/encoding.ts +21 -0
  1047. package/src/substrate/lifo/utils/glob.ts +172 -0
  1048. package/src/substrate/lifo/utils/mime.ts +114 -0
  1049. package/src/substrate/lifo/utils/path.ts +83 -0
  1050. package/src/types/assets.d.ts +4 -0
  1051. package/src/vfs/events.ts +134 -0
  1052. package/src/vfs/path.ts +71 -0
  1053. package/src/vfs/seed-project.ts +1094 -0
  1054. package/src/vfs/sqlite-vfs.ts +5035 -0
  1055. package/src/vfs/symlink-registry.ts +172 -0
  1056. package/src/workspace/nimbus-workspace.ts +213 -0
@@ -0,0 +1,4335 @@
1
+ /**
2
+ * unix-commands.ts — Nimbus v2.0 Unix command implementations.
3
+ *
4
+ * Every command is a real implementation operating on SqliteVFS.
5
+ * No stubs, no "not implemented" — each does actual work.
6
+ *
7
+ * Commands: which, env, export, unset, history, clear, alias, date,
8
+ * uptime, tree, find, grep -r, head, tail, wc, diff, sort, uniq,
9
+ * sed (s///), awk (field extract), xargs, tee, chown, ln -s,
10
+ * du, man/help, basename, dirname, printf, true, false, seq, sleep,
11
+ * touch, stat, file, xxd, base64, sha256sum, id, hostname, realpath
12
+ */
13
+
14
+ import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
15
+ import { getSymlinkRegistry } from '../vfs/symlink-registry.js';
16
+ import { requireVfsCred, type VfsCred } from '../runtime/os-contracts.js';
17
+ import { dec, enc } from '../_shared/bytes.js';
18
+ import { NIMBUS_VERSION } from '../constants.js';
19
+ import { SinkWriter, streamRange } from '../_shared/byte-stream.js';
20
+ import { fileTypeChar, isCharacterDevice } from '../substrate/lifo/kernel/vfs/index.js';
21
+ import { runSed } from '../substrate/lifo/commands/text/sed.js';
22
+ import { parseArgs } from '../substrate/lifo/utils/args.js';
23
+ import {
24
+ findUnixGroupName,
25
+ findUnixUserName,
26
+ parseChownOwnership,
27
+ } from './unix-accounts.js';
28
+ import { createSuCommand, createSudoCommand, createUmaskCommand } from './elevation-commands.js';
29
+
30
+ type Ctx = {
31
+ pid: number;
32
+ args: string[];
33
+ /**
34
+ * `writeBytes` is present on sinks that store bytes verbatim — files,
35
+ * `/dev/null` — and absent on textual ones, so a command with binary output
36
+ * uses it when it is there and falls back to decoded text when it is not.
37
+ */
38
+ stdout: { write(s: string): void; writeBytes?(bytes: Uint8Array): void };
39
+ stderr: { write(s: string): void };
40
+ cwd: string;
41
+ env: Record<string, string>;
42
+ stdin?: string;
43
+ cred: VfsCred;
44
+ vfs: CredentialedVfs;
45
+ signal: AbortSignal;
46
+ setUmask(mask: number): void;
47
+ runAs(cred: VfsCred, argv: string[]): Promise<number>;
48
+ execInterpreterDepth?: number;
49
+ };
50
+
51
+ type CmdFn = (ctx: Ctx) => number | Promise<number>;
52
+ type UnixVfs = CredentialedVfs & {
53
+ readonly symlinks: ReturnType<typeof getSymlinkRegistry>;
54
+ };
55
+
56
+ function unixVfsFor(sqliteVfs: SqliteVFS, cred: VfsCred): UnixVfs {
57
+ return {
58
+ ...sqliteVfs.as(cred),
59
+ symlinks: getSymlinkRegistry(sqliteVfs),
60
+ };
61
+ }
62
+
63
+ function withInvocationVfs(
64
+ sqliteVfs: SqliteVFS,
65
+ factory: (vfs: UnixVfs) => CmdFn,
66
+ ): CmdFn {
67
+ return (ctx) => factory(unixVfsFor(
68
+ sqliteVfs,
69
+ requireVfsCred(ctx.cred, 'unix command dispatch'),
70
+ ))(ctx);
71
+ }
72
+
73
+ function fsErrorMessage(error: unknown): string {
74
+ if (error instanceof Error) {
75
+ const code = 'code' in error && typeof error.code === 'string' ? error.code : null;
76
+ if (code === 'EACCES' || code === 'EPERM') return 'Permission denied';
77
+ if (code === 'ENOENT') return 'No such file or directory';
78
+ return error.message;
79
+ }
80
+ return String(error);
81
+ }
82
+
83
+ /** `drwxr-xr-x`-style permission string, shared by `ls -l` and `stat %A`. */
84
+ function unixModeString(mode: number, isDir: boolean, isLink: boolean): string {
85
+ if (isLink) return 'lrwxrwxrwx';
86
+ const prefix = fileTypeChar(mode, isDir ? 'directory' : 'file');
87
+ const bits = [
88
+ mode & 0o400 ? 'r' : '-',
89
+ mode & 0o200 ? 'w' : '-',
90
+ mode & 0o100 ? 'x' : '-',
91
+ mode & 0o040 ? 'r' : '-',
92
+ mode & 0o020 ? 'w' : '-',
93
+ mode & 0o010 ? 'x' : '-',
94
+ mode & 0o004 ? 'r' : '-',
95
+ mode & 0o002 ? 'w' : '-',
96
+ mode & 0o001 ? 'x' : '-',
97
+ ].join('');
98
+ return prefix + bits;
99
+ }
100
+
101
+ function unixUserLabel(vfs: UnixVfs, uid: number): string {
102
+ try {
103
+ return findUnixUserName(vfs, uid) ?? String(uid);
104
+ } catch {
105
+ return String(uid);
106
+ }
107
+ }
108
+
109
+ function unixGroupLabel(vfs: UnixVfs, gid: number): string {
110
+ try {
111
+ return findUnixGroupName(vfs, gid) ?? String(gid);
112
+ } catch {
113
+ return String(gid);
114
+ }
115
+ }
116
+
117
+ function isRuntimeInstallHintHandler(handler: unknown): boolean {
118
+ return !!handler && !!(handler as any).__nimbusRuntimeInstallHint;
119
+ }
120
+
121
+ // ── Helpers ─────────────────────────────────────────────────────────────
122
+
123
+ function resolvePath(cwd: string, p: string): string {
124
+ if (p.startsWith('/')) return p.replace(/^\/+/, '');
125
+ const c = (cwd || '/home/user').replace(/^\/+/, '');
126
+ const parts = (c + '/' + p).split('/');
127
+ const out: string[] = [];
128
+ for (const s of parts) {
129
+ if (s === '..') out.pop();
130
+ else if (s !== '.' && s !== '') out.push(s);
131
+ }
132
+ return out.join('/');
133
+ }
134
+
135
+ function readSymlinkTarget(vfs: UnixVfs, path: string): string | null {
136
+ if (vfs.isSymlink(path)) return vfs.readlink(path);
137
+ return vfs.symlinks.readlink(path);
138
+ }
139
+
140
+ function resolveSymlinkPath(vfs: UnixVfs, startPath: string): string | null {
141
+ let current = resolvePath('/', startPath);
142
+ for (let hops = 0; hops < 40; hops++) {
143
+ const target = readSymlinkTarget(vfs, current);
144
+ if (target === null) return current;
145
+ current = target.startsWith('/')
146
+ ? resolvePath('/', target)
147
+ : resolvePath('/' + (current.includes('/') ? current.slice(0, current.lastIndexOf('/')) : ''), target);
148
+ }
149
+ return null;
150
+ }
151
+
152
+ function globMatch(pattern: string, name: string): boolean {
153
+ const re = pattern
154
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
155
+ .replace(/\*/g, '.*')
156
+ .replace(/\?/g, '.');
157
+ return new RegExp('^' + re + '$').test(name);
158
+ }
159
+
160
+ // ── Command implementations ─────────────────────────────────────────────
161
+
162
+ /**
163
+ * SHELL-FOLLOWUPS-1 (2026-05-11): POSIX-conformant `which`.
164
+ *
165
+ * Pre-fix: `which clang` printed `clang: nimbus built-in`. Real POSIX
166
+ * which prints the resolved absolute path (`/usr/local/bin/clang`)
167
+ * or exits 1 silently. Scripts using `which` output to feed into
168
+ * `dirname`, `$()`, etc. all broke:
169
+ * PATH_PREFIX=$(dirname $(which clang)) # expected /usr/local/bin
170
+ *
171
+ * Behaviour matches GNU `which`:
172
+ * - PATH search: walk `$PATH`, return first hit's absolute path.
173
+ * - Found in PATH: print path to stdout, exit 0.
174
+ * - Resolved as shell builtin (no -a): exit 1, NO output.
175
+ * - Resolved as shell builtin (with -a): print
176
+ * "<cmd>: shell built-in command" + continue search PATH.
177
+ * - Not found anywhere: stderr "<cmd>: not in (PATH)", exit 1.
178
+ *
179
+ * For our facet-direct runtimes (clang, node, bun, python, ruby,
180
+ * git, npm, etc.), we don't have real on-disk binaries — they
181
+ * dispatch through the registry. To preserve script compatibility,
182
+ * we return canonical POSIX paths under /usr/local/bin and /usr/bin:
183
+ *
184
+ * clang, clang++, cc → /usr/local/bin/clang
185
+ * wasm-ld, lld → /usr/local/bin/wasm-ld
186
+ * node, nodejs → /usr/local/bin/node
187
+ * bun → /usr/local/bin/bun
188
+ * npm, npx → /usr/local/bin/npm
189
+ * git → /usr/bin/git
190
+ * python, python3 → /usr/bin/python3
191
+ * ruby, ruby3 → /usr/bin/ruby
192
+ * wrangler, nimbus-wrangler → /usr/local/bin/wrangler
193
+ * esbuild, tsc, vite, rollup, etc. → /usr/local/bin/<name>
194
+ *
195
+ * These paths are virtual but POSIX-shaped so scripts do the right
196
+ * thing with `dirname $(which X)` etc. The paths don't have to
197
+ * exist on the VFS (real GNU which just stat-walks PATH and
198
+ * doesn't require execute bit at lookup time for stdout — it does
199
+ * for exit code, but match what users expect first).
200
+ *
201
+ * If the user has installed a real binary in PATH (npm install + npx
202
+ * resolution into /home/user/node_modules/.bin/X), prefer the actual
203
+ * VFS-resolved path over the canonical fallback.
204
+ */
205
+ const _CANONICAL_BIN_PATHS: Record<string, string> = {
206
+ // /usr/local/bin (locally-installed runtimes)
207
+ clang: '/usr/local/bin/clang',
208
+ 'clang++': '/usr/local/bin/clang++',
209
+ cc: '/usr/local/bin/clang',
210
+ 'wasm-ld': '/usr/local/bin/wasm-ld',
211
+ lld: '/usr/local/bin/wasm-ld',
212
+ node: '/usr/local/bin/node',
213
+ nodejs: '/usr/local/bin/node',
214
+ bun: '/usr/local/bin/bun',
215
+ npm: '/usr/local/bin/npm',
216
+ npx: '/usr/local/bin/npx',
217
+ pnpm: '/usr/local/bin/pnpm',
218
+ yarn: '/usr/local/bin/yarn',
219
+ esbuild: '/usr/local/bin/esbuild',
220
+ tsc: '/usr/local/bin/tsc',
221
+ vite: '/usr/local/bin/vite',
222
+ rollup: '/usr/local/bin/rollup',
223
+ webpack: '/usr/local/bin/webpack',
224
+ wrangler: '/usr/local/bin/wrangler',
225
+ 'nimbus-wrangler': '/usr/local/bin/nimbus-wrangler',
226
+ // /usr/bin (system runtimes)
227
+ git: '/usr/bin/git',
228
+ python: '/usr/bin/python3',
229
+ python3: '/usr/bin/python3',
230
+ pip: '/usr/bin/pip',
231
+ pip3: '/usr/bin/pip3',
232
+ ruby: '/usr/bin/ruby',
233
+ ruby3: '/usr/bin/ruby',
234
+ gem: '/usr/bin/gem',
235
+ bundle: '/usr/bin/bundle',
236
+ bundler: '/usr/bin/bundler',
237
+ sh: '/usr/bin/sh',
238
+ bash: '/usr/bin/bash',
239
+ // Framework CLIs
240
+ astro: '/usr/local/bin/astro',
241
+ nuxt: '/usr/local/bin/nuxt',
242
+ nuxi: '/usr/local/bin/nuxi',
243
+ next: '/usr/local/bin/next',
244
+ remix: '/usr/local/bin/remix',
245
+ 'svelte-kit': '/usr/local/bin/svelte-kit',
246
+ husky: '/usr/local/bin/husky',
247
+ lefthook: '/usr/local/bin/lefthook',
248
+ 'simple-git-hooks': '/usr/local/bin/simple-git-hooks',
249
+ 'lint-staged': '/usr/local/bin/lint-staged',
250
+ yorkie: '/usr/local/bin/yorkie',
251
+ // Self
252
+ nimbus: '/usr/local/bin/nimbus',
253
+ };
254
+
255
+ function _pathLookup(
256
+ vfs: UnixVfs,
257
+ name: string,
258
+ envPath: string,
259
+ ): string | null {
260
+ const paths = (envPath || '/usr/local/bin:/usr/bin:/bin').split(':');
261
+ for (const dir of paths) {
262
+ if (!dir) continue;
263
+ const stripped = dir.replace(/^\/+/, '').replace(/\/+$/, '');
264
+ const fp = stripped + '/' + name;
265
+ if (vfs.exists(fp) && !vfs.isDirectory(fp)) {
266
+ return '/' + fp;
267
+ }
268
+ }
269
+ return null;
270
+ }
271
+
272
+ async function _registryResolved(
273
+ registry: any,
274
+ name: string,
275
+ options: { includeInstallHints?: boolean } = {},
276
+ ): Promise<CmdFn | null> {
277
+ try {
278
+ const resolved = typeof registry.resolve === 'function'
279
+ ? await registry.resolve(name)
280
+ : null;
281
+ if (resolved && (options.includeInstallHints || !isRuntimeInstallHintHandler(resolved))) {
282
+ return resolved;
283
+ }
284
+ } catch {
285
+ // Registry misses are normal for unknown commands.
286
+ }
287
+ return null;
288
+ }
289
+
290
+ /** Resolve a command name to a path via PATH-walk + canonical-bin
291
+ * fallback. Returns null if not findable. Skip-canonical when the
292
+ * caller knows the command is a shell builtin (no fallback). */
293
+ async function _whichLookup(
294
+ vfs: UnixVfs,
295
+ registry: any,
296
+ name: string,
297
+ envPath: string,
298
+ ): Promise<string | null> {
299
+ const diskPath = _pathLookup(vfs, name, envPath);
300
+ if (diskPath) return diskPath;
301
+ const canonicalPath = _CANONICAL_BIN_PATHS[name];
302
+ if (!canonicalPath) return null;
303
+ return await _registryResolved(registry, name, { includeInstallHints: true })
304
+ ? canonicalPath
305
+ : null;
306
+ }
307
+
308
+ function mkWhich(vfs: UnixVfs, registry: any): CmdFn {
309
+ return async (ctx) => {
310
+ // Parse flags. Supports -a (show all matches), -s (silent — no
311
+ // stdout, only exit code). Default behaviour matches GNU which.
312
+ let showAll = false;
313
+ let silent = false;
314
+ const names: string[] = [];
315
+ for (const a of ctx.args) {
316
+ if (a === '-a' || a === '--all') { showAll = true; continue; }
317
+ if (a === '-s' || a === '--silent') { silent = true; continue; }
318
+ if (a.startsWith('-') && a !== '-') {
319
+ // Combined short flags
320
+ for (const ch of a.slice(1)) {
321
+ if (ch === 'a') showAll = true;
322
+ else if (ch === 's') silent = true;
323
+ }
324
+ continue;
325
+ }
326
+ names.push(a);
327
+ }
328
+ if (names.length === 0) {
329
+ ctx.stderr.write('Usage: which [-as] command [command ...]\n');
330
+ return 1;
331
+ }
332
+ let anyMissing = false;
333
+ for (const name of names) {
334
+ // Classify: is it a registry-resolvable command (shell builtin)?
335
+ const resolved = await _registryResolved(registry, name);
336
+ const isBuiltin = !!resolved;
337
+ // 1. PATH-walk + canonical-bin lookup.
338
+ const path = await _whichLookup(vfs, registry, name, ctx.env.PATH || '');
339
+ let found = false;
340
+ if (path) {
341
+ if (!silent) ctx.stdout.write(path + '\n');
342
+ found = true;
343
+ }
344
+ // 2. With -a, also report builtins (real GNU which behaviour).
345
+ if (showAll && isBuiltin) {
346
+ if (!silent) ctx.stdout.write(`${name}: shell built-in command\n`);
347
+ found = true;
348
+ }
349
+ // 3. Without -a, if no PATH match but is builtin: GNU which
350
+ // exits 1 silently (with -s suppresses stderr too).
351
+ if (!path && isBuiltin && !showAll) {
352
+ // Exit 1; no output. Matches GNU `which` default.
353
+ anyMissing = true;
354
+ continue;
355
+ }
356
+ if (!found) {
357
+ if (!silent) ctx.stderr.write(`which: no ${name} in (${ctx.env.PATH || '/usr/local/bin:/usr/bin:/bin'})\n`);
358
+ anyMissing = true;
359
+ }
360
+ }
361
+ return anyMissing ? 1 : 0;
362
+ };
363
+ }
364
+
365
+ /**
366
+ * SHELL-FOLLOWUPS-2 (2026-05-11): `whereis` companion to `which`.
367
+ * GNU whereis prints binary, source, and manpage paths. We only have
368
+ * binaries on the virtual VFS; print just the binary path. With no
369
+ * match, print just the name (matches `whereis` behavior on missing).
370
+ */
371
+ function mkWhereis(vfs: UnixVfs, registry: any): CmdFn {
372
+ return async (ctx) => {
373
+ const names = ctx.args.filter(a => !a.startsWith('-'));
374
+ if (names.length === 0) {
375
+ ctx.stderr.write('Usage: whereis name [name ...]\n');
376
+ return 1;
377
+ }
378
+ for (const name of names) {
379
+ const path = await _whichLookup(vfs, registry, name, ctx.env.PATH || '');
380
+ if (path) {
381
+ ctx.stdout.write(`${name}: ${path}\n`);
382
+ } else {
383
+ // GNU whereis prints just "name:" when nothing found.
384
+ ctx.stdout.write(`${name}:\n`);
385
+ }
386
+ }
387
+ return 0;
388
+ };
389
+ }
390
+
391
+ /**
392
+ * SHELL-FOLLOWUPS-3 (2026-05-11): POSIX `command -v` / `command -V`.
393
+ * Used by shell scripts as the portable alternative to `which`:
394
+ * command -v clang → prints path, exit 0 if found, exit 1 if not
395
+ * command -V clang → verbose form (similar to `type`)
396
+ * command clang ARG → invoke clang bypassing any function/alias
397
+ *
398
+ * For the invoke case (no -v/-V), we don't have a way to bypass
399
+ * alias/function from here, so we just dispatch to
400
+ * the registry. Aliases are checked at executeLine time so `command
401
+ * X` going through our normal dispatch IS bypassing the alias
402
+ * (because the interpreter only consults aliases
403
+ * for the head word).
404
+ */
405
+ function mkCommand(vfs: UnixVfs, registry: any): CmdFn {
406
+ return async (ctx) => {
407
+ const args = [...ctx.args];
408
+ let mode: '-v' | '-V' | 'invoke' = 'invoke';
409
+ if (args[0] === '-v') { mode = '-v'; args.shift(); }
410
+ else if (args[0] === '-V') { mode = '-V'; args.shift(); }
411
+ if (args.length === 0) {
412
+ if (mode === 'invoke') return 0;
413
+ ctx.stderr.write('command: missing operand\n');
414
+ return 1;
415
+ }
416
+ if (mode === '-v') {
417
+ // Print path or builtin marker; exit 0 if found.
418
+ const name = args[0];
419
+ const path = await _whichLookup(vfs, registry, name, ctx.env.PATH || '');
420
+ if (path) { ctx.stdout.write(path + '\n'); return 0; }
421
+ if (await _registryResolved(registry, name)) { ctx.stdout.write(name + '\n'); return 0; }
422
+ return 1;
423
+ }
424
+ if (mode === '-V') {
425
+ const name = args[0];
426
+ const path = await _whichLookup(vfs, registry, name, ctx.env.PATH || '');
427
+ if (path) { ctx.stdout.write(`${name} is ${path}\n`); return 0; }
428
+ if (await _registryResolved(registry, name)) { ctx.stdout.write(`${name} is a shell builtin\n`); return 0; }
429
+ ctx.stderr.write(`command: ${name}: not found\n`);
430
+ return 1;
431
+ }
432
+ // invoke mode: dispatch directly via registry. Bypasses aliases
433
+ // because we're calling the resolved cmd not the alias name.
434
+ const name = args[0];
435
+ try {
436
+ const resolved = await registry.resolve(name);
437
+ if (!resolved) {
438
+ ctx.stderr.write(`command: ${name}: not found\n`);
439
+ return 127;
440
+ }
441
+ const subCtx = { ...ctx, args: args.slice(1) };
442
+ const code = await resolved(subCtx);
443
+ return typeof code === 'number' ? code : 0;
444
+ } catch (e: any) {
445
+ ctx.stderr.write(`command: ${name}: ${e?.message || e}\n`);
446
+ return 1;
447
+ }
448
+ };
449
+ }
450
+
451
+ /**
452
+ * shell compatibility (2026-05-11): `type` builtin. The shell did not ship
453
+ * one; pre-fix `type echo` → 'type: command not found'. bash's
454
+ * `type X` reports how X would be interpreted (builtin, alias,
455
+ * function, file, or unknown).
456
+ *
457
+ * Our subset (matches bash `type` output for common shapes):
458
+ * type echo → 'echo is a shell builtin' (Shell.builtins entry)
459
+ * type ls → 'ls is a shell builtin' (lazy registry)
460
+ * type rm → 'rm is a shell builtin' (our wrap'd registry)
461
+ * type node → 'node is /usr/bin/node' (registry but facet-direct)
462
+ * type X → 'type: X: not found' + exit 1
463
+ *
464
+ * We can't introspect Shell.builtins from here directly (the ctx
465
+ * doesn't carry shell). Workaround: pass registry which the unix-
466
+ * commands module already has access to; treat any registry resolve
467
+ * as "shell builtin" classification.
468
+ */
469
+ function mkType(_vfs: UnixVfs, registry: any): CmdFn {
470
+ return async (ctx) => {
471
+ if (ctx.args.length === 0) return 0;
472
+ let exit = 0;
473
+ for (const name of ctx.args) {
474
+ try {
475
+ const resolved = typeof registry.resolve === 'function' ? await registry.resolve(name) : null;
476
+ if (resolved && !isRuntimeInstallHintHandler(resolved)) {
477
+ ctx.stdout.write(`${name} is a shell builtin\n`);
478
+ } else {
479
+ ctx.stderr.write(`type: ${name}: not found\n`);
480
+ exit = 1;
481
+ }
482
+ } catch (_e) {
483
+ ctx.stderr.write(`type: ${name}: not found\n`);
484
+ exit = 1;
485
+ }
486
+ }
487
+ return exit;
488
+ };
489
+ }
490
+
491
+ function mkEnv(): CmdFn {
492
+ return (ctx) => {
493
+ for (const [k, v] of Object.entries(ctx.env)) {
494
+ ctx.stdout.write(`${k}=${v}\n`);
495
+ }
496
+ return 0;
497
+ };
498
+ }
499
+
500
+ function mkExport(): CmdFn {
501
+ return (ctx) => {
502
+ for (const arg of ctx.args) {
503
+ const eqIdx = arg.indexOf('=');
504
+ if (eqIdx > 0) {
505
+ ctx.env[arg.substring(0, eqIdx)] = arg.substring(eqIdx + 1);
506
+ } else if (ctx.env[arg] !== undefined) {
507
+ ctx.stdout.write(`export ${arg}="${ctx.env[arg]}"\n`);
508
+ }
509
+ }
510
+ return 0;
511
+ };
512
+ }
513
+
514
+ function mkUnset(): CmdFn {
515
+ return (ctx) => {
516
+ for (const name of ctx.args) { delete ctx.env[name]; }
517
+ return 0;
518
+ };
519
+ }
520
+
521
+ function mkClear(): CmdFn {
522
+ return (ctx) => { ctx.stdout.write('\x1b[2J\x1b[H'); return 0; };
523
+ }
524
+
525
+ /**
526
+ * shell compatibility (2026-05-11): date strftime format support.
527
+ *
528
+ * Pre-fix mkDate only honoured `-u`, `-I`, and `+%s` literal. Any
529
+ * other `+FMT` was a no-op falling to `now.toString()`. Real shell
530
+ * scripts use `date +%Y-%m-%d`, `date +%H:%M:%S`, `date +%F`, etc.
531
+ *
532
+ * Post-fix: full strftime subset:
533
+ * %Y / %C / %y year (4-digit / century / 2-digit)
534
+ * %m / %B / %b / %h month (numeric / full name / abbrev / abbrev)
535
+ * %d / %e day of month (zero-padded / space-padded)
536
+ * %j day of year
537
+ * %H / %I / %M / %S hour-24 / hour-12 / minute / second
538
+ * %p AM/PM
539
+ * %A / %a weekday (full / abbrev)
540
+ * %u / %w ISO weekday (1=Mon..7=Sun) / weekday (0=Sun..6=Sat)
541
+ * %s unix timestamp (seconds)
542
+ * %N nanoseconds (zero-pad to 9 digits)
543
+ * %F %Y-%m-%d
544
+ * %T / %R %H:%M:%S / %H:%M
545
+ * %D %m/%d/%y
546
+ * %z / %Z timezone offset / name
547
+ * %% literal %
548
+ * %n / %t newline / tab
549
+ */
550
+ function mkDate(): CmdFn {
551
+ return (ctx) => {
552
+ const now = new Date();
553
+ const useUtc = ctx.args.includes('-u') || ctx.args.includes('--utc');
554
+ // Find the `+FMT` arg (if any). Real `date +FMT [args]` accepts
555
+ // only one format; we honour the first.
556
+ const fmtArg = ctx.args.find(a => a.startsWith('+'));
557
+ if (fmtArg) {
558
+ ctx.stdout.write(strftime(now, fmtArg.slice(1), useUtc) + '\n');
559
+ return 0;
560
+ }
561
+ if (ctx.args.includes('-I') || ctx.args.includes('--iso-8601')) {
562
+ ctx.stdout.write(now.toISOString() + '\n');
563
+ return 0;
564
+ }
565
+ if (useUtc) {
566
+ ctx.stdout.write(now.toUTCString() + '\n');
567
+ return 0;
568
+ }
569
+ ctx.stdout.write(now.toString() + '\n');
570
+ return 0;
571
+ };
572
+ }
573
+
574
+ const _MONTHS_FULL = ['January','February','March','April','May','June','July','August','September','October','November','December'];
575
+ const _MONTHS_ABBR = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
576
+ const _DAYS_FULL = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
577
+ const _DAYS_ABBR = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
578
+
579
+ function strftime(d: Date, fmt: string, utc: boolean): string {
580
+ const get = (m: string): any => {
581
+ switch (m) {
582
+ case 'FullYear': return utc ? d.getUTCFullYear() : d.getFullYear();
583
+ case 'Month': return utc ? d.getUTCMonth() : d.getMonth();
584
+ case 'Date': return utc ? d.getUTCDate() : d.getDate();
585
+ case 'Hours': return utc ? d.getUTCHours() : d.getHours();
586
+ case 'Minutes': return utc ? d.getUTCMinutes() : d.getMinutes();
587
+ case 'Seconds': return utc ? d.getUTCSeconds() : d.getSeconds();
588
+ case 'Day': return utc ? d.getUTCDay() : d.getDay();
589
+ case 'Milliseconds': return utc ? d.getUTCMilliseconds() : d.getMilliseconds();
590
+ default: return 0;
591
+ }
592
+ };
593
+ const pad = (n: number, w: number, ch = '0') => String(n).padStart(w, ch);
594
+ const yyyy = get('FullYear');
595
+ const mm0 = get('Month'); // 0..11
596
+ const dd = get('Date');
597
+ const hh = get('Hours');
598
+ const mn = get('Minutes');
599
+ const ss = get('Seconds');
600
+ const dow = get('Day'); // 0..6 (Sun..Sat)
601
+ const ms = get('Milliseconds');
602
+ // Day of year: difference from Jan 1.
603
+ const jan1 = utc
604
+ ? Date.UTC(yyyy, 0, 1)
605
+ : new Date(yyyy, 0, 1).getTime();
606
+ const doy = Math.floor((d.getTime() - jan1) / 86400000) + 1;
607
+ // ISO weekday: 1=Mon..7=Sun.
608
+ const isoDow = dow === 0 ? 7 : dow;
609
+ const ampm = hh < 12 ? 'AM' : 'PM';
610
+ const h12 = hh % 12 === 0 ? 12 : hh % 12;
611
+ // TZ offset in ±HHMM form.
612
+ const tzOff = utc ? '+0000' : (() => {
613
+ const off = -d.getTimezoneOffset();
614
+ const sign = off >= 0 ? '+' : '-';
615
+ const abs = Math.abs(off);
616
+ return sign + pad(Math.floor(abs / 60), 2) + pad(abs % 60, 2);
617
+ })();
618
+ const tzName = utc ? 'UTC' : (() => {
619
+ try {
620
+ const parts = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).formatToParts(d);
621
+ const tz = parts.find(p => p.type === 'timeZoneName');
622
+ return tz ? tz.value : 'UTC';
623
+ } catch { return 'UTC'; }
624
+ })();
625
+ let out = '';
626
+ let i = 0;
627
+ while (i < fmt.length) {
628
+ const ch = fmt[i];
629
+ if (ch !== '%') { out += ch; i++; continue; }
630
+ i++;
631
+ const spec = fmt[i] || '';
632
+ i++;
633
+ switch (spec) {
634
+ case 'Y': out += String(yyyy); break;
635
+ case 'C': out += pad(Math.floor(yyyy / 100), 2); break;
636
+ case 'y': out += pad(yyyy % 100, 2); break;
637
+ case 'm': out += pad(mm0 + 1, 2); break;
638
+ case 'B': out += _MONTHS_FULL[mm0]; break;
639
+ case 'b': case 'h': out += _MONTHS_ABBR[mm0]; break;
640
+ case 'd': out += pad(dd, 2); break;
641
+ case 'e': out += String(dd).padStart(2, ' '); break;
642
+ case 'j': out += pad(doy, 3); break;
643
+ case 'H': out += pad(hh, 2); break;
644
+ case 'I': out += pad(h12, 2); break;
645
+ case 'M': out += pad(mn, 2); break;
646
+ case 'S': out += pad(ss, 2); break;
647
+ case 'p': out += ampm; break;
648
+ case 'P': out += ampm.toLowerCase(); break;
649
+ case 'A': out += _DAYS_FULL[dow]; break;
650
+ case 'a': out += _DAYS_ABBR[dow]; break;
651
+ case 'u': out += String(isoDow); break;
652
+ case 'w': out += String(dow); break;
653
+ case 's': out += String(Math.floor(d.getTime() / 1000)); break;
654
+ case 'N': out += pad(ms * 1_000_000, 9); break;
655
+ case 'F': out += `${yyyy}-${pad(mm0 + 1, 2)}-${pad(dd, 2)}`; break;
656
+ case 'T': out += `${pad(hh, 2)}:${pad(mn, 2)}:${pad(ss, 2)}`; break;
657
+ case 'R': out += `${pad(hh, 2)}:${pad(mn, 2)}`; break;
658
+ case 'D': out += `${pad(mm0 + 1, 2)}/${pad(dd, 2)}/${pad(yyyy % 100, 2)}`; break;
659
+ case 'z': out += tzOff; break;
660
+ case 'Z': out += tzName; break;
661
+ case '%': out += '%'; break;
662
+ case 'n': out += '\n'; break;
663
+ case 't': out += '\t'; break;
664
+ default: out += '%' + spec; break; // unknown — preserve literal
665
+ }
666
+ }
667
+ return out;
668
+ }
669
+
670
+ function mkUptime(): CmdFn {
671
+ const start = Date.now();
672
+ return (ctx) => {
673
+ const secs = Math.floor((Date.now() - start) / 1000);
674
+ const h = Math.floor(secs / 3600);
675
+ const m = Math.floor((secs % 3600) / 60);
676
+ ctx.stdout.write(` ${new Date().toTimeString().split(' ')[0]} up ${h}:${String(m).padStart(2, '0')}, 1 user\n`);
677
+ return 0;
678
+ };
679
+ }
680
+
681
+ function mkTree(vfs: UnixVfs): CmdFn {
682
+ return (ctx) => {
683
+ const args = ctx.args.filter(a => !a.startsWith('-') && (ctx.args.indexOf(a) !== ctx.args.indexOf('-L') + 1));
684
+ const root = args[0] ? resolvePath(ctx.cwd, args[0]) : (ctx.cwd || '/home/user').replace(/^\/+/, '');
685
+ const maxDepth = ctx.args.includes('-L') ? parseInt(ctx.args[ctx.args.indexOf('-L') + 1]) || 3 : 3;
686
+ const MAX_ENTRIES = 2000; // Safety limit to prevent hanging on huge repos
687
+ let dirs = 0, files = 0, total = 0;
688
+ let truncated = false;
689
+ function walk(path: string, prefix: string, depth: number) {
690
+ if (depth > maxDepth || truncated) return;
691
+ try {
692
+ const entries = vfs.readdir(path).sort((a, b) => a.name.localeCompare(b.name));
693
+ for (let i = 0; i < entries.length; i++) {
694
+ if (total >= MAX_ENTRIES) { truncated = true; return; }
695
+ total++;
696
+ const e = entries[i];
697
+ const isLast = i === entries.length - 1;
698
+ const connector = isLast ? '└── ' : '├── ';
699
+ const childPrefix = isLast ? ' ' : '│ ';
700
+ ctx.stdout.write(prefix + connector + e.name + '\n');
701
+ if (e.type === 'directory') {
702
+ dirs++;
703
+ walk(path + '/' + e.name, prefix + childPrefix, depth + 1);
704
+ } else { files++; }
705
+ }
706
+ } catch {}
707
+ }
708
+ const name = root.split('/').pop() || root;
709
+ ctx.stdout.write(name + '\n');
710
+ walk(root, '', 1);
711
+ if (truncated) ctx.stdout.write(`\n... truncated at ${MAX_ENTRIES} entries\n`);
712
+ ctx.stdout.write(`\n${dirs} directories, ${files} files\n`);
713
+ return 0;
714
+ };
715
+ }
716
+
717
+ /**
718
+ * `find`'s arguments are an EXPRESSION, not a flag list.
719
+ *
720
+ * The flat AND-list this replaced dropped every token it did not recognise,
721
+ * which does not fail — it answers a different question. `find . ! -name x`
722
+ * ran as `find . -name x`, the exact complement of the requested set, and
723
+ * said nothing. An installer's
724
+ *
725
+ * find "$dir" -mindepth 1 -maxdepth 1 -type d | head -n 1
726
+ *
727
+ * silently became `-maxdepth 1 -type d`, which emits the start directory at
728
+ * depth 0, so the pipeline selected the container instead of the tree in it.
729
+ *
730
+ * So the fix is not `-mindepth`. It is to parse the real grammar —
731
+ *
732
+ * expr := or
733
+ * or := and (('-o' | '-or') and)*
734
+ * and := unary (('-a' | '-and')? unary)*
735
+ * unary := ('!' | '-not') unary | '(' expr ')' | primary
736
+ *
737
+ * — with `-a` binding tighter than `-o`, to evaluate it with short-circuit
738
+ * semantics (which is what makes the `-prune -o -print` idiom work), and to
739
+ * REFUSE any token outside the table below rather than ignore it.
740
+ *
741
+ * Every behaviour here was derived by running the same expression under GNU
742
+ * findutils 4.10.0; tests/unit/find-expression-evaluator.mjs carries the
743
+ * differential.
744
+ *
745
+ * Global options -maxdepth N, -mindepth N, -depth
746
+ * Operators !, -not, -a, -and, -o, -or, ( )
747
+ * Tests -name, -iname, -path, -type, -size, -mtime, -newer, -empty
748
+ * Actions -print, -print0, -delete, -exec … {} \\; | +, -prune, -quit
749
+ *
750
+ * Anything else is `find: unknown predicate '-x'`, exit 1 — the same
751
+ * honesty `uname -m` keeps by answering `wasm`.
752
+ */
753
+
754
+ /** A usage error carrying the message GNU find prints for it. */
755
+ class FindUsageError extends Error {}
756
+
757
+ interface FindEntry {
758
+ /** Canonical VFS path (no leading slash) — what the VFS is asked about. */
759
+ vfsPath: string;
760
+ /** Path as find prints it: the start argument plus the relative remainder. */
761
+ display: string;
762
+ name: string;
763
+ type: string;
764
+ depth: number;
765
+ }
766
+
767
+ interface FindState {
768
+ minDepth: number;
769
+ maxDepth: number;
770
+ /** -depth, or implied by -delete: visit children before their parent. */
771
+ depthFirst: boolean;
772
+ /** Set by -prune while the current entry is being evaluated. */
773
+ prune: boolean;
774
+ /** Set by -quit: stop the walk entirely. */
775
+ quit: boolean;
776
+ }
777
+
778
+ /** One `-exec … +` site: its argv template and the matches batched for it. */
779
+ interface FindExecBatch {
780
+ argv: string[];
781
+ pending: string[];
782
+ }
783
+
784
+ type FindNode = (entry: FindEntry, st: FindState) => Promise<boolean>;
785
+ function mkFind(vfs: UnixVfs, registry: any): CmdFn {
786
+ return async (ctx) => {
787
+ const state: FindState = {
788
+ minDepth: 0,
789
+ maxDepth: Infinity,
790
+ depthFirst: false,
791
+ prune: false,
792
+ quit: false,
793
+ };
794
+ const execBatches: FindExecBatch[] = [];
795
+ let hasAction = false;
796
+
797
+ // ── Emission ──────────────────────────────────────────────────────────
798
+ const emit = (entry: FindEntry, terminator: string): void => {
799
+ ctx.stdout.write(entry.display + terminator);
800
+ };
801
+
802
+ /** Run one command through the registry the session resolves through. */
803
+ const runExec = async (argv: string[]): Promise<boolean> => {
804
+ const [name, ...rest] = argv;
805
+ if (!name) return false;
806
+ let target;
807
+ try { target = await registry.resolve(name); } catch { target = null; }
808
+ if (!target) {
809
+ ctx.stderr.write(`find: ${name}: No such file or directory\n`);
810
+ return false;
811
+ }
812
+ try {
813
+ const code = await target({
814
+ pid: ctx.pid,
815
+ cred: ctx.cred,
816
+ args: rest,
817
+ env: ctx.env,
818
+ cwd: ctx.cwd,
819
+ vfs: ctx.vfs,
820
+ stdout: ctx.stdout,
821
+ stderr: ctx.stderr,
822
+ stdin: '',
823
+ signal: ctx.signal,
824
+ setUmask: ctx.setUmask,
825
+ runAs: ctx.runAs,
826
+ execInterpreterDepth: ctx.execInterpreterDepth,
827
+ });
828
+ return code === 0;
829
+ } catch (e: any) {
830
+ ctx.stderr.write(`find: ${name}: ${e?.message || e}\n`);
831
+ return false;
832
+ }
833
+ };
834
+
835
+ // ── Parser ────────────────────────────────────────────────────────────
836
+ //
837
+ // Start paths come first: GNU reads operands until the first token that
838
+ // begins an expression, so `find a b -type d` walks both a and b.
839
+ const args = [...ctx.args];
840
+ let pos = 0;
841
+ const startsExpression = (tok: string): boolean =>
842
+ tok.startsWith('-') || tok === '(' || tok === ')' || tok === '!' || tok === ',';
843
+
844
+ const startArgs: string[] = [];
845
+ while (pos < args.length && !startsExpression(args[pos])) startArgs.push(args[pos++]);
846
+ if (startArgs.length === 0) startArgs.push('.');
847
+
848
+ const peek = (): string | undefined => args[pos];
849
+ const next = (): string | undefined => args[pos++];
850
+ /** The argument a predicate requires, or GNU's missing-argument error. */
851
+ const value = (pred: string): string => {
852
+ const v = args[pos++];
853
+ if (v === undefined) throw new FindUsageError(`missing argument to \`${pred}'`);
854
+ return v;
855
+ };
856
+ const positiveInt = (pred: string): number => {
857
+ const raw = value(pred);
858
+ if (!/^\d+$/.test(raw)) {
859
+ throw new FindUsageError(
860
+ `Expected a positive decimal integer argument to ${pred}, but got \`${raw}'`,
861
+ );
862
+ }
863
+ return parseInt(raw, 10);
864
+ };
865
+
866
+ const TRUE: FindNode = async () => true;
867
+
868
+ /** Size in the unit's own terms: GNU rounds a partial unit UP. */
869
+ const sizeInUnits = (bytes: number, unit: number): number =>
870
+ unit === 1 ? bytes : Math.ceil(bytes / unit);
871
+
872
+ const statOf = (entry: FindEntry) => {
873
+ try { return vfs.stat(entry.vfsPath); } catch { return null; }
874
+ };
875
+
876
+ function parsePrimary(): FindNode {
877
+ const tok = next();
878
+ if (tok === undefined) throw new FindUsageError('missing expression');
879
+
880
+ switch (tok) {
881
+ // ── Global options: they configure the walk and evaluate true ──
882
+ case '-maxdepth': state.maxDepth = positiveInt('-maxdepth'); return TRUE;
883
+ case '-mindepth': state.minDepth = positiveInt('-mindepth'); return TRUE;
884
+ case '-depth': state.depthFirst = true; return TRUE;
885
+
886
+ // ── Tests ──
887
+ case '-name': {
888
+ const pattern = value('-name');
889
+ return async (e) => globMatch(pattern, e.name);
890
+ }
891
+ case '-iname': {
892
+ const pattern = value('-iname').toLowerCase();
893
+ return async (e) => globMatch(pattern, e.name.toLowerCase());
894
+ }
895
+ case '-path': {
896
+ const pattern = value('-path');
897
+ return async (e) => globMatch(pattern, e.display);
898
+ }
899
+ case '-type': {
900
+ const letter = value('-type');
901
+ if (letter !== 'f' && letter !== 'd' && letter !== 'l') {
902
+ throw new FindUsageError(`Unknown argument to -type: ${letter}`);
903
+ }
904
+ const wanted = letter === 'f' ? 'file' : letter === 'd' ? 'directory' : 'symlink';
905
+ return async (e) => e.type === wanted;
906
+ }
907
+ case '-size': {
908
+ const raw = value('-size');
909
+ const m = raw.match(/^([+-]?)(\d+)([ckMG]?)$/);
910
+ if (!m) throw new FindUsageError(`invalid -size type \`${raw.slice(-1)}'`);
911
+ const cmp = m[1];
912
+ const count = parseInt(m[2], 10);
913
+ const unit = m[3] === 'c' ? 1
914
+ : m[3] === 'k' ? 1024
915
+ : m[3] === 'M' ? 1024 * 1024
916
+ : m[3] === 'G' ? 1024 * 1024 * 1024
917
+ : 512;
918
+ return async (e) => {
919
+ const st = statOf(e);
920
+ if (!st) return false;
921
+ const units = sizeInUnits(st.size || 0, unit);
922
+ return cmp === '+' ? units > count : cmp === '-' ? units < count : units === count;
923
+ };
924
+ }
925
+ case '-mtime': {
926
+ const raw = value('-mtime');
927
+ const m = raw.match(/^([+-]?)(\d+)$/);
928
+ if (!m) throw new FindUsageError(`invalid argument \`${raw}' to \`-mtime'`);
929
+ const cmp = m[1];
930
+ const dayMs = 86400 * 1000;
931
+ const threshold = parseInt(m[2], 10) * dayMs;
932
+ const now = Date.now();
933
+ return async (e) => {
934
+ const st = statOf(e);
935
+ if (!st) return false;
936
+ const age = now - (st.mtime || 0);
937
+ return cmp === '+' ? age > threshold + dayMs
938
+ : cmp === '-' ? age < threshold
939
+ : age >= threshold && age < threshold + dayMs;
940
+ };
941
+ }
942
+ case '-newer': {
943
+ const ref = value('-newer');
944
+ let refMtime: number;
945
+ try {
946
+ refMtime = vfs.stat(resolvePath(ctx.cwd, ref)).mtime;
947
+ } catch {
948
+ throw new FindUsageError(`'${ref}': No such file or directory`);
949
+ }
950
+ return async (e) => {
951
+ const st = statOf(e);
952
+ return !!st && (st.mtime || 0) > refMtime;
953
+ };
954
+ }
955
+ case '-empty':
956
+ return async (e) => {
957
+ if (e.type === 'directory') {
958
+ try { return vfs.readdir(e.vfsPath).length === 0; } catch { return false; }
959
+ }
960
+ const st = statOf(e);
961
+ return !!st && (st.size || 0) === 0;
962
+ };
963
+
964
+ // ── Actions ──
965
+ case '-print':
966
+ hasAction = true;
967
+ return async (e) => { emit(e, '\n'); return true; };
968
+ case '-print0':
969
+ hasAction = true;
970
+ return async (e) => { emit(e, '\0'); return true; };
971
+ case '-delete':
972
+ hasAction = true;
973
+ // GNU's -delete implies -depth: a directory is removable only once
974
+ // its children are gone.
975
+ state.depthFirst = true;
976
+ return async (e) => {
977
+ try {
978
+ if (e.type === 'directory') vfs.rmdir(e.vfsPath);
979
+ else vfs.unlink(e.vfsPath);
980
+ return true;
981
+ } catch (err: any) {
982
+ ctx.stderr.write(`find: cannot delete '${e.display}': ${err?.message || err}\n`);
983
+ return false;
984
+ }
985
+ };
986
+ case '-quit':
987
+ hasAction = true;
988
+ return async (_e, st) => { st.quit = true; return true; };
989
+ case '-prune':
990
+ // NOT an action: GNU still adds the implicit -print alongside it,
991
+ // which is what makes `-prune -o -print` print everything else.
992
+ return async (e, st) => {
993
+ if (e.type === 'directory') st.prune = true;
994
+ return true;
995
+ };
996
+ case '-exec': {
997
+ hasAction = true;
998
+ const argv: string[] = [];
999
+ let terminator: ';' | '+' | null = null;
1000
+ while (pos < args.length) {
1001
+ const a = next()!;
1002
+ // Exactly `;`, as GNU requires. The usual `\;` is the shell's
1003
+ // escaping of it; a quoted '\;' keeps its backslash and GNU
1004
+ // rejects that as a missing terminator, so this does too.
1005
+ if (a === ';') { terminator = ';'; break; }
1006
+ // `+` terminates only directly after the {} placeholder.
1007
+ if (a === '+' && argv[argv.length - 1] === '{}') { terminator = '+'; break; }
1008
+ argv.push(a);
1009
+ }
1010
+ if (terminator === null) {
1011
+ throw new FindUsageError("missing argument to `-exec'");
1012
+ }
1013
+ if (terminator === ';') {
1014
+ return async (e) => runExec(argv.map((a) => a.split('{}').join(e.display)));
1015
+ }
1016
+ // `-exec … {} +`: every match joins one invocation, flushed after
1017
+ // the walk. The trailing {} is where the paths go.
1018
+ const batch: FindExecBatch = { argv: argv.slice(0, -1), pending: [] };
1019
+ execBatches.push(batch);
1020
+ return async (e) => { batch.pending.push(e.display); return true; };
1021
+ }
1022
+
1023
+ // ── Grouping ──
1024
+ case '(': {
1025
+ const inner = parseExpr();
1026
+ if (next() !== ')') throw new FindUsageError("expected expression after `('");
1027
+ return inner;
1028
+ }
1029
+ }
1030
+ throw new FindUsageError(`unknown predicate \`${tok}'`);
1031
+ }
1032
+
1033
+ function parseUnary(): FindNode {
1034
+ const tok = peek();
1035
+ if (tok === '!' || tok === '-not') {
1036
+ pos++;
1037
+ const operand = parseUnary();
1038
+ return async (e, st) => !(await operand(e, st));
1039
+ }
1040
+ return parsePrimary();
1041
+ }
1042
+
1043
+ function parseAnd(): FindNode {
1044
+ let left = parseUnary();
1045
+ while (pos < args.length) {
1046
+ const tok = peek()!;
1047
+ if (tok === ')' || tok === '-o' || tok === '-or') break;
1048
+ if (tok === '-a' || tok === '-and') pos++;
1049
+ const right = parseUnary();
1050
+ const l = left;
1051
+ left = async (e, st) => (await l(e, st)) && (await right(e, st));
1052
+ }
1053
+ return left;
1054
+ }
1055
+
1056
+ function parseExpr(): FindNode {
1057
+ let left = parseAnd();
1058
+ while (peek() === '-o' || peek() === '-or') {
1059
+ pos++;
1060
+ const right = parseAnd();
1061
+ const l = left;
1062
+ left = async (e, st) => (await l(e, st)) || (await right(e, st));
1063
+ }
1064
+ return left;
1065
+ }
1066
+
1067
+ let predicate: FindNode;
1068
+ try {
1069
+ predicate = pos < args.length ? parseExpr() : TRUE;
1070
+ if (pos < args.length) {
1071
+ throw new FindUsageError(`paths must precede expression: \`${args[pos]}'`);
1072
+ }
1073
+ } catch (e: any) {
1074
+ if (e instanceof FindUsageError) {
1075
+ ctx.stderr.write(`find: ${e.message}\n`);
1076
+ return 1;
1077
+ }
1078
+ throw e;
1079
+ }
1080
+
1081
+ // With no action anywhere in the expression the whole of it is printed;
1082
+ // -prune deliberately does not count, so `-prune -o -print` still prints.
1083
+ const test = predicate;
1084
+ if (!hasAction) {
1085
+ predicate = async (e, st) => {
1086
+ const matched = await test(e, st);
1087
+ if (matched) emit(e, '\n');
1088
+ return matched;
1089
+ };
1090
+ }
1091
+
1092
+ // ── Walk ──────────────────────────────────────────────────────────────
1093
+ let status = 0;
1094
+
1095
+ const visit = async (entry: FindEntry): Promise<void> => {
1096
+ state.prune = false;
1097
+ if (entry.depth >= state.minDepth) await predicate(entry, state);
1098
+ };
1099
+
1100
+ const walk = async (entry: FindEntry): Promise<void> => {
1101
+ if (state.quit) return;
1102
+ // A pre-order visit must run before the descent it may prune.
1103
+ if (!state.depthFirst) {
1104
+ await visit(entry);
1105
+ if (state.quit || state.prune) return;
1106
+ }
1107
+ if (entry.type === 'directory' && entry.depth < state.maxDepth) {
1108
+ let entries: { name: string; type: string }[] = [];
1109
+ try { entries = vfs.readdir(entry.vfsPath); } catch { entries = []; }
1110
+ for (const child of entries) {
1111
+ if (state.quit) break;
1112
+ await walk({
1113
+ vfsPath: entry.vfsPath + '/' + child.name,
1114
+ display: entry.display + '/' + child.name,
1115
+ name: child.name,
1116
+ type: child.type,
1117
+ depth: entry.depth + 1,
1118
+ });
1119
+ }
1120
+ }
1121
+ // Post-order: the visit that -depth and -delete need, after the
1122
+ // children it must outlive.
1123
+ if (state.depthFirst && !state.quit) await visit(entry);
1124
+ };
1125
+
1126
+ for (const startArg of startArgs) {
1127
+ if (state.quit) break;
1128
+ // `find dir/` prints `dir/empty.txt`, so the separator is not doubled.
1129
+ const display = startArg.length > 1 ? startArg.replace(/\/+$/, '') : startArg;
1130
+ const vfsPath = resolvePath(ctx.cwd, startArg);
1131
+ let type: string;
1132
+ try {
1133
+ type = vfs.stat(vfsPath).type;
1134
+ } catch {
1135
+ ctx.stderr.write(`find: '${startArg}': No such file or directory\n`);
1136
+ status = 1;
1137
+ continue;
1138
+ }
1139
+ await walk({
1140
+ vfsPath,
1141
+ display,
1142
+ name: display.split('/').pop() || display,
1143
+ type,
1144
+ depth: 0,
1145
+ });
1146
+ }
1147
+
1148
+ for (const batch of execBatches) {
1149
+ if (batch.pending.length > 0) await runExec([...batch.argv, ...batch.pending]);
1150
+ }
1151
+ return status;
1152
+ };
1153
+ }
1154
+
1155
+ /**
1156
+ * shell compatibility (2026-05-11): grep flag handling.
1157
+ *
1158
+ * Pre-fix gaps:
1159
+ * -c X matched lines were printed (count mode ignored from stdin)
1160
+ * -n didn't prepend line number
1161
+ * -w word-boundary not added to regex
1162
+ * -l not implemented (no flag check)
1163
+ * -E flag parsed but didn't enable extended regex (JS RegExp
1164
+ * already does ERE-equivalent)
1165
+ *
1166
+ * Fix: parse flags once into a struct; unify stdin + file + recursive
1167
+ * paths through a single `processLines` helper that honours every
1168
+ * flag consistently.
1169
+ */
1170
+ function mkGrep(vfs: UnixVfs): CmdFn {
1171
+ return (ctx) => {
1172
+ const args = [...ctx.args];
1173
+ // Parse flags. Support combined `-rni` form (single dash + chars).
1174
+ let recursive = false, ignoreCase = false, lineNum = false;
1175
+ let countOnly = false, invertMatch = false, wordMatch = false;
1176
+ let filesOnly = false; // -l
1177
+ let quiet = false; // -q
1178
+ let positional: string[] = [];
1179
+ for (let i = 0; i < args.length; i++) {
1180
+ const a = args[i];
1181
+ if (a === '--') { positional.push(...args.slice(i + 1)); break; }
1182
+ if (a === '-r' || a === '-R' || a === '--recursive') { recursive = true; continue; }
1183
+ if (a === '-i' || a === '--ignore-case') { ignoreCase = true; continue; }
1184
+ if (a === '-n' || a === '--line-number') { lineNum = true; continue; }
1185
+ if (a === '-c' || a === '--count') { countOnly = true; continue; }
1186
+ if (a === '-v' || a === '--invert-match') { invertMatch = true; continue; }
1187
+ if (a === '-w' || a === '--word-regexp') { wordMatch = true; continue; }
1188
+ if (a === '-l' || a === '--files-with-matches') { filesOnly = true; continue; }
1189
+ if (a === '-q' || a === '--quiet' || a === '--silent') { quiet = true; continue; }
1190
+ if (a === '-E' || a === '--extended-regexp') { /* JS regex is ERE-ish */ continue; }
1191
+ if (a === '-F' || a === '--fixed-strings') {
1192
+ // Mark as literal — handled below via escape.
1193
+ (args as any).__fixedStrings = true;
1194
+ continue;
1195
+ }
1196
+ if (a.startsWith('-') && a.length > 1 && !a.startsWith('--')) {
1197
+ // Combined short flags like -rni
1198
+ for (const ch of a.slice(1)) {
1199
+ if (ch === 'r' || ch === 'R') recursive = true;
1200
+ else if (ch === 'i') ignoreCase = true;
1201
+ else if (ch === 'n') lineNum = true;
1202
+ else if (ch === 'c') countOnly = true;
1203
+ else if (ch === 'v') invertMatch = true;
1204
+ else if (ch === 'w') wordMatch = true;
1205
+ else if (ch === 'l') filesOnly = true;
1206
+ else if (ch === 'q') quiet = true;
1207
+ else if (ch === 'E') { /* ERE noop */ }
1208
+ else if (ch === 'F') (args as any).__fixedStrings = true;
1209
+ }
1210
+ continue;
1211
+ }
1212
+ positional.push(a);
1213
+ }
1214
+ if (positional.length < 1) { ctx.stderr.write('Usage: grep [-rnicvlqEFw] PATTERN [FILE...]\n'); return 1; }
1215
+ let pattern = positional[0];
1216
+ const targets = positional.slice(1);
1217
+ if ((args as any).__fixedStrings) {
1218
+ // -F: escape regex metacharacters for literal match.
1219
+ pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1220
+ }
1221
+ if (wordMatch) pattern = `\\b(?:${pattern})\\b`;
1222
+ const flags = ignoreCase ? 'i' : '';
1223
+ let re: RegExp;
1224
+ try { re = new RegExp(pattern, flags); } catch { ctx.stderr.write(`grep: invalid regex: ${pattern}\n`); return 1; }
1225
+ let found = false;
1226
+ let failed = false;
1227
+
1228
+ function processLines(lines: string[], label: string): void {
1229
+ let count = 0;
1230
+ let matchedHere = false;
1231
+ for (let i = 0; i < lines.length; i++) {
1232
+ const line = lines[i];
1233
+ // Skip a trailing empty line from a content that ended with \\n.
1234
+ if (i === lines.length - 1 && line === '') continue;
1235
+ const isMatch = re.test(line);
1236
+ if (isMatch !== invertMatch) {
1237
+ found = true;
1238
+ matchedHere = true;
1239
+ count++;
1240
+ // -q asks only whether anything matched; printing is the caller's
1241
+ // way of saying it wants to see it.
1242
+ if (quiet) return;
1243
+ if (filesOnly) {
1244
+ // -l: emit file label once, stop scanning.
1245
+ ctx.stdout.write(label + '\n');
1246
+ return;
1247
+ }
1248
+ if (!countOnly) {
1249
+ const labelPrefix = (targets.length > 1 || recursive) && label ? label + ':' : '';
1250
+ const linePrefix = lineNum ? (i + 1) + ':' : '';
1251
+ ctx.stdout.write(labelPrefix + linePrefix + line + '\n');
1252
+ }
1253
+ }
1254
+ }
1255
+ if (countOnly && !quiet) {
1256
+ const labelPrefix = (targets.length > 1 || recursive) && label ? label + ':' : '';
1257
+ ctx.stdout.write(labelPrefix + count + '\n');
1258
+ }
1259
+ void matchedHere;
1260
+ }
1261
+
1262
+ function grepFile(path: string, label: string) {
1263
+ try {
1264
+ const content = vfs.readFileString(path);
1265
+ processLines(content.split('\n'), label);
1266
+ } catch (error) {
1267
+ ctx.stderr.write(`grep: ${label}: ${fsErrorMessage(error)}\n`);
1268
+ failed = true;
1269
+ }
1270
+ }
1271
+ function walkDir(dir: string) {
1272
+ try {
1273
+ for (const e of vfs.readdir(dir)) {
1274
+ const fp = dir + '/' + e.name;
1275
+ if (e.type === 'file') grepFile(fp, '/' + fp);
1276
+ else if (e.type === 'directory') walkDir(fp);
1277
+ }
1278
+ } catch {}
1279
+ }
1280
+ if (targets.length === 0 && recursive) {
1281
+ walkDir((ctx.cwd || '/home/user').replace(/^\/+/, ''));
1282
+ } else if (targets.length === 0) {
1283
+ // Read from stdin (if piped) — single virtual "file" with no label.
1284
+ if (ctx.stdin) {
1285
+ processLines(ctx.stdin.split('\n'), '');
1286
+ }
1287
+ } else {
1288
+ for (const target of targets) {
1289
+ const fp = resolvePath(ctx.cwd, target);
1290
+ try {
1291
+ if (vfs.exists(fp) && vfs.isDirectory(fp)) {
1292
+ if (recursive) walkDir(fp);
1293
+ } else {
1294
+ grepFile(fp, target);
1295
+ }
1296
+ } catch (error) {
1297
+ ctx.stderr.write(`grep: ${target}: ${fsErrorMessage(error)}\n`);
1298
+ failed = true;
1299
+ }
1300
+ }
1301
+ }
1302
+ return !failed && found ? 0 : 1;
1303
+ };
1304
+ }
1305
+
1306
+ /**
1307
+ * SHELL-R6-B2 follow-on: streaming head.
1308
+ *
1309
+ * When invoked on a pipeline (`producer | head`), receives a pipe
1310
+ * reader (object with .read()) — NOT a coalesced string — and reads
1311
+ * line-by-line until N lines are emitted. Closes the reader by
1312
+ * draining null (or by the SHELL-R6-2 abort cascade kicking in when
1313
+ * we return).
1314
+ *
1315
+ * Why: the original head implementation read via readAll(), which never returns when
1316
+ * upstream is `yes`. Our SHELL-R6-2 pipeline abort only fires when
1317
+ * the consumer resolves — so head must resolve quickly via its own
1318
+ * line-count termination, which then triggers the cascade.
1319
+ *
1320
+ * Backward compat: file-args (head FILE) and the string-stdin case
1321
+ * (when wrap already coalesced) still work via the same code path.
1322
+ */
1323
+ function mkHead(_vfs: UnixVfs): CmdFn {
1324
+ return async (ctx: any) => {
1325
+ const parsed = parseHeadArgs(ctx.args);
1326
+ if (parsed.error) { ctx.stderr.write(`head: ${parsed.error}\n`); return 1; }
1327
+ const { lines: n, bytes, files } = parsed;
1328
+
1329
+ if (bytes !== undefined) return headBytes(ctx, files, bytes);
1330
+
1331
+ if (files.length === 0) {
1332
+ // Pipe / stdin case.
1333
+ const stdin = ctx.stdin;
1334
+ if (!stdin) return 0;
1335
+ // Streaming pipe-reader path: read chunks until N lines.
1336
+ if (typeof stdin !== 'string' && typeof stdin.read === 'function') {
1337
+ let buffered = '';
1338
+ let emitted = 0;
1339
+ const out: string[] = [];
1340
+ while (emitted < n) {
1341
+ const chunk: string | null = await stdin.read();
1342
+ if (chunk === null) break;
1343
+ buffered += chunk;
1344
+ // Process complete lines while we have them.
1345
+ let nlIdx;
1346
+ while (emitted < n && (nlIdx = buffered.indexOf('\n')) !== -1) {
1347
+ out.push(buffered.substring(0, nlIdx));
1348
+ buffered = buffered.substring(nlIdx + 1);
1349
+ emitted++;
1350
+ }
1351
+ }
1352
+ // Handle a partial line if N hit mid-buffer and producer hasn't
1353
+ // sent a trailing newline yet — emit it only if we haven't
1354
+ // already hit the N-line cap (POSIX head includes partial
1355
+ // tail).
1356
+ if (emitted < n && buffered.length > 0) {
1357
+ out.push(buffered);
1358
+ }
1359
+ ctx.stdout.write(out.join('\n') + '\n');
1360
+ return 0;
1361
+ }
1362
+ // Legacy string-stdin path (kept for wrap's pre-coalesced case).
1363
+ if (typeof stdin === 'string') {
1364
+ ctx.stdout.write(stdin.split('\n').slice(0, n).join('\n') + '\n');
1365
+ return 0;
1366
+ }
1367
+ return 0;
1368
+ }
1369
+
1370
+ for (const [index, f] of files.entries()) {
1371
+ const path = absolutePath(ctx.cwd, f);
1372
+ try {
1373
+ const content = readWholeFileString(ctx, path);
1374
+ if (files.length > 1) ctx.stdout.write(`${index > 0 ? '\n' : ''}==> ${f} <==\n`);
1375
+ ctx.stdout.write(content.split('\n').slice(0, n).join('\n') + '\n');
1376
+ } catch (error) { ctx.stderr.write(`head: ${f}: ${fsErrorMessage(error)}\n`); return 1; }
1377
+ }
1378
+ return 0;
1379
+ };
1380
+ }
1381
+
1382
+ type HeadArgs = { lines: number; bytes?: number; files: string[]; error?: string };
1383
+
1384
+ /** `-c N`, `-cN`, `--bytes=N`, `-n N`, `-nN`, `--lines=N`, `-N`, `-q`, `-v`. */
1385
+ function parseHeadArgs(args: string[]): HeadArgs {
1386
+ const result: HeadArgs = { lines: 10, files: [] };
1387
+ for (let i = 0; i < args.length; i++) {
1388
+ const arg = args[i];
1389
+ const option = matchCountOption(arg, args[i + 1], 'c', 'bytes')
1390
+ ?? matchCountOption(arg, args[i + 1], 'n', 'lines');
1391
+
1392
+ if (option) {
1393
+ i += option.consumed;
1394
+ const count = parseByteCount(option.value);
1395
+ if (count === null) {
1396
+ const what = option.flag === 'c' ? 'bytes' : 'lines';
1397
+ return { ...result, error: `invalid number of ${what}: '${option.value}'` };
1398
+ }
1399
+ if (option.flag === 'c') result.bytes = count; else result.lines = count;
1400
+ } else if (/^-\d+$/.test(arg)) {
1401
+ result.lines = Number.parseInt(arg.slice(1), 10);
1402
+ } else if (arg === '-q' || arg === '--quiet' || arg === '-v' || arg === '--verbose') {
1403
+ continue;
1404
+ } else if (arg !== '-' && arg.startsWith('-') && !arg.startsWith('--') && arg.length > 1) {
1405
+ // A cluster of switches, `-qn 1`; `c` and `n` take the rest of the
1406
+ // cluster or the next argument.
1407
+ let consumed = false;
1408
+ for (let j = 1; j < arg.length && !consumed; j++) {
1409
+ const flag = arg[j];
1410
+ if (flag === 'q' || flag === 'v') continue;
1411
+ if (flag !== 'c' && flag !== 'n') {
1412
+ return { ...result, error: `invalid option -- '${flag}'` };
1413
+ }
1414
+ const text = arg.slice(j + 1) || (args[++i] ?? '');
1415
+ const count = parseByteCount(text);
1416
+ if (count === null) {
1417
+ const what = flag === 'c' ? 'bytes' : 'lines';
1418
+ return { ...result, error: `invalid number of ${what}: '${text}'` };
1419
+ }
1420
+ if (flag === 'c') result.bytes = count; else result.lines = count;
1421
+ consumed = true;
1422
+ }
1423
+ } else if (arg !== '-' && arg.startsWith('--')) {
1424
+ return { ...result, error: `unrecognized option '${arg}'` };
1425
+ } else {
1426
+ result.files.push(arg);
1427
+ }
1428
+ }
1429
+ return result;
1430
+ }
1431
+
1432
+ function matchCountOption(
1433
+ arg: string,
1434
+ next: string | undefined,
1435
+ flag: string,
1436
+ long: string,
1437
+ ): { flag: string; value: string; consumed: number } | null {
1438
+ if (arg === `-${flag}`) return { flag, value: next ?? '', consumed: 1 };
1439
+ if (arg.startsWith(`-${flag}`) && arg.length > 2) return { flag, value: arg.slice(2), consumed: 0 };
1440
+ if (arg === `--${long}`) return { flag, value: next ?? '', consumed: 1 };
1441
+ if (arg.startsWith(`--${long}=`)) return { flag, value: arg.slice(long.length + 3), consumed: 0 };
1442
+ return null;
1443
+ }
1444
+
1445
+ /** `head`/`dd`-style counts: plain digits with an optional binary/SI suffix. */
1446
+ function parseByteCount(value: string): number | null {
1447
+ const match = /^(\d+)([bkKmMgG]?[Bb]?)$/.exec(value.trim());
1448
+ if (!match) return null;
1449
+ const scale: Record<string, number> = {
1450
+ '': 1, b: 512, k: 1024, K: 1024, kB: 1000, KB: 1000,
1451
+ m: 1024 ** 2, M: 1024 ** 2, mB: 1000 ** 2, MB: 1000 ** 2,
1452
+ g: 1024 ** 3, G: 1024 ** 3, gB: 1000 ** 3, GB: 1000 ** 3,
1453
+ };
1454
+ const factor = scale[match[2]];
1455
+ if (factor === undefined) return null;
1456
+ return Number.parseInt(match[1], 10) * factor;
1457
+ }
1458
+
1459
+ /**
1460
+ * `head -c N` — emit the first N bytes. Streams through the positional read
1461
+ * so byte counts hold for any N and character devices such as /dev/zero,
1462
+ * which have no stored content to read whole, work like they do on Unix.
1463
+ */
1464
+ async function headBytes(ctx: Ctx, files: string[], limit: number): Promise<number> {
1465
+ const writer = new SinkWriter(ctx.stdout);
1466
+ if (files.length === 0 || (files.length === 1 && files[0] === '-')) {
1467
+ await streamStdinBytes(ctx, writer, limit);
1468
+ writer.end();
1469
+ return 0;
1470
+ }
1471
+
1472
+ let exit = 0;
1473
+ for (const f of files) {
1474
+ const path = absolutePath(ctx.cwd, f);
1475
+ try {
1476
+ if (files.length > 1) ctx.stdout.write(`==> ${f} <==\n`);
1477
+ streamRange((offset, length) => ctx.vfs.readRange(path, offset, length), writer, {
1478
+ length: limit,
1479
+ signal: ctx.signal,
1480
+ });
1481
+ } catch (error) {
1482
+ ctx.stderr.write(`head: ${f}: ${fsErrorMessage(error)}\n`);
1483
+ exit = 1;
1484
+ }
1485
+ }
1486
+ writer.end();
1487
+ return exit;
1488
+ }
1489
+
1490
+ /**
1491
+ * Pull at most `limit` bytes from stdin, whether the shell handed us an
1492
+ * already-drained string or a live pipe reader. Reading only the string form
1493
+ * would make `producer | head -c N` emit nothing at all.
1494
+ */
1495
+ async function streamStdinBytes(ctx: Ctx, writer: SinkWriter, limit: number): Promise<void> {
1496
+ const stdin: unknown = ctx.stdin;
1497
+ if (typeof stdin === 'string') {
1498
+ writer.write(enc.encode(stdin).subarray(0, limit));
1499
+ return;
1500
+ }
1501
+ const reader = stdin as { read?: () => Promise<string | null>; readBytes?: (n: number) => Promise<string | null> };
1502
+ if (typeof reader?.read !== 'function') return;
1503
+
1504
+ let copied = 0;
1505
+ while (copied < limit) {
1506
+ const want = limit - copied;
1507
+ const chunk = reader.readBytes ? await reader.readBytes(want) : await reader.read();
1508
+ if (chunk === null) break;
1509
+ const bytes = enc.encode(chunk).subarray(0, want);
1510
+ writer.write(bytes);
1511
+ copied += bytes.length;
1512
+ }
1513
+ }
1514
+
1515
+ /** Absolute, mount-aware path — `ctx.vfs` resolves virtual mounts like /dev. */
1516
+ function absolutePath(cwd: string, target: string): string {
1517
+ return '/' + resolvePath(cwd, target);
1518
+ }
1519
+
1520
+ function readWholeFileString(ctx: Ctx, path: string): string {
1521
+ if (ctx.vfs.stat(path).type === 'directory') {
1522
+ throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });
1523
+ }
1524
+ return dec.decode(ctx.vfs.readFile(path));
1525
+ }
1526
+
1527
+ /**
1528
+ * `tail [-n N] [-n +N] [-N] [-q] [-v] [FILE…]`.
1529
+ *
1530
+ * The previous parse only knew a separate `-n N`, matched its operand by
1531
+ * `indexOf` (so a file literally named like the count vanished), and sliced
1532
+ * the split lines without dropping the empty string a trailing newline leaves
1533
+ * behind — `tail -n 1 file` printed a blank line instead of the last line.
1534
+ */
1535
+ function mkTail(vfs: UnixVfs): CmdFn {
1536
+ return (ctx) => {
1537
+ const parsed = parseTailArgs(ctx.args);
1538
+ if (parsed.error) { ctx.stderr.write(`tail: ${parsed.error}\n`); return 1; }
1539
+ const { count, fromStart, files, verbose } = parsed;
1540
+
1541
+ const emit = (content: string): void => {
1542
+ const lines = content.split('\n');
1543
+ if (lines[lines.length - 1] === '') lines.pop();
1544
+ const selected = fromStart ? lines.slice(Math.max(0, count - 1)) : lines.slice(-count);
1545
+ if (selected.length > 0) ctx.stdout.write(selected.join('\n') + '\n');
1546
+ };
1547
+
1548
+ if (files.length === 0) {
1549
+ if (ctx.stdin) emit(ctx.stdin);
1550
+ return 0;
1551
+ }
1552
+ const label = verbose || files.length > 1;
1553
+ let exit = 0;
1554
+ for (const [index, f] of files.entries()) {
1555
+ try {
1556
+ const content = readWholeFileString(ctx, absolutePath(ctx.cwd, f));
1557
+ if (label) ctx.stdout.write(`${index > 0 ? '\n' : ''}==> ${f} <==\n`);
1558
+ emit(content);
1559
+ } catch (error) {
1560
+ ctx.stderr.write(`tail: ${f}: ${fsErrorMessage(error)}\n`);
1561
+ exit = 1;
1562
+ }
1563
+ }
1564
+ void vfs;
1565
+ return exit;
1566
+ };
1567
+ }
1568
+
1569
+ type TailArgs = {
1570
+ count: number;
1571
+ /** `-n +N` counts forward from the first line instead of back from the last. */
1572
+ fromStart: boolean;
1573
+ files: string[];
1574
+ verbose: boolean;
1575
+ error?: string;
1576
+ };
1577
+
1578
+ function applyTailCount(result: TailArgs, spec: string): string | null {
1579
+ const value = /^([+-]?)(\d+)$/.exec(spec.trim());
1580
+ if (value === null) return `invalid number of lines: '${spec}'`;
1581
+ result.fromStart = value[1] === '+';
1582
+ result.count = Number.parseInt(value[2], 10);
1583
+ return null;
1584
+ }
1585
+
1586
+ function parseTailArgs(args: string[]): TailArgs {
1587
+ const result: TailArgs = { count: 10, fromStart: false, files: [], verbose: false };
1588
+ let stop = false;
1589
+ for (let i = 0; i < args.length; i++) {
1590
+ const arg = args[i];
1591
+ if (stop || arg === '-' || !arg.startsWith('-')) { result.files.push(arg); continue; }
1592
+ if (arg === '--') { stop = true; continue; }
1593
+
1594
+ if (arg.startsWith('--lines=')) {
1595
+ const error = applyTailCount(result, arg.slice(8));
1596
+ if (error) return { ...result, error };
1597
+ continue;
1598
+ }
1599
+ if (arg === '--lines') {
1600
+ const error = applyTailCount(result, args[++i] ?? '');
1601
+ if (error) return { ...result, error };
1602
+ continue;
1603
+ }
1604
+ // `-5` is the count on its own; anything else is a cluster of short
1605
+ // options, where `n` takes the rest of the cluster or the next argument.
1606
+ if (/^-\+?\d+$/.test(arg)) {
1607
+ const error = applyTailCount(result, arg.slice(1));
1608
+ if (error) return { ...result, error };
1609
+ continue;
1610
+ }
1611
+ let consumedCount = false;
1612
+ for (let j = 1; j < arg.length && !consumedCount; j++) {
1613
+ const flag = arg[j];
1614
+ if (flag === 'q') result.verbose = false;
1615
+ else if (flag === 'v') result.verbose = true;
1616
+ else if (flag === 'n') {
1617
+ const error = applyTailCount(result, arg.slice(j + 1) || (args[++i] ?? ''));
1618
+ if (error) return { ...result, error };
1619
+ consumedCount = true;
1620
+ } else return { ...result, error: `invalid option -- '${flag}'` };
1621
+ }
1622
+ }
1623
+ return result;
1624
+ }
1625
+
1626
+ const WC_SPEC = {
1627
+ lines: { type: 'boolean' as const, short: 'l' },
1628
+ words: { type: 'boolean' as const, short: 'w' },
1629
+ bytes: { type: 'boolean' as const, short: 'c' },
1630
+ chars: { type: 'boolean' as const, short: 'm' },
1631
+ };
1632
+
1633
+ function mkWc(vfs: UnixVfs): CmdFn {
1634
+ return (ctx) => {
1635
+ const { flags: parsed, positional, unknown } = parseArgs(ctx.args, WC_SPEC);
1636
+ if (unknown.length > 0) {
1637
+ ctx.stderr.write(`wc: invalid option -- '${unknown[0].replace(/^-+/, '')}'\n`);
1638
+ return 1;
1639
+ }
1640
+ const hasFlags = parsed.lines === true || parsed.words === true
1641
+ || parsed.bytes === true || parsed.chars === true;
1642
+ const selected = {
1643
+ lines: !hasFlags || parsed.lines === true,
1644
+ words: !hasFlags || parsed.words === true,
1645
+ bytes: !hasFlags || parsed.bytes === true || parsed.chars === true,
1646
+ };
1647
+ const columns = Number(selected.lines) + Number(selected.words) + Number(selected.bytes);
1648
+ const files = positional;
1649
+
1650
+ // BUG-SWEEP-3 (2026-05-11): the byte count is the raw Uint8Array length,
1651
+ // not enc.encode(decoded).length — decoding a binary file substitutes
1652
+ // U+FFFD for each invalid byte and re-encoding turns one byte into three.
1653
+ const measure = (rawBytes: Uint8Array): number[] => {
1654
+ const text = selected.lines || selected.words
1655
+ ? new TextDecoder('utf-8').decode(rawBytes)
1656
+ : '';
1657
+ const counts: number[] = [];
1658
+ if (selected.lines) counts.push(text.split('\n').length - (text.endsWith('\n') ? 1 : 0));
1659
+ if (selected.words) counts.push(text.split(/\s+/).filter(Boolean).length);
1660
+ if (selected.bytes) counts.push(rawBytes.length);
1661
+ return counts;
1662
+ };
1663
+
1664
+ const emit = (counts: number[], width: number, label: string): void => {
1665
+ ctx.stdout.write(
1666
+ counts.map((c) => String(c).padStart(width)).join(' ') + (label ? ' ' + label : '') + '\n',
1667
+ );
1668
+ };
1669
+
1670
+ if (files.length === 0) {
1671
+ const bytes = enc.encode(ctx.stdin ?? '');
1672
+ // Nothing bounds a stream's counts ahead of time, so a multi-column
1673
+ // report over standard input uses the fixed width GNU falls back to.
1674
+ emit(measure(bytes), columns === 1 ? 0 : 7, '');
1675
+ return 0;
1676
+ }
1677
+
1678
+ const read: Array<{ label: string; bytes: Uint8Array }> = [];
1679
+ let exit = 0;
1680
+ for (const f of files) {
1681
+ try {
1682
+ read.push({ label: f, bytes: vfs.readFile(resolvePath(ctx.cwd, f)) });
1683
+ } catch {
1684
+ ctx.stderr.write(`wc: ${f}: No such file\n`);
1685
+ exit = 1;
1686
+ }
1687
+ }
1688
+
1689
+ // A file's size bounds every count it can produce, which is the width GNU
1690
+ // lays the columns out to. One column of one file needs no padding.
1691
+ const width = columns === 1 && read.length <= 1
1692
+ ? 1
1693
+ : Math.max(1, ...read.map((entry) => String(entry.bytes.length).length));
1694
+
1695
+ const totals = new Array(columns).fill(0);
1696
+ for (const entry of read) {
1697
+ const counts = measure(entry.bytes);
1698
+ counts.forEach((count, i) => { totals[i] += count; });
1699
+ emit(counts, width, entry.label);
1700
+ }
1701
+ if (read.length > 1) emit(totals, width, 'total');
1702
+ return exit;
1703
+ };
1704
+ }
1705
+
1706
+ const SORT_SPEC = {
1707
+ reverse: { type: 'boolean' as const, short: 'r' },
1708
+ numeric: { type: 'boolean' as const, short: 'n' },
1709
+ unique: { type: 'boolean' as const, short: 'u' },
1710
+ 'ignore-case': { type: 'boolean' as const, short: 'f' },
1711
+ };
1712
+
1713
+ function mkSort(vfs: UnixVfs): CmdFn {
1714
+ return (ctx) => {
1715
+ const { flags, positional, unknown } = parseArgs(ctx.args, SORT_SPEC);
1716
+ if (unknown.length > 0) {
1717
+ ctx.stderr.write(`sort: invalid option -- '${unknown[0].replace(/^-+/, '')}'\n`);
1718
+ return 1;
1719
+ }
1720
+ let input = ctx.stdin || '';
1721
+ if (positional.length > 0 && !input) {
1722
+ try { input = vfs.readFileString(resolvePath(ctx.cwd, positional[0])); }
1723
+ catch { ctx.stderr.write(`sort: ${positional[0]}: No such file\n`); return 1; }
1724
+ }
1725
+ const lines = input.split('\n');
1726
+ if (lines[lines.length - 1] === '') lines.pop();
1727
+ const numeric = flags.numeric === true;
1728
+ const fold = flags['ignore-case'] === true;
1729
+ const key = (line: string): string => (fold ? line.toLowerCase() : line);
1730
+ lines.sort((a, b) => (numeric
1731
+ ? (Number.parseFloat(a) || 0) - (Number.parseFloat(b) || 0)
1732
+ : key(a).localeCompare(key(b))));
1733
+ if (flags.reverse) lines.reverse();
1734
+ // -u drops adjacent duplicates after sorting, so it compares by the same
1735
+ // key the sort used rather than by the whole line.
1736
+ const result = flags.unique
1737
+ ? lines.filter((line, i) => i === 0 || compareSortKeys(lines[i - 1], line, numeric, fold) !== 0)
1738
+ : lines;
1739
+ if (result.length > 0) ctx.stdout.write(result.join('\n') + '\n');
1740
+ return 0;
1741
+ };
1742
+ }
1743
+
1744
+ function compareSortKeys(a: string, b: string, numeric: boolean, fold: boolean): number {
1745
+ if (numeric) return (Number.parseFloat(a) || 0) - (Number.parseFloat(b) || 0);
1746
+ return fold ? a.toLowerCase().localeCompare(b.toLowerCase()) : a.localeCompare(b);
1747
+ }
1748
+
1749
+ const UNIQ_SPEC = {
1750
+ count: { type: 'boolean' as const, short: 'c' },
1751
+ repeated: { type: 'boolean' as const, short: 'd' },
1752
+ unique: { type: 'boolean' as const, short: 'u' },
1753
+ 'ignore-case': { type: 'boolean' as const, short: 'i' },
1754
+ };
1755
+
1756
+ function mkUniq(vfs: UnixVfs): CmdFn {
1757
+ return (ctx) => {
1758
+ const { flags, positional, unknown } = parseArgs(ctx.args, UNIQ_SPEC);
1759
+ if (unknown.length > 0) {
1760
+ ctx.stderr.write(`uniq: invalid option -- '${unknown[0].replace(/^-+/, '')}'\n`);
1761
+ return 1;
1762
+ }
1763
+ // File operands were ignored outright, so `uniq file` read stdin and
1764
+ // printed nothing at all.
1765
+ let input = ctx.stdin || '';
1766
+ if (positional.length > 0 && positional[0] !== '-') {
1767
+ try { input = vfs.readFileString(resolvePath(ctx.cwd, positional[0])); }
1768
+ catch { ctx.stderr.write(`uniq: ${positional[0]}: No such file\n`); return 1; }
1769
+ }
1770
+ const lines = input.split('\n');
1771
+ const countFlag = flags.count === true;
1772
+ const dupsOnly = flags.repeated === true;
1773
+ const uniquesOnly = flags.unique === true;
1774
+ const fold = flags['ignore-case'] === true;
1775
+ if (lines[lines.length - 1] === '') lines.pop();
1776
+
1777
+ const same = (a: string, b: string): boolean =>
1778
+ fold ? a.toLowerCase() === b.toLowerCase() : a === b;
1779
+ const result: string[] = [];
1780
+ const flush = (line: string, count: number): void => {
1781
+ if (dupsOnly && count < 2) return;
1782
+ if (uniquesOnly && count > 1) return;
1783
+ result.push(countFlag ? `${String(count).padStart(7)} ${line}` : line);
1784
+ };
1785
+
1786
+ let prev: string | null = null;
1787
+ let count = 0;
1788
+ for (const line of lines) {
1789
+ if (prev !== null && same(line, prev)) { count++; continue; }
1790
+ if (prev !== null) flush(prev, count);
1791
+ prev = line;
1792
+ count = 1;
1793
+ }
1794
+ if (prev !== null) flush(prev, count);
1795
+
1796
+ if (result.length > 0) ctx.stdout.write(result.join('\n') + '\n');
1797
+ return 0;
1798
+ };
1799
+ }
1800
+
1801
+ function mkSed(vfs: UnixVfs): CmdFn {
1802
+ return (ctx) => runSed({
1803
+ args: ctx.args,
1804
+ cwd: ctx.cwd,
1805
+ vfs,
1806
+ stdout: ctx.stdout,
1807
+ stderr: ctx.stderr,
1808
+ stdin: ctx.stdin === undefined ? undefined : stringInput(ctx.stdin),
1809
+ });
1810
+ }
1811
+
1812
+ function stringInput(text: string): { readAll(): Promise<string> } {
1813
+ return {
1814
+ readAll: async () => text,
1815
+ };
1816
+ }
1817
+
1818
+ /**
1819
+ * shell compatibility (2026-05-11): expanded awk subset.
1820
+ *
1821
+ * Pre-fix mkAwk supported only:
1822
+ * {print $N}
1823
+ * /pattern/ [{print}]
1824
+ * Anything else → 'awk: unsupported program'.
1825
+ *
1826
+ * This extension adds (all in pure JS — no embedded awk-interpreter):
1827
+ * BEGIN { stmts } — run before any input line
1828
+ * END { stmts } — run after last line
1829
+ * /pat/ { stmts } — per-line conditional action
1830
+ * { stmts } — per-line unconditional action
1831
+ * $0, $1..$N, $NF — field refs in any expression
1832
+ * NR, NF — record number, field count
1833
+ * print EXPR — write EXPR to stdout + newline (comma-sep)
1834
+ * printf "fmt", a, b — printf-style (%s %d %f %x %o + width.prec)
1835
+ * sum += $N — assignment + compound
1836
+ * simple arithmetic — + - * / % () in expression position
1837
+ * numeric literals — integers and decimals
1838
+ * string literals — "..."
1839
+ * user vars — assigned via name = expr or compound
1840
+ *
1841
+ * NOT supported:
1842
+ * - for/while/if (control flow)
1843
+ * - functions
1844
+ * - arrays (assoc / indexed)
1845
+ * - getline
1846
+ * - regex match operator ~/!~ outside pattern position
1847
+ *
1848
+ * The eval engine is a tiny stmt-list runner that compiles each
1849
+ * statement to a JS closure operating on a shared state {vars,
1850
+ * fields[], NR, NF, separator, stdout, stderr}. Statements are
1851
+ * separated by `;` or `\\n`.
1852
+ *
1853
+ * Failure mode: if we can't parse a statement, write a clear error
1854
+ * to stderr and exit 1 (no silent fail).
1855
+ */
1856
+ function mkAwk(vfs: UnixVfs): CmdFn {
1857
+ return (ctx) => {
1858
+ const allArgs = ctx.args;
1859
+ // Parse -F separator if present.
1860
+ let separator: string | RegExp = /\s+/;
1861
+ const programArgs: string[] = [];
1862
+ const fileArgs: string[] = [];
1863
+ for (let i = 0; i < allArgs.length; i++) {
1864
+ const a = allArgs[i];
1865
+ if (a === '-F') {
1866
+ const s = allArgs[++i];
1867
+ if (s) separator = s.length === 1 ? s : new RegExp(s);
1868
+ } else if (a.startsWith('-F')) {
1869
+ const s = a.slice(2);
1870
+ if (s) separator = s.length === 1 ? s : new RegExp(s);
1871
+ } else if (a.startsWith('-')) {
1872
+ // Ignore other flags (silent compat).
1873
+ } else if (programArgs.length === 0) {
1874
+ programArgs.push(a);
1875
+ } else {
1876
+ fileArgs.push(a);
1877
+ }
1878
+ }
1879
+ const program = programArgs[0] || '';
1880
+ let input = ctx.stdin || '';
1881
+ if (fileArgs.length > 0 && !input) {
1882
+ try { input = vfs.readFileString(resolvePath(ctx.cwd, fileArgs[0])); }
1883
+ catch { ctx.stderr.write(`awk: ${fileArgs[0]}: No such file\n`); return 1; }
1884
+ }
1885
+
1886
+ // ── Parse program into blocks. ──
1887
+ // Block forms:
1888
+ // BEGIN { stmts }
1889
+ // END { stmts }
1890
+ // /pat/ { stmts }
1891
+ // /pat/ (implicit { print })
1892
+ // { stmts }
1893
+ // Multiple blocks may appear (separated by whitespace/newlines).
1894
+ interface Block { kind: 'BEGIN' | 'END' | 'PATTERN' | 'MAIN'; pattern?: RegExp; body: string }
1895
+ const blocks: Block[] = [];
1896
+ let cursor = 0;
1897
+ const src = program.trim();
1898
+ function skipWS() {
1899
+ while (cursor < src.length && /\s/.test(src[cursor])) cursor++;
1900
+ }
1901
+ function parseBraced(): string {
1902
+ // Assumes src[cursor] === '{'
1903
+ let depth = 0;
1904
+ let start = cursor;
1905
+ while (cursor < src.length) {
1906
+ const ch = src[cursor];
1907
+ if (ch === '{') depth++;
1908
+ else if (ch === '}') { depth--; if (depth === 0) { cursor++; return src.slice(start + 1, cursor - 1); } }
1909
+ else if (ch === '"' || ch === "'") {
1910
+ const quote = ch;
1911
+ cursor++;
1912
+ while (cursor < src.length && src[cursor] !== quote) {
1913
+ if (src[cursor] === '\\') cursor++;
1914
+ cursor++;
1915
+ }
1916
+ }
1917
+ cursor++;
1918
+ }
1919
+ return src.slice(start + 1, cursor);
1920
+ }
1921
+ while (cursor < src.length) {
1922
+ skipWS();
1923
+ if (cursor >= src.length) break;
1924
+ if (src.startsWith('BEGIN', cursor)) {
1925
+ cursor += 5;
1926
+ skipWS();
1927
+ if (src[cursor] !== '{') { ctx.stderr.write('awk: BEGIN without {\n'); return 1; }
1928
+ blocks.push({ kind: 'BEGIN', body: parseBraced() });
1929
+ continue;
1930
+ }
1931
+ if (src.startsWith('END', cursor)) {
1932
+ cursor += 3;
1933
+ skipWS();
1934
+ if (src[cursor] !== '{') { ctx.stderr.write('awk: END without {\n'); return 1; }
1935
+ blocks.push({ kind: 'END', body: parseBraced() });
1936
+ continue;
1937
+ }
1938
+ if (src[cursor] === '/') {
1939
+ // Pattern /pat/ optionally followed by {body}
1940
+ const pstart = cursor + 1;
1941
+ cursor++;
1942
+ while (cursor < src.length && src[cursor] !== '/') {
1943
+ if (src[cursor] === '\\') cursor++;
1944
+ cursor++;
1945
+ }
1946
+ const patSrc = src.slice(pstart, cursor);
1947
+ cursor++; // past closing /
1948
+ skipWS();
1949
+ let body = 'print';
1950
+ if (cursor < src.length && src[cursor] === '{') body = parseBraced();
1951
+ let re: RegExp;
1952
+ try { re = new RegExp(patSrc); }
1953
+ catch (e: any) { ctx.stderr.write(`awk: bad regex /${patSrc}/: ${e?.message || e}\n`); return 1; }
1954
+ blocks.push({ kind: 'PATTERN', pattern: re, body });
1955
+ continue;
1956
+ }
1957
+ if (src[cursor] === '{') {
1958
+ blocks.push({ kind: 'MAIN', body: parseBraced() });
1959
+ continue;
1960
+ }
1961
+ ctx.stderr.write(`awk: parse error at "${src.slice(cursor, cursor + 20)}"\n`);
1962
+ return 1;
1963
+ }
1964
+
1965
+ // ── Statement evaluator. ──
1966
+ // The evaluator processes a body string by splitting on `;` or
1967
+ // newline, then executes each statement against a state record.
1968
+ // Each statement is matched against shapes:
1969
+ // print EXPR[, EXPR]* OR print
1970
+ // printf "fmt", EXPR, …
1971
+ // IDENT = EXPR
1972
+ // IDENT (+|-|*|/|%)= EXPR
1973
+ // next (skip rest of body for this line — rare)
1974
+ interface State {
1975
+ vars: Record<string, any>;
1976
+ fields: string[]; // [$0, $1, $2, ...]
1977
+ NR: number;
1978
+ NF: number;
1979
+ printed: boolean;
1980
+ }
1981
+ /**
1982
+ * Expression evaluator without `new Function`. workerd CSP blocks
1983
+ * dynamic code generation at request time. This is a small
1984
+ * recursive-descent evaluator for the subset:
1985
+ * - literals: number, string ("...")
1986
+ * - field refs: $0, $N, $NF
1987
+ * - builtins: NR, NF
1988
+ * - user vars: identifier (looked up in st.vars; default 0)
1989
+ * - binary ops: + - * / % (numeric)
1990
+ * - parens: (expr)
1991
+ * - string concat happens via space-join in `print` call sites
1992
+ *
1993
+ * The grammar:
1994
+ * expr := term (('+'|'-') term)*
1995
+ * term := factor (('*'|'/'|'%') factor)*
1996
+ * factor := number | string | '$' (number | 'NF') | ident | '(' expr ')'
1997
+ */
1998
+ function evalExpr(expr: string, st: State): any {
1999
+ const text = expr.trim();
2000
+ let pos = 0;
2001
+ function skipWs() { while (pos < text.length && /\s/.test(text[pos])) pos++; }
2002
+ function peek(): string { return text[pos]; }
2003
+ function consume(ch: string): boolean { skipWs(); if (text[pos] === ch) { pos++; return true; } return false; }
2004
+ function expect(ch: string): void { if (!consume(ch)) throw new Error(`expected '${ch}' at "${text.slice(pos, pos + 20)}"`); }
2005
+ function parseExpr(): any {
2006
+ let left = parseTerm();
2007
+ for (;;) {
2008
+ skipWs();
2009
+ const op = text[pos];
2010
+ if (op === '+' || op === '-') {
2011
+ pos++;
2012
+ const right = parseTerm();
2013
+ const ln = toNum(left), rn = toNum(right);
2014
+ left = op === '+' ? ln + rn : ln - rn;
2015
+ } else break;
2016
+ }
2017
+ return left;
2018
+ }
2019
+ function parseTerm(): any {
2020
+ let left = parseFactor();
2021
+ for (;;) {
2022
+ skipWs();
2023
+ const op = text[pos];
2024
+ if (op === '*' || op === '/' || op === '%') {
2025
+ pos++;
2026
+ const right = parseFactor();
2027
+ const ln = toNum(left), rn = toNum(right);
2028
+ left = op === '*' ? ln * rn : op === '/' ? ln / rn : ln % rn;
2029
+ } else break;
2030
+ }
2031
+ return left;
2032
+ }
2033
+ function parseFactor(): any {
2034
+ skipWs();
2035
+ if (pos >= text.length) throw new Error(`unexpected end of expression`);
2036
+ const ch = text[pos];
2037
+ // Number
2038
+ if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(text[pos + 1]))) {
2039
+ let start = pos;
2040
+ while (pos < text.length && /[0-9.]/.test(text[pos])) pos++;
2041
+ return parseFloat(text.slice(start, pos));
2042
+ }
2043
+ // String literal (double or single quotes)
2044
+ if (ch === '"' || ch === "'") {
2045
+ const quote = ch;
2046
+ pos++;
2047
+ let s = '';
2048
+ while (pos < text.length && text[pos] !== quote) {
2049
+ if (text[pos] === '\\' && pos + 1 < text.length) {
2050
+ const esc = text[pos + 1];
2051
+ s += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc === 'r' ? '\r' : esc === '\\' ? '\\' : esc === '"' ? '"' : esc === "'" ? "'" : esc;
2052
+ pos += 2;
2053
+ } else {
2054
+ s += text[pos];
2055
+ pos++;
2056
+ }
2057
+ }
2058
+ if (pos < text.length) pos++; // skip closing quote
2059
+ return s;
2060
+ }
2061
+ // Parenthesised
2062
+ if (ch === '(') {
2063
+ pos++;
2064
+ const v = parseExpr();
2065
+ expect(')');
2066
+ return v;
2067
+ }
2068
+ // Unary minus
2069
+ if (ch === '-') {
2070
+ pos++;
2071
+ return -toNum(parseFactor());
2072
+ }
2073
+ // Unary plus
2074
+ if (ch === '+') {
2075
+ pos++;
2076
+ return toNum(parseFactor());
2077
+ }
2078
+ // Field ref: $N or $NF
2079
+ if (ch === '$') {
2080
+ pos++;
2081
+ skipWs();
2082
+ if (text.startsWith('NF', pos)) {
2083
+ pos += 2;
2084
+ return st.fields[st.NF] ?? '';
2085
+ }
2086
+ // Parens around index? $($1+1) etc — not supported, just digits.
2087
+ let nStart = pos;
2088
+ while (pos < text.length && /[0-9]/.test(text[pos])) pos++;
2089
+ if (nStart === pos) throw new Error(`expected field index after $ at "${text.slice(pos, pos + 10)}"`);
2090
+ const idx = parseInt(text.slice(nStart, pos), 10);
2091
+ return st.fields[idx] ?? '';
2092
+ }
2093
+ // Identifier: NR, NF, user var
2094
+ if (/[A-Za-z_]/.test(ch)) {
2095
+ let start = pos;
2096
+ while (pos < text.length && /[A-Za-z0-9_]/.test(text[pos])) pos++;
2097
+ const name = text.slice(start, pos);
2098
+ if (name === 'NR') return st.NR;
2099
+ if (name === 'NF') return st.NF;
2100
+ return st.vars[name] !== undefined ? st.vars[name] : 0;
2101
+ }
2102
+ throw new Error(`unexpected '${ch}' at "${text.slice(pos, pos + 20)}"`);
2103
+ }
2104
+ function toNum(v: any): number {
2105
+ if (typeof v === 'number') return v;
2106
+ const n = parseFloat(v);
2107
+ return Number.isFinite(n) ? n : 0;
2108
+ }
2109
+ try {
2110
+ const v = parseExpr();
2111
+ skipWs();
2112
+ if (pos < text.length) {
2113
+ // Trailing junk — could be intentional (e.g. tail of stmt is
2114
+ // separator). Be permissive — return what we have.
2115
+ }
2116
+ return v;
2117
+ } catch (e: any) {
2118
+ throw new Error(`expr error: ${e?.message || e} in "${expr}"`);
2119
+ }
2120
+ }
2121
+ function stripStringsForScan(s: string): string {
2122
+ // Replace string contents with same-length spaces so positions stay aligned.
2123
+ let out = '';
2124
+ let i = 0;
2125
+ while (i < s.length) {
2126
+ const ch = s[i];
2127
+ if (ch === '"' || ch === "'") {
2128
+ out += ch;
2129
+ i++;
2130
+ while (i < s.length && s[i] !== ch) {
2131
+ if (s[i] === '\\') { out += ' '; i++; }
2132
+ out += ' ';
2133
+ i++;
2134
+ }
2135
+ if (i < s.length) { out += ch; i++; }
2136
+ } else {
2137
+ out += ch;
2138
+ i++;
2139
+ }
2140
+ }
2141
+ return out;
2142
+ }
2143
+ function remapUserVars(s: string): string {
2144
+ // Find identifiers (a-z_), skip ones that are reserved or already
2145
+ // remapped. The simple approach: scan tokens outside string
2146
+ // literals.
2147
+ const RESERVED = new Set([
2148
+ '__f', '__nr', '__nf', '__v',
2149
+ 'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
2150
+ 'Math', 'String', 'Number', 'Array', 'Object',
2151
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
2152
+ 'length', // for str/array .length access — not a free identifier here
2153
+ ]);
2154
+ let out = '';
2155
+ let i = 0;
2156
+ while (i < s.length) {
2157
+ const ch = s[i];
2158
+ if (ch === '"' || ch === "'") {
2159
+ out += ch;
2160
+ i++;
2161
+ while (i < s.length && s[i] !== ch) {
2162
+ if (s[i] === '\\') { out += s[i]; i++; }
2163
+ out += s[i]; i++;
2164
+ }
2165
+ if (i < s.length) { out += s[i]; i++; }
2166
+ continue;
2167
+ }
2168
+ if (/[A-Za-z_]/.test(ch)) {
2169
+ let start = i;
2170
+ while (i < s.length && /[A-Za-z0-9_]/.test(s[i])) i++;
2171
+ const ident = s.slice(start, i);
2172
+ // Skip if previous non-ws char is `.` (member access).
2173
+ let prev = start - 1;
2174
+ while (prev >= 0 && /\s/.test(out[prev])) prev--;
2175
+ if (out[prev] === '.') { out += ident; continue; }
2176
+ if (RESERVED.has(ident)) { out += ident; continue; }
2177
+ // Replace with (__v.ident !== undefined ? __v.ident : 0)
2178
+ out += `(__v.${ident}!==undefined?__v.${ident}:0)`;
2179
+ continue;
2180
+ }
2181
+ out += ch;
2182
+ i++;
2183
+ }
2184
+ return out;
2185
+ }
2186
+ function splitStmts(body: string): string[] {
2187
+ const stmts: string[] = [];
2188
+ let depth = 0;
2189
+ let cur = '';
2190
+ let i = 0;
2191
+ while (i < body.length) {
2192
+ const ch = body[i];
2193
+ if (ch === '"' || ch === "'") {
2194
+ cur += ch;
2195
+ i++;
2196
+ while (i < body.length && body[i] !== ch) {
2197
+ if (body[i] === '\\') { cur += body[i]; i++; }
2198
+ cur += body[i]; i++;
2199
+ }
2200
+ if (i < body.length) { cur += body[i]; i++; }
2201
+ continue;
2202
+ }
2203
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
2204
+ else if (ch === ')' || ch === ']' || ch === '}') depth--;
2205
+ if (depth === 0 && (ch === ';' || ch === '\n')) {
2206
+ const t = cur.trim();
2207
+ if (t) stmts.push(t);
2208
+ cur = '';
2209
+ i++;
2210
+ continue;
2211
+ }
2212
+ cur += ch;
2213
+ i++;
2214
+ }
2215
+ const t = cur.trim();
2216
+ if (t) stmts.push(t);
2217
+ return stmts;
2218
+ }
2219
+ function execStmt(stmt: string, st: State): void {
2220
+ // print [expr[, expr]*]
2221
+ if (stmt === 'print' || stmt.startsWith('print ') || stmt.startsWith('print\t')) {
2222
+ const rest = stmt.slice(5).trim();
2223
+ if (!rest) { ctx.stdout.write(st.fields[0] + '\n'); st.printed = true; return; }
2224
+ // Comma-separated exprs (space joiner). We must split at top-level commas only.
2225
+ const parts = splitTopLevel(rest, ',');
2226
+ const out = parts.map(p => stringify(evalExpr(p, st))).join(' ');
2227
+ ctx.stdout.write(out + '\n');
2228
+ st.printed = true;
2229
+ return;
2230
+ }
2231
+ // printf "fmt", arg, arg, ...
2232
+ if (stmt.startsWith('printf ') || stmt.startsWith('printf(')) {
2233
+ let rest = stmt.startsWith('printf(') ? stmt.slice(7).replace(/\)\s*$/, '') : stmt.slice(7);
2234
+ rest = rest.trim();
2235
+ const parts = splitTopLevel(rest, ',');
2236
+ if (parts.length === 0) return;
2237
+ const fmt = evalExpr(parts[0], st);
2238
+ const fargs = parts.slice(1).map(p => evalExpr(p, st));
2239
+ ctx.stdout.write(printfFormat(String(fmt), fargs));
2240
+ st.printed = true;
2241
+ return;
2242
+ }
2243
+ // next: skip rest of body (no-op here since we re-enter each block fresh)
2244
+ if (stmt === 'next') return;
2245
+ // assignment: IDENT [op]= EXPR
2246
+ // We require a top-level `=` not part of `==` `<=` `>=` `!=`.
2247
+ const eqIdx = findAssignmentEq(stmt);
2248
+ if (eqIdx > 0) {
2249
+ const lhs = stmt.slice(0, eqIdx).trim();
2250
+ const rhs = stmt.slice(eqIdx + 1).trim();
2251
+ // Compound: lhs ends with op (e.g. `sum +`).
2252
+ let op: string | null = null;
2253
+ let name = lhs;
2254
+ if (/[+\-*/%]$/.test(lhs)) {
2255
+ op = lhs[lhs.length - 1];
2256
+ name = lhs.slice(0, -1).trim();
2257
+ }
2258
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
2259
+ throw new Error(`bad assignment target "${name}"`);
2260
+ }
2261
+ const rv = evalExpr(rhs, st);
2262
+ if (op) {
2263
+ const cur = st.vars[name] !== undefined ? st.vars[name] : 0;
2264
+ const lhsNum = typeof cur === 'number' ? cur : parseFloat(cur);
2265
+ const rvNum = typeof rv === 'number' ? rv : parseFloat(rv);
2266
+ const lN = Number.isFinite(lhsNum) ? lhsNum : 0;
2267
+ const rN = Number.isFinite(rvNum) ? rvNum : 0;
2268
+ st.vars[name] =
2269
+ op === '+' ? lN + rN :
2270
+ op === '-' ? lN - rN :
2271
+ op === '*' ? lN * rN :
2272
+ op === '/' ? lN / rN :
2273
+ op === '%' ? lN % rN : rv;
2274
+ } else {
2275
+ st.vars[name] = rv;
2276
+ }
2277
+ return;
2278
+ }
2279
+ // Bare expression — evaluate for side effects (rare in awk).
2280
+ evalExpr(stmt, st);
2281
+ }
2282
+ function findAssignmentEq(s: string): number {
2283
+ let depth = 0;
2284
+ for (let i = 0; i < s.length; i++) {
2285
+ const ch = s[i];
2286
+ if (ch === '"' || ch === "'") {
2287
+ i++;
2288
+ while (i < s.length && s[i] !== ch) {
2289
+ if (s[i] === '\\') i++;
2290
+ i++;
2291
+ }
2292
+ continue;
2293
+ }
2294
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
2295
+ else if (ch === ')' || ch === ']' || ch === '}') depth--;
2296
+ if (depth === 0 && ch === '=') {
2297
+ const next = s[i + 1];
2298
+ const prev = s[i - 1];
2299
+ if (next === '=' || prev === '=' || prev === '!' || prev === '<' || prev === '>') continue;
2300
+ return i;
2301
+ }
2302
+ }
2303
+ return -1;
2304
+ }
2305
+ function splitTopLevel(s: string, sep: string): string[] {
2306
+ const out: string[] = [];
2307
+ let depth = 0;
2308
+ let cur = '';
2309
+ let i = 0;
2310
+ while (i < s.length) {
2311
+ const ch = s[i];
2312
+ if (ch === '"' || ch === "'") {
2313
+ cur += ch;
2314
+ i++;
2315
+ while (i < s.length && s[i] !== ch) {
2316
+ if (s[i] === '\\') { cur += s[i]; i++; }
2317
+ cur += s[i]; i++;
2318
+ }
2319
+ if (i < s.length) { cur += s[i]; i++; }
2320
+ continue;
2321
+ }
2322
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
2323
+ else if (ch === ')' || ch === ']' || ch === '}') depth--;
2324
+ if (depth === 0 && ch === sep) { out.push(cur.trim()); cur = ''; i++; continue; }
2325
+ cur += ch; i++;
2326
+ }
2327
+ if (cur.trim()) out.push(cur.trim());
2328
+ return out;
2329
+ }
2330
+ function stringify(v: any): string {
2331
+ if (v === undefined || v === null) return '';
2332
+ if (typeof v === 'number') {
2333
+ if (Number.isInteger(v)) return String(v);
2334
+ // awk's OFMT default is "%.6g"
2335
+ return printfFormat('%.6g', [v]);
2336
+ }
2337
+ return String(v);
2338
+ }
2339
+ function printfFormat(fmt: string, fargs: any[]): string {
2340
+ let out = '';
2341
+ let i = 0;
2342
+ let argIdx = 0;
2343
+ while (i < fmt.length) {
2344
+ const ch = fmt[i];
2345
+ if (ch === '\\' && i + 1 < fmt.length) {
2346
+ const esc = fmt[i + 1];
2347
+ out += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc === 'r' ? '\r' : esc === '\\' ? '\\' : esc;
2348
+ i += 2;
2349
+ continue;
2350
+ }
2351
+ if (ch === '%' && i + 1 < fmt.length) {
2352
+ // Parse: %[flags][width][.prec]specifier
2353
+ let spec = '%';
2354
+ i++;
2355
+ while (i < fmt.length && /[-+ 0#]/.test(fmt[i])) { spec += fmt[i]; i++; }
2356
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) { spec += fmt[i]; i++; }
2357
+ if (fmt[i] === '.') { spec += fmt[i]; i++; while (i < fmt.length && /[0-9]/.test(fmt[i])) { spec += fmt[i]; i++; } }
2358
+ const conv = fmt[i];
2359
+ i++;
2360
+ if (conv === '%') { out += '%'; continue; }
2361
+ const arg = fargs[argIdx++];
2362
+ out += formatOne(spec + conv, arg);
2363
+ continue;
2364
+ }
2365
+ out += ch;
2366
+ i++;
2367
+ }
2368
+ return out;
2369
+ }
2370
+ function formatOne(spec: string, arg: any): string {
2371
+ const conv = spec[spec.length - 1];
2372
+ const flagsAndWidth = spec.slice(1, -1);
2373
+ const dotIdx = flagsAndWidth.indexOf('.');
2374
+ const widthPart = dotIdx >= 0 ? flagsAndWidth.slice(0, dotIdx) : flagsAndWidth;
2375
+ const precPart = dotIdx >= 0 ? flagsAndWidth.slice(dotIdx + 1) : '';
2376
+ let flags = '';
2377
+ let widthStr = '';
2378
+ for (const c of widthPart) {
2379
+ if (/[-+ 0#]/.test(c)) flags += c;
2380
+ else widthStr += c;
2381
+ }
2382
+ const width = widthStr ? parseInt(widthStr, 10) : 0;
2383
+ const prec = precPart ? parseInt(precPart, 10) : -1;
2384
+ let body: string;
2385
+ switch (conv) {
2386
+ case 's': body = String(arg ?? ''); if (prec >= 0) body = body.slice(0, prec); break;
2387
+ case 'd': case 'i': {
2388
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(arg));
2389
+ body = String(Number.isFinite(n) ? n : 0);
2390
+ break;
2391
+ }
2392
+ case 'f': {
2393
+ const n = typeof arg === 'number' ? arg : parseFloat(arg);
2394
+ const p = prec < 0 ? 6 : prec;
2395
+ body = (Number.isFinite(n) ? n : 0).toFixed(p);
2396
+ break;
2397
+ }
2398
+ case 'g': {
2399
+ const n = typeof arg === 'number' ? arg : parseFloat(arg);
2400
+ const p = prec < 0 ? 6 : prec;
2401
+ body = (Number.isFinite(n) ? n : 0).toPrecision(p).replace(/\.?0+$/, '');
2402
+ break;
2403
+ }
2404
+ case 'x': {
2405
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(arg));
2406
+ body = (Number.isFinite(n) ? n : 0).toString(16);
2407
+ break;
2408
+ }
2409
+ case 'o': {
2410
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(arg));
2411
+ body = (Number.isFinite(n) ? n : 0).toString(8);
2412
+ break;
2413
+ }
2414
+ case 'c': {
2415
+ if (typeof arg === 'number') body = String.fromCharCode(arg);
2416
+ else body = String(arg).charAt(0);
2417
+ break;
2418
+ }
2419
+ default: body = String(arg);
2420
+ }
2421
+ if (width > body.length) {
2422
+ const pad = flags.includes('0') && (conv === 'd' || conv === 'i' || conv === 'f' || conv === 'x' || conv === 'o') ? '0' : ' ';
2423
+ body = flags.includes('-') ? body.padEnd(width, ' ') : body.padStart(width, pad);
2424
+ }
2425
+ return body;
2426
+ }
2427
+
2428
+ const state: State = {
2429
+ vars: {},
2430
+ fields: [],
2431
+ NR: 0,
2432
+ NF: 0,
2433
+ printed: false,
2434
+ };
2435
+
2436
+ function runBlock(block: Block): void {
2437
+ const stmts = splitStmts(block.body);
2438
+ for (const s of stmts) {
2439
+ execStmt(s, state);
2440
+ }
2441
+ }
2442
+
2443
+ try {
2444
+ // BEGIN blocks first.
2445
+ for (const b of blocks) if (b.kind === 'BEGIN') runBlock(b);
2446
+ // Main loop over input lines.
2447
+ const lines = input.split('\n');
2448
+ // awk default: drop the final empty line if input ended with \n.
2449
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
2450
+ for (let li = 0; li < lines.length; li++) {
2451
+ const line = lines[li];
2452
+ const parts = typeof separator === 'string' && separator.length === 1
2453
+ ? line.split(separator)
2454
+ : line.split(separator);
2455
+ state.NR = li + 1;
2456
+ state.NF = parts.filter(p => p !== '').length;
2457
+ state.fields = [line, ...parts];
2458
+ for (const b of blocks) {
2459
+ if (b.kind === 'BEGIN' || b.kind === 'END') continue;
2460
+ if (b.kind === 'PATTERN') {
2461
+ if (b.pattern!.test(line)) runBlock(b);
2462
+ } else {
2463
+ // MAIN block (no pattern) — always runs.
2464
+ runBlock(b);
2465
+ }
2466
+ }
2467
+ }
2468
+ // END blocks last.
2469
+ for (const b of blocks) if (b.kind === 'END') runBlock(b);
2470
+ } catch (e: any) {
2471
+ ctx.stderr.write(`awk: ${e?.message || e}\n`);
2472
+ return 1;
2473
+ }
2474
+
2475
+ return 0;
2476
+ };
2477
+ }
2478
+
2479
+ /**
2480
+ * shell compatibility (2026-05-11): real xargs implementation.
2481
+ *
2482
+ * Pre-fix the impl printed the command-line it WOULD execute and
2483
+ * returned 0. Real xargs runs the command, possibly batched (-n),
2484
+ * with arguments substituted (-I).
2485
+ *
2486
+ * We do cross-command dispatch through the shell registry, so xargs can drive
2487
+ * `echo`, `cat`, `rm`, `seq`, lazy-loaded builtins — anything in the registry. The execution
2488
+ * runs IN-SUPERVISOR (not through facet spawn) which means it
2489
+ * works for pure-builtins but NOT for facet-direct commands like
2490
+ * `node`, `git`, `npm` (the registry resolver returns those by
2491
+ * name but invoking them requires the cp/facet pipeline).
2492
+ *
2493
+ * Supported flags:
2494
+ * -n NUM run command with at most NUM args per invocation
2495
+ * -I REPL replace REPL in command with the input item
2496
+ * -0 null-byte separator (rare; bash xargs -0 idiom)
2497
+ * default args use args.split(/\s+/) from stdin
2498
+ *
2499
+ * Unsupported (document as gap): -P (parallel), -L (per-line), -p (prompt).
2500
+ */
2501
+ function mkXargs(vfs: UnixVfs, registry: any): CmdFn {
2502
+ return async (ctx) => {
2503
+ const input = (ctx.stdin || '').trim();
2504
+ if (!input) return 0;
2505
+
2506
+ // Parse flags first
2507
+ const args = [...ctx.args];
2508
+ let batchSize = Infinity;
2509
+ let replaceTok: string | null = null;
2510
+ let nullSep = false;
2511
+ while (args.length > 0 && args[0].startsWith('-')) {
2512
+ const a = args.shift()!;
2513
+ if (a === '-n') {
2514
+ const n = parseInt(args.shift() || '', 10);
2515
+ if (Number.isFinite(n) && n > 0) batchSize = n;
2516
+ } else if (a.startsWith('-n')) {
2517
+ const n = parseInt(a.slice(2), 10);
2518
+ if (Number.isFinite(n) && n > 0) batchSize = n;
2519
+ } else if (a === '-I') {
2520
+ replaceTok = args.shift() || '{}';
2521
+ batchSize = 1; // -I implies one-arg-per-invocation
2522
+ } else if (a === '-0' || a === '--null') {
2523
+ nullSep = true;
2524
+ } else if (a === '--') {
2525
+ break;
2526
+ } else {
2527
+ // Unknown flag — push back as cmd token (best-effort behavior)
2528
+ args.unshift(a);
2529
+ break;
2530
+ }
2531
+ }
2532
+
2533
+ // Remaining args: cmd + initial-args. Default: echo.
2534
+ const cmdName = args.shift() || 'echo';
2535
+ const cmdArgsInitial = args;
2536
+
2537
+ // Split stdin into items
2538
+ const items = nullSep
2539
+ ? input.split('\u0000').filter(Boolean)
2540
+ : input.split(/\s+/).filter(Boolean);
2541
+
2542
+ // Resolve target command from registry (handles both eager + lazy maps).
2543
+ let target;
2544
+ try {
2545
+ target = await registry.resolve(cmdName);
2546
+ } catch (_e) { target = null; }
2547
+ if (!target) {
2548
+ // Defer to write-to-stderr; mimic real xargs which would exec(2) and fail.
2549
+ ctx.stderr.write(`xargs: ${cmdName}: command not found\n`);
2550
+ return 127;
2551
+ }
2552
+
2553
+ // Run in batches.
2554
+ const newCtx = (newArgs: string[]) => ({
2555
+ pid: ctx.pid,
2556
+ cred: ctx.cred,
2557
+ args: newArgs,
2558
+ env: ctx.env,
2559
+ cwd: ctx.cwd,
2560
+ vfs: ctx.vfs,
2561
+ stdout: ctx.stdout,
2562
+ stderr: ctx.stderr,
2563
+ stdin: '', // xargs doesn't pipe its own stdin to children
2564
+ signal: ctx.signal,
2565
+ setUmask: ctx.setUmask,
2566
+ runAs: ctx.runAs,
2567
+ execInterpreterDepth: ctx.execInterpreterDepth,
2568
+ });
2569
+
2570
+ let exit = 0;
2571
+ if (replaceTok) {
2572
+ // -I: one invocation per item, replacing token in initial args.
2573
+ for (const item of items) {
2574
+ const subbed = cmdArgsInitial.map(a => a.split(replaceTok!).join(item));
2575
+ try {
2576
+ const code = await target(newCtx(subbed));
2577
+ if (typeof code === 'number' && code !== 0) exit = code;
2578
+ } catch (e: any) {
2579
+ ctx.stderr.write(`xargs: ${cmdName}: ${e?.message || e}\n`);
2580
+ exit = 1;
2581
+ }
2582
+ }
2583
+ } else {
2584
+ // -n N (or unlimited): batch items, append to initial args.
2585
+ const step = Number.isFinite(batchSize) ? batchSize : items.length;
2586
+ for (let i = 0; i < items.length; i += step) {
2587
+ const batch = items.slice(i, i + step);
2588
+ try {
2589
+ const code = await target(newCtx([...cmdArgsInitial, ...batch]));
2590
+ if (typeof code === 'number' && code !== 0) exit = code;
2591
+ } catch (e: any) {
2592
+ ctx.stderr.write(`xargs: ${cmdName}: ${e?.message || e}\n`);
2593
+ exit = 1;
2594
+ }
2595
+ if (!Number.isFinite(batchSize)) break; // single batch when no -n
2596
+ }
2597
+ }
2598
+ return exit;
2599
+ };
2600
+ }
2601
+
2602
+ function mkTee(vfs: UnixVfs): CmdFn {
2603
+ return (ctx) => {
2604
+ const input = ctx.stdin || '';
2605
+ const append = ctx.args.includes('-a');
2606
+ const files = ctx.args.filter(a => !a.startsWith('-'));
2607
+ ctx.stdout.write(input);
2608
+ for (const f of files) {
2609
+ const fp = resolvePath(ctx.cwd, f);
2610
+ if (append && vfs.exists(fp)) {
2611
+ const existing = vfs.readFileString(fp);
2612
+ vfs.writeFile(fp, existing + input);
2613
+ } else {
2614
+ vfs.writeFile(fp, input);
2615
+ }
2616
+ }
2617
+ return 0;
2618
+ };
2619
+ }
2620
+
2621
+ /**
2622
+ * shell compatibility (2026-05-11): du flag parsing for combined forms.
2623
+ * Pre-fix `du -sh` didn't activate -h because we checked for literal
2624
+ * `-h` only — `-sh` is a single arg containing both flags. POSIX
2625
+ * conformant short-flag stacking.
2626
+ */
2627
+ function mkDu(vfs: UnixVfs): CmdFn {
2628
+ return (ctx) => {
2629
+ // Parse flags supporting stacked short flags like `-sh`, `-ah`.
2630
+ let showAll = false, human = false, sumOnly = false;
2631
+ const positional: string[] = [];
2632
+ for (const a of ctx.args) {
2633
+ if (a.startsWith('-') && a !== '-' && !a.startsWith('--')) {
2634
+ for (const ch of a.slice(1)) {
2635
+ if (ch === 'a') showAll = true;
2636
+ else if (ch === 'h') human = true;
2637
+ else if (ch === 's') sumOnly = true;
2638
+ }
2639
+ } else if (a.startsWith('--')) {
2640
+ if (a === '--all') showAll = true;
2641
+ else if (a === '--human-readable') human = true;
2642
+ else if (a === '--summarize') sumOnly = true;
2643
+ } else {
2644
+ positional.push(a);
2645
+ }
2646
+ }
2647
+ const target = positional[0] || '.';
2648
+ const root = resolvePath(ctx.cwd, target);
2649
+ const fmt = (b: number) => human ? (b >= 1e6 ? (b / 1e6).toFixed(1) + 'M' : b >= 1e3 ? (b / 1e3).toFixed(1) + 'K' : b + 'B') : String(Math.ceil(b / 1024));
2650
+ let total = 0;
2651
+ function walk(path: string): number {
2652
+ let size = 0;
2653
+ try {
2654
+ const entries = vfs.readdir(path);
2655
+ for (const e of entries) {
2656
+ const fp = path + '/' + e.name;
2657
+ if (e.type === 'directory') {
2658
+ const dirSize = walk(fp);
2659
+ size += dirSize;
2660
+ if (!sumOnly) ctx.stdout.write(`${fmt(dirSize)}\t/${fp}\n`);
2661
+ } else {
2662
+ try {
2663
+ const st = vfs.stat(fp);
2664
+ size += st.size;
2665
+ if (showAll && !sumOnly) ctx.stdout.write(`${fmt(st.size)}\t/${fp}\n`);
2666
+ } catch {}
2667
+ }
2668
+ }
2669
+ } catch {}
2670
+ return size;
2671
+ }
2672
+ total = walk(root);
2673
+ if (sumOnly || !showAll) ctx.stdout.write(`${fmt(total)}\t/${root}\n`);
2674
+ return 0;
2675
+ };
2676
+ }
2677
+
2678
+ function mkDiff(vfs: UnixVfs): CmdFn {
2679
+ return (ctx) => {
2680
+ if (ctx.args.length < 2) { ctx.stderr.write('Usage: diff FILE1 FILE2\n'); return 1; }
2681
+ const f1 = resolvePath(ctx.cwd, ctx.args[0]);
2682
+ const f2 = resolvePath(ctx.cwd, ctx.args[1]);
2683
+ try {
2684
+ const a = vfs.readFileString(f1).split('\n');
2685
+ const b = vfs.readFileString(f2).split('\n');
2686
+ let hasDiff = false;
2687
+ const maxLen = Math.max(a.length, b.length);
2688
+ for (let i = 0; i < maxLen; i++) {
2689
+ if (a[i] !== b[i]) {
2690
+ hasDiff = true;
2691
+ if (a[i] !== undefined && b[i] === undefined) ctx.stdout.write(`${i + 1}d${i}\n< ${a[i]}\n`);
2692
+ else if (a[i] === undefined && b[i] !== undefined) ctx.stdout.write(`${i}a${i + 1}\n> ${b[i]}\n`);
2693
+ else ctx.stdout.write(`${i + 1}c${i + 1}\n< ${a[i]}\n---\n> ${b[i]}\n`);
2694
+ }
2695
+ }
2696
+ return hasDiff ? 1 : 0;
2697
+ } catch (e: any) { ctx.stderr.write(`diff: ${e.message}\n`); return 2; }
2698
+ };
2699
+ }
2700
+
2701
+ /**
2702
+ * shell compatibility (2026-05-11): POSIX rm with proper -f semantics.
2703
+ *
2704
+ * The original rm implementation called `r.vfs.stat(...)` and caught `e instanceof VFSError`.
2705
+ * Our SqliteVFSProvider's stat method delegates to SqliteVFS.stat which
2706
+ * throws raw `Error("ENOENT: ...")` — NOT VFSError. That rm path
2707
+ * therefore falls through to `else throw e`, the error propagates up,
2708
+ * and executeCommand returns exit 1.
2709
+ *
2710
+ * Real-world impact: every `rm -rf <nonexistent> && ...` short-circuits.
2711
+ * The most common cleanup idiom in shell scripts.
2712
+ *
2713
+ * Fix: register rm in the registry's `commands` map. Treat -f silently when target is
2714
+ * missing (return 0). Handle both files (unlink) and directories
2715
+ * (rmdir recursive when -r). Translate raw errors so the unix-command
2716
+ * contract is honoured.
2717
+ */
2718
+ /**
2719
+ * shell compatibilityb (2026-05-11): registry-level echo so `X | xargs echo`
2720
+ * resolves. `echo` is a Shell.builtins entry, NOT in the
2721
+ * registry map. xargs's cross-command dispatch goes through
2722
+ * registry.resolve(name) — without a registry entry for echo it falls
2723
+ * back to 'command not found'. The init.ts override for echo flag
2724
+ * handling targets Shell.builtins; we additionally register a copy
2725
+ * here so registry-driven callers (xargs) can find it. Behaviour
2726
+ * matches the BUG-SWEEP-4 nimbusEcho impl: -n / -e / -E / combined.
2727
+ */
2728
+ function mkEcho(): CmdFn {
2729
+ return (ctx) => {
2730
+ const args = ctx.args;
2731
+ let interpretEscapes = false;
2732
+ let suppressNewline = false;
2733
+ let i = 0;
2734
+ while (i < args.length) {
2735
+ const a = args[i];
2736
+ if (a === '--') { i++; break; }
2737
+ if (a === '-n') { suppressNewline = true; i++; continue; }
2738
+ if (a === '-e') { interpretEscapes = true; i++; continue; }
2739
+ if (a === '-E') { interpretEscapes = false; i++; continue; }
2740
+ if (/^-[neE]+$/.test(a)) {
2741
+ for (const ch of a.slice(1)) {
2742
+ if (ch === 'n') suppressNewline = true;
2743
+ else if (ch === 'e') interpretEscapes = true;
2744
+ else if (ch === 'E') interpretEscapes = false;
2745
+ }
2746
+ i++;
2747
+ continue;
2748
+ }
2749
+ break;
2750
+ }
2751
+ let out = args.slice(i).join(' ');
2752
+ 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, '\\');
2765
+ }
2766
+ ctx.stdout.write(suppressNewline ? out : out + '\n');
2767
+ return 0;
2768
+ };
2769
+ }
2770
+
2771
+ /**
2772
+ * SHELL-R6-4 (2026-05-12): symlink-aware `ls`.
2773
+ *
2774
+ * Pre-fix: the lazy `ls` implementation formatted `mode` with first-char
2775
+ * `d` or `-`; it had no symlink concept,
2776
+ * and our SymlinkRegistry entries are not in the VFS dir listing at all,
2777
+ * so `ls -l` after `ln -s t.txt l.txt` showed ONLY `t.txt`.
2778
+ *
2779
+ * Post-fix:
2780
+ * - `ls` lists VFS dir entries AND SymlinkRegistry entries whose link
2781
+ * path lives in the queried directory.
2782
+ * - `ls -l` shows `lrwxrwxrwx 1 user user N <mtime> <name> -> <target>`
2783
+ * for symlinks (`N` = target string length, matches GNU coreutils).
2784
+ * - Non-symlink rows go through the same formatter so columns line up.
2785
+ * - Hidden-file rule (skip if leading `.`) still honored unless `-a`.
2786
+ *
2787
+ * Args supported: `-l` long, `-a` all, `-1` one-per-line, `-n` numeric
2788
+ * ownership, `-d` directory itself, plus path
2789
+ * positional. Matches the shell `ls` flag surface so we don't regress.
2790
+ */
2791
+ function mkLs(vfs: UnixVfs): CmdFn {
2792
+ return (ctx) => {
2793
+ const args = ctx.args;
2794
+ const flags = new Set(args
2795
+ .filter((arg) => arg.startsWith('-') && !arg.startsWith('--'))
2796
+ .flatMap((arg) => [...arg.slice(1)]));
2797
+ const flagLong = flags.has('l') || flags.has('n');
2798
+ const flagAll = flags.has('a');
2799
+ const flagOne = flags.has('1');
2800
+ const flagNumeric = flags.has('n');
2801
+ const flagDirectory = flags.has('d');
2802
+ const positionals = args.filter(a => !a.startsWith('-'));
2803
+ const targets = positionals.length > 0 ? positionals : [ctx.cwd];
2804
+
2805
+ const reg = vfs.symlinks;
2806
+ const kvfs: any = (ctx as any).vfs;
2807
+
2808
+ function fmtTime(mtime: number): string {
2809
+ const d = new Date(mtime);
2810
+ const mon = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][d.getMonth()];
2811
+ const day = String(d.getDate()).padStart(2, ' ');
2812
+ const hh = String(d.getHours()).padStart(2, '0');
2813
+ const mm = String(d.getMinutes()).padStart(2, '0');
2814
+ return `${mon} ${day} ${hh}:${mm}`;
2815
+ }
2816
+
2817
+ type Entry = {
2818
+ name: string;
2819
+ type: 'file' | 'directory' | 'symlink';
2820
+ size: number;
2821
+ mtime: number;
2822
+ mode: number;
2823
+ uid: number;
2824
+ gid: number;
2825
+ linkTarget?: string;
2826
+ };
2827
+
2828
+ let exit = 0;
2829
+
2830
+ function listDir(dirPath: string): Entry[] {
2831
+ const fp = resolvePath(ctx.cwd, dirPath);
2832
+ const out: Entry[] = [];
2833
+ // Real entries via ctx.vfs.readdirStat (Kernel.VFS — handles
2834
+ // mounts like /dev) with fallback to closure-captured SqliteVFS.
2835
+ let real: any[] = [];
2836
+ try {
2837
+ if (kvfs && typeof kvfs.readdirStat === 'function') {
2838
+ real = kvfs.readdirStat(fp);
2839
+ } else {
2840
+ const names = vfs.readdir(fp);
2841
+ real = names.map(n => {
2842
+ const childPath = (fp === '/' ? '' : fp.replace(/^\/+/, '').replace(/\/+$/, ''))
2843
+ + '/' + n.name;
2844
+ try {
2845
+ 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 };
2849
+ } catch {
2850
+ return { name: n.name, type: n.type, size: 0, mtime: Date.now(), mode: 0o644,
2851
+ uid: ctx.cred.uid, gid: ctx.cred.gid };
2852
+ }
2853
+ });
2854
+ }
2855
+ } catch (e: any) {
2856
+ ctx.stderr.write(`ls: cannot access '${dirPath}': ${fsErrorMessage(e)}\n`);
2857
+ exit = 2;
2858
+ return [];
2859
+ }
2860
+ for (const r of real) {
2861
+ const type = r.type === 'directory'
2862
+ ? 'directory'
2863
+ : r.type === 'symlink'
2864
+ ? 'symlink'
2865
+ : 'file';
2866
+ const childPath = fp ? `${fp}/${r.name}` : r.name;
2867
+ out.push({
2868
+ name: r.name,
2869
+ type,
2870
+ size: r.size ?? 0,
2871
+ mtime: r.mtime ?? Date.now(),
2872
+ mode: r.mode ?? 0o644,
2873
+ uid: r.uid ?? ctx.cred.uid,
2874
+ gid: r.gid ?? ctx.cred.gid,
2875
+ ...(type === 'symlink'
2876
+ ? { linkTarget: readSymlinkTarget(vfs, childPath) ?? undefined }
2877
+ : {}),
2878
+ });
2879
+ }
2880
+ // Inject symlink entries whose link path is in this directory.
2881
+ const normDir = fp.replace(/^\/+/, '').replace(/\/+$/, '');
2882
+ for (const { link, target } of reg.list()) {
2883
+ const linkNorm = link.replace(/^\/+/, '').replace(/\/+$/, '');
2884
+ const lastSlash = linkNorm.lastIndexOf('/');
2885
+ const linkDir = lastSlash >= 0 ? linkNorm.substring(0, lastSlash) : '';
2886
+ if (linkDir === normDir) {
2887
+ const linkName = lastSlash >= 0 ? linkNorm.substring(lastSlash + 1) : linkNorm;
2888
+ if (out.some(entry => entry.name === linkName)) continue;
2889
+ // Filter dotfiles unless -a (consistent with real entries).
2890
+ if (!flagAll && linkName.startsWith('.')) continue;
2891
+ out.push({
2892
+ name: linkName,
2893
+ type: 'symlink',
2894
+ size: target.length,
2895
+ mtime: Date.now(),
2896
+ mode: 0o777,
2897
+ uid: ctx.cred.uid,
2898
+ gid: ctx.cred.gid,
2899
+ linkTarget: target,
2900
+ });
2901
+ }
2902
+ }
2903
+ // Filter dotfiles among real entries unless -a.
2904
+ const filtered = flagAll ? out : out.filter(e => !e.name.startsWith('.'));
2905
+ filtered.sort((a, b) => a.name.localeCompare(b.name));
2906
+ return filtered;
2907
+ }
2908
+
2909
+ function fmtRow(e: Entry, long: boolean): string {
2910
+ if (!long) return e.name;
2911
+ const isDir = e.type === 'directory';
2912
+ const isLink = e.type === 'symlink';
2913
+ const mode = unixModeString(e.mode, isDir, isLink);
2914
+ const size = String(e.size).padStart(6, ' ');
2915
+ const time = fmtTime(e.mtime);
2916
+ const arrow = isLink && e.linkTarget ? ` -> ${e.linkTarget}` : '';
2917
+ const user = flagNumeric ? String(e.uid) : unixUserLabel(vfs, e.uid);
2918
+ const group = flagNumeric ? String(e.gid) : unixGroupLabel(vfs, e.gid);
2919
+ return `${mode} 1 ${user} ${group} ${size} ${time} ${e.name}${arrow}`;
2920
+ }
2921
+
2922
+ // First pass: separate file-args from dir-args (real `ls` lists
2923
+ // each file inline; dirs get listed as their contents).
2924
+ const fileEntries: Entry[] = [];
2925
+ const dirArgs: string[] = [];
2926
+ for (const arg of targets) {
2927
+ const fp = resolvePath(ctx.cwd, arg);
2928
+ // Symlink check: a symlink-arg is displayed as the link itself
2929
+ // (without -L which we don't implement).
2930
+ const target = readSymlinkTarget(vfs, fp);
2931
+ if (target !== null) {
2932
+ fileEntries.push({
2933
+ name: arg,
2934
+ type: 'symlink',
2935
+ size: target.length,
2936
+ mtime: Date.now(),
2937
+ mode: 0o777,
2938
+ uid: ctx.cred.uid,
2939
+ gid: ctx.cred.gid,
2940
+ linkTarget: target,
2941
+ });
2942
+ continue;
2943
+ }
2944
+ try {
2945
+ const s: any = kvfs && typeof kvfs.stat === 'function' ? kvfs.stat(fp) : vfs.stat(fp);
2946
+ if (s.type === 'directory' && !flagDirectory) {
2947
+ dirArgs.push(arg);
2948
+ } else {
2949
+ fileEntries.push({
2950
+ name: arg,
2951
+ type: s.type === 'directory' ? 'directory' : 'file',
2952
+ size: s.size ?? 0,
2953
+ mtime: s.mtime ?? Date.now(),
2954
+ mode: s.mode ?? 0o644,
2955
+ uid: s.uid ?? ctx.cred.uid,
2956
+ gid: s.gid ?? ctx.cred.gid,
2957
+ });
2958
+ }
2959
+ } catch (e: any) {
2960
+ ctx.stderr.write(`ls: cannot access '${arg}': ${e?.message || e}\n`);
2961
+ exit = 1;
2962
+ }
2963
+ }
2964
+
2965
+ // Render file-args first.
2966
+ if (fileEntries.length > 0) {
2967
+ if (flagLong) {
2968
+ for (const e of fileEntries) ctx.stdout.write(fmtRow(e, true) + '\n');
2969
+ } else if (flagOne) {
2970
+ for (const e of fileEntries) ctx.stdout.write(e.name + '\n');
2971
+ } else {
2972
+ ctx.stdout.write(fileEntries.map(e => e.name).join(' ') + '\n');
2973
+ }
2974
+ }
2975
+ // Then dir-args (with header if multiple).
2976
+ for (let i = 0; i < dirArgs.length; i++) {
2977
+ const d = dirArgs[i];
2978
+ if (dirArgs.length > 1 || fileEntries.length > 0) {
2979
+ if (fileEntries.length > 0 || i > 0) ctx.stdout.write('\n');
2980
+ ctx.stdout.write(`${d}:\n`);
2981
+ }
2982
+ const rows = listDir(d);
2983
+ if (flagLong) {
2984
+ for (const e of rows) ctx.stdout.write(fmtRow(e, true) + '\n');
2985
+ } else if (flagOne) {
2986
+ for (const e of rows) ctx.stdout.write(e.name + '\n');
2987
+ } else if (rows.length > 0) {
2988
+ ctx.stdout.write(rows.map(e => e.name).join(' ') + '\n');
2989
+ }
2990
+ }
2991
+ return exit;
2992
+ };
2993
+ }
2994
+
2995
+ /**
2996
+ * shell compatibilityc (2026-05-11): registry-level cat (for xargs cross-
2997
+ * command dispatch). Behaves like the shell cat command: reads files (or
2998
+ * stdin if none), concatenates to stdout.
2999
+ *
3000
+ * Operands stream through one shared writer, so concatenating six 1 MB files
3001
+ * emits 6 MB in 64 KiB steps rather than materialising each file whole.
3002
+ */
3003
+ function mkCat(vfs: UnixVfs): CmdFn {
3004
+ return (ctx) => {
3005
+ const files = ctx.args.filter(a => !a.startsWith('-'));
3006
+ if (files.length === 0) {
3007
+ if (ctx.stdin) ctx.stdout.write(ctx.stdin);
3008
+ return 0;
3009
+ }
3010
+ // Resolve both native VFS symlinks and the legacy registry before reads.
3011
+ let exit = 0;
3012
+ const writer = new SinkWriter(ctx.stdout);
3013
+ for (const fOrig of files) {
3014
+ const f = (() => {
3015
+ const fp = resolvePath(ctx.cwd, fOrig);
3016
+ const resolved = resolveSymlinkPath(vfs, fp);
3017
+ if (resolved === null) {
3018
+ // ELOOP: too many hops
3019
+ ctx.stderr.write(`cat: ${fOrig}: Too many levels of symbolic links\n`);
3020
+ return null;
3021
+ }
3022
+ return resolved === fp ? fOrig : '/' + resolved;
3023
+ })();
3024
+ if (f === null) { exit = 1; continue; }
3025
+ try {
3026
+ const path = f.startsWith('/') ? f : `${ctx.cwd}/${f}`;
3027
+ const stat = ctx.vfs.stat(path);
3028
+ if (stat.type === 'directory') throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });
3029
+ if (stat.size > 0) {
3030
+ // A regular file's size is its exact extent — read precisely that.
3031
+ streamRange((offset, length) => ctx.vfs.readRange(path, offset, length), writer, {
3032
+ length: stat.size,
3033
+ signal: ctx.signal,
3034
+ });
3035
+ } else {
3036
+ // Size 0 covers empty files, /dev/null and synthesised /proc entries.
3037
+ // Endless character devices reject this unbounded read by design.
3038
+ writer.write(ctx.vfs.readFile(path));
3039
+ }
3040
+ } catch (error) {
3041
+ ctx.stderr.write(`cat: ${fOrig}: ${fsErrorMessage(error)}\n`);
3042
+ exit = 1;
3043
+ }
3044
+ }
3045
+ writer.end();
3046
+ return exit;
3047
+ };
3048
+ }
3049
+
3050
+ function mkRm(vfs: UnixVfs): CmdFn {
3051
+ return (ctx) => {
3052
+ const args = ctx.args;
3053
+ const recursive = args.some(a => a === '-r' || a === '-R' || a === '-rf' || a === '-Rf' || a === '-rR' || a === '--recursive' || (a.startsWith('-') && !a.startsWith('--') && (a.includes('r') || a.includes('R'))));
3054
+ const force = args.some(a => a === '-f' || a === '--force' || (a.startsWith('-') && !a.startsWith('--') && a.includes('f')));
3055
+ const targets = args.filter(a => !a.startsWith('-'));
3056
+ if (targets.length === 0) {
3057
+ if (force) return 0; // POSIX: rm -f with no operands is silent success
3058
+ ctx.stderr.write('rm: missing operand\n');
3059
+ return 1;
3060
+ }
3061
+ // SHELL-R6-5: SymlinkRegistry awareness. Real `rm` removes the
3062
+ // LINK, not the target. Pre-fix this loop went straight to
3063
+ // vfs.exists which is false for registry-only symlink entries
3064
+ // (no real file), producing "No such file or directory" while
3065
+ // `readlink` still reported the registry entry.
3066
+ const reg = vfs.symlinks;
3067
+ let exit = 0;
3068
+ for (const t of targets) {
3069
+ const fp = resolvePath(ctx.cwd, t);
3070
+ // Symlink path FIRST. If `fp` is registered as a symlink we
3071
+ // delete the registry entry and skip the vfs-level operations.
3072
+ // Real `rm` never follows symlinks (it removes the link
3073
+ // itself); recursive flag has no effect on the symlink itself
3074
+ // either — it acts on the link node only.
3075
+ if (reg.isSymlink(fp)) {
3076
+ reg.delete(fp);
3077
+ continue;
3078
+ }
3079
+ if (!vfs.exists(fp)) {
3080
+ if (force) continue; // silent success
3081
+ ctx.stderr.write(`rm: cannot remove '${t}': No such file or directory\n`);
3082
+ exit = 1;
3083
+ continue;
3084
+ }
3085
+ try {
3086
+ if (vfs.isDirectory(fp)) {
3087
+ if (!recursive) {
3088
+ ctx.stderr.write(`rm: cannot remove '${t}': Is a directory\n`);
3089
+ exit = 1;
3090
+ continue;
3091
+ }
3092
+ vfs.removeRecursive(fp);
3093
+ } else {
3094
+ vfs.unlink(fp);
3095
+ }
3096
+ } catch (e: any) {
3097
+ // -f suppresses ENOENT only (file disappeared mid-loop); other
3098
+ // errors (ENOTEMPTY because of a logic bug, ENOTDIR mismatches,
3099
+ // permission errors) must still surface. Pre-fix the broad
3100
+ // `if (force) continue` masked the readdir-iteration bug that
3101
+ // left directories undeleted.
3102
+ const msg = String(e?.message || e);
3103
+ if (force && /ENOENT/.test(msg)) continue;
3104
+ ctx.stderr.write(`rm: cannot remove '${t}': ${fsErrorMessage(e)}\n`);
3105
+ exit = 1;
3106
+ }
3107
+ }
3108
+ return exit;
3109
+ };
3110
+ }
3111
+
3112
+ function mkTouch(vfs: UnixVfs): CmdFn {
3113
+ return (ctx) => {
3114
+ const targetVfs = ctx.vfs ?? vfs;
3115
+ for (const f of ctx.args.filter(a => !a.startsWith('-'))) {
3116
+ const fp = resolvePath(ctx.cwd, f);
3117
+ // Ensure parent dirs
3118
+ const parts = fp.split('/');
3119
+ for (let i = 1; i < parts.length; i++) {
3120
+ const dir = parts.slice(0, i).join('/');
3121
+ if (dir && !targetVfs.exists(dir)) targetVfs.mkdir(dir, { recursive: true });
3122
+ }
3123
+ if (targetVfs.exists(fp) && !targetVfs.isDirectory(fp)) {
3124
+ // Update mtime by re-writing the same content
3125
+ const content = targetVfs.readFile(fp);
3126
+ targetVfs.writeFile(fp, content);
3127
+ } else if (!targetVfs.exists(fp)) {
3128
+ targetVfs.writeFile(fp, '');
3129
+ }
3130
+ }
3131
+ return 0;
3132
+ };
3133
+ }
3134
+
3135
+ /**
3136
+ * `stat` formatting.
3137
+ *
3138
+ * Every GNU directive is answered, using what this filesystem actually knows:
3139
+ *
3140
+ * - inode (%i): paths are the identity here — there are no hard links, so a
3141
+ * path maps to exactly one file. %i is a stable hash of the path, which
3142
+ * gives (dev,ino) comparisons the right answer instead of the zero an
3143
+ * inode-less filesystem would otherwise report for everything.
3144
+ * - link count (%h) is always 1, device numbers are 0: one store, no
3145
+ * hard links, no device nodes.
3146
+ * - birth time (%w/%W) prints `-`/`0`, GNU's own convention for a
3147
+ * filesystem that does not record it. Change time (%z/%Z) tracks mtime.
3148
+ * - SELinux context (%C) prints `?`, as GNU does where there is none.
3149
+ */
3150
+ const STAT_TERSE_FORMAT = '%n %s %b %f %u %g %D %i %h %t %T %X %Y %Z %W %o %C';
3151
+ const STATFS_TERSE_FORMAT = '%n %i %l %t %s %S %b %f %a %c %d';
3152
+ /** Longest component the VFS accepts, reported by %l and `Namelen`. */
3153
+ const STAT_NAME_MAX = 255;
3154
+ /** Reported by %o: the VFS reads and writes in 64 KiB chunks. */
3155
+ const STAT_IO_BLOCK_SIZE = 65536;
3156
+ /** %b/%B count 512-byte units, as GNU does. */
3157
+ const STAT_BLOCK_UNIT = 512;
3158
+
3159
+ interface StatFacts {
3160
+ size: number;
3161
+ type: string;
3162
+ mode: number;
3163
+ uid: number;
3164
+ gid: number;
3165
+ atime: number;
3166
+ mtime: number;
3167
+ ctime?: number;
3168
+ }
3169
+
3170
+ interface StatFsFacts {
3171
+ blockSize: number;
3172
+ totalBlocks: number;
3173
+ freeBlocks: number;
3174
+ totalInodes: number;
3175
+ freeInodes: number;
3176
+ }
3177
+
3178
+ /** Stable 53-bit identity for a path — see the %i note above. */
3179
+ function statPathId(path: string): number {
3180
+ let high = 0xdeadbeef;
3181
+ let low = 0x41c6ce57;
3182
+ for (let i = 0; i < path.length; i++) {
3183
+ const code = path.charCodeAt(i);
3184
+ high = Math.imul(high ^ code, 2654435761);
3185
+ low = Math.imul(low ^ code, 1597334677);
3186
+ }
3187
+ high = Math.imul(high ^ (high >>> 16), 2246822507) >>> 0;
3188
+ low = Math.imul(low ^ (low >>> 13), 3266489909) >>> 0;
3189
+ return high * 0x200000 + (low >>> 11);
3190
+ }
3191
+
3192
+ function statDirective(
3193
+ directive: string,
3194
+ stat: StatFacts,
3195
+ path: string,
3196
+ labels: { user: string; group: string },
3197
+ ): string | null {
3198
+ const isDir = stat.type === 'directory';
3199
+ const isLink = stat.type === 'symlink';
3200
+ const changeTime = stat.ctime ?? stat.mtime;
3201
+ switch (directive) {
3202
+ case 'n': return path;
3203
+ case 'N': return `'${path}'`;
3204
+ case 's': return String(stat.size);
3205
+ case 'b': return String(Math.ceil(stat.size / STAT_BLOCK_UNIT));
3206
+ case 'B': return String(STAT_BLOCK_UNIT);
3207
+ case 'o': return String(STAT_IO_BLOCK_SIZE);
3208
+ case 'a': return (stat.mode & 0o7777).toString(8);
3209
+ case 'A': return unixModeString(stat.mode, isDir, isLink);
3210
+ case 'f': return (stat.mode >>> 0).toString(16);
3211
+ case 'u': return String(stat.uid);
3212
+ case 'U': return labels.user;
3213
+ case 'g': return String(stat.gid);
3214
+ case 'G': return labels.group;
3215
+ case 'F':
3216
+ if (isDir) return 'directory';
3217
+ if (isLink) return 'symbolic link';
3218
+ return isCharacterDevice(stat.mode) ? 'character special file' : 'regular file';
3219
+ case 'h': return '1';
3220
+ case 'i': return String(statPathId(path));
3221
+ case 'd': return '0';
3222
+ case 'D': return '0';
3223
+ case 't': return '0';
3224
+ case 'T': return '0';
3225
+ case 'm': return '/';
3226
+ case 'C': return '?';
3227
+ case 'w': return '-';
3228
+ case 'W': return '0';
3229
+ case 'Y': return String(Math.floor(stat.mtime / 1000));
3230
+ case 'y': return new Date(stat.mtime).toISOString();
3231
+ case 'X': return String(Math.floor(stat.atime / 1000));
3232
+ case 'x': return new Date(stat.atime).toISOString();
3233
+ case 'Z': return String(Math.floor(changeTime / 1000));
3234
+ case 'z': return new Date(changeTime).toISOString();
3235
+ case '%': return '%';
3236
+ default: return null;
3237
+ }
3238
+ }
3239
+
3240
+ function statFsDirective(directive: string, fs: StatFsFacts, path: string): string | null {
3241
+ switch (directive) {
3242
+ case 'n': return path;
3243
+ case 'i': return String(statPathId('/'));
3244
+ case 'l': return String(STAT_NAME_MAX);
3245
+ case 't': return '0';
3246
+ case 'T': return 'nimbus-sqlite';
3247
+ case 's': return String(fs.blockSize);
3248
+ case 'S': return String(fs.blockSize);
3249
+ case 'b': return String(fs.totalBlocks);
3250
+ case 'f': return String(fs.freeBlocks);
3251
+ case 'a': return String(fs.freeBlocks);
3252
+ case 'c': return String(fs.totalInodes);
3253
+ case 'd': return String(fs.freeInodes);
3254
+ case '%': return '%';
3255
+ default: return null;
3256
+ }
3257
+ }
3258
+
3259
+ function expandStatFormat(
3260
+ format: string,
3261
+ expand: (directive: string) => string | null,
3262
+ ): { text: string } | { error: string } {
3263
+ let out = '';
3264
+ for (let i = 0; i < format.length; i++) {
3265
+ const ch = format[i];
3266
+ if (ch === '\\' && i + 1 < format.length) {
3267
+ const escape = format[++i];
3268
+ out += escape === 'n' ? '\n' : escape === 't' ? '\t' : escape === '0' ? '\0' : escape;
3269
+ continue;
3270
+ }
3271
+ if (ch !== '%') {
3272
+ out += ch;
3273
+ continue;
3274
+ }
3275
+ const directive = format[++i];
3276
+ if (directive === undefined) return { error: "stat: trailing '%' in format" };
3277
+ const expanded = expand(directive);
3278
+ if (expanded === null) return { error: `stat: unrecognized format directive '%${directive}'` };
3279
+ out += expanded;
3280
+ }
3281
+ return { text: out };
3282
+ }
3283
+
3284
+ const STAT_USAGE = [
3285
+ 'Usage: stat [OPTION]... FILE...',
3286
+ 'Display file or file system status.',
3287
+ '',
3288
+ ' -L, --dereference follow links (Nimbus always follows)',
3289
+ ' -f, --file-system display file system status instead of file status',
3290
+ ' -c, --format=FORMAT use the specified FORMAT instead of the default',
3291
+ ' --printf=FORMAT like --format, but interpret escapes and omit the newline',
3292
+ ' -t, --terse print the information in terse form',
3293
+ ' --cached=MODE always|default|never (Nimbus attributes are never cached)',
3294
+ ' --help display this help and exit',
3295
+ ' --version output version information and exit',
3296
+ '',
3297
+ ].join('\n');
3298
+
3299
+ function mkStat(vfs: UnixVfs, sqliteVfs: SqliteVFS): CmdFn {
3300
+ return (ctx) => {
3301
+ let format: string | null = null;
3302
+ // `--printf` differs from `-c` only in not appending a newline.
3303
+ let formatAddsNewline = true;
3304
+ let fileSystemMode = false;
3305
+ let terse = false;
3306
+ const files: string[] = [];
3307
+ const args = ctx.args;
3308
+ for (let i = 0; i < args.length; i++) {
3309
+ const arg = args[i];
3310
+ if (arg === '-c' || arg === '--format' || arg === '--printf') {
3311
+ const value = args[++i];
3312
+ if (value === undefined) {
3313
+ ctx.stderr.write(`stat: option '${arg}' requires an argument\n`);
3314
+ return 1;
3315
+ }
3316
+ format = value;
3317
+ formatAddsNewline = arg !== '--printf';
3318
+ } else if (arg.startsWith('--format=') || arg.startsWith('--printf=')) {
3319
+ format = arg.slice(arg.indexOf('=') + 1);
3320
+ formatAddsNewline = !arg.startsWith('--printf=');
3321
+ } else if (arg === '-f' || arg === '--file-system') {
3322
+ fileSystemMode = true;
3323
+ } else if (arg === '-t' || arg === '--terse') {
3324
+ terse = true;
3325
+ } else if (arg === '-L' || arg === '--dereference') {
3326
+ // Symlinks are already followed; accept the flag rather than drop it.
3327
+ } else if (arg.startsWith('--cached=')) {
3328
+ const mode = arg.slice('--cached='.length);
3329
+ if (mode !== 'always' && mode !== 'default' && mode !== 'never') {
3330
+ ctx.stderr.write(`stat: invalid argument '${mode}' for '--cached'\n`);
3331
+ return 1;
3332
+ }
3333
+ // Attributes are read live from the VFS, which satisfies every mode.
3334
+ } else if (arg === '--help') {
3335
+ ctx.stdout.write(STAT_USAGE);
3336
+ return 0;
3337
+ } else if (arg === '--version') {
3338
+ ctx.stdout.write(`stat (nimbus coreutils) ${NIMBUS_VERSION}\n`);
3339
+ return 0;
3340
+ } else if (arg === '--') {
3341
+ files.push(...args.slice(i + 1));
3342
+ break;
3343
+ } else if (arg.startsWith('-') && arg !== '-') {
3344
+ ctx.stderr.write(`stat: invalid option '${arg}'\n`);
3345
+ ctx.stderr.write(STAT_USAGE);
3346
+ return 1;
3347
+ } else {
3348
+ files.push(arg);
3349
+ }
3350
+ }
3351
+
3352
+ if (files.length === 0) {
3353
+ ctx.stderr.write('stat: missing operand\n');
3354
+ return 1;
3355
+ }
3356
+
3357
+ const write = (text: string) => {
3358
+ ctx.stdout.write(formatAddsNewline ? text + '\n' : text);
3359
+ };
3360
+
3361
+ if (fileSystemMode) {
3362
+ const stats = sqliteVfs.getStats();
3363
+ const facts: StatFsFacts = {
3364
+ blockSize: STAT_IO_BLOCK_SIZE,
3365
+ totalBlocks: Math.floor(stats.capacityBytes / STAT_IO_BLOCK_SIZE),
3366
+ freeBlocks: Math.max(
3367
+ 0,
3368
+ Math.floor((stats.capacityBytes - stats.usedBytes) / STAT_IO_BLOCK_SIZE),
3369
+ ),
3370
+ totalInodes: stats.files + stats.directories,
3371
+ freeInodes: 0,
3372
+ };
3373
+ const activeFormat = format ?? (terse ? STATFS_TERSE_FORMAT : null);
3374
+ for (const f of files) {
3375
+ const displayPath = f.startsWith('/') ? f : resolvePath(ctx.cwd, f).replace(/^\/*/, '/');
3376
+ if (activeFormat !== null) {
3377
+ const expanded = expandStatFormat(
3378
+ activeFormat,
3379
+ (directive) => statFsDirective(directive, facts, displayPath),
3380
+ );
3381
+ if ('error' in expanded) {
3382
+ ctx.stderr.write(expanded.error + '\n');
3383
+ return 1;
3384
+ }
3385
+ write(expanded.text);
3386
+ continue;
3387
+ }
3388
+ ctx.stdout.write(` File: "${displayPath}"\n`);
3389
+ ctx.stdout.write(` ID: 0 Namelen: ${STAT_NAME_MAX} Type: nimbus-sqlite\n`);
3390
+ ctx.stdout.write(
3391
+ `Block size: ${facts.blockSize} Fundamental block size: ${facts.blockSize}\n`,
3392
+ );
3393
+ ctx.stdout.write(
3394
+ `Blocks: Total: ${facts.totalBlocks} Free: ${facts.freeBlocks} Available: ${facts.freeBlocks}\n`,
3395
+ );
3396
+ ctx.stdout.write(`Inodes: Total: ${facts.totalInodes} Free: ${facts.freeInodes}\n`);
3397
+ }
3398
+ return 0;
3399
+ }
3400
+
3401
+ const activeFormat = format ?? (terse ? STAT_TERSE_FORMAT : null);
3402
+ // shell compatibility follow-up: try Kernel.VFS (ctx.vfs) first so /dev
3403
+ // mount paths resolve. Same pattern as mkCat.
3404
+ const kvfs: any = (ctx as any).vfs;
3405
+ for (const f of files) {
3406
+ let st: any = null;
3407
+ let displayPath = f;
3408
+ // Try Kernel.VFS first (sees mounts).
3409
+ if (kvfs && typeof kvfs.stat === 'function') {
3410
+ try {
3411
+ st = kvfs.stat(f.startsWith('/') ? f : ctx.cwd + '/' + f);
3412
+ displayPath = f.startsWith('/') ? f : `/${ctx.cwd}/${f}`.replace(/^\/+/, '/');
3413
+ } catch (_e) { /* fall through to SqliteVFS */ }
3414
+ }
3415
+ // Fall back to SqliteVFS direct for non-mounted paths.
3416
+ if (!st) {
3417
+ try {
3418
+ const fp = resolvePath(ctx.cwd, f);
3419
+ st = vfs.stat(fp);
3420
+ displayPath = '/' + fp;
3421
+ } catch (_e) {
3422
+ ctx.stderr.write(`stat: cannot statx '${f}': No such file or directory\n`);
3423
+ return 1;
3424
+ }
3425
+ }
3426
+ const uid = st.uid ?? ctx.cred.uid;
3427
+ const gid = st.gid ?? ctx.cred.gid;
3428
+ const labels = {
3429
+ user: unixUserLabel(vfs, uid),
3430
+ group: unixGroupLabel(vfs, gid),
3431
+ };
3432
+ const facts: StatFacts = {
3433
+ size: st.size,
3434
+ type: st.type,
3435
+ mode: st.mode,
3436
+ uid,
3437
+ gid,
3438
+ atime: st.atime ?? st.mtime,
3439
+ mtime: st.mtime,
3440
+ ctime: st.ctime,
3441
+ };
3442
+ if (activeFormat !== null) {
3443
+ const expanded = expandStatFormat(
3444
+ activeFormat,
3445
+ (directive) => statDirective(directive, facts, displayPath, labels),
3446
+ );
3447
+ if ('error' in expanded) {
3448
+ ctx.stderr.write(expanded.error + '\n');
3449
+ return 1;
3450
+ }
3451
+ write(expanded.text);
3452
+ continue;
3453
+ }
3454
+ ctx.stdout.write(` File: ${displayPath}\n`);
3455
+ const kind = isCharacterDevice(st.mode) ? 'character special file' : st.type;
3456
+ ctx.stdout.write(` Size: ${st.size}\tType: ${kind}\n`);
3457
+ ctx.stdout.write(`Access: (0${st.mode.toString(8)}) Uid: (${uid}/${labels.user}) Gid: (${gid}/${labels.group})\n`);
3458
+ ctx.stdout.write(`Modify: ${new Date(st.mtime).toISOString()}\n`);
3459
+ }
3460
+ return 0;
3461
+ };
3462
+ }
3463
+
3464
+ const BASE64_SPEC = {
3465
+ decode: { type: 'boolean' as const, short: 'd' },
3466
+ 'ignore-garbage': { type: 'boolean' as const, short: 'i' },
3467
+ wrap: { type: 'string' as const, short: 'w' },
3468
+ };
3469
+
3470
+ /**
3471
+ * Encodes and decodes the real bytes. Reading the input as a string first put
3472
+ * every byte that is not valid UTF-8 through U+FFFD, so encoding any binary
3473
+ * file produced base64 of something else; `-w`, which GNU wraps at 76 columns
3474
+ * by default, was not implemented at all, so `base64 -w 0` read `0` as a file.
3475
+ */
3476
+ function mkBase64(vfs: UnixVfs): CmdFn {
3477
+ return (ctx) => {
3478
+ const { flags, positional, unknown } = parseArgs(ctx.args, BASE64_SPEC);
3479
+ if (unknown.length > 0) {
3480
+ ctx.stderr.write(`base64: invalid option -- '${unknown[0].replace(/^-+/, '')}'\n`);
3481
+ return 1;
3482
+ }
3483
+ const wrapText = typeof flags.wrap === 'string' && flags.wrap !== '' ? flags.wrap : '76';
3484
+ const wrap = Number.parseInt(wrapText, 10);
3485
+ if (Number.isNaN(wrap) || wrap < 0) {
3486
+ ctx.stderr.write(`base64: invalid wrap size: '${wrapText}'\n`);
3487
+ return 1;
3488
+ }
3489
+
3490
+ const file = positional[0];
3491
+ let bytes: Uint8Array;
3492
+ if (file !== undefined && file !== '-') {
3493
+ try { bytes = vfs.readFile(resolvePath(ctx.cwd, file)); }
3494
+ catch (error) { ctx.stderr.write(`base64: ${file}: ${fsErrorMessage(error)}\n`); return 1; }
3495
+ } else {
3496
+ bytes = enc.encode(ctx.stdin ?? '');
3497
+ }
3498
+
3499
+ if (flags.decode) {
3500
+ const source = dec.decode(bytes).replace(/\s+/g, '');
3501
+ let decoded: Uint8Array;
3502
+ try {
3503
+ const binary = atob(source);
3504
+ decoded = Uint8Array.from(binary, (c) => c.charCodeAt(0));
3505
+ } catch { ctx.stderr.write('base64: invalid input\n'); return 1; }
3506
+ if (ctx.stdout.writeBytes) ctx.stdout.writeBytes(decoded);
3507
+ else ctx.stdout.write(dec.decode(decoded));
3508
+ return 0;
3509
+ }
3510
+
3511
+ let binary = '';
3512
+ for (const byte of bytes) binary += String.fromCharCode(byte);
3513
+ const encoded = btoa(binary);
3514
+ if (encoded === '') return 0;
3515
+ const lines = wrap > 0
3516
+ ? (encoded.match(new RegExp(`.{1,${wrap}}`, 'g')) ?? [encoded])
3517
+ : [encoded];
3518
+ ctx.stdout.write(lines.join('\n') + '\n');
3519
+ return 0;
3520
+ };
3521
+ }
3522
+
3523
+ function mkSeq(): CmdFn {
3524
+ return (ctx) => {
3525
+ const nums = ctx.args.map(Number).filter(n => !isNaN(n));
3526
+ let start = 1, step = 1, end = 1;
3527
+ if (nums.length === 1) end = nums[0];
3528
+ else if (nums.length === 2) { start = nums[0]; end = nums[1]; }
3529
+ else if (nums.length >= 3) { start = nums[0]; step = nums[1]; end = nums[2]; }
3530
+ for (let i = start; step > 0 ? i <= end : i >= end; i += step) ctx.stdout.write(i + '\n');
3531
+ return 0;
3532
+ };
3533
+ }
3534
+
3535
+ function mkId(sqliteVfs: SqliteVFS): CmdFn {
3536
+ return (ctx) => {
3537
+ const vfs = sqliteVfs.as(ctx.cred);
3538
+ const user = findUnixUserName(vfs, ctx.cred.uid) ?? String(ctx.cred.uid);
3539
+ const group = findUnixGroupName(vfs, ctx.cred.gid) ?? String(ctx.cred.gid);
3540
+ const groupIds = [...new Set([ctx.cred.gid, ...ctx.cred.groups])];
3541
+ const groups = groupIds
3542
+ .map((gid) => `${gid}(${findUnixGroupName(vfs, gid) ?? gid})`)
3543
+ .join(',');
3544
+ ctx.stdout.write(`uid=${ctx.cred.uid}(${user}) gid=${ctx.cred.gid}(${group}) groups=${groups}\n`);
3545
+ return 0;
3546
+ };
3547
+ }
3548
+
3549
+ function mkChown(sqliteVfs: SqliteVFS): CmdFn {
3550
+ return (ctx) => {
3551
+ const recursive = ctx.args.includes('-R') || ctx.args.includes('--recursive');
3552
+ const positional = ctx.args.filter((arg) => arg !== '-R' && arg !== '--recursive');
3553
+ if (positional.length < 2) {
3554
+ ctx.stderr.write('chown: missing operand\n');
3555
+ return 1;
3556
+ }
3557
+
3558
+ const vfs = sqliteVfs.as(ctx.cred);
3559
+ let ownership: { uid: number | null; gid: number | null };
3560
+ try {
3561
+ ownership = parseChownOwnership(vfs, positional[0]);
3562
+ } catch (error) {
3563
+ ctx.stderr.write(`chown: ${error instanceof Error ? error.message : String(error)}\n`);
3564
+ return 1;
3565
+ }
3566
+
3567
+ let exitCode = 0;
3568
+ const apply = (path: string): void => {
3569
+ if (recursive && vfs.stat(path).type === 'directory') {
3570
+ for (const child of vfs.readdir(path)) apply(`${path}/${child.name}`);
3571
+ }
3572
+ vfs.chown(path, ownership.uid, ownership.gid);
3573
+ };
3574
+
3575
+ for (const file of positional.slice(1)) {
3576
+ try {
3577
+ apply(resolvePath(ctx.cwd, file));
3578
+ } catch (error) {
3579
+ ctx.stderr.write(`chown: ${file}: ${error instanceof Error ? error.message : String(error)}\n`);
3580
+ exitCode = 1;
3581
+ }
3582
+ }
3583
+ return exitCode;
3584
+ };
3585
+ }
3586
+
3587
+ function mkTest(sqliteVfs: SqliteVFS): CmdFn {
3588
+ return (ctx) => {
3589
+ const args = ctx.args.filter((arg) => arg !== ']');
3590
+ if (args.length === 0) return 1;
3591
+ const vfs = sqliteVfs.as(ctx.cred);
3592
+ const path = resolvePath(ctx.cwd, args[1] ?? '');
3593
+ try {
3594
+ if (args[0] === '-r') vfs.access(path, 0o4);
3595
+ else if (args[0] === '-w') vfs.access(path, 0o2);
3596
+ else if (args[0] === '-x') vfs.access(path, 0o1);
3597
+ else if (args[0] === '-f') return vfs.stat(path).type === 'file' ? 0 : 1;
3598
+ else if (args[0] === '-d') return vfs.stat(path).type === 'directory' ? 0 : 1;
3599
+ else if (args[0] === '-e') vfs.stat(path);
3600
+ else if (args[0] === '-z') return (!args[1] || args[1] === '') ? 0 : 1;
3601
+ else if (args[0] === '-n') return args[1] ? 0 : 1;
3602
+ else if (args[1] === '=') return args[0] === args[2] ? 0 : 1;
3603
+ else if (args[1] === '!=') return args[0] !== args[2] ? 0 : 1;
3604
+ else return args[0] ? 0 : 1;
3605
+ return 0;
3606
+ } catch {
3607
+ return 1;
3608
+ }
3609
+ };
3610
+ }
3611
+
3612
+ function mkHostname(): CmdFn {
3613
+ return (ctx) => { ctx.stdout.write('nimbus\n'); return 0; };
3614
+ }
3615
+
3616
+ function mkBasename(): CmdFn {
3617
+ return (ctx) => {
3618
+ const p = ctx.args[0] || '';
3619
+ const suffix = ctx.args[1] || '';
3620
+ let base = p.split('/').pop() || '';
3621
+ if (suffix && base.endsWith(suffix)) base = base.slice(0, -suffix.length);
3622
+ ctx.stdout.write(base + '\n');
3623
+ return 0;
3624
+ };
3625
+ }
3626
+
3627
+ function mkDirname(): CmdFn {
3628
+ return (ctx) => {
3629
+ const p = ctx.args[0] || '';
3630
+ const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
3631
+ ctx.stdout.write((dir || '/') + '\n');
3632
+ return 0;
3633
+ };
3634
+ }
3635
+
3636
+ function mkRealpath(vfs: UnixVfs): CmdFn {
3637
+ return (ctx) => {
3638
+ for (const p of ctx.args) {
3639
+ const fp = resolvePath(ctx.cwd, p);
3640
+ if (vfs.exists(fp)) ctx.stdout.write('/' + fp + '\n');
3641
+ else { ctx.stderr.write(`realpath: ${p}: No such file\n`); return 1; }
3642
+ }
3643
+ return 0;
3644
+ };
3645
+ }
3646
+
3647
+ /**
3648
+ * shell compatibility (2026-05-11): printf full POSIX format set.
3649
+ *
3650
+ * Pre-fix mkPrintf only handled %s and %d via simple replace —
3651
+ * `printf "%x\\n" 255` output literal `%x`, `printf "%5d" 7` output
3652
+ * literal `%5d`. Common shell scripts use %x (hex), %o (octal),
3653
+ * %f (float), %g (general), %c (char), width+precision specifiers,
3654
+ * and flag chars (- + 0 # space).
3655
+ *
3656
+ * Real bash printf cycles through args, re-running the format
3657
+ * string if there are more args than format specifiers. We
3658
+ * replicate that.
3659
+ */
3660
+ function mkPrintf(): CmdFn {
3661
+ return (ctx) => {
3662
+ if (ctx.args.length === 0) return 0;
3663
+ const rawFmt = ctx.args[0];
3664
+ const vals = ctx.args.slice(1);
3665
+ // 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, '\\');
3678
+
3679
+ let out = '';
3680
+ let argIdx = 0;
3681
+
3682
+ function applyFormat(): boolean {
3683
+ // Run the format string once; return true if it consumed any args.
3684
+ let i = 0;
3685
+ const startArg = argIdx;
3686
+ while (i < fmt.length) {
3687
+ const ch = fmt[i];
3688
+ if (ch !== '%') { out += ch; i++; continue; }
3689
+ if (fmt[i + 1] === '%') { out += '%'; i += 2; continue; }
3690
+ // Parse format spec: %[flags][width][.prec]conversion
3691
+ let spec = '%';
3692
+ i++;
3693
+ while (i < fmt.length && /[-+ 0#]/.test(fmt[i])) { spec += fmt[i]; i++; }
3694
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) { spec += fmt[i]; i++; }
3695
+ if (fmt[i] === '.') {
3696
+ spec += fmt[i]; i++;
3697
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) { spec += fmt[i]; i++; }
3698
+ }
3699
+ const conv = fmt[i];
3700
+ i++;
3701
+ const arg = vals[argIdx++];
3702
+ out += formatOneArg(spec + conv, arg);
3703
+ }
3704
+ return argIdx > startArg;
3705
+ }
3706
+
3707
+ // bash printf: re-run the format until args are exhausted; if
3708
+ // format consumes zero args (no %X specifiers), run it once.
3709
+ if (vals.length === 0) {
3710
+ applyFormat();
3711
+ } else {
3712
+ while (argIdx < vals.length) {
3713
+ if (!applyFormat()) break;
3714
+ }
3715
+ }
3716
+ ctx.stdout.write(out);
3717
+ return 0;
3718
+ };
3719
+ }
3720
+
3721
+ function formatOneArg(spec: string, arg: any): string {
3722
+ const conv = spec[spec.length - 1];
3723
+ const flagsAndWidth = spec.slice(1, -1);
3724
+ const dotIdx = flagsAndWidth.indexOf('.');
3725
+ const widthPart = dotIdx >= 0 ? flagsAndWidth.slice(0, dotIdx) : flagsAndWidth;
3726
+ const precPart = dotIdx >= 0 ? flagsAndWidth.slice(dotIdx + 1) : '';
3727
+ let flags = '';
3728
+ let widthStr = '';
3729
+ for (const c of widthPart) {
3730
+ if (/[-+ 0#]/.test(c)) flags += c;
3731
+ else widthStr += c;
3732
+ }
3733
+ const width = widthStr ? parseInt(widthStr, 10) : 0;
3734
+ const prec = precPart ? parseInt(precPart, 10) : -1;
3735
+ let body: string;
3736
+ switch (conv) {
3737
+ case 's': {
3738
+ body = String(arg ?? '');
3739
+ if (prec >= 0) body = body.slice(0, prec);
3740
+ break;
3741
+ }
3742
+ case 'd': case 'i': {
3743
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3744
+ const v = Number.isFinite(n) ? n : 0;
3745
+ body = String(Math.abs(v));
3746
+ const sign = v < 0 ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '';
3747
+ body = sign + body;
3748
+ break;
3749
+ }
3750
+ case 'u': {
3751
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3752
+ body = String(Math.max(0, Number.isFinite(n) ? n : 0));
3753
+ break;
3754
+ }
3755
+ case 'f': case 'F': {
3756
+ const n = typeof arg === 'number' ? arg : parseFloat(String(arg ?? '0'));
3757
+ const p = prec < 0 ? 6 : prec;
3758
+ body = (Number.isFinite(n) ? n : 0).toFixed(p);
3759
+ if (n >= 0 && flags.includes('+')) body = '+' + body;
3760
+ else if (n >= 0 && flags.includes(' ')) body = ' ' + body;
3761
+ break;
3762
+ }
3763
+ case 'e': case 'E': {
3764
+ const n = typeof arg === 'number' ? arg : parseFloat(String(arg ?? '0'));
3765
+ const p = prec < 0 ? 6 : prec;
3766
+ body = (Number.isFinite(n) ? n : 0).toExponential(p);
3767
+ if (conv === 'E') body = body.toUpperCase();
3768
+ break;
3769
+ }
3770
+ case 'g': case 'G': {
3771
+ const n = typeof arg === 'number' ? arg : parseFloat(String(arg ?? '0'));
3772
+ const p = prec < 0 ? 6 : prec || 1;
3773
+ body = (Number.isFinite(n) ? n : 0).toPrecision(p);
3774
+ // Strip trailing zeros + dot (POSIX %g behavior) unless # flag.
3775
+ if (!flags.includes('#')) body = body.replace(/(\.\d*?)0+($|e)/, '$1$2').replace(/\.($|e)/, '$1');
3776
+ if (conv === 'G') body = body.toUpperCase();
3777
+ break;
3778
+ }
3779
+ case 'x': case 'X': {
3780
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3781
+ body = (Number.isFinite(n) ? n >>> 0 : 0).toString(16);
3782
+ if (conv === 'X') body = body.toUpperCase();
3783
+ if (flags.includes('#') && body !== '0') body = (conv === 'X' ? '0X' : '0x') + body;
3784
+ break;
3785
+ }
3786
+ case 'o': {
3787
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3788
+ body = (Number.isFinite(n) ? n >>> 0 : 0).toString(8);
3789
+ if (flags.includes('#') && !body.startsWith('0')) body = '0' + body;
3790
+ break;
3791
+ }
3792
+ case 'b': {
3793
+ // bash printf %b: interpret backslash escapes in the arg
3794
+ let s = String(arg ?? '');
3795
+ s = s.replace(/\\\\/g, '\u0000')
3796
+ .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
3797
+ .replace(/\\u0000/g, '\\');
3798
+ body = s;
3799
+ break;
3800
+ }
3801
+ case 'c': {
3802
+ if (typeof arg === 'number') body = String.fromCharCode(arg);
3803
+ else body = String(arg ?? '').charAt(0);
3804
+ break;
3805
+ }
3806
+ case 'q': {
3807
+ // bash printf %q: shell-quote
3808
+ const s = String(arg ?? '');
3809
+ if (/^[A-Za-z0-9_/.,:=+@%-]+$/.test(s)) body = s;
3810
+ else body = "'" + s.replace(/'/g, `'\\''`) + "'";
3811
+ break;
3812
+ }
3813
+ default: body = '%' + conv;
3814
+ }
3815
+ // Apply width padding.
3816
+ if (width > body.length) {
3817
+ const zeroPad = flags.includes('0') && /[diouxXfFeEgG]/.test(conv) && !flags.includes('-');
3818
+ const padCh = zeroPad ? '0' : ' ';
3819
+ if (flags.includes('-')) body = body.padEnd(width, ' ');
3820
+ else {
3821
+ // For zero-pad on negative numbers, keep the sign at the front.
3822
+ if (zeroPad && (body.startsWith('-') || body.startsWith('+') || body.startsWith(' '))) {
3823
+ body = body[0] + body.slice(1).padStart(width - 1, padCh);
3824
+ } else {
3825
+ body = body.padStart(width, padCh);
3826
+ }
3827
+ }
3828
+ }
3829
+ return body;
3830
+ }
3831
+
3832
+ function mkTrue(): CmdFn { return () => 0; }
3833
+ function mkFalse(): CmdFn { return () => 1; }
3834
+
3835
+ /**
3836
+ * shell compatibility (2026-05-11): readlink stub.
3837
+ *
3838
+ * Real readlink reads the symlink target. Our VFS doesn't yet
3839
+ * support real symlinks (ln -s currently does a regular file
3840
+ * copy — tracked as deferred). For graceful failure:
3841
+ * - if path is a regular file/dir, exit 1 (matches GNU readlink)
3842
+ * - if path is missing, write error to stderr + exit 1
3843
+ * - explicit handling avoids 'readlink: command not found'.
3844
+ *
3845
+ * When real symlinks land in VFS, this command will become the
3846
+ * read-side of the symlink table.
3847
+ */
3848
+ /**
3849
+ * SHELL-FOLLOWUPS-4 (2026-05-11): real symlink readlink.
3850
+ * Pre-fix: ln -s did file-copy; readlink returned exit 1 with no
3851
+ * output (no symlink table existed). Now backed by SymlinkRegistry:
3852
+ * - GNU readlink prints target (relative or absolute as stored)
3853
+ * - exit 1 silently for non-symlinks (matches GNU readlink default)
3854
+ * - exit 1 + stderr for missing files
3855
+ * Flags: -f (canonicalize — follow chain to final target), default
3856
+ * (one-hop). -e variant (verify) deferred.
3857
+ */
3858
+ function mkReadlink(vfs: UnixVfs): CmdFn {
3859
+ return (ctx) => {
3860
+ const args = [...ctx.args];
3861
+ let canonicalize = false;
3862
+ const targets: string[] = [];
3863
+ for (const a of args) {
3864
+ if (a === '-f' || a === '--canonicalize') { canonicalize = true; continue; }
3865
+ if (a.startsWith('-') && a !== '-') {
3866
+ for (const ch of a.slice(1)) if (ch === 'f') canonicalize = true;
3867
+ continue;
3868
+ }
3869
+ targets.push(a);
3870
+ }
3871
+ if (targets.length === 0) {
3872
+ ctx.stderr.write('readlink: missing operand\n');
3873
+ return 1;
3874
+ }
3875
+ let exit = 0;
3876
+ for (const t of targets) {
3877
+ const fp = resolvePath(ctx.cwd, t);
3878
+ if (canonicalize) {
3879
+ // -f: follow chain; succeed even if target doesn't exist YET
3880
+ // (matches `readlink -f` which canonicalizes anyway).
3881
+ const resolved = resolveSymlinkPath(vfs, fp);
3882
+ if (resolved !== null) {
3883
+ ctx.stdout.write('/' + resolved + '\n');
3884
+ continue;
3885
+ }
3886
+ ctx.stderr.write(`readlink: ${t}: Too many levels of symbolic links\n`);
3887
+ exit = 1;
3888
+ continue;
3889
+ }
3890
+ // Default: one-hop. Print target verbatim (preserves relative/absolute).
3891
+ const direct = readSymlinkTarget(vfs, fp);
3892
+ if (direct !== null) {
3893
+ ctx.stdout.write(direct + '\n');
3894
+ continue;
3895
+ }
3896
+ // Not a symlink. GNU readlink exits 1 silently for regular
3897
+ // files / dirs; emits stderr for missing.
3898
+ if (!vfs.exists(fp)) {
3899
+ ctx.stderr.write(`readlink: ${t}: No such file or directory\n`);
3900
+ }
3901
+ exit = 1;
3902
+ }
3903
+ return exit;
3904
+ };
3905
+ }
3906
+
3907
+ const SHA256SUM_SPEC = {
3908
+ check: { type: 'boolean' as const, short: 'c' },
3909
+ binary: { type: 'boolean' as const, short: 'b' },
3910
+ text: { type: 'boolean' as const, short: 't' },
3911
+ quiet: { type: 'boolean' as const, short: 'q' },
3912
+ status: { type: 'boolean' as const },
3913
+ };
3914
+
3915
+ /**
3916
+ * Real SHA-256 over the file's real bytes.
3917
+ *
3918
+ * The digest used to be taken over `enc.encode(readFileString(path))` — a
3919
+ * UTF-8 decode and re-encode, which replaces every byte that is not valid
3920
+ * UTF-8 with U+FFFD. For any binary file that hashes something the file does
3921
+ * not contain, and it never announced a problem: an installer verifying a
3922
+ * downloaded tarball got a mismatch on a perfectly good download, every time.
3923
+ */
3924
+ function mkSha256sum(vfs: UnixVfs): CmdFn {
3925
+ const digest = async (bytes: Uint8Array): Promise<string> => {
3926
+ const ab = await crypto.subtle.digest('SHA-256', bytes as BufferSource);
3927
+ return Array.from(new Uint8Array(ab)).map((b) => b.toString(16).padStart(2, '0')).join('');
3928
+ };
3929
+
3930
+ return async (ctx) => {
3931
+ const { flags, positional, unknown } = parseArgs(ctx.args, SHA256SUM_SPEC);
3932
+ if (unknown.length > 0) {
3933
+ ctx.stderr.write(`sha256sum: invalid option -- '${unknown[0].replace(/^-+/, '')}'\n`);
3934
+ return 1;
3935
+ }
3936
+
3937
+ if (flags.check) return verifySha256Sums(ctx, vfs, positional, digest, flags.status === true);
3938
+
3939
+ if (positional.length === 0 || (positional.length === 1 && positional[0] === '-')) {
3940
+ ctx.stdout.write(`${await digest(enc.encode(ctx.stdin ?? ''))} -\n`);
3941
+ return 0;
3942
+ }
3943
+
3944
+ let exit = 0;
3945
+ for (const f of positional) {
3946
+ try {
3947
+ ctx.stdout.write(`${await digest(vfs.readFile(resolvePath(ctx.cwd, f)))} ${f}\n`);
3948
+ } catch {
3949
+ ctx.stderr.write(`sha256sum: ${f}: No such file or directory\n`);
3950
+ exit = 1;
3951
+ }
3952
+ }
3953
+ return exit;
3954
+ };
3955
+ }
3956
+
3957
+ /** `sha256sum -c LIST` — each line is `HASH FILENAME`, as this command prints. */
3958
+ async function verifySha256Sums(
3959
+ ctx: Ctx,
3960
+ vfs: UnixVfs,
3961
+ lists: string[],
3962
+ digest: (bytes: Uint8Array) => Promise<string>,
3963
+ quiet: boolean,
3964
+ ): Promise<number> {
3965
+ let exit = 0;
3966
+ for (const list of lists) {
3967
+ let body: string;
3968
+ try {
3969
+ body = vfs.readFileString(resolvePath(ctx.cwd, list));
3970
+ } catch {
3971
+ ctx.stderr.write(`sha256sum: ${list}: No such file or directory\n`);
3972
+ exit = 1;
3973
+ continue;
3974
+ }
3975
+ for (const line of body.split('\n')) {
3976
+ const entry = /^([0-9a-fA-F]{64})\s[\s*](.*)$/.exec(line);
3977
+ if (entry === null) continue;
3978
+ const [, expected, name] = entry;
3979
+ let actual: string | null = null;
3980
+ try {
3981
+ actual = await digest(vfs.readFile(resolvePath(ctx.cwd, name)));
3982
+ } catch { /* reported as FAILED open below */ }
3983
+ if (actual === null) {
3984
+ ctx.stderr.write(`sha256sum: ${name}: No such file or directory\n`);
3985
+ if (!quiet) ctx.stdout.write(`${name}: FAILED open or read\n`);
3986
+ exit = 1;
3987
+ } else if (actual.toLowerCase() === expected.toLowerCase()) {
3988
+ if (!quiet) ctx.stdout.write(`${name}: OK\n`);
3989
+ } else {
3990
+ if (!quiet) ctx.stdout.write(`${name}: FAILED\n`);
3991
+ exit = 1;
3992
+ }
3993
+ }
3994
+ }
3995
+ return exit;
3996
+ }
3997
+
3998
+ function mkFile(vfs: UnixVfs): CmdFn {
3999
+ return (ctx) => {
4000
+ for (const f of ctx.args.filter(a => !a.startsWith('-'))) {
4001
+ const fp = resolvePath(ctx.cwd, f);
4002
+ try {
4003
+ if (vfs.isDirectory(fp)) { ctx.stdout.write(`${f}: directory\n`); continue; }
4004
+ // BUG-SWEEP-3 (2026-05-11): scan raw bytes for NUL or non-text
4005
+ // bytes BEFORE attempting a UTF-8 decode. Pre-fix every binary
4006
+ // file was reported as "UTF-8 text" because readFileString
4007
+ // silently U+FFFD-substituted invalid sequences.
4008
+ const bytes = vfs.readFile(fp);
4009
+ let isBinary = false;
4010
+ const scanLimit = Math.min(bytes.length, 8192);
4011
+ for (let i = 0; i < scanLimit; i++) {
4012
+ const b = bytes[i];
4013
+ if (b === 0) { isBinary = true; break; }
4014
+ // Bytes 0x01-0x08 + 0x0E-0x1F (excluding TAB/LF/CR/FF) are
4015
+ // strong signals of non-text content.
4016
+ if (b < 0x09 || (b > 0x0d && b < 0x20)) { isBinary = true; break; }
4017
+ }
4018
+ if (isBinary) {
4019
+ // Magic-byte sniff for common formats.
4020
+ if (bytes.length >= 4 && bytes[0] === 0x7f && bytes[1] === 0x45 && bytes[2] === 0x4c && bytes[3] === 0x46) {
4021
+ ctx.stdout.write(`${f}: ELF executable\n`);
4022
+ } else if (bytes.length >= 4 && bytes[0] === 0x00 && bytes[1] === 0x61 && bytes[2] === 0x73 && bytes[3] === 0x6d) {
4023
+ ctx.stdout.write(`${f}: WebAssembly (wasm) binary module\n`);
4024
+ } else if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
4025
+ ctx.stdout.write(`${f}: PNG image data\n`);
4026
+ } else if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
4027
+ ctx.stdout.write(`${f}: gzip compressed data\n`);
4028
+ } else if (bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && (bytes[2] === 0x03 || bytes[2] === 0x05)) {
4029
+ ctx.stdout.write(`${f}: Zip archive data\n`);
4030
+ } else {
4031
+ ctx.stdout.write(`${f}: data\n`);
4032
+ }
4033
+ continue;
4034
+ }
4035
+ const content = new TextDecoder('utf-8').decode(bytes);
4036
+ if (content.startsWith('<!DOCTYPE') || content.startsWith('<html')) ctx.stdout.write(`${f}: HTML document\n`);
4037
+ else if (content.startsWith('{') || content.startsWith('[')) ctx.stdout.write(`${f}: JSON data\n`);
4038
+ else if (content.startsWith('#!')) ctx.stdout.write(`${f}: script, ${content.split('\n')[0]}\n`);
4039
+ else if (f.endsWith('.ts') || f.endsWith('.tsx')) ctx.stdout.write(`${f}: TypeScript source\n`);
4040
+ else if (f.endsWith('.js') || f.endsWith('.mjs')) ctx.stdout.write(`${f}: JavaScript source\n`);
4041
+ else if (f.endsWith('.css')) ctx.stdout.write(`${f}: CSS stylesheet\n`);
4042
+ else ctx.stdout.write(`${f}: ASCII text, ${content.split('\n').length} lines\n`);
4043
+ } catch { ctx.stderr.write(`file: ${f}: No such file\n`); return 1; }
4044
+ }
4045
+ return 0;
4046
+ };
4047
+ }
4048
+
4049
+ function mkXxd(vfs: UnixVfs): CmdFn {
4050
+ return (ctx) => {
4051
+ const file = ctx.args.find(a => !a.startsWith('-'));
4052
+ if (!file) { ctx.stderr.write('Usage: xxd FILE\n'); return 1; }
4053
+ const fp = resolvePath(ctx.cwd, file);
4054
+ try {
4055
+ const data = vfs.readFile(fp);
4056
+ const len = Math.min(data.length, ctx.args.includes('-l') ? parseInt(ctx.args[ctx.args.indexOf('-l') + 1]) || 256 : 256);
4057
+ for (let i = 0; i < len; i += 16) {
4058
+ const hex = Array.from(data.slice(i, i + 16)).map(b => b.toString(16).padStart(2, '0')).join(' ');
4059
+ const ascii = Array.from(data.slice(i, i + 16)).map(b => b >= 32 && b < 127 ? String.fromCharCode(b) : '.').join('');
4060
+ ctx.stdout.write(`${i.toString(16).padStart(8, '0')}: ${hex.padEnd(48)} ${ascii}\n`);
4061
+ }
4062
+ return 0;
4063
+ } catch { ctx.stderr.write(`xxd: ${file}: No such file\n`); return 1; }
4064
+ };
4065
+ }
4066
+
4067
+ // ── Registration ────────────────────────────────────────────────────────
4068
+
4069
+ /**
4070
+ * Wrap a sync/async command so it always returns Promise<number>.
4071
+ * The LIFO shell calls .then() on the return value of every command,
4072
+ * so raw numbers cause "E3(...).then is not a function".
4073
+ *
4074
+ * Also resolves ctx.stdin from a stream object to a string.
4075
+ * The shell passes stdin as an object with .readAll() when piping,
4076
+ * but our commands expect a plain string.
4077
+ */
4078
+ /**
4079
+ * SHELL-R6-B2 follow-on: wrapStreaming for commands that handle pipe
4080
+ * readers directly (head, tail, etc — commands that can terminate
4081
+ * before the producer drains).
4082
+ *
4083
+ * Behavior:
4084
+ * - If ctx.stdin is a terminal stdin (has .feed), drain buffered
4085
+ * bytes to a string (same as wrap — terminal stdin's
4086
+ * buffer-then-close pattern doesn't match streaming).
4087
+ * - If ctx.stdin is a pipe reader (has .read), PASS IT THROUGH
4088
+ * unchanged so the command can read line-by-line. The command
4089
+ * is responsible for terminating itself (e.g. head -n N stops
4090
+ * after N lines, triggering the pipeline-abort cascade from
4091
+ * SHELL-R6-2).
4092
+ * - String stdin / other shapes: same as wrap.
4093
+ */
4094
+ function wrapStreaming(fn: CmdFn): (ctx: Ctx) => Promise<number> {
4095
+ return async (ctx: Ctx) => {
4096
+ try {
4097
+ if (ctx.stdin && typeof ctx.stdin !== 'string') {
4098
+ const stdinObj = ctx.stdin as any;
4099
+ const isTerminalStdin = typeof stdinObj.feed === 'function';
4100
+ if (isTerminalStdin) {
4101
+ const buf: string[] = Array.isArray(stdinObj.buffer)
4102
+ ? stdinObj.buffer.splice(0)
4103
+ : [];
4104
+ (ctx as any).stdin = buf.join('');
4105
+ }
4106
+ // else: leave as pipe reader for the command to handle.
4107
+ }
4108
+ const result = fn(ctx);
4109
+ return await result;
4110
+ } catch (e: any) {
4111
+ ctx.stderr.write(`${e?.message || e}\n`);
4112
+ return 1;
4113
+ }
4114
+ };
4115
+ }
4116
+
4117
+ function wrap(fn: CmdFn): (ctx: Ctx) => Promise<number> {
4118
+ return async (ctx: Ctx) => {
4119
+ try {
4120
+ // Resolve stdin: shell passes a stream object with .readAll() when piping.
4121
+ //
4122
+ // BUG-SWEEP fix (2026-05-11): the shell passes its
4123
+ // `terminalStdin` (an Ls instance) as ctx.stdin for EVERY command,
4124
+ // not just piped ones. Ls.readAll() loops until close(), which the
4125
+ // shell only triggers in its executeLine() finally — AFTER the
4126
+ // command returns. Pre-fix, our wrap awaited readAll() and
4127
+ // deadlocked: command waits for stdin EOF, shell waits for command.
4128
+ //
4129
+ // The fix is to distinguish the two stream shapes:
4130
+ // - Pipe reader (Oi.reader): {read, readAll} only. Used when
4131
+ // upstream `echo X |` feeds bytes; upstream calls close()
4132
+ // after writing, so readAll() resolves quickly.
4133
+ // - Terminal stdin (Ls): {feed, close, rawMode, read, readAll,
4134
+ // isWaiting, ...}. close() runs ONLY after the command returns.
4135
+ //
4136
+ // We treat anything with a `feed` method (Ls signature) as the
4137
+ // terminal stdin and drain its already-buffered bytes synchronously
4138
+ // without awaiting EOF. Pipe readers (no `feed`) await readAll().
4139
+ if (ctx.stdin && typeof ctx.stdin !== 'string') {
4140
+ const stdinObj = ctx.stdin as any;
4141
+ const isTerminalStdin = typeof stdinObj.feed === 'function';
4142
+ if (isTerminalStdin) {
4143
+ // Drain any already-queued bytes (typically empty for the
4144
+ // first command on a line; non-empty if the user typed
4145
+ // text + Enter before the command was dispatched). DO NOT
4146
+ // await — that would wait for the user's next Ctrl-D.
4147
+ const buf: string[] = Array.isArray(stdinObj.buffer)
4148
+ ? stdinObj.buffer.splice(0)
4149
+ : [];
4150
+ (ctx as any).stdin = buf.join('');
4151
+ } else if (typeof stdinObj.readAll === 'function') {
4152
+ // Pipe reader — upstream will close() after writing, so
4153
+ // readAll() resolves bounded.
4154
+ (ctx as any).stdin = await stdinObj.readAll();
4155
+ } else if (typeof stdinObj.toString === 'function') {
4156
+ (ctx as any).stdin = stdinObj.toString();
4157
+ }
4158
+ }
4159
+ const result = fn(ctx);
4160
+ return await result;
4161
+ } catch (e: any) {
4162
+ ctx.stderr.write(`${e?.message || e}\n`);
4163
+ return 1;
4164
+ }
4165
+ };
4166
+ }
4167
+
4168
+ export function registerUnixCommands(
4169
+ registry: any,
4170
+ sqliteVfs: SqliteVFS,
4171
+ ): void {
4172
+ registry.register('which', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkWhich(vfs, registry))));
4173
+ registry.register('whereis', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkWhereis(vfs, registry))));
4174
+ registry.register('command', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkCommand(vfs, registry))));
4175
+ registry.register('type', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkType(vfs, registry))));
4176
+ registry.register('env', wrap(mkEnv()));
4177
+ registry.register('export', wrap(mkExport()));
4178
+ registry.register('unset', wrap(mkUnset()));
4179
+ registry.register('clear', wrap(mkClear()));
4180
+ registry.register('date', wrap(mkDate()));
4181
+ registry.register('uptime', wrap(mkUptime()));
4182
+ registry.register('tree', wrap(withInvocationVfs(sqliteVfs, mkTree)));
4183
+ registry.register('find', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkFind(vfs, registry))));
4184
+ registry.register('grep', wrap(withInvocationVfs(sqliteVfs, mkGrep)));
4185
+ // SHELL-R6-B2: head uses streaming wrap so a pipe reader passes
4186
+ // through (head terminates after N lines, triggering the abort
4187
+ // cascade for upstream producers like `yes`).
4188
+ registry.register('head', wrapStreaming(withInvocationVfs(sqliteVfs, mkHead)));
4189
+ registry.register('tail', wrap(withInvocationVfs(sqliteVfs, mkTail)));
4190
+ registry.register('wc', wrap(withInvocationVfs(sqliteVfs, mkWc)));
4191
+ registry.register('sort', wrap(withInvocationVfs(sqliteVfs, mkSort)));
4192
+ registry.register('uniq', wrap(withInvocationVfs(sqliteVfs, mkUniq)));
4193
+ registry.register('sed', wrap(withInvocationVfs(sqliteVfs, mkSed)));
4194
+ registry.register('awk', wrap(withInvocationVfs(sqliteVfs, mkAwk)));
4195
+ registry.register('xargs', wrap(withInvocationVfs(sqliteVfs, (vfs) => mkXargs(vfs, registry))));
4196
+ registry.register('tee', wrap(withInvocationVfs(sqliteVfs, mkTee)));
4197
+ registry.register('du', wrap(withInvocationVfs(sqliteVfs, mkDu)));
4198
+ registry.register('diff', wrap(withInvocationVfs(sqliteVfs, mkDiff)));
4199
+ // Registry-level echo + cat for xargs cross-command dispatch.
4200
+ // Shell.builtins still wins for direct `echo X` invocations; this
4201
+ // entry is only reached when a command (xargs etc.) looks them up
4202
+ // via the registry path.
4203
+ registry.register('echo', wrap(mkEcho()));
4204
+ registry.register('cat', wrap(withInvocationVfs(sqliteVfs, mkCat)));
4205
+ registry.register('ls', wrap(withInvocationVfs(sqliteVfs, mkLs)));
4206
+ registry.register('rm', wrap(withInvocationVfs(sqliteVfs, mkRm)));
4207
+ registry.register('touch', wrap(withInvocationVfs(sqliteVfs, mkTouch)));
4208
+ registry.register('stat', wrap(withInvocationVfs(sqliteVfs, (v) => mkStat(v, sqliteVfs))));
4209
+ registry.register('base64', wrap(withInvocationVfs(sqliteVfs, mkBase64)));
4210
+ registry.register('seq', wrap(mkSeq()));
4211
+ registry.register('id', wrap(mkId(sqliteVfs)));
4212
+ registry.register('hostname', wrap(mkHostname()));
4213
+ registry.register('basename', wrap(mkBasename()));
4214
+ registry.register('dirname', wrap(mkDirname()));
4215
+ registry.register('realpath', wrap(withInvocationVfs(sqliteVfs, mkRealpath)));
4216
+ registry.register('printf', wrap(mkPrintf()));
4217
+ registry.register('true', wrap(mkTrue()));
4218
+ registry.register('false', wrap(mkFalse()));
4219
+ registry.register('readlink', wrap(withInvocationVfs(sqliteVfs, mkReadlink)));
4220
+ registry.register('sha256sum', wrap(withInvocationVfs(sqliteVfs, mkSha256sum)));
4221
+ registry.register('file', wrap(withInvocationVfs(sqliteVfs, mkFile)));
4222
+ registry.register('xxd', wrap(withInvocationVfs(sqliteVfs, mkXxd)));
4223
+
4224
+ registry.register('chown', wrap(mkChown(sqliteVfs)));
4225
+
4226
+ // ln — symlink stub (no-ops on VFS but doesn't error)
4227
+ /**
4228
+ * SHELL-FOLLOWUPS-4 (2026-05-11): real `ln -s` via SymlinkRegistry.
4229
+ * Pre-fix: ln -s did file-copy; modifications to the "link"
4230
+ * created a new file (no two-way reflection); readlink returned
4231
+ * empty.
4232
+ *
4233
+ * Now:
4234
+ * - `ln -s TARGET LINKPATH` registers LINKPATH → TARGET in the
4235
+ * symlink registry.
4236
+ * - Real GNU `ln -s` doesn't require TARGET to exist (dangling
4237
+ * symlinks are valid). We allow that.
4238
+ * - Hard links (`ln` without -s) still do file-copy — Nimbus
4239
+ * VFS doesn't expose inode-level hard-linking and that's
4240
+ * documented out of scope.
4241
+ *
4242
+ * Subsequent operations (cat / ls / cat-via-redirect) need to
4243
+ * consult the registry to see through the symlink. That work
4244
+ * lives in mkCat / mkLs wrappers and the SqliteVFS read-path —
4245
+ * for v1, we wire it in the COMMANDS that need it (cat already
4246
+ * goes through Kernel.VFS which doesn't yet know about the
4247
+ * registry; we patch cat directly here to dereference symlinks).
4248
+ */
4249
+ registry.register('ln', wrap((ctx) => {
4250
+ const vfs = unixVfsFor(sqliteVfs, ctx.cred);
4251
+ const args = ctx.args;
4252
+ const symbolic = args.some(a => a === '-s' || (a.startsWith('-') && !a.startsWith('--') && a.includes('s')));
4253
+ const force = args.some(a => a === '-f' || (a.startsWith('-') && !a.startsWith('--') && a.includes('f')));
4254
+ const positional = args.filter(a => !a.startsWith('-'));
4255
+ if (positional.length < 2) {
4256
+ ctx.stderr.write('ln: missing operand\n');
4257
+ return 1;
4258
+ }
4259
+ const target = positional[0]; // what the link points TO
4260
+ const linkPath = positional[1]; // the link file itself
4261
+ const linkFp = resolvePath(ctx.cwd, linkPath);
4262
+ if (symbolic) {
4263
+ // Symbolic link via registry. Don't require target to exist.
4264
+ const reg = vfs.symlinks;
4265
+ // GNU `ln` without -f errors if link exists.
4266
+ if (!force && (vfs.exists(linkFp) || reg.isSymlink(linkFp))) {
4267
+ ctx.stderr.write(`ln: failed to create symbolic link '${linkPath}': File exists\n`);
4268
+ return 1;
4269
+ }
4270
+ // Remove existing real-file at linkPath if -f and not a symlink.
4271
+ if (force && vfs.exists(linkFp) && !reg.isSymlink(linkFp)) {
4272
+ try { vfs.unlink(linkFp); } catch { /* fail-soft */ }
4273
+ }
4274
+ reg.set(linkFp, target);
4275
+ return 0;
4276
+ }
4277
+ // Hard link mode (default): file-copy semantics (legacy behaviour).
4278
+ const srcFp = resolvePath(ctx.cwd, target);
4279
+ try {
4280
+ const content = vfs.readFileString(srcFp);
4281
+ vfs.writeFile(linkFp, content);
4282
+ } catch (e: any) {
4283
+ ctx.stderr.write(`ln: ${target}: ${e?.message || e}\n`);
4284
+ return 1;
4285
+ }
4286
+ return 0;
4287
+ }));
4288
+
4289
+ registry.register('test', wrap(mkTest(sqliteVfs)));
4290
+ registry.register('[', wrap(mkTest(sqliteVfs)));
4291
+
4292
+ // read — read a line (stub, returns empty for non-interactive)
4293
+ // shell-polish (2026-05-12): `read VAR` is registered here as a
4294
+ // NO-OP fallback (matches the pre-existing stub behaviour). The
4295
+ // REAL working implementation lives in src/session/init.ts as a
4296
+ // shell builtin (shellAny.builtins.set('read', ...)).
4297
+ //
4298
+ // Why two registrations: the interpreter executes registered
4299
+ // shell commands with `ctx.env = { ...this.config.env }` — a SHALLOW
4300
+ // COPY.
4301
+ // Mutating ctx.env inside a registered command therefore CANNOT
4302
+ // propagate the var-assignment back to the shell. Builtins, by
4303
+ // contrast, run inside the interp instance with direct access to
4304
+ // `this.env` (the real shell env). The wait builtin in shell/compat/r6 uses
4305
+ // the same workaround.
4306
+ //
4307
+ // Keep the registry stub so `type read` reports "shell builtin" and
4308
+ // `which read` doesn't error; the builtin always wins dispatch
4309
+ // (interp.executeSimpleCommand checks builtins.get BEFORE
4310
+ // registry.resolve — index-Djm2onjx.js:5182-5186).
4311
+ registry.register('read', wrap((ctx) => {
4312
+ const args = ctx.args.filter((a) => !a.startsWith('-'));
4313
+ const varName = args[0] || 'REPLY';
4314
+ ctx.env[varName] = '';
4315
+ return 0;
4316
+ }));
4317
+
4318
+ // exit — exit with code
4319
+ registry.register('exit', wrap((ctx) => {
4320
+ return parseInt(ctx.args[0] || '0') || 0;
4321
+ }));
4322
+
4323
+ // source / . — source a file (stub)
4324
+ registry.register('source', wrap(() => 0));
4325
+ registry.register('.', wrap(() => 0));
4326
+
4327
+ // noop commands that scripts might call
4328
+ registry.register('set', wrap(() => 0));
4329
+ registry.register('shopt', wrap(() => 0));
4330
+ registry.register('trap', wrap(() => 0));
4331
+ registry.register('umask', createUmaskCommand());
4332
+ registry.register('su', createSuCommand());
4333
+ registry.register('sudo', createSudoCommand());
4334
+ registry.register('ulimit', wrap(() => 0));
4335
+ }