@nimbus-sh/worker 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 (408) hide show
  1. package/README.md +219 -0
  2. package/dist/_shared/bytes.d.ts +18 -0
  3. package/dist/_shared/bytes.d.ts.map +1 -0
  4. package/dist/_shared/bytes.js +17 -0
  5. package/dist/_shared/cache-stats.d.ts +146 -0
  6. package/dist/_shared/cache-stats.d.ts.map +1 -0
  7. package/dist/_shared/cache-stats.js +162 -0
  8. package/dist/_shared/exports-resolver.d.ts +76 -0
  9. package/dist/_shared/exports-resolver.d.ts.map +1 -0
  10. package/dist/_shared/exports-resolver.js +284 -0
  11. package/dist/_shared/install-phase.d.ts +47 -0
  12. package/dist/_shared/install-phase.d.ts.map +1 -0
  13. package/dist/_shared/install-phase.js +1 -0
  14. package/dist/_shared/real-node-imports.d.ts +38 -0
  15. package/dist/_shared/real-node-imports.d.ts.map +1 -0
  16. package/dist/_shared/real-node-imports.js +46 -0
  17. package/dist/_shared/retry.d.ts +93 -0
  18. package/dist/_shared/retry.d.ts.map +1 -0
  19. package/dist/_shared/retry.js +161 -0
  20. package/dist/_shared/session-id.d.ts +39 -0
  21. package/dist/_shared/session-id.d.ts.map +1 -0
  22. package/dist/_shared/session-id.js +127 -0
  23. package/dist/_shared/session-router.d.ts +104 -0
  24. package/dist/_shared/session-router.d.ts.map +1 -0
  25. package/dist/_shared/session-router.js +165 -0
  26. package/dist/_shared/w7-frame.d.ts +93 -0
  27. package/dist/_shared/w7-frame.d.ts.map +1 -0
  28. package/dist/_shared/w7-frame.js +402 -0
  29. package/dist/auth/index.d.ts +16 -0
  30. package/dist/auth/index.d.ts.map +1 -0
  31. package/dist/auth/index.js +15 -0
  32. package/dist/auth/middleware.d.ts +85 -0
  33. package/dist/auth/middleware.d.ts.map +1 -0
  34. package/dist/auth/middleware.js +150 -0
  35. package/dist/auth/token.d.ts +96 -0
  36. package/dist/auth/token.d.ts.map +1 -0
  37. package/dist/auth/token.js +226 -0
  38. package/dist/auth/types.d.ts +141 -0
  39. package/dist/auth/types.d.ts.map +1 -0
  40. package/dist/auth/types.js +114 -0
  41. package/dist/bindings/d1.d.ts +102 -0
  42. package/dist/bindings/d1.d.ts.map +1 -0
  43. package/dist/bindings/d1.js +549 -0
  44. package/dist/bindings/kv.d.ts +82 -0
  45. package/dist/bindings/kv.d.ts.map +1 -0
  46. package/dist/bindings/kv.js +290 -0
  47. package/dist/bindings/r2.d.ts +137 -0
  48. package/dist/bindings/r2.d.ts.map +1 -0
  49. package/dist/bindings/r2.js +379 -0
  50. package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
  51. package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
  52. package/dist/cirrus-npm-cjs.generated.js +41 -0
  53. package/dist/cirrus-plugin-react.generated.d.ts +34 -0
  54. package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
  55. package/dist/cirrus-plugin-react.generated.js +33 -0
  56. package/dist/constants.d.ts +34 -0
  57. package/dist/constants.d.ts.map +1 -0
  58. package/dist/constants.js +116 -0
  59. package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
  60. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
  61. package/dist/esbuild-wasm-bundle.generated.js +40 -0
  62. package/dist/facets/cirrus-real.d.ts +147 -0
  63. package/dist/facets/cirrus-real.d.ts.map +1 -0
  64. package/dist/facets/cirrus-real.js +981 -0
  65. package/dist/facets/inner-do-registry.d.ts +41 -0
  66. package/dist/facets/inner-do-registry.d.ts.map +1 -0
  67. package/dist/facets/inner-do-registry.js +51 -0
  68. package/dist/facets/manager.d.ts +314 -0
  69. package/dist/facets/manager.d.ts.map +1 -0
  70. package/dist/facets/manager.js +2681 -0
  71. package/dist/facets/process.d.ts +318 -0
  72. package/dist/facets/process.d.ts.map +1 -0
  73. package/dist/facets/process.js +567 -0
  74. package/dist/facets/real-vite-fs-shim.d.ts +151 -0
  75. package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
  76. package/dist/facets/real-vite-fs-shim.js +1279 -0
  77. package/dist/facets/real-vite-hmr.d.ts +142 -0
  78. package/dist/facets/real-vite-hmr.d.ts.map +1 -0
  79. package/dist/facets/real-vite-hmr.js +476 -0
  80. package/dist/facets/vite-dev-server.d.ts +293 -0
  81. package/dist/facets/vite-dev-server.d.ts.map +1 -0
  82. package/dist/facets/vite-dev-server.js +2580 -0
  83. package/dist/facets/wasm-swap-registry.d.ts +261 -0
  84. package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
  85. package/dist/facets/wasm-swap-registry.js +551 -0
  86. package/dist/facets/ws-terminal.d.ts +107 -0
  87. package/dist/facets/ws-terminal.d.ts.map +1 -0
  88. package/dist/facets/ws-terminal.js +207 -0
  89. package/dist/git/commands.d.ts +13 -0
  90. package/dist/git/commands.d.ts.map +1 -0
  91. package/dist/git/commands.js +633 -0
  92. package/dist/git/network-facet.d.ts +61 -0
  93. package/dist/git/network-facet.d.ts.map +1 -0
  94. package/dist/git/network-facet.js +700 -0
  95. package/dist/git-bundle.generated.d.ts +16 -0
  96. package/dist/git-bundle.generated.d.ts.map +1 -0
  97. package/dist/git-bundle.generated.js +15 -0
  98. package/dist/index.d.ts +41 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +73 -0
  101. package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
  102. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
  103. package/dist/loaders/child-process/spawn-facet.js +103 -0
  104. package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
  105. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
  106. package/dist/loaders/child-process/spawn-pool.js +161 -0
  107. package/dist/loaders/fanout-pool.d.ts +195 -0
  108. package/dist/loaders/fanout-pool.d.ts.map +1 -0
  109. package/dist/loaders/fanout-pool.js +326 -0
  110. package/dist/loaders/generated-workers.d.ts +26 -0
  111. package/dist/loaders/generated-workers.d.ts.map +1 -0
  112. package/dist/loaders/generated-workers.js +25 -0
  113. package/dist/loaders/index.d.ts +15 -0
  114. package/dist/loaders/index.d.ts.map +1 -0
  115. package/dist/loaders/index.js +13 -0
  116. package/dist/loaders/loader-pool.d.ts +273 -0
  117. package/dist/loaders/loader-pool.d.ts.map +1 -0
  118. package/dist/loaders/loader-pool.js +624 -0
  119. package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
  120. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
  121. package/dist/loaders/npm-resolve-preamble.js +237 -0
  122. package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
  123. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
  124. package/dist/loaders/pre-bundle-preamble.js +114 -0
  125. package/dist/loaders/vendor/codegen.d.ts +24 -0
  126. package/dist/loaders/vendor/codegen.d.ts.map +1 -0
  127. package/dist/loaders/vendor/codegen.js +44 -0
  128. package/dist/loaders/vendor/errors.d.ts +24 -0
  129. package/dist/loaders/vendor/errors.d.ts.map +1 -0
  130. package/dist/loaders/vendor/errors.js +46 -0
  131. package/dist/loaders/vendor/index.d.ts +16 -0
  132. package/dist/loaders/vendor/index.d.ts.map +1 -0
  133. package/dist/loaders/vendor/index.js +11 -0
  134. package/dist/loaders/vendor/pool.d.ts +88 -0
  135. package/dist/loaders/vendor/pool.d.ts.map +1 -0
  136. package/dist/loaders/vendor/pool.js +353 -0
  137. package/dist/loaders/vendor/primitives.d.ts +11 -0
  138. package/dist/loaders/vendor/primitives.d.ts.map +1 -0
  139. package/dist/loaders/vendor/primitives.js +33 -0
  140. package/dist/loaders/vendor/serialize.d.ts +3 -0
  141. package/dist/loaders/vendor/serialize.d.ts.map +1 -0
  142. package/dist/loaders/vendor/serialize.js +25 -0
  143. package/dist/loaders/vendor/types.d.ts +60 -0
  144. package/dist/loaders/vendor/types.d.ts.map +1 -0
  145. package/dist/loaders/vendor/types.js +4 -0
  146. package/dist/npm/cache.d.ts +136 -0
  147. package/dist/npm/cache.d.ts.map +1 -0
  148. package/dist/npm/cache.js +429 -0
  149. package/dist/npm/install-batch-facet.d.ts +129 -0
  150. package/dist/npm/install-batch-facet.d.ts.map +1 -0
  151. package/dist/npm/install-batch-facet.js +598 -0
  152. package/dist/npm/install-facet.d.ts +29 -0
  153. package/dist/npm/install-facet.d.ts.map +1 -0
  154. package/dist/npm/install-facet.js +9 -0
  155. package/dist/npm/installer.d.ts +239 -0
  156. package/dist/npm/installer.d.ts.map +1 -0
  157. package/dist/npm/installer.js +2022 -0
  158. package/dist/npm/npx-install.d.ts +74 -0
  159. package/dist/npm/npx-install.d.ts.map +1 -0
  160. package/dist/npm/npx-install.js +302 -0
  161. package/dist/npm/pre-bundle-facet.d.ts +168 -0
  162. package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
  163. package/dist/npm/pre-bundle-facet.js +653 -0
  164. package/dist/npm/r2-cache.d.ts +286 -0
  165. package/dist/npm/r2-cache.d.ts.map +1 -0
  166. package/dist/npm/r2-cache.js +506 -0
  167. package/dist/npm/resolve-facet.d.ts +202 -0
  168. package/dist/npm/resolve-facet.d.ts.map +1 -0
  169. package/dist/npm/resolve-facet.js +833 -0
  170. package/dist/npm/resolve-one-facet.d.ts +204 -0
  171. package/dist/npm/resolve-one-facet.d.ts.map +1 -0
  172. package/dist/npm/resolve-one-facet.js +437 -0
  173. package/dist/npm/resolver.d.ts +109 -0
  174. package/dist/npm/resolver.d.ts.map +1 -0
  175. package/dist/npm/resolver.js +899 -0
  176. package/dist/npm/tarball-stream.d.ts +85 -0
  177. package/dist/npm/tarball-stream.d.ts.map +1 -0
  178. package/dist/npm/tarball-stream.js +205 -0
  179. package/dist/npm/tarball.d.ts +51 -0
  180. package/dist/npm/tarball.d.ts.map +1 -0
  181. package/dist/npm/tarball.js +185 -0
  182. package/dist/observability/diag-counters.d.ts +292 -0
  183. package/dist/observability/diag-counters.d.ts.map +1 -0
  184. package/dist/observability/diag-counters.js +302 -0
  185. package/dist/observability/heap-estimate.d.ts +166 -0
  186. package/dist/observability/heap-estimate.d.ts.map +1 -0
  187. package/dist/observability/heap-estimate.js +192 -0
  188. package/dist/observability/heavy-alloc-coord.d.ts +95 -0
  189. package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
  190. package/dist/observability/heavy-alloc-coord.js +149 -0
  191. package/dist/observability/oom-classify.d.ts +56 -0
  192. package/dist/observability/oom-classify.d.ts.map +1 -0
  193. package/dist/observability/oom-classify.js +118 -0
  194. package/dist/observability/oom-discriminator.d.ts +192 -0
  195. package/dist/observability/oom-discriminator.d.ts.map +1 -0
  196. package/dist/observability/oom-discriminator.js +233 -0
  197. package/dist/real-vite-bundle.generated.d.ts +46 -0
  198. package/dist/real-vite-bundle.generated.d.ts.map +1 -0
  199. package/dist/real-vite-bundle.generated.js +53 -0
  200. package/dist/replica/routing.d.ts +171 -0
  201. package/dist/replica/routing.d.ts.map +1 -0
  202. package/dist/replica/routing.js +288 -0
  203. package/dist/replica/suspension.d.ts +36 -0
  204. package/dist/replica/suspension.d.ts.map +1 -0
  205. package/dist/replica/suspension.js +49 -0
  206. package/dist/router/index.d.ts +156 -0
  207. package/dist/router/index.d.ts.map +1 -0
  208. package/dist/router/index.js +220 -0
  209. package/dist/runtime/assets-loader.d.ts +71 -0
  210. package/dist/runtime/assets-loader.d.ts.map +1 -0
  211. package/dist/runtime/assets-loader.js +128 -0
  212. package/dist/runtime/barrel-detect.d.ts +46 -0
  213. package/dist/runtime/barrel-detect.d.ts.map +1 -0
  214. package/dist/runtime/barrel-detect.js +73 -0
  215. package/dist/runtime/barrel-synthesizer.d.ts +181 -0
  216. package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
  217. package/dist/runtime/barrel-synthesizer.js +541 -0
  218. package/dist/runtime/bun-repl.d.ts +46 -0
  219. package/dist/runtime/bun-repl.d.ts.map +1 -0
  220. package/dist/runtime/bun-repl.js +326 -0
  221. package/dist/runtime/bun-runner.d.ts +66 -0
  222. package/dist/runtime/bun-runner.d.ts.map +1 -0
  223. package/dist/runtime/bun-runner.js +219 -0
  224. package/dist/runtime/clang-runner.d.ts +38 -0
  225. package/dist/runtime/clang-runner.d.ts.map +1 -0
  226. package/dist/runtime/clang-runner.js +1033 -0
  227. package/dist/runtime/comment-strip.d.ts +37 -0
  228. package/dist/runtime/comment-strip.d.ts.map +1 -0
  229. package/dist/runtime/comment-strip.js +74 -0
  230. package/dist/runtime/esbuild-service.d.ts +201 -0
  231. package/dist/runtime/esbuild-service.d.ts.map +1 -0
  232. package/dist/runtime/esbuild-service.js +1444 -0
  233. package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
  234. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
  235. package/dist/runtime/esbuild-wasm-bytes.js +131 -0
  236. package/dist/runtime/framework-detect.d.ts +71 -0
  237. package/dist/runtime/framework-detect.d.ts.map +1 -0
  238. package/dist/runtime/framework-detect.js +178 -0
  239. package/dist/runtime/long-running-handle.d.ts +133 -0
  240. package/dist/runtime/long-running-handle.d.ts.map +1 -0
  241. package/dist/runtime/long-running-handle.js +192 -0
  242. package/dist/runtime/node-repl.d.ts +36 -0
  243. package/dist/runtime/node-repl.d.ts.map +1 -0
  244. package/dist/runtime/node-repl.js +293 -0
  245. package/dist/runtime/node-runner.d.ts +111 -0
  246. package/dist/runtime/node-runner.d.ts.map +1 -0
  247. package/dist/runtime/node-runner.js +194 -0
  248. package/dist/runtime/node-shims.d.ts +24 -0
  249. package/dist/runtime/node-shims.d.ts.map +1 -0
  250. package/dist/runtime/node-shims.js +3019 -0
  251. package/dist/runtime/package-manager.d.ts +118 -0
  252. package/dist/runtime/package-manager.d.ts.map +1 -0
  253. package/dist/runtime/package-manager.js +420 -0
  254. package/dist/runtime/port-registry.d.ts +80 -0
  255. package/dist/runtime/port-registry.d.ts.map +1 -0
  256. package/dist/runtime/port-registry.js +166 -0
  257. package/dist/runtime/process-logs-api.d.ts +102 -0
  258. package/dist/runtime/process-logs-api.d.ts.map +1 -0
  259. package/dist/runtime/process-logs-api.js +253 -0
  260. package/dist/runtime/process-logs.d.ts +311 -0
  261. package/dist/runtime/process-logs.d.ts.map +1 -0
  262. package/dist/runtime/process-logs.js +656 -0
  263. package/dist/runtime/process-table.d.ts +65 -0
  264. package/dist/runtime/process-table.d.ts.map +1 -0
  265. package/dist/runtime/process-table.js +107 -0
  266. package/dist/runtime/project-detect.d.ts +21 -0
  267. package/dist/runtime/project-detect.d.ts.map +1 -0
  268. package/dist/runtime/project-detect.js +46 -0
  269. package/dist/runtime/python-repl.d.ts +66 -0
  270. package/dist/runtime/python-repl.d.ts.map +1 -0
  271. package/dist/runtime/python-repl.js +563 -0
  272. package/dist/runtime/python-runner.d.ts +50 -0
  273. package/dist/runtime/python-runner.d.ts.map +1 -0
  274. package/dist/runtime/python-runner.js +870 -0
  275. package/dist/runtime/repl-session.d.ts +160 -0
  276. package/dist/runtime/repl-session.d.ts.map +1 -0
  277. package/dist/runtime/repl-session.js +393 -0
  278. package/dist/runtime/require-resolver.d.ts +52 -0
  279. package/dist/runtime/require-resolver.d.ts.map +1 -0
  280. package/dist/runtime/require-resolver.js +690 -0
  281. package/dist/runtime/router-basename.d.ts +36 -0
  282. package/dist/runtime/router-basename.d.ts.map +1 -0
  283. package/dist/runtime/router-basename.js +310 -0
  284. package/dist/runtime/ruby-repl.d.ts +43 -0
  285. package/dist/runtime/ruby-repl.d.ts.map +1 -0
  286. package/dist/runtime/ruby-repl.js +229 -0
  287. package/dist/runtime/ruby-runner.d.ts +62 -0
  288. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  289. package/dist/runtime/ruby-runner.js +737 -0
  290. package/dist/runtime/runtime-catalog.d.ts +99 -0
  291. package/dist/runtime/runtime-catalog.d.ts.map +1 -0
  292. package/dist/runtime/runtime-catalog.js +183 -0
  293. package/dist/runtime/runtime-registry.d.ts +136 -0
  294. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  295. package/dist/runtime/runtime-registry.js +318 -0
  296. package/dist/runtime/streams.d.ts +13 -0
  297. package/dist/runtime/streams.d.ts.map +1 -0
  298. package/dist/runtime/streams.js +402 -0
  299. package/dist/runtime/wasi-instance.d.ts +190 -0
  300. package/dist/runtime/wasi-instance.d.ts.map +1 -0
  301. package/dist/runtime/wasi-instance.js +2044 -0
  302. package/dist/runtime/wasm-runner.d.ts +117 -0
  303. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  304. package/dist/runtime/wasm-runner.js +705 -0
  305. package/dist/session/bindings.d.ts +168 -0
  306. package/dist/session/bindings.d.ts.map +1 -0
  307. package/dist/session/bindings.js +524 -0
  308. package/dist/session/ctx-exports.d.ts +16 -0
  309. package/dist/session/ctx-exports.d.ts.map +1 -0
  310. package/dist/session/ctx-exports.js +22 -0
  311. package/dist/session/diag.d.ts +62 -0
  312. package/dist/session/diag.d.ts.map +1 -0
  313. package/dist/session/diag.js +118 -0
  314. package/dist/session/fs-watch.d.ts +132 -0
  315. package/dist/session/fs-watch.d.ts.map +1 -0
  316. package/dist/session/fs-watch.js +255 -0
  317. package/dist/session/helpers.d.ts +150 -0
  318. package/dist/session/helpers.d.ts.map +1 -0
  319. package/dist/session/helpers.js +472 -0
  320. package/dist/session/hibernation.d.ts +145 -0
  321. package/dist/session/hibernation.d.ts.map +1 -0
  322. package/dist/session/hibernation.js +385 -0
  323. package/dist/session/init-phases.d.ts +87 -0
  324. package/dist/session/init-phases.d.ts.map +1 -0
  325. package/dist/session/init-phases.js +137 -0
  326. package/dist/session/init.d.ts +46 -0
  327. package/dist/session/init.d.ts.map +1 -0
  328. package/dist/session/init.js +3579 -0
  329. package/dist/session/keys.d.ts +56 -0
  330. package/dist/session/keys.d.ts.map +1 -0
  331. package/dist/session/keys.js +55 -0
  332. package/dist/session/nimbus-session.d.ts +434 -0
  333. package/dist/session/nimbus-session.d.ts.map +1 -0
  334. package/dist/session/nimbus-session.js +1084 -0
  335. package/dist/session/programmatic.d.ts +105 -0
  336. package/dist/session/programmatic.d.ts.map +1 -0
  337. package/dist/session/programmatic.js +326 -0
  338. package/dist/session/replica-routes.d.ts +68 -0
  339. package/dist/session/replica-routes.d.ts.map +1 -0
  340. package/dist/session/replica-routes.js +77 -0
  341. package/dist/session/routes.d.ts +29 -0
  342. package/dist/session/routes.d.ts.map +1 -0
  343. package/dist/session/routes.js +1331 -0
  344. package/dist/session/rpc.d.ts +323 -0
  345. package/dist/session/rpc.d.ts.map +1 -0
  346. package/dist/session/rpc.js +808 -0
  347. package/dist/session/state-store.d.ts +210 -0
  348. package/dist/session/state-store.d.ts.map +1 -0
  349. package/dist/session/state-store.js +459 -0
  350. package/dist/session/supervisor-rpc.d.ts +269 -0
  351. package/dist/session/supervisor-rpc.d.ts.map +1 -0
  352. package/dist/session/supervisor-rpc.js +437 -0
  353. package/dist/session/ws-hibernation-config.d.ts +64 -0
  354. package/dist/session/ws-hibernation-config.d.ts.map +1 -0
  355. package/dist/session/ws-hibernation-config.js +89 -0
  356. package/dist/session/ws.d.ts +95 -0
  357. package/dist/session/ws.d.ts.map +1 -0
  358. package/dist/session/ws.js +395 -0
  359. package/dist/shell/features.d.ts +410 -0
  360. package/dist/shell/features.d.ts.map +1 -0
  361. package/dist/shell/features.js +1885 -0
  362. package/dist/shell/unix-commands.d.ts +15 -0
  363. package/dist/shell/unix-commands.d.ts.map +1 -0
  364. package/dist/shell/unix-commands.js +3703 -0
  365. package/dist/stubs/lifo-ui.d.ts +10 -0
  366. package/dist/stubs/lifo-ui.d.ts.map +1 -0
  367. package/dist/stubs/lifo-ui.js +1 -0
  368. package/dist/tailwind-play.generated.d.ts +19 -0
  369. package/dist/tailwind-play.generated.d.ts.map +1 -0
  370. package/dist/tailwind-play.generated.js +18 -0
  371. package/dist/vfs/dev-provider.d.ts +64 -0
  372. package/dist/vfs/dev-provider.d.ts.map +1 -0
  373. package/dist/vfs/dev-provider.js +191 -0
  374. package/dist/vfs/events.d.ts +51 -0
  375. package/dist/vfs/events.d.ts.map +1 -0
  376. package/dist/vfs/events.js +127 -0
  377. package/dist/vfs/path.d.ts +30 -0
  378. package/dist/vfs/path.d.ts.map +1 -0
  379. package/dist/vfs/path.js +41 -0
  380. package/dist/vfs/seed-project.d.ts +62 -0
  381. package/dist/vfs/seed-project.d.ts.map +1 -0
  382. package/dist/vfs/seed-project.js +1045 -0
  383. package/dist/vfs/sqlite-vfs.d.ts +435 -0
  384. package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
  385. package/dist/vfs/sqlite-vfs.js +1670 -0
  386. package/dist/vfs/symlink-registry.d.ts +64 -0
  387. package/dist/vfs/symlink-registry.d.ts.map +1 -0
  388. package/dist/vfs/symlink-registry.js +137 -0
  389. package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
  390. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
  391. package/dist/wrangler/nimbus-wrangler.js +931 -0
  392. package/package.json +77 -0
  393. package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
  394. package/public/_assets/esbuild-0.24.2.wasm +0 -0
  395. package/public/_assets/real-vite-bundle.js +101478 -0
  396. package/public/_assets/rollup.wasm +0 -0
  397. package/public/favicon.svg +8 -0
  398. package/public/index.html +565 -0
  399. package/public/s/index.html +2379 -0
  400. package/scripts/bundle-esbuild-wasm.mjs +244 -0
  401. package/scripts/bundle-facet-workers.mjs +137 -0
  402. package/scripts/bundle-git.mjs +118 -0
  403. package/scripts/bundle-npm-cjs.mjs +363 -0
  404. package/scripts/bundle-plugin-react.mjs +480 -0
  405. package/scripts/bundle-real-vite.mjs +959 -0
  406. package/scripts/bundle-runtime.mjs +600 -0
  407. package/scripts/bundle-tailwind-play.mjs +130 -0
  408. package/scripts/patch-lifo-ui.mjs +131 -0
