@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.
- package/README.md +219 -0
- package/dist/_shared/bytes.d.ts +18 -0
- package/dist/_shared/bytes.d.ts.map +1 -0
- package/dist/_shared/bytes.js +17 -0
- package/dist/_shared/cache-stats.d.ts +146 -0
- package/dist/_shared/cache-stats.d.ts.map +1 -0
- package/dist/_shared/cache-stats.js +162 -0
- package/dist/_shared/exports-resolver.d.ts +76 -0
- package/dist/_shared/exports-resolver.d.ts.map +1 -0
- package/dist/_shared/exports-resolver.js +284 -0
- package/dist/_shared/install-phase.d.ts +47 -0
- package/dist/_shared/install-phase.d.ts.map +1 -0
- package/dist/_shared/install-phase.js +1 -0
- package/dist/_shared/real-node-imports.d.ts +38 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -0
- package/dist/_shared/real-node-imports.js +46 -0
- package/dist/_shared/retry.d.ts +93 -0
- package/dist/_shared/retry.d.ts.map +1 -0
- package/dist/_shared/retry.js +161 -0
- package/dist/_shared/session-id.d.ts +39 -0
- package/dist/_shared/session-id.d.ts.map +1 -0
- package/dist/_shared/session-id.js +127 -0
- package/dist/_shared/session-router.d.ts +104 -0
- package/dist/_shared/session-router.d.ts.map +1 -0
- package/dist/_shared/session-router.js +165 -0
- package/dist/_shared/w7-frame.d.ts +93 -0
- package/dist/_shared/w7-frame.d.ts.map +1 -0
- package/dist/_shared/w7-frame.js +402 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/middleware.d.ts +85 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +150 -0
- package/dist/auth/token.d.ts +96 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +226 -0
- package/dist/auth/types.d.ts +141 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +114 -0
- package/dist/bindings/d1.d.ts +102 -0
- package/dist/bindings/d1.d.ts.map +1 -0
- package/dist/bindings/d1.js +549 -0
- package/dist/bindings/kv.d.ts +82 -0
- package/dist/bindings/kv.d.ts.map +1 -0
- package/dist/bindings/kv.js +290 -0
- package/dist/bindings/r2.d.ts +137 -0
- package/dist/bindings/r2.d.ts.map +1 -0
- package/dist/bindings/r2.js +379 -0
- package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
- package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
- package/dist/cirrus-npm-cjs.generated.js +41 -0
- package/dist/cirrus-plugin-react.generated.d.ts +34 -0
- package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
- package/dist/cirrus-plugin-react.generated.js +33 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +116 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
- package/dist/esbuild-wasm-bundle.generated.js +40 -0
- package/dist/facets/cirrus-real.d.ts +147 -0
- package/dist/facets/cirrus-real.d.ts.map +1 -0
- package/dist/facets/cirrus-real.js +981 -0
- package/dist/facets/inner-do-registry.d.ts +41 -0
- package/dist/facets/inner-do-registry.d.ts.map +1 -0
- package/dist/facets/inner-do-registry.js +51 -0
- package/dist/facets/manager.d.ts +314 -0
- package/dist/facets/manager.d.ts.map +1 -0
- package/dist/facets/manager.js +2681 -0
- package/dist/facets/process.d.ts +318 -0
- package/dist/facets/process.d.ts.map +1 -0
- package/dist/facets/process.js +567 -0
- package/dist/facets/real-vite-fs-shim.d.ts +151 -0
- package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
- package/dist/facets/real-vite-fs-shim.js +1279 -0
- package/dist/facets/real-vite-hmr.d.ts +142 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -0
- package/dist/facets/real-vite-hmr.js +476 -0
- package/dist/facets/vite-dev-server.d.ts +293 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -0
- package/dist/facets/vite-dev-server.js +2580 -0
- package/dist/facets/wasm-swap-registry.d.ts +261 -0
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
- package/dist/facets/wasm-swap-registry.js +551 -0
- package/dist/facets/ws-terminal.d.ts +107 -0
- package/dist/facets/ws-terminal.d.ts.map +1 -0
- package/dist/facets/ws-terminal.js +207 -0
- package/dist/git/commands.d.ts +13 -0
- package/dist/git/commands.d.ts.map +1 -0
- package/dist/git/commands.js +633 -0
- package/dist/git/network-facet.d.ts +61 -0
- package/dist/git/network-facet.d.ts.map +1 -0
- package/dist/git/network-facet.js +700 -0
- package/dist/git-bundle.generated.d.ts +16 -0
- package/dist/git-bundle.generated.d.ts.map +1 -0
- package/dist/git-bundle.generated.js +15 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-facet.js +103 -0
- package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-pool.js +161 -0
- package/dist/loaders/fanout-pool.d.ts +195 -0
- package/dist/loaders/fanout-pool.d.ts.map +1 -0
- package/dist/loaders/fanout-pool.js +326 -0
- package/dist/loaders/generated-workers.d.ts +26 -0
- package/dist/loaders/generated-workers.d.ts.map +1 -0
- package/dist/loaders/generated-workers.js +25 -0
- package/dist/loaders/index.d.ts +15 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +13 -0
- package/dist/loaders/loader-pool.d.ts +273 -0
- package/dist/loaders/loader-pool.d.ts.map +1 -0
- package/dist/loaders/loader-pool.js +624 -0
- package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
- package/dist/loaders/npm-resolve-preamble.js +237 -0
- package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
- package/dist/loaders/pre-bundle-preamble.js +114 -0
- package/dist/loaders/vendor/codegen.d.ts +24 -0
- package/dist/loaders/vendor/codegen.d.ts.map +1 -0
- package/dist/loaders/vendor/codegen.js +44 -0
- package/dist/loaders/vendor/errors.d.ts +24 -0
- package/dist/loaders/vendor/errors.d.ts.map +1 -0
- package/dist/loaders/vendor/errors.js +46 -0
- package/dist/loaders/vendor/index.d.ts +16 -0
- package/dist/loaders/vendor/index.d.ts.map +1 -0
- package/dist/loaders/vendor/index.js +11 -0
- package/dist/loaders/vendor/pool.d.ts +88 -0
- package/dist/loaders/vendor/pool.d.ts.map +1 -0
- package/dist/loaders/vendor/pool.js +353 -0
- package/dist/loaders/vendor/primitives.d.ts +11 -0
- package/dist/loaders/vendor/primitives.d.ts.map +1 -0
- package/dist/loaders/vendor/primitives.js +33 -0
- package/dist/loaders/vendor/serialize.d.ts +3 -0
- package/dist/loaders/vendor/serialize.d.ts.map +1 -0
- package/dist/loaders/vendor/serialize.js +25 -0
- package/dist/loaders/vendor/types.d.ts +60 -0
- package/dist/loaders/vendor/types.d.ts.map +1 -0
- package/dist/loaders/vendor/types.js +4 -0
- package/dist/npm/cache.d.ts +136 -0
- package/dist/npm/cache.d.ts.map +1 -0
- package/dist/npm/cache.js +429 -0
- package/dist/npm/install-batch-facet.d.ts +129 -0
- package/dist/npm/install-batch-facet.d.ts.map +1 -0
- package/dist/npm/install-batch-facet.js +598 -0
- package/dist/npm/install-facet.d.ts +29 -0
- package/dist/npm/install-facet.d.ts.map +1 -0
- package/dist/npm/install-facet.js +9 -0
- package/dist/npm/installer.d.ts +239 -0
- package/dist/npm/installer.d.ts.map +1 -0
- package/dist/npm/installer.js +2022 -0
- package/dist/npm/npx-install.d.ts +74 -0
- package/dist/npm/npx-install.d.ts.map +1 -0
- package/dist/npm/npx-install.js +302 -0
- package/dist/npm/pre-bundle-facet.d.ts +168 -0
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
- package/dist/npm/pre-bundle-facet.js +653 -0
- package/dist/npm/r2-cache.d.ts +286 -0
- package/dist/npm/r2-cache.d.ts.map +1 -0
- package/dist/npm/r2-cache.js +506 -0
- package/dist/npm/resolve-facet.d.ts +202 -0
- package/dist/npm/resolve-facet.d.ts.map +1 -0
- package/dist/npm/resolve-facet.js +833 -0
- package/dist/npm/resolve-one-facet.d.ts +204 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -0
- package/dist/npm/resolve-one-facet.js +437 -0
- package/dist/npm/resolver.d.ts +109 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/resolver.js +899 -0
- package/dist/npm/tarball-stream.d.ts +85 -0
- package/dist/npm/tarball-stream.d.ts.map +1 -0
- package/dist/npm/tarball-stream.js +205 -0
- package/dist/npm/tarball.d.ts +51 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/npm/tarball.js +185 -0
- package/dist/observability/diag-counters.d.ts +292 -0
- package/dist/observability/diag-counters.d.ts.map +1 -0
- package/dist/observability/diag-counters.js +302 -0
- package/dist/observability/heap-estimate.d.ts +166 -0
- package/dist/observability/heap-estimate.d.ts.map +1 -0
- package/dist/observability/heap-estimate.js +192 -0
- package/dist/observability/heavy-alloc-coord.d.ts +95 -0
- package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
- package/dist/observability/heavy-alloc-coord.js +149 -0
- package/dist/observability/oom-classify.d.ts +56 -0
- package/dist/observability/oom-classify.d.ts.map +1 -0
- package/dist/observability/oom-classify.js +118 -0
- package/dist/observability/oom-discriminator.d.ts +192 -0
- package/dist/observability/oom-discriminator.d.ts.map +1 -0
- package/dist/observability/oom-discriminator.js +233 -0
- package/dist/real-vite-bundle.generated.d.ts +46 -0
- package/dist/real-vite-bundle.generated.d.ts.map +1 -0
- package/dist/real-vite-bundle.generated.js +53 -0
- package/dist/replica/routing.d.ts +171 -0
- package/dist/replica/routing.d.ts.map +1 -0
- package/dist/replica/routing.js +288 -0
- package/dist/replica/suspension.d.ts +36 -0
- package/dist/replica/suspension.d.ts.map +1 -0
- package/dist/replica/suspension.js +49 -0
- package/dist/router/index.d.ts +156 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +220 -0
- package/dist/runtime/assets-loader.d.ts +71 -0
- package/dist/runtime/assets-loader.d.ts.map +1 -0
- package/dist/runtime/assets-loader.js +128 -0
- package/dist/runtime/barrel-detect.d.ts +46 -0
- package/dist/runtime/barrel-detect.d.ts.map +1 -0
- package/dist/runtime/barrel-detect.js +73 -0
- package/dist/runtime/barrel-synthesizer.d.ts +181 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
- package/dist/runtime/barrel-synthesizer.js +541 -0
- package/dist/runtime/bun-repl.d.ts +46 -0
- package/dist/runtime/bun-repl.d.ts.map +1 -0
- package/dist/runtime/bun-repl.js +326 -0
- package/dist/runtime/bun-runner.d.ts +66 -0
- package/dist/runtime/bun-runner.d.ts.map +1 -0
- package/dist/runtime/bun-runner.js +219 -0
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +1033 -0
- package/dist/runtime/comment-strip.d.ts +37 -0
- package/dist/runtime/comment-strip.d.ts.map +1 -0
- package/dist/runtime/comment-strip.js +74 -0
- package/dist/runtime/esbuild-service.d.ts +201 -0
- package/dist/runtime/esbuild-service.d.ts.map +1 -0
- package/dist/runtime/esbuild-service.js +1444 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
- package/dist/runtime/esbuild-wasm-bytes.js +131 -0
- package/dist/runtime/framework-detect.d.ts +71 -0
- package/dist/runtime/framework-detect.d.ts.map +1 -0
- package/dist/runtime/framework-detect.js +178 -0
- package/dist/runtime/long-running-handle.d.ts +133 -0
- package/dist/runtime/long-running-handle.d.ts.map +1 -0
- package/dist/runtime/long-running-handle.js +192 -0
- package/dist/runtime/node-repl.d.ts +36 -0
- package/dist/runtime/node-repl.d.ts.map +1 -0
- package/dist/runtime/node-repl.js +293 -0
- package/dist/runtime/node-runner.d.ts +111 -0
- package/dist/runtime/node-runner.d.ts.map +1 -0
- package/dist/runtime/node-runner.js +194 -0
- package/dist/runtime/node-shims.d.ts +24 -0
- package/dist/runtime/node-shims.d.ts.map +1 -0
- package/dist/runtime/node-shims.js +3019 -0
- package/dist/runtime/package-manager.d.ts +118 -0
- package/dist/runtime/package-manager.d.ts.map +1 -0
- package/dist/runtime/package-manager.js +420 -0
- package/dist/runtime/port-registry.d.ts +80 -0
- package/dist/runtime/port-registry.d.ts.map +1 -0
- package/dist/runtime/port-registry.js +166 -0
- package/dist/runtime/process-logs-api.d.ts +102 -0
- package/dist/runtime/process-logs-api.d.ts.map +1 -0
- package/dist/runtime/process-logs-api.js +253 -0
- package/dist/runtime/process-logs.d.ts +311 -0
- package/dist/runtime/process-logs.d.ts.map +1 -0
- package/dist/runtime/process-logs.js +656 -0
- package/dist/runtime/process-table.d.ts +65 -0
- package/dist/runtime/process-table.d.ts.map +1 -0
- package/dist/runtime/process-table.js +107 -0
- package/dist/runtime/project-detect.d.ts +21 -0
- package/dist/runtime/project-detect.d.ts.map +1 -0
- package/dist/runtime/project-detect.js +46 -0
- package/dist/runtime/python-repl.d.ts +66 -0
- package/dist/runtime/python-repl.d.ts.map +1 -0
- package/dist/runtime/python-repl.js +563 -0
- package/dist/runtime/python-runner.d.ts +50 -0
- package/dist/runtime/python-runner.d.ts.map +1 -0
- package/dist/runtime/python-runner.js +870 -0
- package/dist/runtime/repl-session.d.ts +160 -0
- package/dist/runtime/repl-session.d.ts.map +1 -0
- package/dist/runtime/repl-session.js +393 -0
- package/dist/runtime/require-resolver.d.ts +52 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -0
- package/dist/runtime/require-resolver.js +690 -0
- package/dist/runtime/router-basename.d.ts +36 -0
- package/dist/runtime/router-basename.d.ts.map +1 -0
- package/dist/runtime/router-basename.js +310 -0
- package/dist/runtime/ruby-repl.d.ts +43 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -0
- package/dist/runtime/ruby-repl.js +229 -0
- package/dist/runtime/ruby-runner.d.ts +62 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +737 -0
- package/dist/runtime/runtime-catalog.d.ts +99 -0
- package/dist/runtime/runtime-catalog.d.ts.map +1 -0
- package/dist/runtime/runtime-catalog.js +183 -0
- package/dist/runtime/runtime-registry.d.ts +136 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +318 -0
- package/dist/runtime/streams.d.ts +13 -0
- package/dist/runtime/streams.d.ts.map +1 -0
- package/dist/runtime/streams.js +402 -0
- package/dist/runtime/wasi-instance.d.ts +190 -0
- package/dist/runtime/wasi-instance.d.ts.map +1 -0
- package/dist/runtime/wasi-instance.js +2044 -0
- package/dist/runtime/wasm-runner.d.ts +117 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +705 -0
- package/dist/session/bindings.d.ts +168 -0
- package/dist/session/bindings.d.ts.map +1 -0
- package/dist/session/bindings.js +524 -0
- package/dist/session/ctx-exports.d.ts +16 -0
- package/dist/session/ctx-exports.d.ts.map +1 -0
- package/dist/session/ctx-exports.js +22 -0
- package/dist/session/diag.d.ts +62 -0
- package/dist/session/diag.d.ts.map +1 -0
- package/dist/session/diag.js +118 -0
- package/dist/session/fs-watch.d.ts +132 -0
- package/dist/session/fs-watch.d.ts.map +1 -0
- package/dist/session/fs-watch.js +255 -0
- package/dist/session/helpers.d.ts +150 -0
- package/dist/session/helpers.d.ts.map +1 -0
- package/dist/session/helpers.js +472 -0
- package/dist/session/hibernation.d.ts +145 -0
- package/dist/session/hibernation.d.ts.map +1 -0
- package/dist/session/hibernation.js +385 -0
- package/dist/session/init-phases.d.ts +87 -0
- package/dist/session/init-phases.d.ts.map +1 -0
- package/dist/session/init-phases.js +137 -0
- package/dist/session/init.d.ts +46 -0
- package/dist/session/init.d.ts.map +1 -0
- package/dist/session/init.js +3579 -0
- package/dist/session/keys.d.ts +56 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +55 -0
- package/dist/session/nimbus-session.d.ts +434 -0
- package/dist/session/nimbus-session.d.ts.map +1 -0
- package/dist/session/nimbus-session.js +1084 -0
- package/dist/session/programmatic.d.ts +105 -0
- package/dist/session/programmatic.d.ts.map +1 -0
- package/dist/session/programmatic.js +326 -0
- package/dist/session/replica-routes.d.ts +68 -0
- package/dist/session/replica-routes.d.ts.map +1 -0
- package/dist/session/replica-routes.js +77 -0
- package/dist/session/routes.d.ts +29 -0
- package/dist/session/routes.d.ts.map +1 -0
- package/dist/session/routes.js +1331 -0
- package/dist/session/rpc.d.ts +323 -0
- package/dist/session/rpc.d.ts.map +1 -0
- package/dist/session/rpc.js +808 -0
- package/dist/session/state-store.d.ts +210 -0
- package/dist/session/state-store.d.ts.map +1 -0
- package/dist/session/state-store.js +459 -0
- package/dist/session/supervisor-rpc.d.ts +269 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -0
- package/dist/session/supervisor-rpc.js +437 -0
- package/dist/session/ws-hibernation-config.d.ts +64 -0
- package/dist/session/ws-hibernation-config.d.ts.map +1 -0
- package/dist/session/ws-hibernation-config.js +89 -0
- package/dist/session/ws.d.ts +95 -0
- package/dist/session/ws.d.ts.map +1 -0
- package/dist/session/ws.js +395 -0
- package/dist/shell/features.d.ts +410 -0
- package/dist/shell/features.d.ts.map +1 -0
- package/dist/shell/features.js +1885 -0
- package/dist/shell/unix-commands.d.ts +15 -0
- package/dist/shell/unix-commands.d.ts.map +1 -0
- package/dist/shell/unix-commands.js +3703 -0
- package/dist/stubs/lifo-ui.d.ts +10 -0
- package/dist/stubs/lifo-ui.d.ts.map +1 -0
- package/dist/stubs/lifo-ui.js +1 -0
- package/dist/tailwind-play.generated.d.ts +19 -0
- package/dist/tailwind-play.generated.d.ts.map +1 -0
- package/dist/tailwind-play.generated.js +18 -0
- package/dist/vfs/dev-provider.d.ts +64 -0
- package/dist/vfs/dev-provider.d.ts.map +1 -0
- package/dist/vfs/dev-provider.js +191 -0
- package/dist/vfs/events.d.ts +51 -0
- package/dist/vfs/events.d.ts.map +1 -0
- package/dist/vfs/events.js +127 -0
- package/dist/vfs/path.d.ts +30 -0
- package/dist/vfs/path.d.ts.map +1 -0
- package/dist/vfs/path.js +41 -0
- package/dist/vfs/seed-project.d.ts +62 -0
- package/dist/vfs/seed-project.d.ts.map +1 -0
- package/dist/vfs/seed-project.js +1045 -0
- package/dist/vfs/sqlite-vfs.d.ts +435 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
- package/dist/vfs/sqlite-vfs.js +1670 -0
- package/dist/vfs/symlink-registry.d.ts +64 -0
- package/dist/vfs/symlink-registry.d.ts.map +1 -0
- package/dist/vfs/symlink-registry.js +137 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
- package/dist/wrangler/nimbus-wrangler.js +931 -0
- package/package.json +77 -0
- package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
- package/public/_assets/esbuild-0.24.2.wasm +0 -0
- package/public/_assets/real-vite-bundle.js +101478 -0
- package/public/_assets/rollup.wasm +0 -0
- package/public/favicon.svg +8 -0
- package/public/index.html +565 -0
- package/public/s/index.html +2379 -0
- package/scripts/bundle-esbuild-wasm.mjs +244 -0
- package/scripts/bundle-facet-workers.mjs +137 -0
- package/scripts/bundle-git.mjs +118 -0
- package/scripts/bundle-npm-cjs.mjs +363 -0
- package/scripts/bundle-plugin-react.mjs +480 -0
- package/scripts/bundle-real-vite.mjs +959 -0
- package/scripts/bundle-runtime.mjs +600 -0
- package/scripts/bundle-tailwind-play.mjs +130 -0
- 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
|
+
}
|