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