@@ -0,0 +1,3703 @@
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, chmod, 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
+ import { SymlinkRegistry } from '../vfs/symlink-registry.js';
14
+ import { enc } from '../_shared/bytes.js';
15
+ /**
16
+ * SHELL-FOLLOWUPS-4 (2026-05-11): per-vfs symlink registry singleton.
17
+ * Cached on the VFS instance via a side property so `ln -s` + `readlink`
18
+ * + `ls -la` share the same registry within a session.
19
+ */
20
+ function getSymlinkRegistry(vfs) {
21
+ const v = vfs;
22
+ if (!v.__nimbus_symlink_registry) {
23
+ v.__nimbus_symlink_registry = new SymlinkRegistry(vfs);
24
+ }
25
+ return v.__nimbus_symlink_registry;
26
+ }
27
+ // ── Helpers ─────────────────────────────────────────────────────────────
28
+ function resolvePath(cwd, p) {
29
+ if (p.startsWith('/'))
30
+ return p.replace(/^\/+/, '');
31
+ const c = (cwd || '/home/user').replace(/^\/+/, '');
32
+ const parts = (c + '/' + p).split('/');
33
+ const out = [];
34
+ for (const s of parts) {
35
+ if (s === '..')
36
+ out.pop();
37
+ else if (s !== '.' && s !== '')
38
+ out.push(s);
39
+ }
40
+ return out.join('/');
41
+ }
42
+ function globMatch(pattern, name) {
43
+ const re = pattern
44
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
45
+ .replace(/\*/g, '.*')
46
+ .replace(/\?/g, '.');
47
+ return new RegExp('^' + re + '$').test(name);
48
+ }
49
+ // ── Command implementations ─────────────────────────────────────────────
50
+ /**
51
+ * SHELL-FOLLOWUPS-1 (2026-05-11): POSIX-conformant `which`.
52
+ *
53
+ * Pre-fix: `which clang` printed `clang: nimbus built-in`. Real POSIX
54
+ * which prints the resolved absolute path (`/usr/local/bin/clang`)
55
+ * or exits 1 silently. Scripts using `which` output to feed into
56
+ * `dirname`, `$()`, etc. all broke:
57
+ * PATH_PREFIX=$(dirname $(which clang)) # expected /usr/local/bin
58
+ *
59
+ * Behaviour matches GNU `which`:
60
+ * - PATH search: walk `$PATH`, return first hit's absolute path.
61
+ * - Found in PATH: print path to stdout, exit 0.
62
+ * - Resolved as shell builtin (no -a): exit 1, NO output.
63
+ * - Resolved as shell builtin (with -a): print
64
+ * "<cmd>: shell built-in command" + continue search PATH.
65
+ * - Not found anywhere: stderr "<cmd>: not in (PATH)", exit 1.
66
+ *
67
+ * For our facet-direct runtimes (clang, node, bun, python, ruby,
68
+ * git, npm, etc.), we don't have real on-disk binaries — they
69
+ * dispatch through the registry. To preserve script compatibility,
70
+ * we return canonical POSIX paths under /usr/local/bin and /usr/bin:
71
+ *
72
+ * clang, clang++, cc → /usr/local/bin/clang
73
+ * wasm-ld, lld → /usr/local/bin/wasm-ld
74
+ * node, nodejs → /usr/local/bin/node
75
+ * bun → /usr/local/bin/bun
76
+ * npm, npx → /usr/local/bin/npm
77
+ * git → /usr/bin/git
78
+ * python, python3 → /usr/bin/python3
79
+ * ruby, ruby3 → /usr/bin/ruby
80
+ * wrangler, nimbus-wrangler → /usr/local/bin/wrangler
81
+ * esbuild, tsc, vite, rollup, etc. → /usr/local/bin/<name>
82
+ *
83
+ * These paths are virtual but POSIX-shaped so scripts do the right
84
+ * thing with `dirname $(which X)` etc. The paths don't have to
85
+ * exist on the VFS (real GNU which just stat-walks PATH and
86
+ * doesn't require execute bit at lookup time for stdout — it does
87
+ * for exit code, but match what users expect first).
88
+ *
89
+ * If the user has installed a real binary in PATH (npm install + npx
90
+ * resolution into /home/user/node_modules/.bin/X), prefer the actual
91
+ * VFS-resolved path over the canonical fallback.
92
+ */
93
+ const _CANONICAL_BIN_PATHS = {
94
+ // /usr/local/bin (locally-installed runtimes)
95
+ clang: '/usr/local/bin/clang',
96
+ 'clang++': '/usr/local/bin/clang++',
97
+ cc: '/usr/local/bin/clang',
98
+ 'wasm-ld': '/usr/local/bin/wasm-ld',
99
+ lld: '/usr/local/bin/wasm-ld',
100
+ node: '/usr/local/bin/node',
101
+ nodejs: '/usr/local/bin/node',
102
+ bun: '/usr/local/bin/bun',
103
+ npm: '/usr/local/bin/npm',
104
+ npx: '/usr/local/bin/npx',
105
+ pnpm: '/usr/local/bin/pnpm',
106
+ yarn: '/usr/local/bin/yarn',
107
+ esbuild: '/usr/local/bin/esbuild',
108
+ tsc: '/usr/local/bin/tsc',
109
+ vite: '/usr/local/bin/vite',
110
+ rollup: '/usr/local/bin/rollup',
111
+ webpack: '/usr/local/bin/webpack',
112
+ wrangler: '/usr/local/bin/wrangler',
113
+ 'nimbus-wrangler': '/usr/local/bin/nimbus-wrangler',
114
+ // /usr/bin (system runtimes)
115
+ git: '/usr/bin/git',
116
+ python: '/usr/bin/python3',
117
+ python3: '/usr/bin/python3',
118
+ ruby: '/usr/bin/ruby',
119
+ ruby3: '/usr/bin/ruby',
120
+ sh: '/usr/bin/sh',
121
+ bash: '/usr/bin/bash',
122
+ // Framework CLIs
123
+ astro: '/usr/local/bin/astro',
124
+ nuxt: '/usr/local/bin/nuxt',
125
+ nuxi: '/usr/local/bin/nuxi',
126
+ next: '/usr/local/bin/next',
127
+ remix: '/usr/local/bin/remix',
128
+ 'svelte-kit': '/usr/local/bin/svelte-kit',
129
+ husky: '/usr/local/bin/husky',
130
+ lefthook: '/usr/local/bin/lefthook',
131
+ 'simple-git-hooks': '/usr/local/bin/simple-git-hooks',
132
+ 'lint-staged': '/usr/local/bin/lint-staged',
133
+ yorkie: '/usr/local/bin/yorkie',
134
+ // Self
135
+ nimbus: '/usr/local/bin/nimbus',
136
+ };
137
+ /** Resolve a command name to a path via PATH-walk + canonical-bin
138
+ * fallback. Returns null if not findable. Skip-canonical when the
139
+ * caller knows the command is a shell builtin (no fallback). */
140
+ function _whichLookup(vfs, name, envPath) {
141
+ // 1. Real on-disk binary in $PATH (npm-installed node_modules/.bin,
142
+ // user-placed scripts, etc.).
143
+ const paths = (envPath || '/usr/local/bin:/usr/bin:/bin').split(':');
144
+ for (const dir of paths) {
145
+ if (!dir)
146
+ continue;
147
+ const stripped = dir.replace(/^\/+/, '').replace(/\/+$/, '');
148
+ const fp = stripped + '/' + name;
149
+ if (vfs.exists(fp) && !vfs.isDirectory(fp)) {
150
+ return '/' + fp;
151
+ }
152
+ }
153
+ // 2. Canonical-bin fallback for our facet-direct runtimes.
154
+ if (_CANONICAL_BIN_PATHS[name]) {
155
+ return _CANONICAL_BIN_PATHS[name];
156
+ }
157
+ return null;
158
+ }
159
+ function mkWhich(vfs, registry) {
160
+ return async (ctx) => {
161
+ // Parse flags. Supports -a (show all matches), -s (silent — no
162
+ // stdout, only exit code). Default behaviour matches GNU which.
163
+ let showAll = false;
164
+ let silent = false;
165
+ const names = [];
166
+ for (const a of ctx.args) {
167
+ if (a === '-a' || a === '--all') {
168
+ showAll = true;
169
+ continue;
170
+ }
171
+ if (a === '-s' || a === '--silent') {
172
+ silent = true;
173
+ continue;
174
+ }
175
+ if (a.startsWith('-') && a !== '-') {
176
+ // Combined short flags
177
+ for (const ch of a.slice(1)) {
178
+ if (ch === 'a')
179
+ showAll = true;
180
+ else if (ch === 's')
181
+ silent = true;
182
+ }
183
+ continue;
184
+ }
185
+ names.push(a);
186
+ }
187
+ if (names.length === 0) {
188
+ ctx.stderr.write('Usage: which [-as] command [command ...]\n');
189
+ return 1;
190
+ }
191
+ let anyMissing = false;
192
+ for (const name of names) {
193
+ // Classify: is it a registry-resolvable command (shell builtin)?
194
+ let isBuiltin = false;
195
+ try {
196
+ const resolved = typeof registry.resolve === 'function'
197
+ ? await registry.resolve(name)
198
+ : null;
199
+ isBuiltin = !!resolved;
200
+ }
201
+ catch { /* registry unavailable; treat as not-builtin */ }
202
+ // 1. PATH-walk + canonical-bin lookup.
203
+ const path = _whichLookup(vfs, name, ctx.env.PATH || '');
204
+ let found = false;
205
+ if (path) {
206
+ if (!silent)
207
+ ctx.stdout.write(path + '\n');
208
+ found = true;
209
+ }
210
+ // 2. With -a, also report builtins (real GNU which behaviour).
211
+ if (showAll && isBuiltin) {
212
+ if (!silent)
213
+ ctx.stdout.write(`${name}: shell built-in command\n`);
214
+ found = true;
215
+ }
216
+ // 3. Without -a, if no PATH match but is builtin: GNU which
217
+ // exits 1 silently (with -s suppresses stderr too).
218
+ if (!path && isBuiltin && !showAll) {
219
+ // Exit 1; no output. Matches GNU `which` default.
220
+ anyMissing = true;
221
+ continue;
222
+ }
223
+ if (!found) {
224
+ if (!silent)
225
+ ctx.stderr.write(`which: no ${name} in (${ctx.env.PATH || '/usr/local/bin:/usr/bin:/bin'})\n`);
226
+ anyMissing = true;
227
+ }
228
+ }
229
+ return anyMissing ? 1 : 0;
230
+ };
231
+ }
232
+ /**
233
+ * SHELL-FOLLOWUPS-2 (2026-05-11): `whereis` companion to `which`.
234
+ * GNU whereis prints binary, source, and manpage paths. We only have
235
+ * binaries on the virtual VFS; print just the binary path. With no
236
+ * match, print just the name (matches `whereis` behavior on missing).
237
+ */
238
+ function mkWhereis(vfs) {
239
+ return (ctx) => {
240
+ const names = ctx.args.filter(a => !a.startsWith('-'));
241
+ if (names.length === 0) {
242
+ ctx.stderr.write('Usage: whereis name [name ...]\n');
243
+ return 1;
244
+ }
245
+ for (const name of names) {
246
+ const path = _whichLookup(vfs, name, ctx.env.PATH || '');
247
+ if (path) {
248
+ ctx.stdout.write(`${name}: ${path}\n`);
249
+ }
250
+ else {
251
+ // GNU whereis prints just "name:" when nothing found.
252
+ ctx.stdout.write(`${name}:\n`);
253
+ }
254
+ }
255
+ return 0;
256
+ };
257
+ }
258
+ /**
259
+ * SHELL-FOLLOWUPS-3 (2026-05-11): POSIX `command -v` / `command -V`.
260
+ * Used by shell scripts as the portable alternative to `which`:
261
+ * command -v clang → prints path, exit 0 if found, exit 1 if not
262
+ * command -V clang → verbose form (similar to `type`)
263
+ * command clang ARG → invoke clang bypassing any function/alias
264
+ *
265
+ * For the invoke case (no -v/-V), we don't have a way to bypass
266
+ * alias/function within lifo-sh from here, so we just dispatch to
267
+ * the registry. Aliases are checked at executeLine time so `command
268
+ * X` going through our normal dispatch IS bypassing the alias
269
+ * (because lifo-sh's executeSimpleCommand only consults aliases
270
+ * for the head word).
271
+ */
272
+ function mkCommand(vfs, registry) {
273
+ return async (ctx) => {
274
+ const args = [...ctx.args];
275
+ let mode = 'invoke';
276
+ if (args[0] === '-v') {
277
+ mode = '-v';
278
+ args.shift();
279
+ }
280
+ else if (args[0] === '-V') {
281
+ mode = '-V';
282
+ args.shift();
283
+ }
284
+ if (args.length === 0) {
285
+ if (mode === 'invoke')
286
+ return 0;
287
+ ctx.stderr.write('command: missing operand\n');
288
+ return 1;
289
+ }
290
+ if (mode === '-v') {
291
+ // Print path or builtin marker; exit 0 if found.
292
+ const name = args[0];
293
+ const path = _whichLookup(vfs, name, ctx.env.PATH || '');
294
+ if (path) {
295
+ ctx.stdout.write(path + '\n');
296
+ return 0;
297
+ }
298
+ try {
299
+ const resolved = await registry.resolve(name);
300
+ if (resolved) {
301
+ ctx.stdout.write(name + '\n');
302
+ return 0;
303
+ }
304
+ }
305
+ catch { /* registry miss */ }
306
+ return 1;
307
+ }
308
+ if (mode === '-V') {
309
+ const name = args[0];
310
+ const path = _whichLookup(vfs, name, ctx.env.PATH || '');
311
+ if (path) {
312
+ ctx.stdout.write(`${name} is ${path}\n`);
313
+ return 0;
314
+ }
315
+ try {
316
+ const resolved = await registry.resolve(name);
317
+ if (resolved) {
318
+ ctx.stdout.write(`${name} is a shell builtin\n`);
319
+ return 0;
320
+ }
321
+ }
322
+ catch { /* miss */ }
323
+ ctx.stderr.write(`command: ${name}: not found\n`);
324
+ return 1;
325
+ }
326
+ // invoke mode: dispatch directly via registry. Bypasses aliases
327
+ // because we're calling the resolved cmd not the alias name.
328
+ const name = args[0];
329
+ try {
330
+ const resolved = await registry.resolve(name);
331
+ if (!resolved) {
332
+ ctx.stderr.write(`command: ${name}: not found\n`);
333
+ return 127;
334
+ }
335
+ const subCtx = { ...ctx, args: args.slice(1) };
336
+ const code = await resolved(subCtx);
337
+ return typeof code === 'number' ? code : 0;
338
+ }
339
+ catch (e) {
340
+ ctx.stderr.write(`command: ${name}: ${e?.message || e}\n`);
341
+ return 1;
342
+ }
343
+ };
344
+ }
345
+ /**
346
+ * BUG-SWEEP-R3-6 (2026-05-11): `type` builtin. lifo-sh doesn't ship
347
+ * one; pre-fix `type echo` → 'type: command not found'. bash's
348
+ * `type X` reports how X would be interpreted (builtin, alias,
349
+ * function, file, or unknown).
350
+ *
351
+ * Our subset (matches bash `type` output for common shapes):
352
+ * type echo → 'echo is a shell builtin' (Shell.builtins entry)
353
+ * type ls → 'ls is a shell builtin' (lifo-sh lazy registry)
354
+ * type rm → 'rm is a shell builtin' (our wrap'd registry)
355
+ * type node → 'node is /usr/bin/node' (registry but facet-direct)
356
+ * type X → 'type: X: not found' + exit 1
357
+ *
358
+ * We can't introspect Shell.builtins from here directly (the ctx
359
+ * doesn't carry shell). Workaround: pass registry which the unix-
360
+ * commands module already has access to; treat any registry resolve
361
+ * as "shell builtin" classification.
362
+ */
363
+ function mkType(_vfs, registry) {
364
+ return async (ctx) => {
365
+ if (ctx.args.length === 0)
366
+ return 0;
367
+ let exit = 0;
368
+ for (const name of ctx.args) {
369
+ try {
370
+ const resolved = typeof registry.resolve === 'function' ? await registry.resolve(name) : null;
371
+ if (resolved) {
372
+ ctx.stdout.write(`${name} is a shell builtin\n`);
373
+ }
374
+ else {
375
+ ctx.stderr.write(`type: ${name}: not found\n`);
376
+ exit = 1;
377
+ }
378
+ }
379
+ catch (_e) {
380
+ ctx.stderr.write(`type: ${name}: not found\n`);
381
+ exit = 1;
382
+ }
383
+ }
384
+ return exit;
385
+ };
386
+ }
387
+ function mkEnv() {
388
+ return (ctx) => {
389
+ for (const [k, v] of Object.entries(ctx.env)) {
390
+ ctx.stdout.write(`${k}=${v}\n`);
391
+ }
392
+ return 0;
393
+ };
394
+ }
395
+ function mkExport() {
396
+ return (ctx) => {
397
+ for (const arg of ctx.args) {
398
+ const eqIdx = arg.indexOf('=');
399
+ if (eqIdx > 0) {
400
+ ctx.env[arg.substring(0, eqIdx)] = arg.substring(eqIdx + 1);
401
+ }
402
+ else if (ctx.env[arg] !== undefined) {
403
+ ctx.stdout.write(`export ${arg}="${ctx.env[arg]}"\n`);
404
+ }
405
+ }
406
+ return 0;
407
+ };
408
+ }
409
+ function mkUnset() {
410
+ return (ctx) => {
411
+ for (const name of ctx.args) {
412
+ delete ctx.env[name];
413
+ }
414
+ return 0;
415
+ };
416
+ }
417
+ function mkClear() {
418
+ return (ctx) => { ctx.stdout.write('\x1b[2J\x1b[H'); return 0; };
419
+ }
420
+ /**
421
+ * BUG-SWEEP-R2-5 (2026-05-11): date strftime format support.
422
+ *
423
+ * Pre-fix mkDate only honoured `-u`, `-I`, and `+%s` literal. Any
424
+ * other `+FMT` was a no-op falling to `now.toString()`. Real shell
425
+ * scripts use `date +%Y-%m-%d`, `date +%H:%M:%S`, `date +%F`, etc.
426
+ *
427
+ * Post-fix: full strftime subset:
428
+ * %Y / %C / %y year (4-digit / century / 2-digit)
429
+ * %m / %B / %b / %h month (numeric / full name / abbrev / abbrev)
430
+ * %d / %e day of month (zero-padded / space-padded)
431
+ * %j day of year
432
+ * %H / %I / %M / %S hour-24 / hour-12 / minute / second
433
+ * %p AM/PM
434
+ * %A / %a weekday (full / abbrev)
435
+ * %u / %w ISO weekday (1=Mon..7=Sun) / weekday (0=Sun..6=Sat)
436
+ * %s unix timestamp (seconds)
437
+ * %N nanoseconds (zero-pad to 9 digits)
438
+ * %F %Y-%m-%d
439
+ * %T / %R %H:%M:%S / %H:%M
440
+ * %D %m/%d/%y
441
+ * %z / %Z timezone offset / name
442
+ * %% literal %
443
+ * %n / %t newline / tab
444
+ */
445
+ function mkDate() {
446
+ return (ctx) => {
447
+ const now = new Date();
448
+ const useUtc = ctx.args.includes('-u') || ctx.args.includes('--utc');
449
+ // Find the `+FMT` arg (if any). Real `date +FMT [args]` accepts
450
+ // only one format; we honour the first.
451
+ const fmtArg = ctx.args.find(a => a.startsWith('+'));
452
+ if (fmtArg) {
453
+ ctx.stdout.write(strftime(now, fmtArg.slice(1), useUtc) + '\n');
454
+ return 0;
455
+ }
456
+ if (ctx.args.includes('-I') || ctx.args.includes('--iso-8601')) {
457
+ ctx.stdout.write(now.toISOString() + '\n');
458
+ return 0;
459
+ }
460
+ if (useUtc) {
461
+ ctx.stdout.write(now.toUTCString() + '\n');
462
+ return 0;
463
+ }
464
+ ctx.stdout.write(now.toString() + '\n');
465
+ return 0;
466
+ };
467
+ }
468
+ const _MONTHS_FULL = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
469
+ const _MONTHS_ABBR = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
470
+ const _DAYS_FULL = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
471
+ const _DAYS_ABBR = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
472
+ function strftime(d, fmt, utc) {
473
+ const get = (m) => {
474
+ switch (m) {
475
+ case 'FullYear': return utc ? d.getUTCFullYear() : d.getFullYear();
476
+ case 'Month': return utc ? d.getUTCMonth() : d.getMonth();
477
+ case 'Date': return utc ? d.getUTCDate() : d.getDate();
478
+ case 'Hours': return utc ? d.getUTCHours() : d.getHours();
479
+ case 'Minutes': return utc ? d.getUTCMinutes() : d.getMinutes();
480
+ case 'Seconds': return utc ? d.getUTCSeconds() : d.getSeconds();
481
+ case 'Day': return utc ? d.getUTCDay() : d.getDay();
482
+ case 'Milliseconds': return utc ? d.getUTCMilliseconds() : d.getMilliseconds();
483
+ default: return 0;
484
+ }
485
+ };
486
+ const pad = (n, w, ch = '0') => String(n).padStart(w, ch);
487
+ const yyyy = get('FullYear');
488
+ const mm0 = get('Month'); // 0..11
489
+ const dd = get('Date');
490
+ const hh = get('Hours');
491
+ const mn = get('Minutes');
492
+ const ss = get('Seconds');
493
+ const dow = get('Day'); // 0..6 (Sun..Sat)
494
+ const ms = get('Milliseconds');
495
+ // Day of year: difference from Jan 1.
496
+ const jan1 = utc
497
+ ? Date.UTC(yyyy, 0, 1)
498
+ : new Date(yyyy, 0, 1).getTime();
499
+ const doy = Math.floor((d.getTime() - jan1) / 86400000) + 1;
500
+ // ISO weekday: 1=Mon..7=Sun.
501
+ const isoDow = dow === 0 ? 7 : dow;
502
+ const ampm = hh < 12 ? 'AM' : 'PM';
503
+ const h12 = hh % 12 === 0 ? 12 : hh % 12;
504
+ // TZ offset in ±HHMM form.
505
+ const tzOff = utc ? '+0000' : (() => {
506
+ const off = -d.getTimezoneOffset();
507
+ const sign = off >= 0 ? '+' : '-';
508
+ const abs = Math.abs(off);
509
+ return sign + pad(Math.floor(abs / 60), 2) + pad(abs % 60, 2);
510
+ })();
511
+ const tzName = utc ? 'UTC' : (() => {
512
+ try {
513
+ const parts = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' }).formatToParts(d);
514
+ const tz = parts.find(p => p.type === 'timeZoneName');
515
+ return tz ? tz.value : 'UTC';
516
+ }
517
+ catch {
518
+ return 'UTC';
519
+ }
520
+ })();
521
+ let out = '';
522
+ let i = 0;
523
+ while (i < fmt.length) {
524
+ const ch = fmt[i];
525
+ if (ch !== '%') {
526
+ out += ch;
527
+ i++;
528
+ continue;
529
+ }
530
+ i++;
531
+ const spec = fmt[i] || '';
532
+ i++;
533
+ switch (spec) {
534
+ case 'Y':
535
+ out += String(yyyy);
536
+ break;
537
+ case 'C':
538
+ out += pad(Math.floor(yyyy / 100), 2);
539
+ break;
540
+ case 'y':
541
+ out += pad(yyyy % 100, 2);
542
+ break;
543
+ case 'm':
544
+ out += pad(mm0 + 1, 2);
545
+ break;
546
+ case 'B':
547
+ out += _MONTHS_FULL[mm0];
548
+ break;
549
+ case 'b':
550
+ case 'h':
551
+ out += _MONTHS_ABBR[mm0];
552
+ break;
553
+ case 'd':
554
+ out += pad(dd, 2);
555
+ break;
556
+ case 'e':
557
+ out += String(dd).padStart(2, ' ');
558
+ break;
559
+ case 'j':
560
+ out += pad(doy, 3);
561
+ break;
562
+ case 'H':
563
+ out += pad(hh, 2);
564
+ break;
565
+ case 'I':
566
+ out += pad(h12, 2);
567
+ break;
568
+ case 'M':
569
+ out += pad(mn, 2);
570
+ break;
571
+ case 'S':
572
+ out += pad(ss, 2);
573
+ break;
574
+ case 'p':
575
+ out += ampm;
576
+ break;
577
+ case 'P':
578
+ out += ampm.toLowerCase();
579
+ break;
580
+ case 'A':
581
+ out += _DAYS_FULL[dow];
582
+ break;
583
+ case 'a':
584
+ out += _DAYS_ABBR[dow];
585
+ break;
586
+ case 'u':
587
+ out += String(isoDow);
588
+ break;
589
+ case 'w':
590
+ out += String(dow);
591
+ break;
592
+ case 's':
593
+ out += String(Math.floor(d.getTime() / 1000));
594
+ break;
595
+ case 'N':
596
+ out += pad(ms * 1_000_000, 9);
597
+ break;
598
+ case 'F':
599
+ out += `${yyyy}-${pad(mm0 + 1, 2)}-${pad(dd, 2)}`;
600
+ break;
601
+ case 'T':
602
+ out += `${pad(hh, 2)}:${pad(mn, 2)}:${pad(ss, 2)}`;
603
+ break;
604
+ case 'R':
605
+ out += `${pad(hh, 2)}:${pad(mn, 2)}`;
606
+ break;
607
+ case 'D':
608
+ out += `${pad(mm0 + 1, 2)}/${pad(dd, 2)}/${pad(yyyy % 100, 2)}`;
609
+ break;
610
+ case 'z':
611
+ out += tzOff;
612
+ break;
613
+ case 'Z':
614
+ out += tzName;
615
+ break;
616
+ case '%':
617
+ out += '%';
618
+ break;
619
+ case 'n':
620
+ out += '\n';
621
+ break;
622
+ case 't':
623
+ out += '\t';
624
+ break;
625
+ default:
626
+ out += '%' + spec;
627
+ break; // unknown — preserve literal
628
+ }
629
+ }
630
+ return out;
631
+ }
632
+ function mkUptime() {
633
+ const start = Date.now();
634
+ return (ctx) => {
635
+ const secs = Math.floor((Date.now() - start) / 1000);
636
+ const h = Math.floor(secs / 3600);
637
+ const m = Math.floor((secs % 3600) / 60);
638
+ ctx.stdout.write(` ${new Date().toTimeString().split(' ')[0]} up ${h}:${String(m).padStart(2, '0')}, 1 user\n`);
639
+ return 0;
640
+ };
641
+ }
642
+ function mkTree(vfs) {
643
+ return (ctx) => {
644
+ const args = ctx.args.filter(a => !a.startsWith('-') && (ctx.args.indexOf(a) !== ctx.args.indexOf('-L') + 1));
645
+ const root = args[0] ? resolvePath(ctx.cwd, args[0]) : (ctx.cwd || '/home/user').replace(/^\/+/, '');
646
+ const maxDepth = ctx.args.includes('-L') ? parseInt(ctx.args[ctx.args.indexOf('-L') + 1]) || 3 : 3;
647
+ const MAX_ENTRIES = 2000; // Safety limit to prevent hanging on huge repos
648
+ let dirs = 0, files = 0, total = 0;
649
+ let truncated = false;
650
+ function walk(path, prefix, depth) {
651
+ if (depth > maxDepth || truncated)
652
+ return;
653
+ try {
654
+ const entries = vfs.readdir(path).sort((a, b) => a.name.localeCompare(b.name));
655
+ for (let i = 0; i < entries.length; i++) {
656
+ if (total >= MAX_ENTRIES) {
657
+ truncated = true;
658
+ return;
659
+ }
660
+ total++;
661
+ const e = entries[i];
662
+ const isLast = i === entries.length - 1;
663
+ const connector = isLast ? '└── ' : '├── ';
664
+ const childPrefix = isLast ? ' ' : '│ ';
665
+ ctx.stdout.write(prefix + connector + e.name + '\n');
666
+ if (e.type === 'directory') {
667
+ dirs++;
668
+ walk(path + '/' + e.name, prefix + childPrefix, depth + 1);
669
+ }
670
+ else {
671
+ files++;
672
+ }
673
+ }
674
+ }
675
+ catch { }
676
+ }
677
+ const name = root.split('/').pop() || root;
678
+ ctx.stdout.write(name + '\n');
679
+ walk(root, '', 1);
680
+ if (truncated)
681
+ ctx.stdout.write(`\n... truncated at ${MAX_ENTRIES} entries\n`);
682
+ ctx.stdout.write(`\n${dirs} directories, ${files} files\n`);
683
+ return 0;
684
+ };
685
+ }
686
+ /**
687
+ * BUG-SWEEP-R4-5 (2026-05-11): find predicates (-size, -mtime).
688
+ *
689
+ * Pre-fix only -name and -type were honoured. `find /x -size 0`
690
+ * and `find /x -mtime -1` were no-ops (returned all files). Common
691
+ * cleanup-script patterns broken:
692
+ * find /tmp -mtime +7 -delete # delete files older than 7d
693
+ * find . -size 0 -type f # find empty files
694
+ * find . -name "*.log" -exec rm {} \\;
695
+ *
696
+ * Predicates supported here:
697
+ * -name PATTERN glob match against basename (existing)
698
+ * -type f|d file/directory (existing)
699
+ * -size [+|-]NUM[c|k|M|G]
700
+ * file size in 512-byte blocks default; with c
701
+ * char (bytes), k KiB, M MiB, G GiB. Prefix
702
+ * '+' = greater, '-' = less.
703
+ * -mtime [+|-]N modification time relative to now (in days)
704
+ * -newer FILE modified more recently than FILE
705
+ * -empty zero-size files OR empty directories
706
+ * -maxdepth N recursion depth limit
707
+ *
708
+ * -exec CMD [ARGS] {} \\; per-match exec of CMD (already supported
709
+ * for limited cases — preserved here).
710
+ * -print, -print0 explicit output formatters
711
+ * -delete delete matching entries (cleanup idiom)
712
+ */
713
+ function mkFind(vfs) {
714
+ return (ctx) => {
715
+ const args = [...ctx.args];
716
+ // First non-flag arg is the start path.
717
+ const root = args[0] && !args[0].startsWith('-')
718
+ ? resolvePath(ctx.cwd, args.shift())
719
+ : (ctx.cwd || '/home/user').replace(/^\/+/, '');
720
+ // Parse predicates in order. We support a flat list of AND'd
721
+ // predicates (real find supports more — sufficient for v1).
722
+ let namePattern = null;
723
+ let typeFilter = null;
724
+ let sizeOp = null;
725
+ let mtimeOp = null;
726
+ let newerThanMtime = null;
727
+ let emptyFilter = false;
728
+ let maxDepth = Infinity;
729
+ let execArgv = null;
730
+ let printNull = false;
731
+ let deleteAction = false;
732
+ for (let i = 0; i < args.length; i++) {
733
+ const a = args[i];
734
+ if (a === '-name') {
735
+ namePattern = args[++i];
736
+ continue;
737
+ }
738
+ if (a === '-type') {
739
+ typeFilter = args[++i];
740
+ continue;
741
+ }
742
+ if (a === '-size') {
743
+ const raw = args[++i] || '';
744
+ const m = raw.match(/^([+-]?)(\d+)([ckMG]?)$/);
745
+ if (m) {
746
+ const cmp = (m[1] === '+' ? '+' : m[1] === '-' ? '-' : '=');
747
+ const n = parseInt(m[2], 10);
748
+ const unit = m[3];
749
+ const bytes = unit === 'c' ? n
750
+ : unit === 'k' ? n * 1024
751
+ : unit === 'M' ? n * 1024 * 1024
752
+ : unit === 'G' ? n * 1024 * 1024 * 1024
753
+ : n * 512; // default: 512-byte blocks
754
+ sizeOp = { cmp, bytes };
755
+ }
756
+ continue;
757
+ }
758
+ if (a === '-mtime') {
759
+ const raw = args[++i] || '';
760
+ const m = raw.match(/^([+-]?)(\d+)$/);
761
+ if (m) {
762
+ const cmp = (m[1] === '+' ? '+' : m[1] === '-' ? '-' : '=');
763
+ const days = parseInt(m[2], 10);
764
+ mtimeOp = { cmp, ms: days * 86400 * 1000 };
765
+ }
766
+ continue;
767
+ }
768
+ if (a === '-newer') {
769
+ const ref = args[++i];
770
+ if (ref) {
771
+ try {
772
+ newerThanMtime = vfs.stat(resolvePath(ctx.cwd, ref)).mtime;
773
+ }
774
+ catch { /* ignore */ }
775
+ }
776
+ continue;
777
+ }
778
+ if (a === '-empty') {
779
+ emptyFilter = true;
780
+ continue;
781
+ }
782
+ if (a === '-maxdepth') {
783
+ const d = parseInt(args[++i] || '', 10);
784
+ if (Number.isFinite(d) && d >= 0)
785
+ maxDepth = d;
786
+ continue;
787
+ }
788
+ if (a === '-exec') {
789
+ // Collect args up to ';'.
790
+ const collected = [];
791
+ i++;
792
+ while (i < args.length && args[i] !== ';' && args[i] !== '\\;') {
793
+ collected.push(args[i]);
794
+ i++;
795
+ }
796
+ execArgv = collected;
797
+ continue;
798
+ }
799
+ if (a === '-print0') {
800
+ printNull = true;
801
+ continue;
802
+ }
803
+ if (a === '-print') { /* default — noop */
804
+ continue;
805
+ }
806
+ if (a === '-delete') {
807
+ deleteAction = true;
808
+ continue;
809
+ }
810
+ // Unknown predicate: ignore (real find would error)
811
+ }
812
+ const now = Date.now();
813
+ function matches(fullPath, name, e) {
814
+ if (namePattern && !globMatch(namePattern, name))
815
+ return false;
816
+ if (typeFilter) {
817
+ if (typeFilter === 'f' && e.type !== 'file')
818
+ return false;
819
+ if (typeFilter === 'd' && e.type !== 'directory')
820
+ return false;
821
+ }
822
+ // For size/mtime/newer we need a stat. Skip for directories
823
+ // unless the predicate cares about them.
824
+ let needsStat = sizeOp || mtimeOp || newerThanMtime !== null || emptyFilter;
825
+ if (!needsStat)
826
+ return true;
827
+ try {
828
+ const st = vfs.stat(fullPath);
829
+ if (sizeOp) {
830
+ const sz = st.size || 0;
831
+ if (sizeOp.cmp === '+' && !(sz > sizeOp.bytes))
832
+ return false;
833
+ if (sizeOp.cmp === '-' && !(sz < sizeOp.bytes))
834
+ return false;
835
+ if (sizeOp.cmp === '=' && sz !== sizeOp.bytes)
836
+ return false;
837
+ }
838
+ if (mtimeOp) {
839
+ const ageMs = now - (st.mtime || 0);
840
+ // bash find -mtime n: file modified n*24h ago.
841
+ // +n → strictly more than n*24h ago (older)
842
+ // -n → less than n*24h ago (newer)
843
+ // n → between (n)*24h and (n+1)*24h ago
844
+ const dayMs = 86400 * 1000;
845
+ if (mtimeOp.cmp === '+' && !(ageMs > mtimeOp.ms + dayMs))
846
+ return false;
847
+ if (mtimeOp.cmp === '-' && !(ageMs < mtimeOp.ms))
848
+ return false;
849
+ if (mtimeOp.cmp === '=' && !(ageMs >= mtimeOp.ms && ageMs < mtimeOp.ms + dayMs))
850
+ return false;
851
+ }
852
+ if (newerThanMtime !== null && !((st.mtime || 0) > newerThanMtime))
853
+ return false;
854
+ if (emptyFilter && (st.size || 0) > 0)
855
+ return false;
856
+ return true;
857
+ }
858
+ catch {
859
+ return false;
860
+ }
861
+ }
862
+ function emit(fullPath) {
863
+ const slashPath = '/' + fullPath;
864
+ if (execArgv) {
865
+ // Substitute {} with the path and invoke. We do NOT have
866
+ // cross-registry execution here in a sync context; lifo-sh's
867
+ // -exec usually runs the cmd via the registry. The R2-3
868
+ // xargs fix used registry.resolve; we can do same. For now
869
+ // emit a marker that the test harness can recognize OR
870
+ // attempt limited shell-builtin exec via the registry.
871
+ // Conservative: write the substituted command line. Real
872
+ // execution via registry would require ctx.registry which
873
+ // mkFind doesn't take.
874
+ const cmdLine = execArgv.map(a => a.split('{}').join(slashPath)).join(' ');
875
+ ctx.stdout.write(cmdLine + '\n');
876
+ }
877
+ else if (deleteAction) {
878
+ try {
879
+ const st = vfs.stat(fullPath);
880
+ if (st.type === 'directory')
881
+ vfs.rmdir(fullPath);
882
+ else
883
+ vfs.unlink(fullPath);
884
+ }
885
+ catch { /* ignore */ }
886
+ }
887
+ else {
888
+ ctx.stdout.write(slashPath + (printNull ? '\0' : '\n'));
889
+ }
890
+ }
891
+ function walk(path, depth) {
892
+ try {
893
+ // Emit the current path itself if it matches (find prints the
894
+ // root directory line too when type filter doesn't exclude).
895
+ if (depth === 0) {
896
+ try {
897
+ const st = vfs.stat(path);
898
+ const synthEntry = { type: st.type };
899
+ const baseName = path.split('/').pop() || path;
900
+ if (matches(path, baseName, synthEntry))
901
+ emit(path);
902
+ }
903
+ catch { /* root may not exist; bail */ }
904
+ }
905
+ if (depth >= maxDepth)
906
+ return;
907
+ const entries = vfs.readdir(path);
908
+ for (const e of entries) {
909
+ const fullPath = path + '/' + e.name;
910
+ if (matches(fullPath, e.name, e))
911
+ emit(fullPath);
912
+ if (e.type === 'directory')
913
+ walk(fullPath, depth + 1);
914
+ }
915
+ }
916
+ catch { /* unreadable dir */ }
917
+ }
918
+ walk(root, 0);
919
+ return 0;
920
+ };
921
+ }
922
+ /**
923
+ * BUG-SWEEP-R4-3 (2026-05-11): grep flag handling.
924
+ *
925
+ * Pre-fix gaps:
926
+ * -c X matched lines were printed (count mode ignored from stdin)
927
+ * -n didn't prepend line number
928
+ * -w word-boundary not added to regex
929
+ * -l not implemented (no flag check)
930
+ * -E flag parsed but didn't enable extended regex (JS RegExp
931
+ * already does ERE-equivalent)
932
+ *
933
+ * Fix: parse flags once into a struct; unify stdin + file + recursive
934
+ * paths through a single `processLines` helper that honours every
935
+ * flag consistently.
936
+ */
937
+ function mkGrep(vfs) {
938
+ return (ctx) => {
939
+ const args = [...ctx.args];
940
+ // Parse flags. Support combined `-rni` form (single dash + chars).
941
+ let recursive = false, ignoreCase = false, lineNum = false;
942
+ let countOnly = false, invertMatch = false, wordMatch = false;
943
+ let filesOnly = false; // -l
944
+ let positional = [];
945
+ for (let i = 0; i < args.length; i++) {
946
+ const a = args[i];
947
+ if (a === '--') {
948
+ positional.push(...args.slice(i + 1));
949
+ break;
950
+ }
951
+ if (a === '-r' || a === '-R' || a === '--recursive') {
952
+ recursive = true;
953
+ continue;
954
+ }
955
+ if (a === '-i' || a === '--ignore-case') {
956
+ ignoreCase = true;
957
+ continue;
958
+ }
959
+ if (a === '-n' || a === '--line-number') {
960
+ lineNum = true;
961
+ continue;
962
+ }
963
+ if (a === '-c' || a === '--count') {
964
+ countOnly = true;
965
+ continue;
966
+ }
967
+ if (a === '-v' || a === '--invert-match') {
968
+ invertMatch = true;
969
+ continue;
970
+ }
971
+ if (a === '-w' || a === '--word-regexp') {
972
+ wordMatch = true;
973
+ continue;
974
+ }
975
+ if (a === '-l' || a === '--files-with-matches') {
976
+ filesOnly = true;
977
+ continue;
978
+ }
979
+ if (a === '-E' || a === '--extended-regexp') { /* JS regex is ERE-ish */
980
+ continue;
981
+ }
982
+ if (a === '-F' || a === '--fixed-strings') {
983
+ // Mark as literal — handled below via escape.
984
+ args.__fixedStrings = true;
985
+ continue;
986
+ }
987
+ if (a.startsWith('-') && a.length > 1 && !a.startsWith('--')) {
988
+ // Combined short flags like -rni
989
+ for (const ch of a.slice(1)) {
990
+ if (ch === 'r' || ch === 'R')
991
+ recursive = true;
992
+ else if (ch === 'i')
993
+ ignoreCase = true;
994
+ else if (ch === 'n')
995
+ lineNum = true;
996
+ else if (ch === 'c')
997
+ countOnly = true;
998
+ else if (ch === 'v')
999
+ invertMatch = true;
1000
+ else if (ch === 'w')
1001
+ wordMatch = true;
1002
+ else if (ch === 'l')
1003
+ filesOnly = true;
1004
+ else if (ch === 'E') { /* ERE noop */ }
1005
+ else if (ch === 'F')
1006
+ args.__fixedStrings = true;
1007
+ }
1008
+ continue;
1009
+ }
1010
+ positional.push(a);
1011
+ }
1012
+ if (positional.length < 1) {
1013
+ ctx.stderr.write('Usage: grep [-rnicvlEFw] PATTERN [FILE...]\n');
1014
+ return 1;
1015
+ }
1016
+ let pattern = positional[0];
1017
+ const targets = positional.slice(1);
1018
+ if (args.__fixedStrings) {
1019
+ // -F: escape regex metacharacters for literal match.
1020
+ pattern = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1021
+ }
1022
+ if (wordMatch)
1023
+ pattern = `\\b(?:${pattern})\\b`;
1024
+ const flags = ignoreCase ? 'i' : '';
1025
+ let re;
1026
+ try {
1027
+ re = new RegExp(pattern, flags);
1028
+ }
1029
+ catch {
1030
+ ctx.stderr.write(`grep: invalid regex: ${pattern}\n`);
1031
+ return 1;
1032
+ }
1033
+ let found = false;
1034
+ function processLines(lines, label) {
1035
+ let count = 0;
1036
+ let matchedHere = false;
1037
+ for (let i = 0; i < lines.length; i++) {
1038
+ const line = lines[i];
1039
+ // Skip a trailing empty line from a content that ended with \\n.
1040
+ if (i === lines.length - 1 && line === '')
1041
+ continue;
1042
+ const isMatch = re.test(line);
1043
+ if (isMatch !== invertMatch) {
1044
+ found = true;
1045
+ matchedHere = true;
1046
+ count++;
1047
+ if (filesOnly) {
1048
+ // -l: emit file label once, stop scanning.
1049
+ ctx.stdout.write(label + '\n');
1050
+ return;
1051
+ }
1052
+ if (!countOnly) {
1053
+ const labelPrefix = (targets.length > 1 || recursive) && label ? label + ':' : '';
1054
+ const linePrefix = lineNum ? (i + 1) + ':' : '';
1055
+ ctx.stdout.write(labelPrefix + linePrefix + line + '\n');
1056
+ }
1057
+ }
1058
+ }
1059
+ if (countOnly) {
1060
+ const labelPrefix = (targets.length > 1 || recursive) && label ? label + ':' : '';
1061
+ ctx.stdout.write(labelPrefix + count + '\n');
1062
+ }
1063
+ void matchedHere;
1064
+ }
1065
+ function grepFile(path, label) {
1066
+ try {
1067
+ const content = vfs.readFileString(path);
1068
+ processLines(content.split('\n'), label);
1069
+ }
1070
+ catch { /* ignore unreadable file */ }
1071
+ }
1072
+ function walkDir(dir) {
1073
+ try {
1074
+ for (const e of vfs.readdir(dir)) {
1075
+ const fp = dir + '/' + e.name;
1076
+ if (e.type === 'file')
1077
+ grepFile(fp, '/' + fp);
1078
+ else if (e.type === 'directory')
1079
+ walkDir(fp);
1080
+ }
1081
+ }
1082
+ catch { }
1083
+ }
1084
+ if (targets.length === 0 && recursive) {
1085
+ walkDir((ctx.cwd || '/home/user').replace(/^\/+/, ''));
1086
+ }
1087
+ else if (targets.length === 0) {
1088
+ // Read from stdin (if piped) — single virtual "file" with no label.
1089
+ if (ctx.stdin) {
1090
+ processLines(ctx.stdin.split('\n'), '');
1091
+ }
1092
+ }
1093
+ else {
1094
+ for (const target of targets) {
1095
+ const fp = resolvePath(ctx.cwd, target);
1096
+ if (vfs.exists(fp) && vfs.isDirectory(fp)) {
1097
+ if (recursive)
1098
+ walkDir(fp);
1099
+ }
1100
+ else
1101
+ grepFile(fp, target);
1102
+ }
1103
+ }
1104
+ return found ? 0 : 1;
1105
+ };
1106
+ }
1107
+ /**
1108
+ * SHELL-R6-B2 follow-on: streaming head.
1109
+ *
1110
+ * When invoked on a pipeline (`producer | head`), receives a pipe
1111
+ * reader (object with .read()) — NOT a coalesced string — and reads
1112
+ * line-by-line until N lines are emitted. Closes the reader by
1113
+ * draining null (or by the SHELL-R6-2 abort cascade kicking in when
1114
+ * we return).
1115
+ *
1116
+ * Why: lifo-sh's head reads via readAll(), which never returns when
1117
+ * upstream is `yes`. Our SHELL-R6-2 pipeline abort only fires when
1118
+ * the consumer resolves — so head must resolve quickly via its own
1119
+ * line-count termination, which then triggers the cascade.
1120
+ *
1121
+ * Backward compat: file-args (head FILE) and the string-stdin case
1122
+ * (when wrap already coalesced) still work via the same code path.
1123
+ */
1124
+ function mkHead(vfs) {
1125
+ return async (ctx) => {
1126
+ let n = 10;
1127
+ const nIdx = ctx.args.indexOf('-n');
1128
+ if (nIdx >= 0)
1129
+ n = parseInt(ctx.args[nIdx + 1]) || 10;
1130
+ const files = ctx.args.filter((a) => !a.startsWith('-') && (ctx.args.indexOf(a) !== nIdx + 1));
1131
+ if (files.length === 0) {
1132
+ // Pipe / stdin case.
1133
+ const stdin = ctx.stdin;
1134
+ if (!stdin)
1135
+ return 0;
1136
+ // Streaming pipe-reader path: read chunks until N lines.
1137
+ if (typeof stdin !== 'string' && typeof stdin.read === 'function') {
1138
+ let buffered = '';
1139
+ let emitted = 0;
1140
+ const out = [];
1141
+ while (emitted < n) {
1142
+ const chunk = await stdin.read();
1143
+ if (chunk === null)
1144
+ break;
1145
+ buffered += chunk;
1146
+ // Process complete lines while we have them.
1147
+ let nlIdx;
1148
+ while (emitted < n && (nlIdx = buffered.indexOf('\n')) !== -1) {
1149
+ out.push(buffered.substring(0, nlIdx));
1150
+ buffered = buffered.substring(nlIdx + 1);
1151
+ emitted++;
1152
+ }
1153
+ }
1154
+ // Handle a partial line if N hit mid-buffer and producer hasn't
1155
+ // sent a trailing newline yet — emit it only if we haven't
1156
+ // already hit the N-line cap (POSIX head includes partial
1157
+ // tail).
1158
+ if (emitted < n && buffered.length > 0) {
1159
+ out.push(buffered);
1160
+ }
1161
+ ctx.stdout.write(out.join('\n') + '\n');
1162
+ return 0;
1163
+ }
1164
+ // Legacy string-stdin path (kept for wrap's pre-coalesced case).
1165
+ if (typeof stdin === 'string') {
1166
+ ctx.stdout.write(stdin.split('\n').slice(0, n).join('\n') + '\n');
1167
+ return 0;
1168
+ }
1169
+ return 0;
1170
+ }
1171
+ for (const f of files) {
1172
+ const fp = resolvePath(ctx.cwd, f);
1173
+ try {
1174
+ const content = vfs.readFileString(fp);
1175
+ if (files.length > 1)
1176
+ ctx.stdout.write(`==> ${f} <==\n`);
1177
+ ctx.stdout.write(content.split('\n').slice(0, n).join('\n') + '\n');
1178
+ }
1179
+ catch {
1180
+ ctx.stderr.write(`head: ${f}: No such file\n`);
1181
+ return 1;
1182
+ }
1183
+ }
1184
+ return 0;
1185
+ };
1186
+ }
1187
+ function mkTail(vfs) {
1188
+ return (ctx) => {
1189
+ let n = 10;
1190
+ const nIdx = ctx.args.indexOf('-n');
1191
+ if (nIdx >= 0)
1192
+ n = parseInt(ctx.args[nIdx + 1]) || 10;
1193
+ const files = ctx.args.filter(a => !a.startsWith('-') && (ctx.args.indexOf(a) !== nIdx + 1));
1194
+ if (files.length === 0 && ctx.stdin) {
1195
+ const lines = ctx.stdin.split('\n');
1196
+ ctx.stdout.write(lines.slice(-n).join('\n') + '\n');
1197
+ return 0;
1198
+ }
1199
+ for (const f of files) {
1200
+ const fp = resolvePath(ctx.cwd, f);
1201
+ try {
1202
+ const content = vfs.readFileString(fp);
1203
+ if (files.length > 1)
1204
+ ctx.stdout.write(`==> ${f} <==\n`);
1205
+ const lines = content.split('\n');
1206
+ ctx.stdout.write(lines.slice(-n).join('\n') + '\n');
1207
+ }
1208
+ catch {
1209
+ ctx.stderr.write(`tail: ${f}: No such file\n`);
1210
+ return 1;
1211
+ }
1212
+ }
1213
+ return 0;
1214
+ };
1215
+ }
1216
+ function mkWc(vfs) {
1217
+ return (ctx) => {
1218
+ const flags = ctx.args.filter(a => a.startsWith('-'));
1219
+ const hasFlags = flags.some(f => f.includes('l') || f.includes('w') || f.includes('c'));
1220
+ const countLines = !hasFlags || ctx.args.includes('-l');
1221
+ const countWords = !hasFlags || ctx.args.includes('-w');
1222
+ const countBytes = !hasFlags || ctx.args.includes('-c');
1223
+ const files = ctx.args.filter(a => !a.startsWith('-'));
1224
+ // BUG-SWEEP-3 (2026-05-11): byte count uses raw Uint8Array length,
1225
+ // not enc.encode(decoded) length. Pre-fix, binary files were
1226
+ // decoded as UTF-8 (substituting U+FFFD for invalid sequences) and
1227
+ // re-encoded — turning each invalid byte into 3 bytes. A 5-byte
1228
+ // file `[ff fe 00 01 42]` reported 9 bytes; `stat` reported the
1229
+ // correct 5. Fixed by reading raw bytes when -c is requested.
1230
+ function wcEmit(rawBytes, label) {
1231
+ const text = (countLines || countWords)
1232
+ ? new TextDecoder('utf-8').decode(rawBytes)
1233
+ : '';
1234
+ const lines = (countLines || countWords)
1235
+ ? text.split('\n').length - (text.endsWith('\n') ? 1 : 0)
1236
+ : 0;
1237
+ const words = (countLines || countWords)
1238
+ ? text.split(/\s+/).filter(Boolean).length
1239
+ : 0;
1240
+ const parts = [];
1241
+ if (countLines)
1242
+ parts.push(String(lines).padStart(8));
1243
+ if (countWords)
1244
+ parts.push(String(words).padStart(8));
1245
+ if (countBytes)
1246
+ parts.push(String(rawBytes.length).padStart(8));
1247
+ ctx.stdout.write(parts.join('') + (label ? ' ' + label : '') + '\n');
1248
+ }
1249
+ if (files.length === 0 && ctx.stdin) {
1250
+ // stdin path: string in, encode to UTF-8 for byte count.
1251
+ const bytes = enc.encode(ctx.stdin);
1252
+ wcEmit(bytes, '');
1253
+ return 0;
1254
+ }
1255
+ for (const f of files) {
1256
+ try {
1257
+ wcEmit(vfs.readFile(resolvePath(ctx.cwd, f)), f);
1258
+ }
1259
+ catch {
1260
+ ctx.stderr.write(`wc: ${f}: No such file\n`);
1261
+ return 1;
1262
+ }
1263
+ }
1264
+ return 0;
1265
+ };
1266
+ }
1267
+ function mkSort(vfs) {
1268
+ return (ctx) => {
1269
+ const files = ctx.args.filter(a => !a.startsWith('-'));
1270
+ let input = ctx.stdin || '';
1271
+ // Read from file if specified
1272
+ if (files.length > 0 && !input) {
1273
+ try {
1274
+ input = vfs.readFileString(resolvePath(ctx.cwd, files[0]));
1275
+ }
1276
+ catch {
1277
+ ctx.stderr.write(`sort: ${files[0]}: No such file\n`);
1278
+ return 1;
1279
+ }
1280
+ }
1281
+ const lines = input.split('\n');
1282
+ // Keep trailing empty line if input ends with newline
1283
+ if (lines[lines.length - 1] === '')
1284
+ lines.pop();
1285
+ const reverse = ctx.args.includes('-r');
1286
+ const numeric = ctx.args.includes('-n');
1287
+ const unique = ctx.args.includes('-u');
1288
+ lines.sort((a, b) => numeric ? parseFloat(a) - parseFloat(b) : a.localeCompare(b));
1289
+ if (reverse)
1290
+ lines.reverse();
1291
+ const result = unique ? [...new Set(lines)] : lines;
1292
+ ctx.stdout.write(result.join('\n') + '\n');
1293
+ return 0;
1294
+ };
1295
+ }
1296
+ function mkUniq() {
1297
+ return (ctx) => {
1298
+ const input = ctx.stdin || '';
1299
+ const lines = input.split('\n');
1300
+ const countFlag = ctx.args.includes('-c');
1301
+ const dupsOnly = ctx.args.includes('-d');
1302
+ const result = [];
1303
+ let prev = '', count = 0;
1304
+ for (const line of lines) {
1305
+ if (line === prev) {
1306
+ count++;
1307
+ }
1308
+ else {
1309
+ if (prev !== '' || count > 0) {
1310
+ if (!dupsOnly || count > 1) {
1311
+ result.push(countFlag ? `${String(count).padStart(7)} ${prev}` : prev);
1312
+ }
1313
+ }
1314
+ prev = line;
1315
+ count = 1;
1316
+ }
1317
+ }
1318
+ if (prev !== '') {
1319
+ if (!dupsOnly || count > 1) {
1320
+ result.push(countFlag ? `${String(count).padStart(7)} ${prev}` : prev);
1321
+ }
1322
+ }
1323
+ ctx.stdout.write(result.join('\n') + '\n');
1324
+ return 0;
1325
+ };
1326
+ }
1327
+ function mkSed(vfs) {
1328
+ return (ctx) => {
1329
+ const expr = ctx.args[0] || '';
1330
+ const files = ctx.args.slice(1).filter(a => !a.startsWith('-'));
1331
+ let input = ctx.stdin || '';
1332
+ if (files.length > 0 && !input) {
1333
+ try {
1334
+ input = vfs.readFileString(resolvePath(ctx.cwd, files[0]));
1335
+ }
1336
+ catch {
1337
+ ctx.stderr.write(`sed: ${files[0]}: No such file\n`);
1338
+ return 1;
1339
+ }
1340
+ }
1341
+ // Support s/pattern/replacement/flags
1342
+ const m = expr.match(/^s(.)(.*?)\1(.*?)\1([gi]*)$/);
1343
+ if (!m) {
1344
+ ctx.stderr.write(`sed: invalid expression: ${expr}\n`);
1345
+ return 1;
1346
+ }
1347
+ const [, , pattern, replacement, flags] = m;
1348
+ const re = new RegExp(pattern, flags.includes('g') ? 'g' + (flags.includes('i') ? 'i' : '') : (flags.includes('i') ? 'i' : ''));
1349
+ const lines = input.split('\n');
1350
+ for (const line of lines) {
1351
+ ctx.stdout.write(line.replace(re, replacement) + '\n');
1352
+ }
1353
+ return 0;
1354
+ };
1355
+ }
1356
+ /**
1357
+ * BUG-SWEEP-R2-4 (2026-05-11): expanded awk subset.
1358
+ *
1359
+ * Pre-fix mkAwk supported only:
1360
+ * {print $N}
1361
+ * /pattern/ [{print}]
1362
+ * Anything else → 'awk: unsupported program'.
1363
+ *
1364
+ * This extension adds (all in pure JS — no embedded awk-interpreter):
1365
+ * BEGIN { stmts } — run before any input line
1366
+ * END { stmts } — run after last line
1367
+ * /pat/ { stmts } — per-line conditional action
1368
+ * { stmts } — per-line unconditional action
1369
+ * $0, $1..$N, $NF — field refs in any expression
1370
+ * NR, NF — record number, field count
1371
+ * print EXPR — write EXPR to stdout + newline (comma-sep)
1372
+ * printf "fmt", a, b — printf-style (%s %d %f %x %o + width.prec)
1373
+ * sum += $N — assignment + compound
1374
+ * simple arithmetic — + - * / % () in expression position
1375
+ * numeric literals — integers and decimals
1376
+ * string literals — "..."
1377
+ * user vars — assigned via name = expr or compound
1378
+ *
1379
+ * NOT supported:
1380
+ * - for/while/if (control flow)
1381
+ * - functions
1382
+ * - arrays (assoc / indexed)
1383
+ * - getline
1384
+ * - regex match operator ~/!~ outside pattern position
1385
+ *
1386
+ * The eval engine is a tiny stmt-list runner that compiles each
1387
+ * statement to a JS closure operating on a shared state {vars,
1388
+ * fields[], NR, NF, separator, stdout, stderr}. Statements are
1389
+ * separated by `;` or `\\n`.
1390
+ *
1391
+ * Failure mode: if we can't parse a statement, write a clear error
1392
+ * to stderr and exit 1 (no silent fail).
1393
+ */
1394
+ function mkAwk(vfs) {
1395
+ return (ctx) => {
1396
+ const allArgs = ctx.args;
1397
+ // Parse -F separator if present.
1398
+ let separator = /\s+/;
1399
+ const programArgs = [];
1400
+ const fileArgs = [];
1401
+ for (let i = 0; i < allArgs.length; i++) {
1402
+ const a = allArgs[i];
1403
+ if (a === '-F') {
1404
+ const s = allArgs[++i];
1405
+ if (s)
1406
+ separator = s.length === 1 ? s : new RegExp(s);
1407
+ }
1408
+ else if (a.startsWith('-F')) {
1409
+ const s = a.slice(2);
1410
+ if (s)
1411
+ separator = s.length === 1 ? s : new RegExp(s);
1412
+ }
1413
+ else if (a.startsWith('-')) {
1414
+ // Ignore other flags (silent compat).
1415
+ }
1416
+ else if (programArgs.length === 0) {
1417
+ programArgs.push(a);
1418
+ }
1419
+ else {
1420
+ fileArgs.push(a);
1421
+ }
1422
+ }
1423
+ const program = programArgs[0] || '';
1424
+ let input = ctx.stdin || '';
1425
+ if (fileArgs.length > 0 && !input) {
1426
+ try {
1427
+ input = vfs.readFileString(resolvePath(ctx.cwd, fileArgs[0]));
1428
+ }
1429
+ catch {
1430
+ ctx.stderr.write(`awk: ${fileArgs[0]}: No such file\n`);
1431
+ return 1;
1432
+ }
1433
+ }
1434
+ const blocks = [];
1435
+ let cursor = 0;
1436
+ const src = program.trim();
1437
+ function skipWS() {
1438
+ while (cursor < src.length && /\s/.test(src[cursor]))
1439
+ cursor++;
1440
+ }
1441
+ function parseBraced() {
1442
+ // Assumes src[cursor] === '{'
1443
+ let depth = 0;
1444
+ let start = cursor;
1445
+ while (cursor < src.length) {
1446
+ const ch = src[cursor];
1447
+ if (ch === '{')
1448
+ depth++;
1449
+ else if (ch === '}') {
1450
+ depth--;
1451
+ if (depth === 0) {
1452
+ cursor++;
1453
+ return src.slice(start + 1, cursor - 1);
1454
+ }
1455
+ }
1456
+ else if (ch === '"' || ch === "'") {
1457
+ const quote = ch;
1458
+ cursor++;
1459
+ while (cursor < src.length && src[cursor] !== quote) {
1460
+ if (src[cursor] === '\\')
1461
+ cursor++;
1462
+ cursor++;
1463
+ }
1464
+ }
1465
+ cursor++;
1466
+ }
1467
+ return src.slice(start + 1, cursor);
1468
+ }
1469
+ while (cursor < src.length) {
1470
+ skipWS();
1471
+ if (cursor >= src.length)
1472
+ break;
1473
+ if (src.startsWith('BEGIN', cursor)) {
1474
+ cursor += 5;
1475
+ skipWS();
1476
+ if (src[cursor] !== '{') {
1477
+ ctx.stderr.write('awk: BEGIN without {\n');
1478
+ return 1;
1479
+ }
1480
+ blocks.push({ kind: 'BEGIN', body: parseBraced() });
1481
+ continue;
1482
+ }
1483
+ if (src.startsWith('END', cursor)) {
1484
+ cursor += 3;
1485
+ skipWS();
1486
+ if (src[cursor] !== '{') {
1487
+ ctx.stderr.write('awk: END without {\n');
1488
+ return 1;
1489
+ }
1490
+ blocks.push({ kind: 'END', body: parseBraced() });
1491
+ continue;
1492
+ }
1493
+ if (src[cursor] === '/') {
1494
+ // Pattern /pat/ optionally followed by {body}
1495
+ const pstart = cursor + 1;
1496
+ cursor++;
1497
+ while (cursor < src.length && src[cursor] !== '/') {
1498
+ if (src[cursor] === '\\')
1499
+ cursor++;
1500
+ cursor++;
1501
+ }
1502
+ const patSrc = src.slice(pstart, cursor);
1503
+ cursor++; // past closing /
1504
+ skipWS();
1505
+ let body = 'print';
1506
+ if (cursor < src.length && src[cursor] === '{')
1507
+ body = parseBraced();
1508
+ let re;
1509
+ try {
1510
+ re = new RegExp(patSrc);
1511
+ }
1512
+ catch (e) {
1513
+ ctx.stderr.write(`awk: bad regex /${patSrc}/: ${e?.message || e}\n`);
1514
+ return 1;
1515
+ }
1516
+ blocks.push({ kind: 'PATTERN', pattern: re, body });
1517
+ continue;
1518
+ }
1519
+ if (src[cursor] === '{') {
1520
+ blocks.push({ kind: 'MAIN', body: parseBraced() });
1521
+ continue;
1522
+ }
1523
+ ctx.stderr.write(`awk: parse error at "${src.slice(cursor, cursor + 20)}"\n`);
1524
+ return 1;
1525
+ }
1526
+ /**
1527
+ * Expression evaluator without `new Function`. workerd CSP blocks
1528
+ * dynamic code generation at request time. This is a small
1529
+ * recursive-descent evaluator for the subset:
1530
+ * - literals: number, string ("...")
1531
+ * - field refs: $0, $N, $NF
1532
+ * - builtins: NR, NF
1533
+ * - user vars: identifier (looked up in st.vars; default 0)
1534
+ * - binary ops: + - * / % (numeric)
1535
+ * - parens: (expr)
1536
+ * - string concat happens via space-join in `print` call sites
1537
+ *
1538
+ * The grammar:
1539
+ * expr := term (('+'|'-') term)*
1540
+ * term := factor (('*'|'/'|'%') factor)*
1541
+ * factor := number | string | '$' (number | 'NF') | ident | '(' expr ')'
1542
+ */
1543
+ function evalExpr(expr, st) {
1544
+ const text = expr.trim();
1545
+ let pos = 0;
1546
+ function skipWs() { while (pos < text.length && /\s/.test(text[pos]))
1547
+ pos++; }
1548
+ function peek() { return text[pos]; }
1549
+ function consume(ch) { skipWs(); if (text[pos] === ch) {
1550
+ pos++;
1551
+ return true;
1552
+ } return false; }
1553
+ function expect(ch) { if (!consume(ch))
1554
+ throw new Error(`expected '${ch}' at "${text.slice(pos, pos + 20)}"`); }
1555
+ function parseExpr() {
1556
+ let left = parseTerm();
1557
+ for (;;) {
1558
+ skipWs();
1559
+ const op = text[pos];
1560
+ if (op === '+' || op === '-') {
1561
+ pos++;
1562
+ const right = parseTerm();
1563
+ const ln = toNum(left), rn = toNum(right);
1564
+ left = op === '+' ? ln + rn : ln - rn;
1565
+ }
1566
+ else
1567
+ break;
1568
+ }
1569
+ return left;
1570
+ }
1571
+ function parseTerm() {
1572
+ let left = parseFactor();
1573
+ for (;;) {
1574
+ skipWs();
1575
+ const op = text[pos];
1576
+ if (op === '*' || op === '/' || op === '%') {
1577
+ pos++;
1578
+ const right = parseFactor();
1579
+ const ln = toNum(left), rn = toNum(right);
1580
+ left = op === '*' ? ln * rn : op === '/' ? ln / rn : ln % rn;
1581
+ }
1582
+ else
1583
+ break;
1584
+ }
1585
+ return left;
1586
+ }
1587
+ function parseFactor() {
1588
+ skipWs();
1589
+ if (pos >= text.length)
1590
+ throw new Error(`unexpected end of expression`);
1591
+ const ch = text[pos];
1592
+ // Number
1593
+ if (/[0-9]/.test(ch) || (ch === '.' && /[0-9]/.test(text[pos + 1]))) {
1594
+ let start = pos;
1595
+ while (pos < text.length && /[0-9.]/.test(text[pos]))
1596
+ pos++;
1597
+ return parseFloat(text.slice(start, pos));
1598
+ }
1599
+ // String literal (double or single quotes)
1600
+ if (ch === '"' || ch === "'") {
1601
+ const quote = ch;
1602
+ pos++;
1603
+ let s = '';
1604
+ while (pos < text.length && text[pos] !== quote) {
1605
+ if (text[pos] === '\\' && pos + 1 < text.length) {
1606
+ const esc = text[pos + 1];
1607
+ s += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc === 'r' ? '\r' : esc === '\\' ? '\\' : esc === '"' ? '"' : esc === "'" ? "'" : esc;
1608
+ pos += 2;
1609
+ }
1610
+ else {
1611
+ s += text[pos];
1612
+ pos++;
1613
+ }
1614
+ }
1615
+ if (pos < text.length)
1616
+ pos++; // skip closing quote
1617
+ return s;
1618
+ }
1619
+ // Parenthesised
1620
+ if (ch === '(') {
1621
+ pos++;
1622
+ const v = parseExpr();
1623
+ expect(')');
1624
+ return v;
1625
+ }
1626
+ // Unary minus
1627
+ if (ch === '-') {
1628
+ pos++;
1629
+ return -toNum(parseFactor());
1630
+ }
1631
+ // Unary plus
1632
+ if (ch === '+') {
1633
+ pos++;
1634
+ return toNum(parseFactor());
1635
+ }
1636
+ // Field ref: $N or $NF
1637
+ if (ch === '$') {
1638
+ pos++;
1639
+ skipWs();
1640
+ if (text.startsWith('NF', pos)) {
1641
+ pos += 2;
1642
+ return st.fields[st.NF] ?? '';
1643
+ }
1644
+ // Parens around index? $($1+1) etc — not supported, just digits.
1645
+ let nStart = pos;
1646
+ while (pos < text.length && /[0-9]/.test(text[pos]))
1647
+ pos++;
1648
+ if (nStart === pos)
1649
+ throw new Error(`expected field index after $ at "${text.slice(pos, pos + 10)}"`);
1650
+ const idx = parseInt(text.slice(nStart, pos), 10);
1651
+ return st.fields[idx] ?? '';
1652
+ }
1653
+ // Identifier: NR, NF, user var
1654
+ if (/[A-Za-z_]/.test(ch)) {
1655
+ let start = pos;
1656
+ while (pos < text.length && /[A-Za-z0-9_]/.test(text[pos]))
1657
+ pos++;
1658
+ const name = text.slice(start, pos);
1659
+ if (name === 'NR')
1660
+ return st.NR;
1661
+ if (name === 'NF')
1662
+ return st.NF;
1663
+ return st.vars[name] !== undefined ? st.vars[name] : 0;
1664
+ }
1665
+ throw new Error(`unexpected '${ch}' at "${text.slice(pos, pos + 20)}"`);
1666
+ }
1667
+ function toNum(v) {
1668
+ if (typeof v === 'number')
1669
+ return v;
1670
+ const n = parseFloat(v);
1671
+ return Number.isFinite(n) ? n : 0;
1672
+ }
1673
+ try {
1674
+ const v = parseExpr();
1675
+ skipWs();
1676
+ if (pos < text.length) {
1677
+ // Trailing junk — could be intentional (e.g. tail of stmt is
1678
+ // separator). Be permissive — return what we have.
1679
+ }
1680
+ return v;
1681
+ }
1682
+ catch (e) {
1683
+ throw new Error(`expr error: ${e?.message || e} in "${expr}"`);
1684
+ }
1685
+ }
1686
+ function stripStringsForScan(s) {
1687
+ // Replace string contents with same-length spaces so positions stay aligned.
1688
+ let out = '';
1689
+ let i = 0;
1690
+ while (i < s.length) {
1691
+ const ch = s[i];
1692
+ if (ch === '"' || ch === "'") {
1693
+ out += ch;
1694
+ i++;
1695
+ while (i < s.length && s[i] !== ch) {
1696
+ if (s[i] === '\\') {
1697
+ out += ' ';
1698
+ i++;
1699
+ }
1700
+ out += ' ';
1701
+ i++;
1702
+ }
1703
+ if (i < s.length) {
1704
+ out += ch;
1705
+ i++;
1706
+ }
1707
+ }
1708
+ else {
1709
+ out += ch;
1710
+ i++;
1711
+ }
1712
+ }
1713
+ return out;
1714
+ }
1715
+ function remapUserVars(s) {
1716
+ // Find identifiers (a-z_), skip ones that are reserved or already
1717
+ // remapped. The simple approach: scan tokens outside string
1718
+ // literals.
1719
+ const RESERVED = new Set([
1720
+ '__f', '__nr', '__nf', '__v',
1721
+ 'true', 'false', 'null', 'undefined', 'NaN', 'Infinity',
1722
+ 'Math', 'String', 'Number', 'Array', 'Object',
1723
+ 'parseInt', 'parseFloat', 'isNaN', 'isFinite',
1724
+ 'length', // for str/array .length access — not a free identifier here
1725
+ ]);
1726
+ let out = '';
1727
+ let i = 0;
1728
+ while (i < s.length) {
1729
+ const ch = s[i];
1730
+ if (ch === '"' || ch === "'") {
1731
+ out += ch;
1732
+ i++;
1733
+ while (i < s.length && s[i] !== ch) {
1734
+ if (s[i] === '\\') {
1735
+ out += s[i];
1736
+ i++;
1737
+ }
1738
+ out += s[i];
1739
+ i++;
1740
+ }
1741
+ if (i < s.length) {
1742
+ out += s[i];
1743
+ i++;
1744
+ }
1745
+ continue;
1746
+ }
1747
+ if (/[A-Za-z_]/.test(ch)) {
1748
+ let start = i;
1749
+ while (i < s.length && /[A-Za-z0-9_]/.test(s[i]))
1750
+ i++;
1751
+ const ident = s.slice(start, i);
1752
+ // Skip if previous non-ws char is `.` (member access).
1753
+ let prev = start - 1;
1754
+ while (prev >= 0 && /\s/.test(out[prev]))
1755
+ prev--;
1756
+ if (out[prev] === '.') {
1757
+ out += ident;
1758
+ continue;
1759
+ }
1760
+ if (RESERVED.has(ident)) {
1761
+ out += ident;
1762
+ continue;
1763
+ }
1764
+ // Replace with (__v.ident !== undefined ? __v.ident : 0)
1765
+ out += `(__v.${ident}!==undefined?__v.${ident}:0)`;
1766
+ continue;
1767
+ }
1768
+ out += ch;
1769
+ i++;
1770
+ }
1771
+ return out;
1772
+ }
1773
+ function splitStmts(body) {
1774
+ const stmts = [];
1775
+ let depth = 0;
1776
+ let cur = '';
1777
+ let i = 0;
1778
+ while (i < body.length) {
1779
+ const ch = body[i];
1780
+ if (ch === '"' || ch === "'") {
1781
+ cur += ch;
1782
+ i++;
1783
+ while (i < body.length && body[i] !== ch) {
1784
+ if (body[i] === '\\') {
1785
+ cur += body[i];
1786
+ i++;
1787
+ }
1788
+ cur += body[i];
1789
+ i++;
1790
+ }
1791
+ if (i < body.length) {
1792
+ cur += body[i];
1793
+ i++;
1794
+ }
1795
+ continue;
1796
+ }
1797
+ if (ch === '(' || ch === '[' || ch === '{')
1798
+ depth++;
1799
+ else if (ch === ')' || ch === ']' || ch === '}')
1800
+ depth--;
1801
+ if (depth === 0 && (ch === ';' || ch === '\n')) {
1802
+ const t = cur.trim();
1803
+ if (t)
1804
+ stmts.push(t);
1805
+ cur = '';
1806
+ i++;
1807
+ continue;
1808
+ }
1809
+ cur += ch;
1810
+ i++;
1811
+ }
1812
+ const t = cur.trim();
1813
+ if (t)
1814
+ stmts.push(t);
1815
+ return stmts;
1816
+ }
1817
+ function execStmt(stmt, st) {
1818
+ // print [expr[, expr]*]
1819
+ if (stmt === 'print' || stmt.startsWith('print ') || stmt.startsWith('print\t')) {
1820
+ const rest = stmt.slice(5).trim();
1821
+ if (!rest) {
1822
+ ctx.stdout.write(st.fields[0] + '\n');
1823
+ st.printed = true;
1824
+ return;
1825
+ }
1826
+ // Comma-separated exprs (space joiner). We must split at top-level commas only.
1827
+ const parts = splitTopLevel(rest, ',');
1828
+ const out = parts.map(p => stringify(evalExpr(p, st))).join(' ');
1829
+ ctx.stdout.write(out + '\n');
1830
+ st.printed = true;
1831
+ return;
1832
+ }
1833
+ // printf "fmt", arg, arg, ...
1834
+ if (stmt.startsWith('printf ') || stmt.startsWith('printf(')) {
1835
+ let rest = stmt.startsWith('printf(') ? stmt.slice(7).replace(/\)\s*$/, '') : stmt.slice(7);
1836
+ rest = rest.trim();
1837
+ const parts = splitTopLevel(rest, ',');
1838
+ if (parts.length === 0)
1839
+ return;
1840
+ const fmt = evalExpr(parts[0], st);
1841
+ const fargs = parts.slice(1).map(p => evalExpr(p, st));
1842
+ ctx.stdout.write(printfFormat(String(fmt), fargs));
1843
+ st.printed = true;
1844
+ return;
1845
+ }
1846
+ // next: skip rest of body (no-op here since we re-enter each block fresh)
1847
+ if (stmt === 'next')
1848
+ return;
1849
+ // assignment: IDENT [op]= EXPR
1850
+ // We require a top-level `=` not part of `==` `<=` `>=` `!=`.
1851
+ const eqIdx = findAssignmentEq(stmt);
1852
+ if (eqIdx > 0) {
1853
+ const lhs = stmt.slice(0, eqIdx).trim();
1854
+ const rhs = stmt.slice(eqIdx + 1).trim();
1855
+ // Compound: lhs ends with op (e.g. `sum +`).
1856
+ let op = null;
1857
+ let name = lhs;
1858
+ if (/[+\-*/%]$/.test(lhs)) {
1859
+ op = lhs[lhs.length - 1];
1860
+ name = lhs.slice(0, -1).trim();
1861
+ }
1862
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
1863
+ throw new Error(`bad assignment target "${name}"`);
1864
+ }
1865
+ const rv = evalExpr(rhs, st);
1866
+ if (op) {
1867
+ const cur = st.vars[name] !== undefined ? st.vars[name] : 0;
1868
+ const lhsNum = typeof cur === 'number' ? cur : parseFloat(cur);
1869
+ const rvNum = typeof rv === 'number' ? rv : parseFloat(rv);
1870
+ const lN = Number.isFinite(lhsNum) ? lhsNum : 0;
1871
+ const rN = Number.isFinite(rvNum) ? rvNum : 0;
1872
+ st.vars[name] =
1873
+ op === '+' ? lN + rN :
1874
+ op === '-' ? lN - rN :
1875
+ op === '*' ? lN * rN :
1876
+ op === '/' ? lN / rN :
1877
+ op === '%' ? lN % rN : rv;
1878
+ }
1879
+ else {
1880
+ st.vars[name] = rv;
1881
+ }
1882
+ return;
1883
+ }
1884
+ // Bare expression — evaluate for side effects (rare in awk).
1885
+ evalExpr(stmt, st);
1886
+ }
1887
+ function findAssignmentEq(s) {
1888
+ let depth = 0;
1889
+ for (let i = 0; i < s.length; i++) {
1890
+ const ch = s[i];
1891
+ if (ch === '"' || ch === "'") {
1892
+ i++;
1893
+ while (i < s.length && s[i] !== ch) {
1894
+ if (s[i] === '\\')
1895
+ i++;
1896
+ i++;
1897
+ }
1898
+ continue;
1899
+ }
1900
+ if (ch === '(' || ch === '[' || ch === '{')
1901
+ depth++;
1902
+ else if (ch === ')' || ch === ']' || ch === '}')
1903
+ depth--;
1904
+ if (depth === 0 && ch === '=') {
1905
+ const next = s[i + 1];
1906
+ const prev = s[i - 1];
1907
+ if (next === '=' || prev === '=' || prev === '!' || prev === '<' || prev === '>')
1908
+ continue;
1909
+ return i;
1910
+ }
1911
+ }
1912
+ return -1;
1913
+ }
1914
+ function splitTopLevel(s, sep) {
1915
+ const out = [];
1916
+ let depth = 0;
1917
+ let cur = '';
1918
+ let i = 0;
1919
+ while (i < s.length) {
1920
+ const ch = s[i];
1921
+ if (ch === '"' || ch === "'") {
1922
+ cur += ch;
1923
+ i++;
1924
+ while (i < s.length && s[i] !== ch) {
1925
+ if (s[i] === '\\') {
1926
+ cur += s[i];
1927
+ i++;
1928
+ }
1929
+ cur += s[i];
1930
+ i++;
1931
+ }
1932
+ if (i < s.length) {
1933
+ cur += s[i];
1934
+ i++;
1935
+ }
1936
+ continue;
1937
+ }
1938
+ if (ch === '(' || ch === '[' || ch === '{')
1939
+ depth++;
1940
+ else if (ch === ')' || ch === ']' || ch === '}')
1941
+ depth--;
1942
+ if (depth === 0 && ch === sep) {
1943
+ out.push(cur.trim());
1944
+ cur = '';
1945
+ i++;
1946
+ continue;
1947
+ }
1948
+ cur += ch;
1949
+ i++;
1950
+ }
1951
+ if (cur.trim())
1952
+ out.push(cur.trim());
1953
+ return out;
1954
+ }
1955
+ function stringify(v) {
1956
+ if (v === undefined || v === null)
1957
+ return '';
1958
+ if (typeof v === 'number') {
1959
+ if (Number.isInteger(v))
1960
+ return String(v);
1961
+ // awk's OFMT default is "%.6g"
1962
+ return printfFormat('%.6g', [v]);
1963
+ }
1964
+ return String(v);
1965
+ }
1966
+ function printfFormat(fmt, fargs) {
1967
+ let out = '';
1968
+ let i = 0;
1969
+ let argIdx = 0;
1970
+ while (i < fmt.length) {
1971
+ const ch = fmt[i];
1972
+ if (ch === '\\' && i + 1 < fmt.length) {
1973
+ const esc = fmt[i + 1];
1974
+ out += esc === 'n' ? '\n' : esc === 't' ? '\t' : esc === 'r' ? '\r' : esc === '\\' ? '\\' : esc;
1975
+ i += 2;
1976
+ continue;
1977
+ }
1978
+ if (ch === '%' && i + 1 < fmt.length) {
1979
+ // Parse: %[flags][width][.prec]specifier
1980
+ let spec = '%';
1981
+ i++;
1982
+ while (i < fmt.length && /[-+ 0#]/.test(fmt[i])) {
1983
+ spec += fmt[i];
1984
+ i++;
1985
+ }
1986
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) {
1987
+ spec += fmt[i];
1988
+ i++;
1989
+ }
1990
+ if (fmt[i] === '.') {
1991
+ spec += fmt[i];
1992
+ i++;
1993
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) {
1994
+ spec += fmt[i];
1995
+ i++;
1996
+ }
1997
+ }
1998
+ const conv = fmt[i];
1999
+ i++;
2000
+ if (conv === '%') {
2001
+ out += '%';
2002
+ continue;
2003
+ }
2004
+ const arg = fargs[argIdx++];
2005
+ out += formatOne(spec + conv, arg);
2006
+ continue;
2007
+ }
2008
+ out += ch;
2009
+ i++;
2010
+ }
2011
+ return out;
2012
+ }
2013
+ function formatOne(spec, arg) {
2014
+ const conv = spec[spec.length - 1];
2015
+ const flagsAndWidth = spec.slice(1, -1);
2016
+ const dotIdx = flagsAndWidth.indexOf('.');
2017
+ const widthPart = dotIdx >= 0 ? flagsAndWidth.slice(0, dotIdx) : flagsAndWidth;
2018
+ const precPart = dotIdx >= 0 ? flagsAndWidth.slice(dotIdx + 1) : '';
2019
+ let flags = '';
2020
+ let widthStr = '';
2021
+ for (const c of widthPart) {
2022
+ if (/[-+ 0#]/.test(c))
2023
+ flags += c;
2024
+ else
2025
+ widthStr += c;
2026
+ }
2027
+ const width = widthStr ? parseInt(widthStr, 10) : 0;
2028
+ const prec = precPart ? parseInt(precPart, 10) : -1;
2029
+ let body;
2030
+ switch (conv) {
2031
+ case 's':
2032
+ body = String(arg ?? '');
2033
+ if (prec >= 0)
2034
+ body = body.slice(0, prec);
2035
+ break;
2036
+ case 'd':
2037
+ case 'i': {
2038
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(arg));
2039
+ body = String(Number.isFinite(n) ? n : 0);
2040
+ break;
2041
+ }
2042
+ case 'f': {
2043
+ const n = typeof arg === 'number' ? arg : parseFloat(arg);
2044
+ const p = prec < 0 ? 6 : prec;
2045
+ body = (Number.isFinite(n) ? n : 0).toFixed(p);
2046
+ break;
2047
+ }
2048
+ case 'g': {
2049
+ const n = typeof arg === 'number' ? arg : parseFloat(arg);
2050
+ const p = prec < 0 ? 6 : prec;
2051
+ body = (Number.isFinite(n) ? n : 0).toPrecision(p).replace(/\.?0+$/, '');
2052
+ break;
2053
+ }
2054
+ case 'x': {
2055
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(arg));
2056
+ body = (Number.isFinite(n) ? n : 0).toString(16);
2057
+ break;
2058
+ }
2059
+ case 'o': {
2060
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(arg));
2061
+ body = (Number.isFinite(n) ? n : 0).toString(8);
2062
+ break;
2063
+ }
2064
+ case 'c': {
2065
+ if (typeof arg === 'number')
2066
+ body = String.fromCharCode(arg);
2067
+ else
2068
+ body = String(arg).charAt(0);
2069
+ break;
2070
+ }
2071
+ default: body = String(arg);
2072
+ }
2073
+ if (width > body.length) {
2074
+ const pad = flags.includes('0') && (conv === 'd' || conv === 'i' || conv === 'f' || conv === 'x' || conv === 'o') ? '0' : ' ';
2075
+ body = flags.includes('-') ? body.padEnd(width, ' ') : body.padStart(width, pad);
2076
+ }
2077
+ return body;
2078
+ }
2079
+ const state = {
2080
+ vars: {},
2081
+ fields: [],
2082
+ NR: 0,
2083
+ NF: 0,
2084
+ printed: false,
2085
+ };
2086
+ function runBlock(block) {
2087
+ const stmts = splitStmts(block.body);
2088
+ for (const s of stmts) {
2089
+ execStmt(s, state);
2090
+ }
2091
+ }
2092
+ try {
2093
+ // BEGIN blocks first.
2094
+ for (const b of blocks)
2095
+ if (b.kind === 'BEGIN')
2096
+ runBlock(b);
2097
+ // Main loop over input lines.
2098
+ const lines = input.split('\n');
2099
+ // awk default: drop the final empty line if input ended with \n.
2100
+ if (lines.length > 0 && lines[lines.length - 1] === '')
2101
+ lines.pop();
2102
+ for (let li = 0; li < lines.length; li++) {
2103
+ const line = lines[li];
2104
+ const parts = typeof separator === 'string' && separator.length === 1
2105
+ ? line.split(separator)
2106
+ : line.split(separator);
2107
+ state.NR = li + 1;
2108
+ state.NF = parts.filter(p => p !== '').length;
2109
+ state.fields = [line, ...parts];
2110
+ for (const b of blocks) {
2111
+ if (b.kind === 'BEGIN' || b.kind === 'END')
2112
+ continue;
2113
+ if (b.kind === 'PATTERN') {
2114
+ if (b.pattern.test(line))
2115
+ runBlock(b);
2116
+ }
2117
+ else {
2118
+ // MAIN block (no pattern) — always runs.
2119
+ runBlock(b);
2120
+ }
2121
+ }
2122
+ }
2123
+ // END blocks last.
2124
+ for (const b of blocks)
2125
+ if (b.kind === 'END')
2126
+ runBlock(b);
2127
+ }
2128
+ catch (e) {
2129
+ ctx.stderr.write(`awk: ${e?.message || e}\n`);
2130
+ return 1;
2131
+ }
2132
+ return 0;
2133
+ };
2134
+ }
2135
+ /**
2136
+ * BUG-SWEEP-R2-3 (2026-05-11): real xargs implementation.
2137
+ *
2138
+ * Pre-fix the impl printed the command-line it WOULD execute and
2139
+ * returned 0. Real xargs runs the command, possibly batched (-n),
2140
+ * with arguments substituted (-I).
2141
+ *
2142
+ * We do cross-command dispatch through the same `registry` lifo-sh
2143
+ * uses, so xargs can drive `echo`, `cat`, `rm`, `seq`, lifo-sh
2144
+ * lazy-loaded builtins — anything in the registry. The execution
2145
+ * runs IN-SUPERVISOR (not through facet spawn) which means it
2146
+ * works for pure-builtins but NOT for facet-direct commands like
2147
+ * `node`, `git`, `npm` (the registry resolver returns those by
2148
+ * name but invoking them requires the cp/facet pipeline).
2149
+ *
2150
+ * Supported flags:
2151
+ * -n NUM run command with at most NUM args per invocation
2152
+ * -I REPL replace REPL in command with the input item
2153
+ * -0 null-byte separator (rare; bash xargs -0 idiom)
2154
+ * default args use args.split(/\s+/) from stdin
2155
+ *
2156
+ * Unsupported (document as gap): -P (parallel), -L (per-line), -p (prompt).
2157
+ */
2158
+ function mkXargs(vfs, registry) {
2159
+ return async (ctx) => {
2160
+ const input = (ctx.stdin || '').trim();
2161
+ if (!input)
2162
+ return 0;
2163
+ // Parse flags first
2164
+ const args = [...ctx.args];
2165
+ let batchSize = Infinity;
2166
+ let replaceTok = null;
2167
+ let nullSep = false;
2168
+ while (args.length > 0 && args[0].startsWith('-')) {
2169
+ const a = args.shift();
2170
+ if (a === '-n') {
2171
+ const n = parseInt(args.shift() || '', 10);
2172
+ if (Number.isFinite(n) && n > 0)
2173
+ batchSize = n;
2174
+ }
2175
+ else if (a.startsWith('-n')) {
2176
+ const n = parseInt(a.slice(2), 10);
2177
+ if (Number.isFinite(n) && n > 0)
2178
+ batchSize = n;
2179
+ }
2180
+ else if (a === '-I') {
2181
+ replaceTok = args.shift() || '{}';
2182
+ batchSize = 1; // -I implies one-arg-per-invocation
2183
+ }
2184
+ else if (a === '-0' || a === '--null') {
2185
+ nullSep = true;
2186
+ }
2187
+ else if (a === '--') {
2188
+ break;
2189
+ }
2190
+ else {
2191
+ // Unknown flag — push back as cmd token (best-effort behavior)
2192
+ args.unshift(a);
2193
+ break;
2194
+ }
2195
+ }
2196
+ // Remaining args: cmd + initial-args. Default: echo.
2197
+ const cmdName = args.shift() || 'echo';
2198
+ const cmdArgsInitial = args;
2199
+ // Split stdin into items
2200
+ const items = nullSep
2201
+ ? input.split('\u0000').filter(Boolean)
2202
+ : input.split(/\s+/).filter(Boolean);
2203
+ // Resolve target command from registry (handles both eager + lazy maps).
2204
+ let target;
2205
+ try {
2206
+ target = await registry.resolve(cmdName);
2207
+ }
2208
+ catch (_e) {
2209
+ target = null;
2210
+ }
2211
+ if (!target) {
2212
+ // Defer to write-to-stderr; mimic real xargs which would exec(2) and fail.
2213
+ ctx.stderr.write(`xargs: ${cmdName}: command not found\n`);
2214
+ return 127;
2215
+ }
2216
+ // Run in batches.
2217
+ const newCtx = (newArgs) => ({
2218
+ args: newArgs,
2219
+ env: ctx.env,
2220
+ cwd: ctx.cwd,
2221
+ vfs: ctx.vfs,
2222
+ stdout: ctx.stdout,
2223
+ stderr: ctx.stderr,
2224
+ stdin: '', // xargs doesn't pipe its own stdin to children
2225
+ signal: ctx.signal,
2226
+ });
2227
+ let exit = 0;
2228
+ if (replaceTok) {
2229
+ // -I: one invocation per item, replacing token in initial args.
2230
+ for (const item of items) {
2231
+ const subbed = cmdArgsInitial.map(a => a.split(replaceTok).join(item));
2232
+ try {
2233
+ const code = await target(newCtx(subbed));
2234
+ if (typeof code === 'number' && code !== 0)
2235
+ exit = code;
2236
+ }
2237
+ catch (e) {
2238
+ ctx.stderr.write(`xargs: ${cmdName}: ${e?.message || e}\n`);
2239
+ exit = 1;
2240
+ }
2241
+ }
2242
+ }
2243
+ else {
2244
+ // -n N (or unlimited): batch items, append to initial args.
2245
+ const step = Number.isFinite(batchSize) ? batchSize : items.length;
2246
+ for (let i = 0; i < items.length; i += step) {
2247
+ const batch = items.slice(i, i + step);
2248
+ try {
2249
+ const code = await target(newCtx([...cmdArgsInitial, ...batch]));
2250
+ if (typeof code === 'number' && code !== 0)
2251
+ exit = code;
2252
+ }
2253
+ catch (e) {
2254
+ ctx.stderr.write(`xargs: ${cmdName}: ${e?.message || e}\n`);
2255
+ exit = 1;
2256
+ }
2257
+ if (!Number.isFinite(batchSize))
2258
+ break; // single batch when no -n
2259
+ }
2260
+ }
2261
+ return exit;
2262
+ };
2263
+ }
2264
+ function mkTee(vfs) {
2265
+ return (ctx) => {
2266
+ const input = ctx.stdin || '';
2267
+ const append = ctx.args.includes('-a');
2268
+ const files = ctx.args.filter(a => !a.startsWith('-'));
2269
+ ctx.stdout.write(input);
2270
+ for (const f of files) {
2271
+ const fp = resolvePath(ctx.cwd, f);
2272
+ if (append && vfs.exists(fp)) {
2273
+ const existing = vfs.readFileString(fp);
2274
+ vfs.writeFile(fp, existing + input);
2275
+ }
2276
+ else {
2277
+ vfs.writeFile(fp, input);
2278
+ }
2279
+ }
2280
+ return 0;
2281
+ };
2282
+ }
2283
+ /**
2284
+ * BUG-SWEEP-R4-6 (2026-05-11): du flag parsing for combined forms.
2285
+ * Pre-fix `du -sh` didn't activate -h because we checked for literal
2286
+ * `-h` only — `-sh` is a single arg containing both flags. POSIX
2287
+ * conformant short-flag stacking.
2288
+ */
2289
+ function mkDu(vfs) {
2290
+ return (ctx) => {
2291
+ // Parse flags supporting stacked short flags like `-sh`, `-ah`.
2292
+ let showAll = false, human = false, sumOnly = false;
2293
+ const positional = [];
2294
+ for (const a of ctx.args) {
2295
+ if (a.startsWith('-') && a !== '-' && !a.startsWith('--')) {
2296
+ for (const ch of a.slice(1)) {
2297
+ if (ch === 'a')
2298
+ showAll = true;
2299
+ else if (ch === 'h')
2300
+ human = true;
2301
+ else if (ch === 's')
2302
+ sumOnly = true;
2303
+ }
2304
+ }
2305
+ else if (a.startsWith('--')) {
2306
+ if (a === '--all')
2307
+ showAll = true;
2308
+ else if (a === '--human-readable')
2309
+ human = true;
2310
+ else if (a === '--summarize')
2311
+ sumOnly = true;
2312
+ }
2313
+ else {
2314
+ positional.push(a);
2315
+ }
2316
+ }
2317
+ const target = positional[0] || '.';
2318
+ const root = resolvePath(ctx.cwd, target);
2319
+ const fmt = (b) => human ? (b >= 1e6 ? (b / 1e6).toFixed(1) + 'M' : b >= 1e3 ? (b / 1e3).toFixed(1) + 'K' : b + 'B') : String(Math.ceil(b / 1024));
2320
+ let total = 0;
2321
+ function walk(path) {
2322
+ let size = 0;
2323
+ try {
2324
+ const entries = vfs.readdir(path);
2325
+ for (const e of entries) {
2326
+ const fp = path + '/' + e.name;
2327
+ if (e.type === 'directory') {
2328
+ const dirSize = walk(fp);
2329
+ size += dirSize;
2330
+ if (!sumOnly)
2331
+ ctx.stdout.write(`${fmt(dirSize)}\t/${fp}\n`);
2332
+ }
2333
+ else {
2334
+ try {
2335
+ const st = vfs.stat(fp);
2336
+ size += st.size;
2337
+ if (showAll && !sumOnly)
2338
+ ctx.stdout.write(`${fmt(st.size)}\t/${fp}\n`);
2339
+ }
2340
+ catch { }
2341
+ }
2342
+ }
2343
+ }
2344
+ catch { }
2345
+ return size;
2346
+ }
2347
+ total = walk(root);
2348
+ if (sumOnly || !showAll)
2349
+ ctx.stdout.write(`${fmt(total)}\t/${root}\n`);
2350
+ return 0;
2351
+ };
2352
+ }
2353
+ function mkDiff(vfs) {
2354
+ return (ctx) => {
2355
+ if (ctx.args.length < 2) {
2356
+ ctx.stderr.write('Usage: diff FILE1 FILE2\n');
2357
+ return 1;
2358
+ }
2359
+ const f1 = resolvePath(ctx.cwd, ctx.args[0]);
2360
+ const f2 = resolvePath(ctx.cwd, ctx.args[1]);
2361
+ try {
2362
+ const a = vfs.readFileString(f1).split('\n');
2363
+ const b = vfs.readFileString(f2).split('\n');
2364
+ let hasDiff = false;
2365
+ const maxLen = Math.max(a.length, b.length);
2366
+ for (let i = 0; i < maxLen; i++) {
2367
+ if (a[i] !== b[i]) {
2368
+ hasDiff = true;
2369
+ if (a[i] !== undefined && b[i] === undefined)
2370
+ ctx.stdout.write(`${i + 1}d${i}\n< ${a[i]}\n`);
2371
+ else if (a[i] === undefined && b[i] !== undefined)
2372
+ ctx.stdout.write(`${i}a${i + 1}\n> ${b[i]}\n`);
2373
+ else
2374
+ ctx.stdout.write(`${i + 1}c${i + 1}\n< ${a[i]}\n---\n> ${b[i]}\n`);
2375
+ }
2376
+ }
2377
+ return hasDiff ? 1 : 0;
2378
+ }
2379
+ catch (e) {
2380
+ ctx.stderr.write(`diff: ${e.message}\n`);
2381
+ return 2;
2382
+ }
2383
+ };
2384
+ }
2385
+ /**
2386
+ * BUG-SWEEP-R2-1 (2026-05-11): POSIX rm with proper -f semantics.
2387
+ *
2388
+ * lifo-sh's rm calls `r.vfs.stat(...)` and catches `e instanceof VFSError`.
2389
+ * Our SqliteVFSProvider's stat method delegates to SqliteVFS.stat which
2390
+ * throws raw `Error("ENOENT: ...")` — NOT VFSError. lifo-sh's rm
2391
+ * therefore falls through to `else throw e`, the error propagates up,
2392
+ * and executeCommand returns exit 1.
2393
+ *
2394
+ * Real-world impact: every `rm -rf <nonexistent> && ...` short-circuits.
2395
+ * The most common cleanup idiom in shell scripts.
2396
+ *
2397
+ * Fix: register our own rm in the registry's `commands` map (takes
2398
+ * precedence over lifo-sh's lazy). Treat -f silently when target is
2399
+ * missing (return 0). Handle both files (unlink) and directories
2400
+ * (rmdir recursive when -r). Translate raw errors so the unix-command
2401
+ * contract is honoured.
2402
+ */
2403
+ /**
2404
+ * BUG-SWEEP-R2-3b (2026-05-11): registry-level echo so `X | xargs echo`
2405
+ * resolves. lifo-sh's `echo` is a Shell.builtins entry, NOT in the
2406
+ * registry map. xargs's cross-command dispatch goes through
2407
+ * registry.resolve(name) — without a registry entry for echo it falls
2408
+ * back to 'command not found'. The init.ts override for echo flag
2409
+ * handling targets Shell.builtins; we additionally register a copy
2410
+ * here so registry-driven callers (xargs) can find it. Behaviour
2411
+ * matches the BUG-SWEEP-4 nimbusEcho impl: -n / -e / -E / combined.
2412
+ */
2413
+ function mkEcho() {
2414
+ return (ctx) => {
2415
+ const args = ctx.args;
2416
+ let interpretEscapes = false;
2417
+ let suppressNewline = false;
2418
+ let i = 0;
2419
+ while (i < args.length) {
2420
+ const a = args[i];
2421
+ if (a === '--') {
2422
+ i++;
2423
+ break;
2424
+ }
2425
+ if (a === '-n') {
2426
+ suppressNewline = true;
2427
+ i++;
2428
+ continue;
2429
+ }
2430
+ if (a === '-e') {
2431
+ interpretEscapes = true;
2432
+ i++;
2433
+ continue;
2434
+ }
2435
+ if (a === '-E') {
2436
+ interpretEscapes = false;
2437
+ i++;
2438
+ continue;
2439
+ }
2440
+ if (/^-[neE]+$/.test(a)) {
2441
+ for (const ch of a.slice(1)) {
2442
+ if (ch === 'n')
2443
+ suppressNewline = true;
2444
+ else if (ch === 'e')
2445
+ interpretEscapes = true;
2446
+ else if (ch === 'E')
2447
+ interpretEscapes = false;
2448
+ }
2449
+ i++;
2450
+ continue;
2451
+ }
2452
+ break;
2453
+ }
2454
+ let out = args.slice(i).join(' ');
2455
+ if (interpretEscapes) {
2456
+ out = out
2457
+ .replace(/\\\\/g, '\u0000')
2458
+ .replace(/\\n/g, '\n')
2459
+ .replace(/\\t/g, '\t')
2460
+ .replace(/\\r/g, '\r')
2461
+ .replace(/\\b/g, '\b')
2462
+ .replace(/\\f/g, '\f')
2463
+ .replace(/\\v/g, '\v')
2464
+ .replace(/\\a/g, '\x07')
2465
+ .replace(/\\0([0-7]{1,3})?/g, (_m, oct) => String.fromCharCode(oct ? parseInt(oct, 8) : 0))
2466
+ .replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))
2467
+ .replace(/\u0000/g, '\\');
2468
+ }
2469
+ ctx.stdout.write(suppressNewline ? out : out + '\n');
2470
+ return 0;
2471
+ };
2472
+ }
2473
+ /**
2474
+ * SHELL-R6-4 (2026-05-12): symlink-aware `ls`.
2475
+ *
2476
+ * Pre-fix: lifo-sh's lazy `ls` (node_modules/@lifo-sh/core/dist/ls-*.js)
2477
+ * formats `mode` with first-char `d` or `-`; it has no symlink concept,
2478
+ * and our SymlinkRegistry entries are not in the VFS dir listing at all,
2479
+ * so `ls -l` after `ln -s t.txt l.txt` showed ONLY `t.txt`.
2480
+ *
2481
+ * Post-fix:
2482
+ * - `ls` lists VFS dir entries AND SymlinkRegistry entries whose link
2483
+ * path lives in the queried directory.
2484
+ * - `ls -l` shows `lrwxrwxrwx 1 user user N <mtime> <name> -> <target>`
2485
+ * for symlinks (`N` = target string length, matches GNU coreutils).
2486
+ * - Non-symlink rows go through the same formatter so columns line up.
2487
+ * - Hidden-file rule (skip if leading `.`) still honored unless `-a`.
2488
+ *
2489
+ * Args supported: `-l` long, `-a` all, `-1` one-per-line, plus path
2490
+ * positional. Matches lifo-sh's flag surface so we don't regress.
2491
+ */
2492
+ function mkLs(vfs) {
2493
+ return (ctx) => {
2494
+ const args = ctx.args;
2495
+ const flagLong = args.some(a => /^-[la]*l[la]*$/.test(a));
2496
+ const flagAll = args.some(a => /^-[la]*a[la]*$/.test(a));
2497
+ const flagOne = args.some(a => /^-[la1]*1[la1]*$/.test(a));
2498
+ const positionals = args.filter(a => !a.startsWith('-'));
2499
+ const targets = positionals.length > 0 ? positionals : [ctx.cwd];
2500
+ const reg = getSymlinkRegistry(vfs);
2501
+ const kvfs = ctx.vfs;
2502
+ function modeStr(mode, isDir, isLink) {
2503
+ if (isLink)
2504
+ return 'lrwxrwxrwx';
2505
+ const prefix = isDir ? 'd' : '-';
2506
+ const bits = [
2507
+ mode & 0o400 ? 'r' : '-',
2508
+ mode & 0o200 ? 'w' : '-',
2509
+ mode & 0o100 ? 'x' : '-',
2510
+ mode & 0o040 ? 'r' : '-',
2511
+ mode & 0o020 ? 'w' : '-',
2512
+ mode & 0o010 ? 'x' : '-',
2513
+ mode & 0o004 ? 'r' : '-',
2514
+ mode & 0o002 ? 'w' : '-',
2515
+ mode & 0o001 ? 'x' : '-',
2516
+ ].join('');
2517
+ return prefix + bits;
2518
+ }
2519
+ function fmtTime(mtime) {
2520
+ const d = new Date(mtime);
2521
+ const mon = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()];
2522
+ const day = String(d.getDate()).padStart(2, ' ');
2523
+ const hh = String(d.getHours()).padStart(2, '0');
2524
+ const mm = String(d.getMinutes()).padStart(2, '0');
2525
+ return `${mon} ${day} ${hh}:${mm}`;
2526
+ }
2527
+ function listDir(dirPath) {
2528
+ const fp = resolvePath(ctx.cwd, dirPath);
2529
+ const out = [];
2530
+ // Real entries via ctx.vfs.readdirStat (Kernel.VFS — handles
2531
+ // mounts like /dev) with fallback to closure-captured SqliteVFS.
2532
+ let real = [];
2533
+ try {
2534
+ if (kvfs && typeof kvfs.readdirStat === 'function') {
2535
+ real = kvfs.readdirStat(fp);
2536
+ }
2537
+ else {
2538
+ const names = vfs.readdir(fp);
2539
+ real = names.map(n => {
2540
+ const childPath = (fp === '/' ? '' : fp.replace(/^\/+/, '').replace(/\/+$/, ''))
2541
+ + '/' + n.name;
2542
+ try {
2543
+ const s = vfs.stat(childPath);
2544
+ return { name: n.name, type: n.type, size: s.size ?? 0,
2545
+ mtime: s.mtime ?? Date.now(), mode: s.mode ?? 0o644 };
2546
+ }
2547
+ catch {
2548
+ return { name: n.name, type: n.type, size: 0, mtime: Date.now(), mode: 0o644 };
2549
+ }
2550
+ });
2551
+ }
2552
+ }
2553
+ catch (e) {
2554
+ ctx.stderr.write(`ls: cannot access '${dirPath}': ${e?.message || e}\n`);
2555
+ return [];
2556
+ }
2557
+ for (const r of real) {
2558
+ out.push({
2559
+ name: r.name,
2560
+ type: r.type === 'directory' ? 'directory' : 'file',
2561
+ size: r.size ?? 0,
2562
+ mtime: r.mtime ?? Date.now(),
2563
+ mode: r.mode ?? 0o644,
2564
+ });
2565
+ }
2566
+ // Inject symlink entries whose link path is in this directory.
2567
+ const normDir = fp.replace(/^\/+/, '').replace(/\/+$/, '');
2568
+ for (const { link, target } of reg.list()) {
2569
+ const linkNorm = link.replace(/^\/+/, '').replace(/\/+$/, '');
2570
+ const lastSlash = linkNorm.lastIndexOf('/');
2571
+ const linkDir = lastSlash >= 0 ? linkNorm.substring(0, lastSlash) : '';
2572
+ if (linkDir === normDir) {
2573
+ const linkName = lastSlash >= 0 ? linkNorm.substring(lastSlash + 1) : linkNorm;
2574
+ // Filter dotfiles unless -a (consistent with real entries).
2575
+ if (!flagAll && linkName.startsWith('.'))
2576
+ continue;
2577
+ out.push({
2578
+ name: linkName,
2579
+ type: 'symlink',
2580
+ size: target.length,
2581
+ mtime: Date.now(),
2582
+ mode: 0o777,
2583
+ linkTarget: target,
2584
+ });
2585
+ }
2586
+ }
2587
+ // Filter dotfiles among real entries unless -a.
2588
+ const filtered = flagAll ? out : out.filter(e => !e.name.startsWith('.'));
2589
+ filtered.sort((a, b) => a.name.localeCompare(b.name));
2590
+ return filtered;
2591
+ }
2592
+ function fmtRow(e, long) {
2593
+ if (!long)
2594
+ return e.name;
2595
+ const isDir = e.type === 'directory';
2596
+ const isLink = e.type === 'symlink';
2597
+ const mode = modeStr(e.mode, isDir, isLink);
2598
+ const size = String(e.size).padStart(6, ' ');
2599
+ const time = fmtTime(e.mtime);
2600
+ const arrow = isLink && e.linkTarget ? ` -> ${e.linkTarget}` : '';
2601
+ return `${mode} 1 user user ${size} ${time} ${e.name}${arrow}`;
2602
+ }
2603
+ let exit = 0;
2604
+ // First pass: separate file-args from dir-args (real `ls` lists
2605
+ // each file inline; dirs get listed as their contents).
2606
+ const fileEntries = [];
2607
+ const dirArgs = [];
2608
+ for (const arg of targets) {
2609
+ const fp = resolvePath(ctx.cwd, arg);
2610
+ // Symlink check: a symlink-arg is displayed as the link itself
2611
+ // (without -L which we don't implement).
2612
+ if (reg.isSymlink(fp)) {
2613
+ const target = reg.readlink(fp) || '';
2614
+ fileEntries.push({
2615
+ name: arg,
2616
+ type: 'symlink',
2617
+ size: target.length,
2618
+ mtime: Date.now(),
2619
+ mode: 0o777,
2620
+ linkTarget: target,
2621
+ });
2622
+ continue;
2623
+ }
2624
+ try {
2625
+ const s = kvfs && typeof kvfs.stat === 'function' ? kvfs.stat(fp) : vfs.stat(fp);
2626
+ if (s.type === 'directory') {
2627
+ dirArgs.push(arg);
2628
+ }
2629
+ else {
2630
+ fileEntries.push({
2631
+ name: arg,
2632
+ type: 'file',
2633
+ size: s.size ?? 0,
2634
+ mtime: s.mtime ?? Date.now(),
2635
+ mode: s.mode ?? 0o644,
2636
+ });
2637
+ }
2638
+ }
2639
+ catch (e) {
2640
+ ctx.stderr.write(`ls: cannot access '${arg}': ${e?.message || e}\n`);
2641
+ exit = 1;
2642
+ }
2643
+ }
2644
+ // Render file-args first.
2645
+ if (fileEntries.length > 0) {
2646
+ if (flagLong) {
2647
+ for (const e of fileEntries)
2648
+ ctx.stdout.write(fmtRow(e, true) + '\n');
2649
+ }
2650
+ else if (flagOne) {
2651
+ for (const e of fileEntries)
2652
+ ctx.stdout.write(e.name + '\n');
2653
+ }
2654
+ else {
2655
+ ctx.stdout.write(fileEntries.map(e => e.name).join(' ') + '\n');
2656
+ }
2657
+ }
2658
+ // Then dir-args (with header if multiple).
2659
+ for (let i = 0; i < dirArgs.length; i++) {
2660
+ const d = dirArgs[i];
2661
+ if (dirArgs.length > 1 || fileEntries.length > 0) {
2662
+ if (fileEntries.length > 0 || i > 0)
2663
+ ctx.stdout.write('\n');
2664
+ ctx.stdout.write(`${d}:\n`);
2665
+ }
2666
+ const rows = listDir(d);
2667
+ if (flagLong) {
2668
+ for (const e of rows)
2669
+ ctx.stdout.write(fmtRow(e, true) + '\n');
2670
+ }
2671
+ else if (flagOne) {
2672
+ for (const e of rows)
2673
+ ctx.stdout.write(e.name + '\n');
2674
+ }
2675
+ else if (rows.length > 0) {
2676
+ ctx.stdout.write(rows.map(e => e.name).join(' ') + '\n');
2677
+ }
2678
+ }
2679
+ return exit;
2680
+ };
2681
+ }
2682
+ /**
2683
+ * BUG-SWEEP-R2-3c (2026-05-11): registry-level cat (for xargs cross-
2684
+ * command dispatch). Behaves like lifo-sh's lazy cat: reads files (or
2685
+ * stdin if none), concatenates to stdout.
2686
+ */
2687
+ function mkCat(vfs) {
2688
+ return (ctx) => {
2689
+ const files = ctx.args.filter(a => !a.startsWith('-'));
2690
+ if (files.length === 0) {
2691
+ if (ctx.stdin)
2692
+ ctx.stdout.write(ctx.stdin);
2693
+ return 0;
2694
+ }
2695
+ // BUG-SWEEP-R3 follow-up: prefer ctx.vfs (Kernel.VFS, sees /dev
2696
+ // mount) over the closure-captured SqliteVFS. Without this fallback,
2697
+ // `cat /dev/null` errors with ENOENT because SqliteVFS doesn't know
2698
+ // about the /dev provider mounted on Kernel.VFS. lifo-sh's
2699
+ // executeCommand passes Kernel.VFS as ctx.vfs.
2700
+ const kvfs = ctx.vfs;
2701
+ // SHELL-FOLLOWUPS-4: dereference symlinks via SymlinkRegistry
2702
+ // before any read. Real cat reads through symlinks transparently.
2703
+ const reg = getSymlinkRegistry(vfs);
2704
+ let exit = 0;
2705
+ for (const fOrig of files) {
2706
+ const f = (() => {
2707
+ const fp = resolvePath(ctx.cwd, fOrig);
2708
+ const resolved = reg.resolveChain(fp);
2709
+ if (resolved === null) {
2710
+ // ELOOP: too many hops
2711
+ ctx.stderr.write(`cat: ${fOrig}: Too many levels of symbolic links\n`);
2712
+ return null;
2713
+ }
2714
+ return resolved === fp ? fOrig : '/' + resolved;
2715
+ })();
2716
+ if (f === null) {
2717
+ exit = 1;
2718
+ continue;
2719
+ }
2720
+ try {
2721
+ let content;
2722
+ // Try Kernel.VFS first (handles mounted providers like /dev).
2723
+ if (kvfs && typeof kvfs.readFile === 'function') {
2724
+ try {
2725
+ const raw = kvfs.readFile(f.startsWith('/') ? f : ctx.cwd + '/' + f);
2726
+ content = typeof raw === 'string'
2727
+ ? raw
2728
+ : new TextDecoder('utf-8').decode(raw);
2729
+ }
2730
+ catch (kErr) {
2731
+ const fp = resolvePath(ctx.cwd, f);
2732
+ content = vfs.readFileString(fp);
2733
+ void kErr;
2734
+ }
2735
+ }
2736
+ else {
2737
+ const fp = resolvePath(ctx.cwd, f);
2738
+ content = vfs.readFileString(fp);
2739
+ }
2740
+ ctx.stdout.write(content);
2741
+ }
2742
+ catch (e) {
2743
+ ctx.stderr.write(`cat: ${fOrig}: ${e?.message || e}\n`);
2744
+ exit = 1;
2745
+ }
2746
+ }
2747
+ return exit;
2748
+ };
2749
+ }
2750
+ function mkRm(vfs) {
2751
+ return (ctx) => {
2752
+ const args = ctx.args;
2753
+ 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'))));
2754
+ const force = args.some(a => a === '-f' || a === '--force' || (a.startsWith('-') && !a.startsWith('--') && a.includes('f')));
2755
+ const targets = args.filter(a => !a.startsWith('-'));
2756
+ if (targets.length === 0) {
2757
+ if (force)
2758
+ return 0; // POSIX: rm -f with no operands is silent success
2759
+ ctx.stderr.write('rm: missing operand\n');
2760
+ return 1;
2761
+ }
2762
+ // SHELL-R6-5: SymlinkRegistry awareness. Real `rm` removes the
2763
+ // LINK, not the target. Pre-fix this loop went straight to
2764
+ // vfs.exists which is false for registry-only symlink entries
2765
+ // (no real file), producing "No such file or directory" while
2766
+ // `readlink` still reported the registry entry.
2767
+ const reg = getSymlinkRegistry(vfs);
2768
+ let exit = 0;
2769
+ for (const t of targets) {
2770
+ const fp = resolvePath(ctx.cwd, t);
2771
+ // Symlink path FIRST. If `fp` is registered as a symlink we
2772
+ // delete the registry entry and skip the vfs-level operations.
2773
+ // Real `rm` never follows symlinks (it removes the link
2774
+ // itself); recursive flag has no effect on the symlink itself
2775
+ // either — it acts on the link node only.
2776
+ if (reg.isSymlink(fp)) {
2777
+ reg.delete(fp);
2778
+ continue;
2779
+ }
2780
+ if (!vfs.exists(fp)) {
2781
+ if (force)
2782
+ continue; // silent success
2783
+ ctx.stderr.write(`rm: cannot remove '${t}': No such file or directory\n`);
2784
+ exit = 1;
2785
+ continue;
2786
+ }
2787
+ try {
2788
+ if (vfs.isDirectory(fp)) {
2789
+ if (!recursive) {
2790
+ ctx.stderr.write(`rm: cannot remove '${t}': Is a directory\n`);
2791
+ exit = 1;
2792
+ continue;
2793
+ }
2794
+ // Recursive delete: walk children, unlink files, rmdir dirs.
2795
+ rmDirRec(vfs, fp);
2796
+ }
2797
+ else {
2798
+ vfs.unlink(fp);
2799
+ }
2800
+ }
2801
+ catch (e) {
2802
+ // -f suppresses ENOENT only (file disappeared mid-loop); other
2803
+ // errors (ENOTEMPTY because of a logic bug, ENOTDIR mismatches,
2804
+ // permission errors) must still surface. Pre-fix the broad
2805
+ // `if (force) continue` masked the readdir-iteration bug that
2806
+ // left directories undeleted.
2807
+ const msg = String(e?.message || e);
2808
+ if (force && /ENOENT/.test(msg))
2809
+ continue;
2810
+ ctx.stderr.write(`rm: cannot remove '${t}': ${msg}\n`);
2811
+ exit = 1;
2812
+ }
2813
+ }
2814
+ return exit;
2815
+ };
2816
+ }
2817
+ /**
2818
+ * Internal helper: recursive directory delete via SqliteVFS readdir +
2819
+ * unlink/rmdir. vfs.readdir returns `{name, type}[]` not `string[]` —
2820
+ * iterate the name property explicitly.
2821
+ */
2822
+ function rmDirRec(vfs, path) {
2823
+ const entries = vfs.readdir(path);
2824
+ for (const entry of entries) {
2825
+ const childPath = path + '/' + entry.name;
2826
+ if (entry.type === 'directory')
2827
+ rmDirRec(vfs, childPath);
2828
+ else
2829
+ vfs.unlink(childPath);
2830
+ }
2831
+ vfs.rmdir(path);
2832
+ }
2833
+ function mkTouch(vfs) {
2834
+ return (ctx) => {
2835
+ for (const f of ctx.args.filter(a => !a.startsWith('-'))) {
2836
+ const fp = resolvePath(ctx.cwd, f);
2837
+ // Ensure parent dirs
2838
+ const parts = fp.split('/');
2839
+ for (let i = 1; i < parts.length; i++) {
2840
+ const dir = parts.slice(0, i).join('/');
2841
+ if (dir && !vfs.exists(dir))
2842
+ vfs.mkdir(dir, { recursive: true });
2843
+ }
2844
+ if (vfs.exists(fp) && !vfs.isDirectory(fp)) {
2845
+ // Update mtime by re-writing the same content
2846
+ const content = vfs.readFile(fp);
2847
+ vfs.writeFile(fp, content);
2848
+ }
2849
+ else if (!vfs.exists(fp)) {
2850
+ vfs.writeFile(fp, '');
2851
+ }
2852
+ }
2853
+ return 0;
2854
+ };
2855
+ }
2856
+ function mkStat(vfs) {
2857
+ return (ctx) => {
2858
+ // BUG-SWEEP-R3 follow-up: try Kernel.VFS (ctx.vfs) first so /dev
2859
+ // mount paths resolve. Same pattern as mkCat.
2860
+ const kvfs = ctx.vfs;
2861
+ for (const f of ctx.args.filter(a => !a.startsWith('-'))) {
2862
+ let st = null;
2863
+ let displayPath = f;
2864
+ // Try Kernel.VFS first (sees mounts).
2865
+ if (kvfs && typeof kvfs.stat === 'function') {
2866
+ try {
2867
+ st = kvfs.stat(f.startsWith('/') ? f : ctx.cwd + '/' + f);
2868
+ displayPath = f.startsWith('/') ? f : `/${ctx.cwd}/${f}`.replace(/^\/+/, '/');
2869
+ }
2870
+ catch (_e) { /* fall through to SqliteVFS */ }
2871
+ }
2872
+ // Fall back to SqliteVFS direct for non-mounted paths.
2873
+ if (!st) {
2874
+ try {
2875
+ const fp = resolvePath(ctx.cwd, f);
2876
+ st = vfs.stat(fp);
2877
+ displayPath = '/' + fp;
2878
+ }
2879
+ catch (_e) {
2880
+ ctx.stderr.write(`stat: '${f}': No such file\n`);
2881
+ return 1;
2882
+ }
2883
+ }
2884
+ ctx.stdout.write(` File: ${displayPath}\n`);
2885
+ ctx.stdout.write(` Size: ${st.size}\tType: ${st.type}\n`);
2886
+ ctx.stdout.write(` Mode: ${st.mode.toString(8)}\n`);
2887
+ ctx.stdout.write(`Modify: ${new Date(st.mtime).toISOString()}\n`);
2888
+ }
2889
+ return 0;
2890
+ };
2891
+ }
2892
+ function mkBase64(vfs) {
2893
+ return (ctx) => {
2894
+ const decode = ctx.args.includes('-d') || ctx.args.includes('--decode');
2895
+ const file = ctx.args.find(a => !a.startsWith('-'));
2896
+ let input = ctx.stdin || '';
2897
+ if (file) {
2898
+ try {
2899
+ input = vfs.readFileString(resolvePath(ctx.cwd, file));
2900
+ }
2901
+ catch {
2902
+ ctx.stderr.write(`base64: ${file}: No such file\n`);
2903
+ return 1;
2904
+ }
2905
+ }
2906
+ if (decode) {
2907
+ try {
2908
+ ctx.stdout.write(atob(input.trim()) + '\n');
2909
+ }
2910
+ catch {
2911
+ ctx.stderr.write('base64: invalid input\n');
2912
+ return 1;
2913
+ }
2914
+ }
2915
+ else {
2916
+ ctx.stdout.write(btoa(input) + '\n');
2917
+ }
2918
+ return 0;
2919
+ };
2920
+ }
2921
+ function mkSeq() {
2922
+ return (ctx) => {
2923
+ const nums = ctx.args.map(Number).filter(n => !isNaN(n));
2924
+ let start = 1, step = 1, end = 1;
2925
+ if (nums.length === 1)
2926
+ end = nums[0];
2927
+ else if (nums.length === 2) {
2928
+ start = nums[0];
2929
+ end = nums[1];
2930
+ }
2931
+ else if (nums.length >= 3) {
2932
+ start = nums[0];
2933
+ step = nums[1];
2934
+ end = nums[2];
2935
+ }
2936
+ for (let i = start; step > 0 ? i <= end : i >= end; i += step)
2937
+ ctx.stdout.write(i + '\n');
2938
+ return 0;
2939
+ };
2940
+ }
2941
+ function mkSleep() {
2942
+ return async (ctx) => {
2943
+ const secs = parseFloat(ctx.args[0] || '1');
2944
+ await new Promise(r => setTimeout(r, secs * 1000));
2945
+ return 0;
2946
+ };
2947
+ }
2948
+ function mkId() {
2949
+ return (ctx) => {
2950
+ ctx.stdout.write('uid=1000(user) gid=1000(user) groups=1000(user)\n');
2951
+ return 0;
2952
+ };
2953
+ }
2954
+ function mkHostname() {
2955
+ return (ctx) => { ctx.stdout.write('nimbus\n'); return 0; };
2956
+ }
2957
+ function mkBasename() {
2958
+ return (ctx) => {
2959
+ const p = ctx.args[0] || '';
2960
+ const suffix = ctx.args[1] || '';
2961
+ let base = p.split('/').pop() || '';
2962
+ if (suffix && base.endsWith(suffix))
2963
+ base = base.slice(0, -suffix.length);
2964
+ ctx.stdout.write(base + '\n');
2965
+ return 0;
2966
+ };
2967
+ }
2968
+ function mkDirname() {
2969
+ return (ctx) => {
2970
+ const p = ctx.args[0] || '';
2971
+ const dir = p.includes('/') ? p.substring(0, p.lastIndexOf('/')) : '.';
2972
+ ctx.stdout.write((dir || '/') + '\n');
2973
+ return 0;
2974
+ };
2975
+ }
2976
+ function mkRealpath(vfs) {
2977
+ return (ctx) => {
2978
+ for (const p of ctx.args) {
2979
+ const fp = resolvePath(ctx.cwd, p);
2980
+ if (vfs.exists(fp))
2981
+ ctx.stdout.write('/' + fp + '\n');
2982
+ else {
2983
+ ctx.stderr.write(`realpath: ${p}: No such file\n`);
2984
+ return 1;
2985
+ }
2986
+ }
2987
+ return 0;
2988
+ };
2989
+ }
2990
+ /**
2991
+ * BUG-SWEEP-R4-2 (2026-05-11): printf full POSIX format set.
2992
+ *
2993
+ * Pre-fix mkPrintf only handled %s and %d via simple replace —
2994
+ * `printf "%x\\n" 255` output literal `%x`, `printf "%5d" 7` output
2995
+ * literal `%5d`. Common shell scripts use %x (hex), %o (octal),
2996
+ * %f (float), %g (general), %c (char), width+precision specifiers,
2997
+ * and flag chars (- + 0 # space).
2998
+ *
2999
+ * Real bash printf cycles through args, re-running the format
3000
+ * string if there are more args than format specifiers. We
3001
+ * replicate that.
3002
+ */
3003
+ function mkPrintf() {
3004
+ return (ctx) => {
3005
+ if (ctx.args.length === 0)
3006
+ return 0;
3007
+ const rawFmt = ctx.args[0];
3008
+ const vals = ctx.args.slice(1);
3009
+ // Process backslash escapes in the format string first.
3010
+ const fmt = rawFmt
3011
+ .replace(/\\\\/g, '\u0000') // protect literal \\\\
3012
+ .replace(/\\n/g, '\n')
3013
+ .replace(/\\t/g, '\t')
3014
+ .replace(/\\r/g, '\r')
3015
+ .replace(/\\a/g, '\x07')
3016
+ .replace(/\\b/g, '\b')
3017
+ .replace(/\\f/g, '\f')
3018
+ .replace(/\\v/g, '\v')
3019
+ .replace(/\\0([0-7]{1,3})?/g, (_m, oct) => String.fromCharCode(oct ? parseInt(oct, 8) : 0))
3020
+ .replace(/\\x([0-9a-fA-F]{1,2})/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))
3021
+ .replace(/\u0000/g, '\\');
3022
+ let out = '';
3023
+ let argIdx = 0;
3024
+ function applyFormat() {
3025
+ // Run the format string once; return true if it consumed any args.
3026
+ let i = 0;
3027
+ const startArg = argIdx;
3028
+ while (i < fmt.length) {
3029
+ const ch = fmt[i];
3030
+ if (ch !== '%') {
3031
+ out += ch;
3032
+ i++;
3033
+ continue;
3034
+ }
3035
+ if (fmt[i + 1] === '%') {
3036
+ out += '%';
3037
+ i += 2;
3038
+ continue;
3039
+ }
3040
+ // Parse format spec: %[flags][width][.prec]conversion
3041
+ let spec = '%';
3042
+ i++;
3043
+ while (i < fmt.length && /[-+ 0#]/.test(fmt[i])) {
3044
+ spec += fmt[i];
3045
+ i++;
3046
+ }
3047
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) {
3048
+ spec += fmt[i];
3049
+ i++;
3050
+ }
3051
+ if (fmt[i] === '.') {
3052
+ spec += fmt[i];
3053
+ i++;
3054
+ while (i < fmt.length && /[0-9]/.test(fmt[i])) {
3055
+ spec += fmt[i];
3056
+ i++;
3057
+ }
3058
+ }
3059
+ const conv = fmt[i];
3060
+ i++;
3061
+ const arg = vals[argIdx++];
3062
+ out += formatOneArg(spec + conv, arg);
3063
+ }
3064
+ return argIdx > startArg;
3065
+ }
3066
+ // bash printf: re-run the format until args are exhausted; if
3067
+ // format consumes zero args (no %X specifiers), run it once.
3068
+ if (vals.length === 0) {
3069
+ applyFormat();
3070
+ }
3071
+ else {
3072
+ while (argIdx < vals.length) {
3073
+ if (!applyFormat())
3074
+ break;
3075
+ }
3076
+ }
3077
+ ctx.stdout.write(out);
3078
+ return 0;
3079
+ };
3080
+ }
3081
+ function formatOneArg(spec, arg) {
3082
+ const conv = spec[spec.length - 1];
3083
+ const flagsAndWidth = spec.slice(1, -1);
3084
+ const dotIdx = flagsAndWidth.indexOf('.');
3085
+ const widthPart = dotIdx >= 0 ? flagsAndWidth.slice(0, dotIdx) : flagsAndWidth;
3086
+ const precPart = dotIdx >= 0 ? flagsAndWidth.slice(dotIdx + 1) : '';
3087
+ let flags = '';
3088
+ let widthStr = '';
3089
+ for (const c of widthPart) {
3090
+ if (/[-+ 0#]/.test(c))
3091
+ flags += c;
3092
+ else
3093
+ widthStr += c;
3094
+ }
3095
+ const width = widthStr ? parseInt(widthStr, 10) : 0;
3096
+ const prec = precPart ? parseInt(precPart, 10) : -1;
3097
+ let body;
3098
+ switch (conv) {
3099
+ case 's': {
3100
+ body = String(arg ?? '');
3101
+ if (prec >= 0)
3102
+ body = body.slice(0, prec);
3103
+ break;
3104
+ }
3105
+ case 'd':
3106
+ case 'i': {
3107
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3108
+ const v = Number.isFinite(n) ? n : 0;
3109
+ body = String(Math.abs(v));
3110
+ const sign = v < 0 ? '-' : flags.includes('+') ? '+' : flags.includes(' ') ? ' ' : '';
3111
+ body = sign + body;
3112
+ break;
3113
+ }
3114
+ case 'u': {
3115
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3116
+ body = String(Math.max(0, Number.isFinite(n) ? n : 0));
3117
+ break;
3118
+ }
3119
+ case 'f':
3120
+ case 'F': {
3121
+ const n = typeof arg === 'number' ? arg : parseFloat(String(arg ?? '0'));
3122
+ const p = prec < 0 ? 6 : prec;
3123
+ body = (Number.isFinite(n) ? n : 0).toFixed(p);
3124
+ if (n >= 0 && flags.includes('+'))
3125
+ body = '+' + body;
3126
+ else if (n >= 0 && flags.includes(' '))
3127
+ body = ' ' + body;
3128
+ break;
3129
+ }
3130
+ case 'e':
3131
+ case 'E': {
3132
+ const n = typeof arg === 'number' ? arg : parseFloat(String(arg ?? '0'));
3133
+ const p = prec < 0 ? 6 : prec;
3134
+ body = (Number.isFinite(n) ? n : 0).toExponential(p);
3135
+ if (conv === 'E')
3136
+ body = body.toUpperCase();
3137
+ break;
3138
+ }
3139
+ case 'g':
3140
+ case 'G': {
3141
+ const n = typeof arg === 'number' ? arg : parseFloat(String(arg ?? '0'));
3142
+ const p = prec < 0 ? 6 : prec || 1;
3143
+ body = (Number.isFinite(n) ? n : 0).toPrecision(p);
3144
+ // Strip trailing zeros + dot (POSIX %g behavior) unless # flag.
3145
+ if (!flags.includes('#'))
3146
+ body = body.replace(/(\.\d*?)0+($|e)/, '$1$2').replace(/\.($|e)/, '$1');
3147
+ if (conv === 'G')
3148
+ body = body.toUpperCase();
3149
+ break;
3150
+ }
3151
+ case 'x':
3152
+ case 'X': {
3153
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3154
+ body = (Number.isFinite(n) ? n >>> 0 : 0).toString(16);
3155
+ if (conv === 'X')
3156
+ body = body.toUpperCase();
3157
+ if (flags.includes('#') && body !== '0')
3158
+ body = (conv === 'X' ? '0X' : '0x') + body;
3159
+ break;
3160
+ }
3161
+ case 'o': {
3162
+ const n = typeof arg === 'number' ? Math.trunc(arg) : Math.trunc(parseFloat(String(arg ?? '0')));
3163
+ body = (Number.isFinite(n) ? n >>> 0 : 0).toString(8);
3164
+ if (flags.includes('#') && !body.startsWith('0'))
3165
+ body = '0' + body;
3166
+ break;
3167
+ }
3168
+ case 'b': {
3169
+ // bash printf %b: interpret backslash escapes in the arg
3170
+ let s = String(arg ?? '');
3171
+ s = s.replace(/\\\\/g, '\u0000')
3172
+ .replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r')
3173
+ .replace(/\\u0000/g, '\\');
3174
+ body = s;
3175
+ break;
3176
+ }
3177
+ case 'c': {
3178
+ if (typeof arg === 'number')
3179
+ body = String.fromCharCode(arg);
3180
+ else
3181
+ body = String(arg ?? '').charAt(0);
3182
+ break;
3183
+ }
3184
+ case 'q': {
3185
+ // bash printf %q: shell-quote
3186
+ const s = String(arg ?? '');
3187
+ if (/^[A-Za-z0-9_/.,:=+@%-]+$/.test(s))
3188
+ body = s;
3189
+ else
3190
+ body = "'" + s.replace(/'/g, `'\\''`) + "'";
3191
+ break;
3192
+ }
3193
+ default: body = '%' + conv;
3194
+ }
3195
+ // Apply width padding.
3196
+ if (width > body.length) {
3197
+ const zeroPad = flags.includes('0') && /[diouxXfFeEgG]/.test(conv) && !flags.includes('-');
3198
+ const padCh = zeroPad ? '0' : ' ';
3199
+ if (flags.includes('-'))
3200
+ body = body.padEnd(width, ' ');
3201
+ else {
3202
+ // For zero-pad on negative numbers, keep the sign at the front.
3203
+ if (zeroPad && (body.startsWith('-') || body.startsWith('+') || body.startsWith(' '))) {
3204
+ body = body[0] + body.slice(1).padStart(width - 1, padCh);
3205
+ }
3206
+ else {
3207
+ body = body.padStart(width, padCh);
3208
+ }
3209
+ }
3210
+ }
3211
+ return body;
3212
+ }
3213
+ function mkTrue() { return () => 0; }
3214
+ function mkFalse() { return () => 1; }
3215
+ /**
3216
+ * BUG-SWEEP-R4-7 (2026-05-11): readlink stub.
3217
+ *
3218
+ * Real readlink reads the symlink target. Our VFS doesn't yet
3219
+ * support real symlinks (ln -s currently does a regular file
3220
+ * copy — tracked as deferred). For graceful failure:
3221
+ * - if path is a regular file/dir, exit 1 (matches GNU readlink)
3222
+ * - if path is missing, write error to stderr + exit 1
3223
+ * - explicit handling avoids 'readlink: command not found'.
3224
+ *
3225
+ * When real symlinks land in VFS, this command will become the
3226
+ * read-side of the symlink table.
3227
+ */
3228
+ /**
3229
+ * SHELL-FOLLOWUPS-4 (2026-05-11): real symlink readlink.
3230
+ * Pre-fix: ln -s did file-copy; readlink returned exit 1 with no
3231
+ * output (no symlink table existed). Now backed by SymlinkRegistry:
3232
+ * - GNU readlink prints target (relative or absolute as stored)
3233
+ * - exit 1 silently for non-symlinks (matches GNU readlink default)
3234
+ * - exit 1 + stderr for missing files
3235
+ * Flags: -f (canonicalize — follow chain to final target), default
3236
+ * (one-hop). -e variant (verify) deferred.
3237
+ */
3238
+ function mkReadlink(vfs) {
3239
+ return (ctx) => {
3240
+ const args = [...ctx.args];
3241
+ let canonicalize = false;
3242
+ const targets = [];
3243
+ for (const a of args) {
3244
+ if (a === '-f' || a === '--canonicalize') {
3245
+ canonicalize = true;
3246
+ continue;
3247
+ }
3248
+ if (a.startsWith('-') && a !== '-') {
3249
+ for (const ch of a.slice(1))
3250
+ if (ch === 'f')
3251
+ canonicalize = true;
3252
+ continue;
3253
+ }
3254
+ targets.push(a);
3255
+ }
3256
+ if (targets.length === 0) {
3257
+ ctx.stderr.write('readlink: missing operand\n');
3258
+ return 1;
3259
+ }
3260
+ const reg = getSymlinkRegistry(vfs);
3261
+ let exit = 0;
3262
+ for (const t of targets) {
3263
+ const fp = resolvePath(ctx.cwd, t);
3264
+ if (canonicalize) {
3265
+ // -f: follow chain; succeed even if target doesn't exist YET
3266
+ // (matches `readlink -f` which canonicalizes anyway).
3267
+ const resolved = reg.resolveChain(fp);
3268
+ if (resolved !== null) {
3269
+ ctx.stdout.write('/' + resolved + '\n');
3270
+ continue;
3271
+ }
3272
+ ctx.stderr.write(`readlink: ${t}: Too many levels of symbolic links\n`);
3273
+ exit = 1;
3274
+ continue;
3275
+ }
3276
+ // Default: one-hop. Print target verbatim (preserves relative/absolute).
3277
+ const direct = reg.readlink(fp);
3278
+ if (direct !== null) {
3279
+ ctx.stdout.write(direct + '\n');
3280
+ continue;
3281
+ }
3282
+ // Not a symlink. GNU readlink exits 1 silently for regular
3283
+ // files / dirs; emits stderr for missing.
3284
+ if (!vfs.exists(fp)) {
3285
+ ctx.stderr.write(`readlink: ${t}: No such file or directory\n`);
3286
+ }
3287
+ exit = 1;
3288
+ }
3289
+ return exit;
3290
+ };
3291
+ }
3292
+ function mkSha256sum(vfs) {
3293
+ // W3: real SHA-256 via WebCrypto (crypto.subtle.digest).
3294
+ // Pre-W3 was a 4-state FNV-1a fake — second silent-correctness bug
3295
+ // discovered during W3 plan grep (the first being node-shims crypto).
3296
+ // The harness type CmdFn = (ctx) => number | Promise<number> already
3297
+ // accepts async; convert sync→async to use SubtleCrypto.
3298
+ return async (ctx) => {
3299
+ for (const f of ctx.args.filter(a => !a.startsWith('-'))) {
3300
+ const fp = resolvePath(ctx.cwd, f);
3301
+ try {
3302
+ const content = vfs.readFileString(fp);
3303
+ const buf = enc.encode(content);
3304
+ const ab = await crypto.subtle.digest('SHA-256', buf);
3305
+ const bytes = new Uint8Array(ab);
3306
+ const hash = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
3307
+ ctx.stdout.write(`${hash} ${f}\n`);
3308
+ }
3309
+ catch {
3310
+ ctx.stderr.write(`sha256sum: ${f}: No such file\n`);
3311
+ return 1;
3312
+ }
3313
+ }
3314
+ return 0;
3315
+ };
3316
+ }
3317
+ function mkFile(vfs) {
3318
+ return (ctx) => {
3319
+ for (const f of ctx.args.filter(a => !a.startsWith('-'))) {
3320
+ const fp = resolvePath(ctx.cwd, f);
3321
+ try {
3322
+ if (vfs.isDirectory(fp)) {
3323
+ ctx.stdout.write(`${f}: directory\n`);
3324
+ continue;
3325
+ }
3326
+ // BUG-SWEEP-3 (2026-05-11): scan raw bytes for NUL or non-text
3327
+ // bytes BEFORE attempting a UTF-8 decode. Pre-fix every binary
3328
+ // file was reported as "UTF-8 text" because readFileString
3329
+ // silently U+FFFD-substituted invalid sequences.
3330
+ const bytes = vfs.readFile(fp);
3331
+ let isBinary = false;
3332
+ const scanLimit = Math.min(bytes.length, 8192);
3333
+ for (let i = 0; i < scanLimit; i++) {
3334
+ const b = bytes[i];
3335
+ if (b === 0) {
3336
+ isBinary = true;
3337
+ break;
3338
+ }
3339
+ // Bytes 0x01-0x08 + 0x0E-0x1F (excluding TAB/LF/CR/FF) are
3340
+ // strong signals of non-text content.
3341
+ if (b < 0x09 || (b > 0x0d && b < 0x20)) {
3342
+ isBinary = true;
3343
+ break;
3344
+ }
3345
+ }
3346
+ if (isBinary) {
3347
+ // Magic-byte sniff for common formats.
3348
+ if (bytes.length >= 4 && bytes[0] === 0x7f && bytes[1] === 0x45 && bytes[2] === 0x4c && bytes[3] === 0x46) {
3349
+ ctx.stdout.write(`${f}: ELF executable\n`);
3350
+ }
3351
+ else if (bytes.length >= 4 && bytes[0] === 0x00 && bytes[1] === 0x61 && bytes[2] === 0x73 && bytes[3] === 0x6d) {
3352
+ ctx.stdout.write(`${f}: WebAssembly (wasm) binary module\n`);
3353
+ }
3354
+ else if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) {
3355
+ ctx.stdout.write(`${f}: PNG image data\n`);
3356
+ }
3357
+ else if (bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b) {
3358
+ ctx.stdout.write(`${f}: gzip compressed data\n`);
3359
+ }
3360
+ else if (bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && (bytes[2] === 0x03 || bytes[2] === 0x05)) {
3361
+ ctx.stdout.write(`${f}: Zip archive data\n`);
3362
+ }
3363
+ else {
3364
+ ctx.stdout.write(`${f}: data\n`);
3365
+ }
3366
+ continue;
3367
+ }
3368
+ const content = new TextDecoder('utf-8').decode(bytes);
3369
+ if (content.startsWith('<!DOCTYPE') || content.startsWith('<html'))
3370
+ ctx.stdout.write(`${f}: HTML document\n`);
3371
+ else if (content.startsWith('{') || content.startsWith('['))
3372
+ ctx.stdout.write(`${f}: JSON data\n`);
3373
+ else if (content.startsWith('#!'))
3374
+ ctx.stdout.write(`${f}: script, ${content.split('\n')[0]}\n`);
3375
+ else if (f.endsWith('.ts') || f.endsWith('.tsx'))
3376
+ ctx.stdout.write(`${f}: TypeScript source\n`);
3377
+ else if (f.endsWith('.js') || f.endsWith('.mjs'))
3378
+ ctx.stdout.write(`${f}: JavaScript source\n`);
3379
+ else if (f.endsWith('.css'))
3380
+ ctx.stdout.write(`${f}: CSS stylesheet\n`);
3381
+ else
3382
+ ctx.stdout.write(`${f}: ASCII text, ${content.split('\n').length} lines\n`);
3383
+ }
3384
+ catch {
3385
+ ctx.stderr.write(`file: ${f}: No such file\n`);
3386
+ return 1;
3387
+ }
3388
+ }
3389
+ return 0;
3390
+ };
3391
+ }
3392
+ function mkXxd(vfs) {
3393
+ return (ctx) => {
3394
+ const file = ctx.args.find(a => !a.startsWith('-'));
3395
+ if (!file) {
3396
+ ctx.stderr.write('Usage: xxd FILE\n');
3397
+ return 1;
3398
+ }
3399
+ const fp = resolvePath(ctx.cwd, file);
3400
+ try {
3401
+ const data = vfs.readFile(fp);
3402
+ const len = Math.min(data.length, ctx.args.includes('-l') ? parseInt(ctx.args[ctx.args.indexOf('-l') + 1]) || 256 : 256);
3403
+ for (let i = 0; i < len; i += 16) {
3404
+ const hex = Array.from(data.slice(i, i + 16)).map(b => b.toString(16).padStart(2, '0')).join(' ');
3405
+ const ascii = Array.from(data.slice(i, i + 16)).map(b => b >= 32 && b < 127 ? String.fromCharCode(b) : '.').join('');
3406
+ ctx.stdout.write(`${i.toString(16).padStart(8, '0')}: ${hex.padEnd(48)} ${ascii}\n`);
3407
+ }
3408
+ return 0;
3409
+ }
3410
+ catch {
3411
+ ctx.stderr.write(`xxd: ${file}: No such file\n`);
3412
+ return 1;
3413
+ }
3414
+ };
3415
+ }
3416
+ // ── Registration ────────────────────────────────────────────────────────
3417
+ /**
3418
+ * Wrap a sync/async command so it always returns Promise<number>.
3419
+ * The LIFO shell calls .then() on the return value of every command,
3420
+ * so raw numbers cause "E3(...).then is not a function".
3421
+ *
3422
+ * Also resolves ctx.stdin from a stream object to a string.
3423
+ * The shell passes stdin as an object with .readAll() when piping,
3424
+ * but our commands expect a plain string.
3425
+ */
3426
+ /**
3427
+ * SHELL-R6-B2 follow-on: wrapStreaming for commands that handle pipe
3428
+ * readers directly (head, tail, etc — commands that can terminate
3429
+ * before the producer drains).
3430
+ *
3431
+ * Behavior:
3432
+ * - If ctx.stdin is a terminal stdin (has .feed), drain buffered
3433
+ * bytes to a string (same as wrap — terminal stdin's
3434
+ * buffer-then-close pattern doesn't match streaming).
3435
+ * - If ctx.stdin is a pipe reader (has .read), PASS IT THROUGH
3436
+ * unchanged so the command can read line-by-line. The command
3437
+ * is responsible for terminating itself (e.g. head -n N stops
3438
+ * after N lines, triggering the pipeline-abort cascade from
3439
+ * SHELL-R6-2).
3440
+ * - String stdin / other shapes: same as wrap.
3441
+ */
3442
+ function wrapStreaming(fn) {
3443
+ return async (ctx) => {
3444
+ try {
3445
+ if (ctx.stdin && typeof ctx.stdin !== 'string') {
3446
+ const stdinObj = ctx.stdin;
3447
+ const isTerminalStdin = typeof stdinObj.feed === 'function';
3448
+ if (isTerminalStdin) {
3449
+ const buf = Array.isArray(stdinObj.buffer)
3450
+ ? stdinObj.buffer.splice(0)
3451
+ : [];
3452
+ ctx.stdin = buf.join('');
3453
+ }
3454
+ // else: leave as pipe reader for the command to handle.
3455
+ }
3456
+ const result = fn(ctx);
3457
+ return await result;
3458
+ }
3459
+ catch (e) {
3460
+ ctx.stderr.write(`${e?.message || e}\n`);
3461
+ return 1;
3462
+ }
3463
+ };
3464
+ }
3465
+ function wrap(fn) {
3466
+ return async (ctx) => {
3467
+ try {
3468
+ // Resolve stdin: shell passes a stream object with .readAll() when piping.
3469
+ //
3470
+ // BUG-SWEEP fix (2026-05-11): lifo-sh ≥0.5.5 passes the shell's
3471
+ // `terminalStdin` (an Ls instance) as ctx.stdin for EVERY command,
3472
+ // not just piped ones. Ls.readAll() loops until close(), which the
3473
+ // shell only triggers in its executeLine() finally — AFTER the
3474
+ // command returns. Pre-fix, our wrap awaited readAll() and
3475
+ // deadlocked: command waits for stdin EOF, shell waits for command.
3476
+ //
3477
+ // The fix is to distinguish the two stream shapes:
3478
+ // - Pipe reader (Oi.reader): {read, readAll} only. Used when
3479
+ // upstream `echo X |` feeds bytes; upstream calls close()
3480
+ // after writing, so readAll() resolves quickly.
3481
+ // - Terminal stdin (Ls): {feed, close, rawMode, read, readAll,
3482
+ // isWaiting, ...}. close() runs ONLY after the command returns.
3483
+ //
3484
+ // We treat anything with a `feed` method (Ls signature) as the
3485
+ // terminal stdin and drain its already-buffered bytes synchronously
3486
+ // without awaiting EOF. Pipe readers (no `feed`) await readAll().
3487
+ if (ctx.stdin && typeof ctx.stdin !== 'string') {
3488
+ const stdinObj = ctx.stdin;
3489
+ const isTerminalStdin = typeof stdinObj.feed === 'function';
3490
+ if (isTerminalStdin) {
3491
+ // Drain any already-queued bytes (typically empty for the
3492
+ // first command on a line; non-empty if the user typed
3493
+ // text + Enter before the command was dispatched). DO NOT
3494
+ // await — that would wait for the user's next Ctrl-D.
3495
+ const buf = Array.isArray(stdinObj.buffer)
3496
+ ? stdinObj.buffer.splice(0)
3497
+ : [];
3498
+ ctx.stdin = buf.join('');
3499
+ }
3500
+ else if (typeof stdinObj.readAll === 'function') {
3501
+ // Pipe reader — upstream will close() after writing, so
3502
+ // readAll() resolves bounded.
3503
+ ctx.stdin = await stdinObj.readAll();
3504
+ }
3505
+ else if (typeof stdinObj.toString === 'function') {
3506
+ ctx.stdin = stdinObj.toString();
3507
+ }
3508
+ }
3509
+ const result = fn(ctx);
3510
+ return await result;
3511
+ }
3512
+ catch (e) {
3513
+ ctx.stderr.write(`${e?.message || e}\n`);
3514
+ return 1;
3515
+ }
3516
+ };
3517
+ }
3518
+ export function registerUnixCommands(registry, vfs) {
3519
+ registry.register('which', wrap(mkWhich(vfs, registry)));
3520
+ registry.register('whereis', wrap(mkWhereis(vfs)));
3521
+ registry.register('command', wrap(mkCommand(vfs, registry)));
3522
+ registry.register('type', wrap(mkType(vfs, registry)));
3523
+ registry.register('env', wrap(mkEnv()));
3524
+ registry.register('export', wrap(mkExport()));
3525
+ registry.register('unset', wrap(mkUnset()));
3526
+ registry.register('clear', wrap(mkClear()));
3527
+ registry.register('date', wrap(mkDate()));
3528
+ registry.register('uptime', wrap(mkUptime()));
3529
+ registry.register('tree', wrap(mkTree(vfs)));
3530
+ registry.register('find', wrap(mkFind(vfs)));
3531
+ registry.register('grep', wrap(mkGrep(vfs)));
3532
+ // SHELL-R6-B2: head uses streaming wrap so a pipe reader passes
3533
+ // through (head terminates after N lines, triggering the abort
3534
+ // cascade for upstream producers like `yes`).
3535
+ registry.register('head', wrapStreaming(mkHead(vfs)));
3536
+ registry.register('tail', wrap(mkTail(vfs)));
3537
+ registry.register('wc', wrap(mkWc(vfs)));
3538
+ registry.register('sort', wrap(mkSort(vfs)));
3539
+ registry.register('uniq', wrap(mkUniq()));
3540
+ registry.register('sed', wrap(mkSed(vfs)));
3541
+ registry.register('awk', wrap(mkAwk(vfs)));
3542
+ registry.register('xargs', wrap(mkXargs(vfs, registry)));
3543
+ registry.register('tee', wrap(mkTee(vfs)));
3544
+ registry.register('du', wrap(mkDu(vfs)));
3545
+ registry.register('diff', wrap(mkDiff(vfs)));
3546
+ // Registry-level echo + cat for xargs cross-command dispatch.
3547
+ // Shell.builtins still wins for direct `echo X` invocations; this
3548
+ // entry is only reached when a command (xargs etc.) looks them up
3549
+ // via the registry path.
3550
+ registry.register('echo', wrap(mkEcho()));
3551
+ registry.register('cat', wrap(mkCat(vfs)));
3552
+ registry.register('ls', wrap(mkLs(vfs)));
3553
+ registry.register('rm', wrap(mkRm(vfs)));
3554
+ registry.register('touch', wrap(mkTouch(vfs)));
3555
+ registry.register('stat', wrap(mkStat(vfs)));
3556
+ registry.register('base64', wrap(mkBase64(vfs)));
3557
+ registry.register('seq', wrap(mkSeq()));
3558
+ registry.register('sleep', wrap(mkSleep()));
3559
+ registry.register('id', wrap(mkId()));
3560
+ registry.register('hostname', wrap(mkHostname()));
3561
+ registry.register('basename', wrap(mkBasename()));
3562
+ registry.register('dirname', wrap(mkDirname()));
3563
+ registry.register('realpath', wrap(mkRealpath(vfs)));
3564
+ registry.register('printf', wrap(mkPrintf()));
3565
+ registry.register('true', wrap(mkTrue()));
3566
+ registry.register('false', wrap(mkFalse()));
3567
+ registry.register('readlink', wrap(mkReadlink(vfs)));
3568
+ registry.register('sha256sum', wrap(mkSha256sum(vfs)));
3569
+ registry.register('file', wrap(mkFile(vfs)));
3570
+ registry.register('xxd', wrap(mkXxd(vfs)));
3571
+ // chmod/chown — no-ops (succeed silently, many npm scripts call these)
3572
+ registry.register('chmod', wrap(() => 0));
3573
+ registry.register('chown', wrap(() => 0));
3574
+ // ln — symlink stub (no-ops on VFS but doesn't error)
3575
+ /**
3576
+ * SHELL-FOLLOWUPS-4 (2026-05-11): real `ln -s` via SymlinkRegistry.
3577
+ * Pre-fix: ln -s did file-copy; modifications to the "link"
3578
+ * created a new file (no two-way reflection); readlink returned
3579
+ * empty.
3580
+ *
3581
+ * Now:
3582
+ * - `ln -s TARGET LINKPATH` registers LINKPATH → TARGET in the
3583
+ * symlink registry.
3584
+ * - Real GNU `ln -s` doesn't require TARGET to exist (dangling
3585
+ * symlinks are valid). We allow that.
3586
+ * - Hard links (`ln` without -s) still do file-copy — Nimbus
3587
+ * VFS doesn't expose inode-level hard-linking and that's
3588
+ * documented out of scope.
3589
+ *
3590
+ * Subsequent operations (cat / ls / cat-via-redirect) need to
3591
+ * consult the registry to see through the symlink. That work
3592
+ * lives in mkCat / mkLs wrappers and the SqliteVFS read-path —
3593
+ * for v1, we wire it in the COMMANDS that need it (cat already
3594
+ * goes through Kernel.VFS which doesn't yet know about the
3595
+ * registry; we patch cat directly here to dereference symlinks).
3596
+ */
3597
+ registry.register('ln', wrap((ctx) => {
3598
+ const args = ctx.args;
3599
+ const symbolic = args.some(a => a === '-s' || (a.startsWith('-') && !a.startsWith('--') && a.includes('s')));
3600
+ const force = args.some(a => a === '-f' || (a.startsWith('-') && !a.startsWith('--') && a.includes('f')));
3601
+ const positional = args.filter(a => !a.startsWith('-'));
3602
+ if (positional.length < 2) {
3603
+ ctx.stderr.write('ln: missing operand\n');
3604
+ return 1;
3605
+ }
3606
+ const target = positional[0]; // what the link points TO
3607
+ const linkPath = positional[1]; // the link file itself
3608
+ const linkFp = resolvePath(ctx.cwd, linkPath);
3609
+ if (symbolic) {
3610
+ // Symbolic link via registry. Don't require target to exist.
3611
+ const reg = getSymlinkRegistry(vfs);
3612
+ // GNU `ln` without -f errors if link exists.
3613
+ if (!force && (vfs.exists(linkFp) || reg.isSymlink(linkFp))) {
3614
+ ctx.stderr.write(`ln: failed to create symbolic link '${linkPath}': File exists\n`);
3615
+ return 1;
3616
+ }
3617
+ // Remove existing real-file at linkPath if -f and not a symlink.
3618
+ if (force && vfs.exists(linkFp) && !reg.isSymlink(linkFp)) {
3619
+ try {
3620
+ vfs.unlink(linkFp);
3621
+ }
3622
+ catch { /* fail-soft */ }
3623
+ }
3624
+ reg.set(linkFp, target);
3625
+ return 0;
3626
+ }
3627
+ // Hard link mode (default): file-copy semantics (legacy behaviour).
3628
+ const srcFp = resolvePath(ctx.cwd, target);
3629
+ try {
3630
+ const content = vfs.readFileString(srcFp);
3631
+ vfs.writeFile(linkFp, content);
3632
+ }
3633
+ catch (e) {
3634
+ ctx.stderr.write(`ln: ${target}: ${e?.message || e}\n`);
3635
+ return 1;
3636
+ }
3637
+ return 0;
3638
+ }));
3639
+ // test / [ — basic test command for shell scripts
3640
+ registry.register('test', wrap((ctx) => {
3641
+ const args = ctx.args.filter((a) => a !== ']');
3642
+ if (args.length === 0)
3643
+ return 1;
3644
+ if (args[0] === '-f')
3645
+ return vfs.exists(resolvePath(ctx.cwd, args[1] || '')) && !vfs.isDirectory(resolvePath(ctx.cwd, args[1] || '')) ? 0 : 1;
3646
+ if (args[0] === '-d')
3647
+ return vfs.exists(resolvePath(ctx.cwd, args[1] || '')) && vfs.isDirectory(resolvePath(ctx.cwd, args[1] || '')) ? 0 : 1;
3648
+ if (args[0] === '-e')
3649
+ return vfs.exists(resolvePath(ctx.cwd, args[1] || '')) ? 0 : 1;
3650
+ if (args[0] === '-z')
3651
+ return (!args[1] || args[1] === '') ? 0 : 1;
3652
+ if (args[0] === '-n')
3653
+ return (args[1] && args[1] !== '') ? 0 : 1;
3654
+ if (args[1] === '=')
3655
+ return args[0] === args[2] ? 0 : 1;
3656
+ if (args[1] === '!=')
3657
+ return args[0] !== args[2] ? 0 : 1;
3658
+ return args[0] ? 0 : 1;
3659
+ }));
3660
+ registry.register('[', wrap((ctx) => {
3661
+ // [ is alias for test with mandatory closing ]
3662
+ const args = ctx.args.filter((a) => a !== ']');
3663
+ return vfs.exists(resolvePath(ctx.cwd, args[1] || '')) ? 0 : 1;
3664
+ }));
3665
+ // read — read a line (stub, returns empty for non-interactive)
3666
+ // shell-polish (2026-05-12): `read VAR` is registered here as a
3667
+ // NO-OP fallback (matches the pre-existing stub behaviour). The
3668
+ // REAL working implementation lives in src/session/init.ts as a
3669
+ // lifo-sh shell builtin (shellAny.builtins.set('read', ...)).
3670
+ //
3671
+ // Why two registrations: lifo-sh's interpreter executes registered
3672
+ // shell commands with `ctx.env = { ...this.config.env }` — a SHALLOW
3673
+ // COPY (see node_modules/@lifo-sh/core/dist/index-Djm2onjx.js:5197).
3674
+ // Mutating ctx.env inside a registered command therefore CANNOT
3675
+ // propagate the var-assignment back to the shell. Builtins, by
3676
+ // contrast, run inside the interp instance with direct access to
3677
+ // `this.env` (the real shell env). The wait builtin in shell-r6 uses
3678
+ // the same workaround.
3679
+ //
3680
+ // Keep the registry stub so `type read` reports "shell builtin" and
3681
+ // `which read` doesn't error; the builtin always wins dispatch
3682
+ // (interp.executeSimpleCommand checks builtins.get BEFORE
3683
+ // registry.resolve — index-Djm2onjx.js:5182-5186).
3684
+ registry.register('read', wrap((ctx) => {
3685
+ const args = ctx.args.filter((a) => !a.startsWith('-'));
3686
+ const varName = args[0] || 'REPLY';
3687
+ ctx.env[varName] = '';
3688
+ return 0;
3689
+ }));
3690
+ // exit — exit with code
3691
+ registry.register('exit', wrap((ctx) => {
3692
+ return parseInt(ctx.args[0] || '0') || 0;
3693
+ }));
3694
+ // source / . — source a file (stub)
3695
+ registry.register('source', wrap(() => 0));
3696
+ registry.register('.', wrap(() => 0));
3697
+ // noop commands that scripts might call
3698
+ registry.register('set', wrap(() => 0));
3699
+ registry.register('shopt', wrap(() => 0));
3700
+ registry.register('trap', wrap(() => 0));
3701
+ registry.register('umask', wrap(() => 0));
3702
+ registry.register('ulimit', wrap(() => 0));
3703
+ }