@seanhogg/builderforce-webcontainers-core 2026.10.2 → 2026.10.3
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/build.d.ts +42 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +65 -0
- package/dist/build.js.map +1 -0
- package/dist/buildHost.d.ts +31 -0
- package/dist/buildHost.d.ts.map +1 -0
- package/dist/buildHost.js +125 -0
- package/dist/buildHost.js.map +1 -0
- package/dist/buildHtml.d.ts +28 -0
- package/dist/buildHtml.d.ts.map +1 -0
- package/dist/buildHtml.js +74 -0
- package/dist/buildHtml.js.map +1 -0
- package/dist/bundler.d.ts +129 -0
- package/dist/bundler.d.ts.map +1 -0
- package/dist/bundler.js +79 -0
- package/dist/bundler.js.map +1 -0
- package/dist/check/acquire.d.ts +46 -0
- package/dist/check/acquire.d.ts.map +1 -0
- package/dist/check/acquire.js +179 -0
- package/dist/check/acquire.js.map +1 -0
- package/dist/check/compilerHost.d.ts +22 -0
- package/dist/check/compilerHost.d.ts.map +1 -0
- package/dist/check/compilerHost.js +128 -0
- package/dist/check/compilerHost.js.map +1 -0
- package/dist/check/diagnostics.d.ts +23 -0
- package/dist/check/diagnostics.d.ts.map +1 -0
- package/dist/check/diagnostics.js +28 -0
- package/dist/check/diagnostics.js.map +1 -0
- package/dist/check/index.d.ts +19 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +13 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/projects.d.ts +26 -0
- package/dist/check/projects.d.ts.map +1 -0
- package/dist/check/projects.js +124 -0
- package/dist/check/projects.js.map +1 -0
- package/dist/check/shims.d.ts +29 -0
- package/dist/check/shims.d.ts.map +1 -0
- package/dist/check/shims.js +76 -0
- package/dist/check/shims.js.map +1 -0
- package/dist/check/typeStore.d.ts +57 -0
- package/dist/check/typeStore.d.ts.map +1 -0
- package/dist/check/typeStore.js +89 -0
- package/dist/check/typeStore.js.map +1 -0
- package/dist/check/typecheck.d.ts +29 -0
- package/dist/check/typecheck.d.ts.map +1 -0
- package/dist/check/typecheck.js +91 -0
- package/dist/check/typecheck.js.map +1 -0
- package/dist/compileComponent.d.ts +8 -0
- package/dist/compileComponent.d.ts.map +1 -0
- package/dist/compileComponent.js +22 -0
- package/dist/compileComponent.js.map +1 -0
- package/dist/compileScript.d.ts +19 -4
- package/dist/compileScript.d.ts.map +1 -1
- package/dist/compileScript.js +37 -12
- package/dist/compileScript.js.map +1 -1
- package/dist/components.d.ts +41 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +14 -0
- package/dist/components.js.map +1 -0
- package/dist/detectProject.d.ts +3 -0
- package/dist/detectProject.d.ts.map +1 -1
- package/dist/detectProject.js +16 -6
- package/dist/detectProject.js.map +1 -1
- package/dist/devServer.d.ts +5 -2
- package/dist/devServer.d.ts.map +1 -1
- package/dist/devServer.js +10 -6
- package/dist/devServer.js.map +1 -1
- package/dist/hash.d.ts +2 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +22 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +24 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/installer/bins.d.ts +24 -0
- package/dist/installer/bins.d.ts.map +1 -0
- package/dist/installer/bins.js +56 -0
- package/dist/installer/bins.js.map +1 -0
- package/dist/installer/index.d.ts +14 -0
- package/dist/installer/index.d.ts.map +1 -0
- package/dist/installer/index.js +9 -0
- package/dist/installer/index.js.map +1 -0
- package/dist/installer/install.d.ts +46 -0
- package/dist/installer/install.d.ts.map +1 -0
- package/dist/installer/install.js +245 -0
- package/dist/installer/install.js.map +1 -0
- package/dist/installer/lockfile.d.ts +49 -0
- package/dist/installer/lockfile.d.ts.map +1 -0
- package/dist/installer/lockfile.js +110 -0
- package/dist/installer/lockfile.js.map +1 -0
- package/dist/installer/registry.d.ts +84 -0
- package/dist/installer/registry.d.ts.map +1 -0
- package/dist/installer/registry.js +130 -0
- package/dist/installer/registry.js.map +1 -0
- package/dist/installer/semver.d.ts +38 -0
- package/dist/installer/semver.d.ts.map +1 -0
- package/dist/installer/semver.js +187 -0
- package/dist/installer/semver.js.map +1 -0
- package/dist/installer/spec.d.ts +30 -0
- package/dist/installer/spec.d.ts.map +1 -0
- package/dist/installer/spec.js +30 -0
- package/dist/installer/spec.js.map +1 -0
- package/dist/installer/tar.d.ts +17 -0
- package/dist/installer/tar.d.ts.map +1 -0
- package/dist/installer/tar.js +150 -0
- package/dist/installer/tar.js.map +1 -0
- package/dist/installer/tree.d.ts +48 -0
- package/dist/installer/tree.d.ts.map +1 -0
- package/dist/installer/tree.js +179 -0
- package/dist/installer/tree.js.map +1 -0
- package/dist/node/assert.d.ts +15 -0
- package/dist/node/assert.d.ts.map +1 -0
- package/dist/node/assert.js +137 -0
- package/dist/node/assert.js.map +1 -0
- package/dist/node/buffer.d.ts +84 -0
- package/dist/node/buffer.d.ts.map +1 -0
- package/dist/node/buffer.js +304 -0
- package/dist/node/buffer.js.map +1 -0
- package/dist/node/builtins.d.ts +25 -0
- package/dist/node/builtins.d.ts.map +1 -0
- package/dist/node/builtins.js +101 -0
- package/dist/node/builtins.js.map +1 -0
- package/dist/node/callable.d.ts +13 -0
- package/dist/node/callable.d.ts.map +1 -0
- package/dist/node/callable.js +26 -0
- package/dist/node/callable.js.map +1 -0
- package/dist/node/childProcess.d.ts +13 -0
- package/dist/node/childProcess.d.ts.map +1 -0
- package/dist/node/childProcess.js +143 -0
- package/dist/node/childProcess.js.map +1 -0
- package/dist/node/crypto.d.ts +3 -0
- package/dist/node/crypto.d.ts.map +1 -0
- package/dist/node/crypto.js +180 -0
- package/dist/node/crypto.js.map +1 -0
- package/dist/node/esm.d.ts +9 -0
- package/dist/node/esm.d.ts.map +1 -0
- package/dist/node/esm.js +202 -0
- package/dist/node/esm.js.map +1 -0
- package/dist/node/events.d.ts +40 -0
- package/dist/node/events.d.ts.map +1 -0
- package/dist/node/events.js +160 -0
- package/dist/node/events.js.map +1 -0
- package/dist/node/fs/index.d.ts +7 -0
- package/dist/node/fs/index.d.ts.map +1 -0
- package/dist/node/fs/index.js +270 -0
- package/dist/node/fs/index.js.map +1 -0
- package/dist/node/fs/stats.d.ts +93 -0
- package/dist/node/fs/stats.d.ts.map +1 -0
- package/dist/node/fs/stats.js +104 -0
- package/dist/node/fs/stats.js.map +1 -0
- package/dist/node/fs/sync.d.ts +98 -0
- package/dist/node/fs/sync.d.ts.map +1 -0
- package/dist/node/fs/sync.js +348 -0
- package/dist/node/fs/sync.js.map +1 -0
- package/dist/node/hashes.d.ts +15 -0
- package/dist/node/hashes.d.ts.map +1 -0
- package/dist/node/hashes.js +195 -0
- package/dist/node/hashes.js.map +1 -0
- package/dist/node/http/client.d.ts +35 -0
- package/dist/node/http/client.d.ts.map +1 -0
- package/dist/node/http/client.js +161 -0
- package/dist/node/http/client.js.map +1 -0
- package/dist/node/http/index.d.ts +5 -0
- package/dist/node/http/index.d.ts.map +1 -0
- package/dist/node/http/index.js +125 -0
- package/dist/node/http/index.js.map +1 -0
- package/dist/node/http/server.d.ts +135 -0
- package/dist/node/http/server.d.ts.map +1 -0
- package/dist/node/http/server.js +308 -0
- package/dist/node/http/server.js.map +1 -0
- package/dist/node/http/status.d.ts +3 -0
- package/dist/node/http/status.d.ts.map +1 -0
- package/dist/node/http/status.js +13 -0
- package/dist/node/http/status.js.map +1 -0
- package/dist/node/inspect.d.ts +22 -0
- package/dist/node/inspect.d.ts.map +1 -0
- package/dist/node/inspect.js +172 -0
- package/dist/node/inspect.js.map +1 -0
- package/dist/node/loader.d.ts +91 -0
- package/dist/node/loader.d.ts.map +1 -0
- package/dist/node/loader.js +265 -0
- package/dist/node/loader.js.map +1 -0
- package/dist/node/loop.d.ts +71 -0
- package/dist/node/loop.d.ts.map +1 -0
- package/dist/node/loop.js +189 -0
- package/dist/node/loop.js.map +1 -0
- package/dist/node/misc.d.ts +10 -0
- package/dist/node/misc.d.ts.map +1 -0
- package/dist/node/misc.js +256 -0
- package/dist/node/misc.js.map +1 -0
- package/dist/node/path.d.ts +14 -0
- package/dist/node/path.d.ts.map +1 -0
- package/dist/node/path.js +128 -0
- package/dist/node/path.js.map +1 -0
- package/dist/node/process.d.ts +31 -0
- package/dist/node/process.d.ts.map +1 -0
- package/dist/node/process.js +309 -0
- package/dist/node/process.js.map +1 -0
- package/dist/node/program.d.ts +19 -0
- package/dist/node/program.d.ts.map +1 -0
- package/dist/node/program.js +165 -0
- package/dist/node/program.js.map +1 -0
- package/dist/node/readline.d.ts +2 -0
- package/dist/node/readline.d.ts.map +1 -0
- package/dist/node/readline.js +155 -0
- package/dist/node/readline.js.map +1 -0
- package/dist/node/resolve.d.ts +45 -0
- package/dist/node/resolve.d.ts.map +1 -0
- package/dist/node/resolve.js +206 -0
- package/dist/node/resolve.js.map +1 -0
- package/dist/node/runtime.d.ts +40 -0
- package/dist/node/runtime.d.ts.map +1 -0
- package/dist/node/runtime.js +149 -0
- package/dist/node/runtime.js.map +1 -0
- package/dist/node/stream/index.d.ts +52 -0
- package/dist/node/stream/index.d.ts.map +1 -0
- package/dist/node/stream/index.js +184 -0
- package/dist/node/stream/index.js.map +1 -0
- package/dist/node/stream/readable.d.ts +83 -0
- package/dist/node/stream/readable.d.ts.map +1 -0
- package/dist/node/stream/readable.js +325 -0
- package/dist/node/stream/readable.js.map +1 -0
- package/dist/node/stream/writable.d.ts +80 -0
- package/dist/node/stream/writable.d.ts.map +1 -0
- package/dist/node/stream/writable.js +224 -0
- package/dist/node/stream/writable.js.map +1 -0
- package/dist/node/urlqs.d.ts +22 -0
- package/dist/node/urlqs.d.ts.map +1 -0
- package/dist/node/urlqs.js +165 -0
- package/dist/node/urlqs.js.map +1 -0
- package/dist/node/util.d.ts +37 -0
- package/dist/node/util.d.ts.map +1 -0
- package/dist/node/util.js +160 -0
- package/dist/node/util.js.map +1 -0
- package/dist/node/zlib.d.ts +2 -0
- package/dist/node/zlib.d.ts.map +1 -0
- package/dist/node/zlib.js +133 -0
- package/dist/node/zlib.js.map +1 -0
- package/dist/packageCdn.d.ts +12 -0
- package/dist/packageCdn.d.ts.map +1 -1
- package/dist/packageCdn.js +3 -1
- package/dist/packageCdn.js.map +1 -1
- package/dist/projectConfig.d.ts +6 -2
- package/dist/projectConfig.d.ts.map +1 -1
- package/dist/projectConfig.js +32 -8
- package/dist/projectConfig.js.map +1 -1
- package/dist/shell/builtins.d.ts +23 -0
- package/dist/shell/builtins.d.ts.map +1 -0
- package/dist/shell/builtins.js +131 -0
- package/dist/shell/builtins.js.map +1 -0
- package/dist/shell/context.d.ts +41 -0
- package/dist/shell/context.d.ts.map +1 -0
- package/dist/shell/context.js +65 -0
- package/dist/shell/context.js.map +1 -0
- package/dist/shell/devHandoff.d.ts +18 -0
- package/dist/shell/devHandoff.d.ts.map +1 -0
- package/dist/shell/devHandoff.js +36 -0
- package/dist/shell/devHandoff.js.map +1 -0
- package/dist/shell/exec.d.ts +67 -0
- package/dist/shell/exec.d.ts.map +1 -0
- package/dist/shell/exec.js +247 -0
- package/dist/shell/exec.js.map +1 -0
- package/dist/shell/expand.d.ts +28 -0
- package/dist/shell/expand.d.ts.map +1 -0
- package/dist/shell/expand.js +149 -0
- package/dist/shell/expand.js.map +1 -0
- package/dist/shell/fileCommands.d.ts +10 -0
- package/dist/shell/fileCommands.d.ts.map +1 -0
- package/dist/shell/fileCommands.js +208 -0
- package/dist/shell/fileCommands.js.map +1 -0
- package/dist/shell/lineEditor.d.ts +43 -0
- package/dist/shell/lineEditor.d.ts.map +1 -0
- package/dist/shell/lineEditor.js +234 -0
- package/dist/shell/lineEditor.js.map +1 -0
- package/dist/shell/npm.d.ts +6 -0
- package/dist/shell/npm.d.ts.map +1 -0
- package/dist/shell/npm.js +234 -0
- package/dist/shell/npm.js.map +1 -0
- package/dist/shell/parse.d.ts +73 -0
- package/dist/shell/parse.d.ts.map +1 -0
- package/dist/shell/parse.js +300 -0
- package/dist/shell/parse.js.map +1 -0
- package/dist/shell/program.d.ts +12 -0
- package/dist/shell/program.d.ts.map +1 -0
- package/dist/shell/program.js +219 -0
- package/dist/shell/program.js.map +1 -0
- package/dist/shell/textCommands.d.ts +15 -0
- package/dist/shell/textCommands.d.ts.map +1 -0
- package/dist/shell/textCommands.js +185 -0
- package/dist/shell/textCommands.js.map +1 -0
- package/dist/styleModules.d.ts +6 -0
- package/dist/styleModules.d.ts.map +1 -1
- package/dist/styleModules.js +14 -4
- package/dist/styleModules.js.map +1 -1
- package/dist/svelteCompiler.d.ts +28 -0
- package/dist/svelteCompiler.d.ts.map +1 -0
- package/dist/svelteCompiler.js +13 -0
- package/dist/svelteCompiler.js.map +1 -0
- package/dist/system/commands.d.ts +26 -0
- package/dist/system/commands.d.ts.map +1 -0
- package/dist/system/commands.js +53 -0
- package/dist/system/commands.js.map +1 -0
- package/dist/system/index.d.ts +23 -0
- package/dist/system/index.d.ts.map +1 -0
- package/dist/system/index.js +16 -0
- package/dist/system/index.js.map +1 -0
- package/dist/system/kernel.d.ts +54 -0
- package/dist/system/kernel.d.ts.map +1 -0
- package/dist/system/kernel.js +81 -0
- package/dist/system/kernel.js.map +1 -0
- package/dist/system/ports.d.ts +32 -0
- package/dist/system/ports.d.ts.map +1 -0
- package/dist/system/ports.js +57 -0
- package/dist/system/ports.js.map +1 -0
- package/dist/system/process.d.ts +32 -0
- package/dist/system/process.d.ts.map +1 -0
- package/dist/system/process.js +130 -0
- package/dist/system/process.js.map +1 -0
- package/dist/system/programs.d.ts +10 -0
- package/dist/system/programs.d.ts.map +1 -0
- package/dist/system/programs.js +22 -0
- package/dist/system/programs.js.map +1 -0
- package/dist/system/terminal.d.ts +45 -0
- package/dist/system/terminal.d.ts.map +1 -0
- package/dist/system/terminal.js +127 -0
- package/dist/system/terminal.js.map +1 -0
- package/dist/system/types.d.ts +100 -0
- package/dist/system/types.d.ts.map +1 -0
- package/dist/system/types.js +2 -0
- package/dist/system/types.js.map +1 -0
- package/dist/vfs.d.ts +28 -4
- package/dist/vfs.d.ts.map +1 -1
- package/dist/vfs.js +95 -32
- package/dist/vfs.js.map +1 -1
- package/dist/vueCompiler.d.ts +75 -0
- package/dist/vueCompiler.d.ts.map +1 -0
- package/dist/vueCompiler.js +67 -0
- package/dist/vueCompiler.js.map +1 -0
- package/package.json +18 -2
- package/src/build.ts +105 -0
- package/src/buildHost.ts +154 -0
- package/src/buildHtml.ts +101 -0
- package/src/bundler.ts +190 -0
- package/src/check/acquire.ts +212 -0
- package/src/check/compilerHost.ts +138 -0
- package/src/check/diagnostics.ts +48 -0
- package/src/check/index.ts +28 -0
- package/src/check/projects.ts +149 -0
- package/src/check/shims.ts +88 -0
- package/src/check/typeStore.ts +103 -0
- package/src/check/typecheck.ts +134 -0
- package/src/compileComponent.ts +21 -0
- package/src/compileScript.ts +42 -13
- package/src/components.ts +59 -0
- package/src/detectProject.ts +18 -6
- package/src/devServer.ts +13 -8
- package/src/hash.ts +22 -0
- package/src/index.ts +35 -6
- package/src/installer/bins.ts +64 -0
- package/src/installer/index.ts +13 -0
- package/src/installer/install.ts +275 -0
- package/src/installer/lockfile.ts +146 -0
- package/src/installer/registry.ts +175 -0
- package/src/installer/semver.ts +190 -0
- package/src/installer/spec.ts +53 -0
- package/src/installer/tar.ts +155 -0
- package/src/installer/tree.ts +236 -0
- package/src/node/assert.ts +127 -0
- package/src/node/buffer.ts +304 -0
- package/src/node/builtins.ts +123 -0
- package/src/node/callable.ts +25 -0
- package/src/node/childProcess.ts +164 -0
- package/src/node/crypto.ts +187 -0
- package/src/node/esm.ts +196 -0
- package/src/node/events.ts +176 -0
- package/src/node/fs/index.ts +277 -0
- package/src/node/fs/stats.ts +110 -0
- package/src/node/fs/sync.ts +335 -0
- package/src/node/hashes.ts +171 -0
- package/src/node/http/client.ts +173 -0
- package/src/node/http/index.ts +122 -0
- package/src/node/http/server.ts +308 -0
- package/src/node/http/status.ts +13 -0
- package/src/node/inspect.ts +166 -0
- package/src/node/loader.ts +293 -0
- package/src/node/loop.ts +193 -0
- package/src/node/misc.ts +250 -0
- package/src/node/path.ts +135 -0
- package/src/node/process.ts +324 -0
- package/src/node/program.ts +170 -0
- package/src/node/readline.ts +157 -0
- package/src/node/resolve.ts +216 -0
- package/src/node/runtime.ts +184 -0
- package/src/node/stream/index.ts +196 -0
- package/src/node/stream/readable.ts +345 -0
- package/src/node/stream/writable.ts +244 -0
- package/src/node/urlqs.ts +162 -0
- package/src/node/util.ts +145 -0
- package/src/node/zlib.ts +145 -0
- package/src/packageCdn.ts +11 -1
- package/src/projectConfig.ts +34 -7
- package/src/shell/builtins.ts +141 -0
- package/src/shell/context.ts +89 -0
- package/src/shell/devHandoff.ts +49 -0
- package/src/shell/exec.ts +268 -0
- package/src/shell/expand.ts +145 -0
- package/src/shell/fileCommands.ts +206 -0
- package/src/shell/lineEditor.ts +233 -0
- package/src/shell/npm.ts +229 -0
- package/src/shell/parse.ts +295 -0
- package/src/shell/program.ts +223 -0
- package/src/shell/textCommands.ts +177 -0
- package/src/styleModules.ts +15 -4
- package/src/svelteCompiler.ts +31 -0
- package/src/system/commands.ts +72 -0
- package/src/system/index.ts +22 -0
- package/src/system/kernel.ts +106 -0
- package/src/system/ports.ts +73 -0
- package/src/system/process.ts +158 -0
- package/src/system/programs.ts +31 -0
- package/src/system/terminal.ts +135 -0
- package/src/system/types.ts +103 -0
- package/src/vfs.ts +92 -28
- package/src/vueCompiler.ts +121 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builtins that change or inspect the shell itself (cd, export, exit…), plus
|
|
3
|
+
* the command tables the executor dispatches on.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { findExecutable } from '../system/commands.js';
|
|
7
|
+
import type { CommandContext, Command } from './context.js';
|
|
8
|
+
import { resolvePath } from './context.js';
|
|
9
|
+
import { cat, cp, ls, mkdir, mv, pwd, rm, touch } from './fileCommands.js';
|
|
10
|
+
import { basename, clear, dirname, echo, env, falseCommand, grep, head, printf, sleep, tail, trueCommand, wc } from './textCommands.js';
|
|
11
|
+
|
|
12
|
+
/** The slice of the shell a builtin may change. */
|
|
13
|
+
export interface ShellState {
|
|
14
|
+
cwd: string;
|
|
15
|
+
env: Record<string, string>;
|
|
16
|
+
history: string[];
|
|
17
|
+
lastStatus: number;
|
|
18
|
+
/** Set by `exit`: the shell stops after the current command. */
|
|
19
|
+
exitRequested?: number;
|
|
20
|
+
/** Run a script in this shell (for `source`). */
|
|
21
|
+
run(source: string): Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Builtin = (ctx: CommandContext, shell: ShellState) => Promise<number>;
|
|
25
|
+
|
|
26
|
+
/** Coreutils: plain commands, also spawnable as standalone programs. */
|
|
27
|
+
export const COREUTILS: Record<string, Command> = {
|
|
28
|
+
ls, cat, mkdir, rm, cp, mv, touch, pwd, echo, printf, head, tail, grep, wc, env, clear, sleep, basename, dirname,
|
|
29
|
+
true: trueCommand,
|
|
30
|
+
false: falseCommand,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Programs the runtime provides besides coreutils (reported by `which`/`type`). */
|
|
34
|
+
export const SYSTEM_PROGRAMS = ['node', 'npm', 'npx', 'jsh', 'sh', 'bash'];
|
|
35
|
+
|
|
36
|
+
const HELP = `jsh — BuilderForce WebContainers shell
|
|
37
|
+
|
|
38
|
+
Builtins: cd, pwd, export, unset, exit, source, history, type, which, help
|
|
39
|
+
Files: ls [-la], cat, mkdir [-p], rm [-rf], cp [-r], mv, touch
|
|
40
|
+
Text: echo, printf, head, tail, grep, wc, env, clear, sleep, basename, dirname
|
|
41
|
+
Node: node <file>, npm install|ci|run|start|test, npx <bin>
|
|
42
|
+
Syntax: 'quotes' "\$VARS" a && b || c; a | b > out.txt >> log 2>&1 2> err; FOO=bar cmd; $(cmd); *.js
|
|
43
|
+
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
export const BUILTINS: Record<string, Builtin> = {
|
|
47
|
+
async cd(ctx, shell) {
|
|
48
|
+
const home = shell.env.HOME && ctx.fs.isDirectory(shell.env.HOME) ? shell.env.HOME : '/';
|
|
49
|
+
const target = ctx.args[0] === '-' ? shell.env.OLDPWD ?? shell.cwd : ctx.args[0] ?? home;
|
|
50
|
+
const path = resolvePath(ctx, target);
|
|
51
|
+
if (!ctx.fs.isDirectory(path)) {
|
|
52
|
+
ctx.io.stderr(`cd: ${ctx.fs.isFile(path) ? 'not a directory' : 'no such file or directory'}: ${target}\n`);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
shell.env.OLDPWD = shell.cwd;
|
|
56
|
+
shell.cwd = path;
|
|
57
|
+
shell.env.PWD = path;
|
|
58
|
+
if (ctx.args[0] === '-') ctx.io.stdout(path + '\n');
|
|
59
|
+
return 0;
|
|
60
|
+
},
|
|
61
|
+
async export(ctx, shell) {
|
|
62
|
+
if (!ctx.args.length) {
|
|
63
|
+
ctx.io.stdout(Object.entries(shell.env).map(([k, v]) => `export ${k}=${JSON.stringify(v)}`).join('\n') + '\n');
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
for (const arg of ctx.args) {
|
|
67
|
+
const eq = arg.indexOf('=');
|
|
68
|
+
if (eq > 0) shell.env[arg.slice(0, eq)] = arg.slice(eq + 1);
|
|
69
|
+
else if (!(arg in shell.env)) shell.env[arg] = '';
|
|
70
|
+
}
|
|
71
|
+
return 0;
|
|
72
|
+
},
|
|
73
|
+
async unset(ctx, shell) {
|
|
74
|
+
for (const name of ctx.args) delete shell.env[name];
|
|
75
|
+
return 0;
|
|
76
|
+
},
|
|
77
|
+
async exit(ctx, shell) {
|
|
78
|
+
const code = ctx.args[0] === undefined ? shell.lastStatus : Number(ctx.args[0]);
|
|
79
|
+
shell.exitRequested = Number.isFinite(code) ? code & 255 : 2;
|
|
80
|
+
return shell.exitRequested;
|
|
81
|
+
},
|
|
82
|
+
async source(ctx, shell) {
|
|
83
|
+
const file = ctx.args[0];
|
|
84
|
+
if (!file) {
|
|
85
|
+
ctx.io.stderr('source: filename argument required\n');
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
const text = ctx.fs.readText(resolvePath(ctx, file));
|
|
89
|
+
if (text === undefined) {
|
|
90
|
+
ctx.io.stderr(`source: ${file}: No such file or directory\n`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
return shell.run(text);
|
|
94
|
+
},
|
|
95
|
+
async history(ctx, shell) {
|
|
96
|
+
ctx.io.stdout(shell.history.map((line, i) => `${String(i + 1).padStart(5)} ${line}`).join('\n') + (shell.history.length ? '\n' : ''));
|
|
97
|
+
return 0;
|
|
98
|
+
},
|
|
99
|
+
async help(ctx) {
|
|
100
|
+
ctx.io.stdout(HELP);
|
|
101
|
+
return 0;
|
|
102
|
+
},
|
|
103
|
+
async which(ctx) {
|
|
104
|
+
let status = 0;
|
|
105
|
+
for (const name of ctx.args.filter((a) => !a.startsWith('-'))) {
|
|
106
|
+
if (name in COREUTILS || SYSTEM_PROGRAMS.includes(name)) ctx.io.stdout(`/usr/local/bin/${name}\n`);
|
|
107
|
+
else if (name in BUILTINS) ctx.io.stdout(`${name}: shell built-in command\n`);
|
|
108
|
+
else {
|
|
109
|
+
const file = findExecutable(ctx.fs, name, ctx.cwd, ctx.env);
|
|
110
|
+
if (file) ctx.io.stdout(file + '\n');
|
|
111
|
+
else {
|
|
112
|
+
ctx.io.stderr(`${name} not found\n`);
|
|
113
|
+
status = 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return status;
|
|
118
|
+
},
|
|
119
|
+
async type(ctx) {
|
|
120
|
+
let status = 0;
|
|
121
|
+
for (const name of ctx.args) {
|
|
122
|
+
if (name in BUILTINS) ctx.io.stdout(`${name} is a shell builtin\n`);
|
|
123
|
+
else if (name in COREUTILS || SYSTEM_PROGRAMS.includes(name)) ctx.io.stdout(`${name} is /usr/local/bin/${name}\n`);
|
|
124
|
+
else {
|
|
125
|
+
const file = findExecutable(ctx.fs, name, ctx.cwd, ctx.env);
|
|
126
|
+
if (file) ctx.io.stdout(`${name} is ${file}\n`);
|
|
127
|
+
else {
|
|
128
|
+
ctx.io.stderr(`type: ${name}: not found\n`);
|
|
129
|
+
status = 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return status;
|
|
134
|
+
},
|
|
135
|
+
async set() {
|
|
136
|
+
return 0; // options (-e, -x) are accepted and ignored
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
BUILTINS['.'] = BUILTINS.source!;
|
|
140
|
+
BUILTINS[':'] = async () => 0;
|
|
141
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a shell command runs with. Builtins get the shell's own state (to `cd`
|
|
3
|
+
* and `export`); coreutils only need IO, cwd and the file system, which is why
|
|
4
|
+
* they can also be spawned as standalone programs (`spawn('ls', ['-la'])`).
|
|
5
|
+
*/
|
|
6
|
+
import type { VirtualFileSystem } from '../vfs.js';
|
|
7
|
+
import type { Program, System } from '../system/types.js';
|
|
8
|
+
import { InputQueue } from '../system/terminal.js';
|
|
9
|
+
import { join, normalizePath } from '../paths.js';
|
|
10
|
+
|
|
11
|
+
export interface CommandIO {
|
|
12
|
+
stdout(text: string): void;
|
|
13
|
+
stderr(text: string): void;
|
|
14
|
+
stdin: InputQueue;
|
|
15
|
+
/** stdout is a terminal (colours, `clear`). */
|
|
16
|
+
isTerminal: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CommandContext {
|
|
20
|
+
/** Arguments after the command name. */
|
|
21
|
+
args: string[];
|
|
22
|
+
/** The command name as typed (for messages). */
|
|
23
|
+
name: string;
|
|
24
|
+
cwd: string;
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
fs: VirtualFileSystem;
|
|
27
|
+
system: System;
|
|
28
|
+
io: CommandIO;
|
|
29
|
+
signal: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type Command = (ctx: CommandContext) => Promise<number>;
|
|
33
|
+
|
|
34
|
+
export function resolvePath(ctx: { cwd: string; env: Record<string, string> }, path: string): string {
|
|
35
|
+
if (path === '~' || path.startsWith('~/')) return join(ctx.env.HOME ?? '/home/user', path.slice(1));
|
|
36
|
+
return path.startsWith('/') ? normalizePath(path) : join(ctx.cwd, path);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Split `-abc` style flags from operands (`--` ends flags). */
|
|
40
|
+
export function parseFlags(args: string[], withValue: string[] = []): { flags: Set<string>; values: Record<string, string>; operands: string[] } {
|
|
41
|
+
const flags = new Set<string>();
|
|
42
|
+
const values: Record<string, string> = {};
|
|
43
|
+
const operands: string[] = [];
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
const arg = args[i]!;
|
|
46
|
+
if (arg === '--') {
|
|
47
|
+
operands.push(...args.slice(i + 1));
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith('--') && arg.length > 2) {
|
|
51
|
+
const [k, v] = arg.slice(2).split('=', 2) as [string, string | undefined];
|
|
52
|
+
if (v !== undefined) values[k] = v;
|
|
53
|
+
else if (withValue.includes(k)) values[k] = args[++i] ?? '';
|
|
54
|
+
else flags.add(k);
|
|
55
|
+
} else if (arg.startsWith('-') && arg.length > 1 && !/^-\d/.test(arg)) {
|
|
56
|
+
for (let j = 1; j < arg.length; j++) {
|
|
57
|
+
const f = arg[j]!;
|
|
58
|
+
if (withValue.includes(f)) {
|
|
59
|
+
values[f] = arg.slice(j + 1) || args[++i] || '';
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
flags.add(f);
|
|
63
|
+
}
|
|
64
|
+
} else operands.push(arg);
|
|
65
|
+
}
|
|
66
|
+
return { flags, values, operands };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Run a coreutil as a standalone program (spawned directly rather than from the shell). */
|
|
70
|
+
export function asProgram(command: Command, name: string): Program {
|
|
71
|
+
return async (ctx) => {
|
|
72
|
+
const stdin = new InputQueue();
|
|
73
|
+
const stop = ctx.io.onStdin((chunk) => stdin.push(chunk));
|
|
74
|
+
try {
|
|
75
|
+
return await command({
|
|
76
|
+
args: ctx.args,
|
|
77
|
+
name,
|
|
78
|
+
cwd: ctx.cwd,
|
|
79
|
+
env: ctx.env,
|
|
80
|
+
fs: ctx.system.fs,
|
|
81
|
+
system: ctx.system,
|
|
82
|
+
io: { stdout: ctx.io.stdout, stderr: ctx.io.stderr, stdin, isTerminal: !!ctx.io.terminal },
|
|
83
|
+
signal: ctx.signal,
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
stop();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-server handoff. `vite`, `vite dev`, `react-scripts start` (what `npm run
|
|
3
|
+
* dev` / `npm start` usually run) cannot run here as-is — Vite itself needs
|
|
4
|
+
* esbuild's native binary — but the runtime already IS a Vite-style dev server:
|
|
5
|
+
* the in-browser DevServer serving the preview. So these commands announce that
|
|
6
|
+
* preview as their server (`server-ready`) and stay "running" until Ctrl-C,
|
|
7
|
+
* which is exactly what a host waiting on `npm run dev` expects.
|
|
8
|
+
*/
|
|
9
|
+
import type { CommandContext } from './context.js';
|
|
10
|
+
|
|
11
|
+
interface Handoff {
|
|
12
|
+
tool: string;
|
|
13
|
+
defaultPort: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Does this command line start a dev server we hand off? */
|
|
17
|
+
export function matchDevServer(argv: string[]): Handoff | undefined {
|
|
18
|
+
const [command, ...rest] = argv;
|
|
19
|
+
const sub = rest[0] !== undefined && !rest[0].startsWith('-') ? rest[0] : undefined;
|
|
20
|
+
if (command === 'vite' && (sub === undefined || sub === 'dev' || sub === 'serve')) return { tool: 'vite', defaultPort: 5173 };
|
|
21
|
+
if ((command === 'react-scripts' || command === 'craco') && sub === 'start') return { tool: 'react-scripts', defaultPort: 3000 };
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function portOf(argv: string[], fallback: number, env: Record<string, string>): number {
|
|
26
|
+
const at = argv.findIndex((a) => a === '--port' || a.startsWith('--port='));
|
|
27
|
+
const value = at < 0 ? env.PORT : argv[at]!.includes('=') ? argv[at]!.split('=')[1] : argv[at + 1];
|
|
28
|
+
const port = Number(value);
|
|
29
|
+
return Number.isInteger(port) && port > 0 ? port : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function runDevServerHandoff(ctx: CommandContext, argv: string[], handoff: Handoff): Promise<number> {
|
|
33
|
+
const url = ctx.system.previewUrl;
|
|
34
|
+
if (!url) {
|
|
35
|
+
ctx.io.stderr(`${handoff.tool}: no in-browser preview is attached to this runtime, so there is nothing to serve.\n`);
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const port = portOf(argv, handoff.defaultPort, ctx.env);
|
|
39
|
+
const banner = handoff.tool === 'vite'
|
|
40
|
+
? `\n VITE (BuilderForce in-browser dev server) ready\n\n ➜ Local: ${url}\n\n`
|
|
41
|
+
: `\nCompiled successfully!\n\nYou can now view the app in the browser.\n\n Local: ${url}\n\n`;
|
|
42
|
+
ctx.io.stdout(banner);
|
|
43
|
+
ctx.system.announceServer(port, url);
|
|
44
|
+
await new Promise<void>((resolve) => {
|
|
45
|
+
if (ctx.signal.aborted) resolve();
|
|
46
|
+
ctx.signal.addEventListener('abort', () => resolve());
|
|
47
|
+
});
|
|
48
|
+
return 130;
|
|
49
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The executor: runs a parsed script. Lists honour `&&`/`||`/`;`/`&`; pipeline
|
|
3
|
+
* stages run concurrently, joined by in-memory pipes; redirects swap a stage's
|
|
4
|
+
* stdin/stdout/stderr for files. Dispatch order per command: shell builtins,
|
|
5
|
+
* extra in-process commands (npm, npx), the dev-server handoff, coreutils, and
|
|
6
|
+
* finally `system.spawn` (node, scripts on PATH, `.bin` entries) — which in the
|
|
7
|
+
* browser is a separate Web Worker.
|
|
8
|
+
*/
|
|
9
|
+
import type { System } from '../system/types.js';
|
|
10
|
+
import { InputQueue } from '../system/terminal.js';
|
|
11
|
+
import { parse, ShellSyntaxError, type Command as CommandNode, type Pipeline, type Script } from './parse.js';
|
|
12
|
+
import { expandWord, expandWords, type ExpandContext } from './expand.js';
|
|
13
|
+
import { BUILTINS, COREUTILS, type ShellState } from './builtins.js';
|
|
14
|
+
import { resolvePath, type CommandContext } from './context.js';
|
|
15
|
+
import { matchDevServer, runDevServerHandoff } from './devHandoff.js';
|
|
16
|
+
|
|
17
|
+
export interface ShellIO {
|
|
18
|
+
stdout(text: string): void;
|
|
19
|
+
stderr(text: string): void;
|
|
20
|
+
/** stdout is a terminal. */
|
|
21
|
+
isTerminal: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A job's stdin: a queue, and (interactive) a raw keystroke tap for a terminal-attached child. */
|
|
25
|
+
export interface JobInput {
|
|
26
|
+
queue: InputQueue;
|
|
27
|
+
/** Route raw terminal input straight to `sink` (a child with its own line discipline), or back to the queue. */
|
|
28
|
+
setRawSink?(sink: ((data: string) => void) | undefined): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** In-process commands with access to the shell (npm runs scripts through it). */
|
|
32
|
+
export type ShellCommand = (ctx: CommandContext, shell: Shell, input: JobInput) => Promise<number>;
|
|
33
|
+
|
|
34
|
+
export interface ShellOptions {
|
|
35
|
+
system: System;
|
|
36
|
+
cwd: string;
|
|
37
|
+
env: Record<string, string>;
|
|
38
|
+
positional?: string[];
|
|
39
|
+
/** The terminal size, when attached to one. */
|
|
40
|
+
terminalSize?: () => { cols: number; rows: number } | undefined;
|
|
41
|
+
commands?: Record<string, ShellCommand>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let nextShellPid = 500;
|
|
45
|
+
|
|
46
|
+
export class Shell implements ShellState {
|
|
47
|
+
cwd: string;
|
|
48
|
+
env: Record<string, string>;
|
|
49
|
+
lastStatus = 0;
|
|
50
|
+
history: string[] = [];
|
|
51
|
+
exitRequested?: number;
|
|
52
|
+
readonly pid = nextShellPid++;
|
|
53
|
+
readonly system: System;
|
|
54
|
+
private readonly positional: string[];
|
|
55
|
+
private current?: { io: ShellIO; input: JobInput; signal: AbortSignal };
|
|
56
|
+
|
|
57
|
+
constructor(readonly options: ShellOptions) {
|
|
58
|
+
this.system = options.system;
|
|
59
|
+
this.cwd = options.cwd;
|
|
60
|
+
this.env = { ...options.env, PWD: options.cwd };
|
|
61
|
+
this.positional = options.positional ?? [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A child shell for `npm run` / subshells: same IO, its own cwd and environment. */
|
|
65
|
+
fork(overrides: { cwd?: string; env?: Record<string, string>; positional?: string[] }): Shell {
|
|
66
|
+
return new Shell({ ...this.options, cwd: overrides.cwd ?? this.cwd, env: { ...this.env, ...overrides.env }, positional: overrides.positional });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Parse and run `source`. Syntax errors print and return 2. */
|
|
70
|
+
async exec(source: string, io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number> {
|
|
71
|
+
let script: Script;
|
|
72
|
+
try {
|
|
73
|
+
script = parse(source);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!(error instanceof ShellSyntaxError)) throw error;
|
|
76
|
+
io.stderr(`jsh: ${error.message}\n`);
|
|
77
|
+
this.lastStatus = 2;
|
|
78
|
+
return 2;
|
|
79
|
+
}
|
|
80
|
+
const previous = this.current;
|
|
81
|
+
this.current = { io, input, signal };
|
|
82
|
+
try {
|
|
83
|
+
for (const list of script) {
|
|
84
|
+
if (this.exitRequested !== undefined || signal.aborted) break;
|
|
85
|
+
const run = async () => {
|
|
86
|
+
let status = await this.runPipeline(list.first, io, input, signal);
|
|
87
|
+
for (const { op, pipeline } of list.rest) {
|
|
88
|
+
if (this.exitRequested !== undefined || signal.aborted) break;
|
|
89
|
+
if ((op === '&&') === (status === 0)) status = await this.runPipeline(pipeline, io, input, signal);
|
|
90
|
+
}
|
|
91
|
+
return status;
|
|
92
|
+
};
|
|
93
|
+
if (list.background) {
|
|
94
|
+
void run();
|
|
95
|
+
this.lastStatus = 0;
|
|
96
|
+
} else this.lastStatus = await run();
|
|
97
|
+
}
|
|
98
|
+
} finally {
|
|
99
|
+
this.current = previous;
|
|
100
|
+
}
|
|
101
|
+
if (signal.aborted && this.lastStatus === 0) this.lastStatus = 130;
|
|
102
|
+
return this.exitRequested ?? this.lastStatus;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** `source` and friends: run in this shell with the current job's IO. */
|
|
106
|
+
run(source: string): Promise<number> {
|
|
107
|
+
const job = this.current;
|
|
108
|
+
if (!job) throw new Error('Shell.run outside a job');
|
|
109
|
+
return this.exec(source, job.io, job.input, job.signal);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private expandContext(io: ShellIO, signal: AbortSignal): ExpandContext {
|
|
113
|
+
return {
|
|
114
|
+
fs: this.system.fs,
|
|
115
|
+
cwd: this.cwd,
|
|
116
|
+
get: (name) => this.env[name],
|
|
117
|
+
set: (name, value) => void (this.env[name] = value),
|
|
118
|
+
positional: this.positional,
|
|
119
|
+
lastStatus: this.lastStatus,
|
|
120
|
+
pid: this.pid,
|
|
121
|
+
substitute: async (source) => {
|
|
122
|
+
let out = '';
|
|
123
|
+
const sub = this.fork({});
|
|
124
|
+
await sub.exec(source, { stdout: (t) => (out += t), stderr: io.stderr, isTerminal: false }, { queue: emptyInput() }, signal);
|
|
125
|
+
return out;
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async runPipeline(pipeline: Pipeline, io: ShellIO, input: JobInput, signal: AbortSignal): Promise<number> {
|
|
131
|
+
const count = pipeline.commands.length;
|
|
132
|
+
const pipes = Array.from({ length: count - 1 }, () => new InputQueue());
|
|
133
|
+
const statuses = await Promise.all(
|
|
134
|
+
pipeline.commands.map(async (command, i) => {
|
|
135
|
+
const stdin: JobInput = i === 0 ? input : { queue: pipes[i - 1]! };
|
|
136
|
+
const stdout = i === count - 1 ? io.stdout : (text: string) => pipes[i]!.push(text);
|
|
137
|
+
const stageIO: ShellIO = { stdout, stderr: io.stderr, isTerminal: io.isTerminal && i === count - 1 };
|
|
138
|
+
try {
|
|
139
|
+
return await this.runCommand(command, stageIO, stdin, signal, count === 1);
|
|
140
|
+
} finally {
|
|
141
|
+
if (i < count - 1) pipes[i]!.push(null);
|
|
142
|
+
}
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
const status = statuses[statuses.length - 1]!;
|
|
146
|
+
return pipeline.negate ? (status === 0 ? 1 : 0) : status;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async runCommand(node: CommandNode, io: ShellIO, input: JobInput, signal: AbortSignal, alone: boolean): Promise<number> {
|
|
150
|
+
const ectx = this.expandContext(io, signal);
|
|
151
|
+
const argv = await expandWords(node.words, ectx);
|
|
152
|
+
const assigned: Record<string, string> = {};
|
|
153
|
+
for (const { name, value } of node.assignments) assigned[name] = (await expandWord(value, ectx, { split: false, glob: false })).join('');
|
|
154
|
+
if (!argv.length) {
|
|
155
|
+
Object.assign(this.env, assigned);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Redirects.
|
|
160
|
+
let stdout = io.stdout;
|
|
161
|
+
let stderr = io.stderr;
|
|
162
|
+
let stdin = input;
|
|
163
|
+
let isTerminal = io.isTerminal;
|
|
164
|
+
const flushes: Array<() => void> = [];
|
|
165
|
+
for (const redirect of node.redirects) {
|
|
166
|
+
if (redirect.op === '2>&1') {
|
|
167
|
+
stderr = stdout;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (redirect.op === '>&2') {
|
|
171
|
+
stdout = stderr;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const target = (await expandWord(redirect.target!, ectx)).join(' ');
|
|
175
|
+
const path = resolvePath(this, target);
|
|
176
|
+
if (redirect.op === '<') {
|
|
177
|
+
const text = this.system.fs.readText(path);
|
|
178
|
+
if (text === undefined) {
|
|
179
|
+
io.stderr(`jsh: ${target}: No such file or directory\n`);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
const queue = new InputQueue();
|
|
183
|
+
queue.push(text);
|
|
184
|
+
queue.push(null);
|
|
185
|
+
stdin = { queue };
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (this.system.fs.isDirectory(path)) {
|
|
189
|
+
io.stderr(`jsh: ${target}: Is a directory\n`);
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
const append = redirect.op === '>>' || redirect.op === '2>>';
|
|
193
|
+
let content = append ? this.system.fs.readText(path) ?? '' : '';
|
|
194
|
+
this.system.fs.writeFile(path, content);
|
|
195
|
+
const sink = (text: string) => void (content += text);
|
|
196
|
+
flushes.push(() => this.system.fs.writeFile(path, content));
|
|
197
|
+
if (redirect.op === '2>' || redirect.op === '2>>') stderr = sink;
|
|
198
|
+
else if (redirect.op === '&>') stdout = stderr = sink;
|
|
199
|
+
else {
|
|
200
|
+
stdout = sink;
|
|
201
|
+
isTerminal = false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
return await this.dispatch(argv, assigned, { stdout, stderr, isTerminal }, stdin, signal, alone && isTerminal);
|
|
207
|
+
} finally {
|
|
208
|
+
for (const flush of flushes) flush();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private async dispatch(argv: string[], assigned: Record<string, string>, io: ShellIO, input: JobInput, signal: AbortSignal, attachTerminal: boolean): Promise<number> {
|
|
213
|
+
const [name, ...args] = argv as [string, ...string[]];
|
|
214
|
+
const env = { ...this.env, ...assigned };
|
|
215
|
+
const ctx: CommandContext = {
|
|
216
|
+
args,
|
|
217
|
+
name,
|
|
218
|
+
cwd: this.cwd,
|
|
219
|
+
env,
|
|
220
|
+
fs: this.system.fs,
|
|
221
|
+
system: this.system,
|
|
222
|
+
io: { stdout: io.stdout, stderr: io.stderr, stdin: input.queue, isTerminal: io.isTerminal },
|
|
223
|
+
signal,
|
|
224
|
+
};
|
|
225
|
+
const builtin = BUILTINS[name];
|
|
226
|
+
if (builtin) return builtin(ctx, this);
|
|
227
|
+
const command = this.options.commands?.[name];
|
|
228
|
+
if (command) return command(ctx, this, input);
|
|
229
|
+
const handoff = matchDevServer(argv);
|
|
230
|
+
if (handoff) return runDevServerHandoff(ctx, argv, handoff);
|
|
231
|
+
const coreutil = COREUTILS[name];
|
|
232
|
+
if (coreutil) return coreutil(ctx);
|
|
233
|
+
return this.spawnExternal(name, args, env, io, input, signal, attachTerminal);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private async spawnExternal(name: string, args: string[], env: Record<string, string>, io: ShellIO, input: JobInput, signal: AbortSignal, attachTerminal: boolean): Promise<number> {
|
|
237
|
+
const size = attachTerminal ? this.options.terminalSize?.() : undefined;
|
|
238
|
+
let child;
|
|
239
|
+
try {
|
|
240
|
+
child = this.system.spawn(name, args, { cwd: this.cwd, env, terminal: size });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if ((error as { code?: string }).code !== 'ENOENT') throw error;
|
|
243
|
+
io.stderr(`jsh: command not found: ${name}\n`);
|
|
244
|
+
return 127;
|
|
245
|
+
}
|
|
246
|
+
child.onStdout(io.stdout);
|
|
247
|
+
child.onStderr(io.stderr);
|
|
248
|
+
let stop: (() => void) | undefined;
|
|
249
|
+
if (size && input.setRawSink) input.setRawSink((data) => child.write(data));
|
|
250
|
+
else stop = input.queue.subscribe((chunk) => (chunk === null ? child.closeStdin() : child.write(chunk)));
|
|
251
|
+
const onAbort = () => child.kill(typeof signal.reason === 'string' ? signal.reason : 'SIGINT');
|
|
252
|
+
if (signal.aborted) onAbort();
|
|
253
|
+
signal.addEventListener('abort', onAbort);
|
|
254
|
+
try {
|
|
255
|
+
return await child.exit;
|
|
256
|
+
} finally {
|
|
257
|
+
signal.removeEventListener('abort', onAbort);
|
|
258
|
+
if (size && input.setRawSink) input.setRawSink(undefined);
|
|
259
|
+
stop?.();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function emptyInput(): InputQueue {
|
|
265
|
+
const queue = new InputQueue();
|
|
266
|
+
queue.push(null);
|
|
267
|
+
return queue;
|
|
268
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Word expansion, in sh order: tilde, parameters (`$X`, `${X:-d}`, `$?`, `$1`),
|
|
3
|
+
* command substitution, field splitting of UNQUOTED results, then pathname
|
|
4
|
+
* globbing (`*`, `?`, `[…]`) of unquoted patterns. A glob with no match stays
|
|
5
|
+
* literal, as in bash without `nullglob`.
|
|
6
|
+
*/
|
|
7
|
+
import type { VirtualFileSystem } from '../vfs.js';
|
|
8
|
+
import { join, normalizePath } from '../paths.js';
|
|
9
|
+
import type { Segment, Word } from './parse.js';
|
|
10
|
+
|
|
11
|
+
export interface ExpandContext {
|
|
12
|
+
fs: VirtualFileSystem;
|
|
13
|
+
cwd: string;
|
|
14
|
+
get(name: string): string | undefined;
|
|
15
|
+
set(name: string, value: string): void;
|
|
16
|
+
positional: string[];
|
|
17
|
+
lastStatus: number;
|
|
18
|
+
pid: number;
|
|
19
|
+
/** Run `$(…)` and return its stdout. */
|
|
20
|
+
substitute(source: string): Promise<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function paramValue(seg: Extract<Segment, { kind: 'var' }>, ctx: ExpandContext): Promise<string> {
|
|
24
|
+
let value: string | undefined;
|
|
25
|
+
switch (seg.name) {
|
|
26
|
+
case '?': value = String(ctx.lastStatus); break;
|
|
27
|
+
case '$': value = String(ctx.pid); break;
|
|
28
|
+
case '#': value = String(ctx.positional.length); break;
|
|
29
|
+
case '@':
|
|
30
|
+
case '*': value = ctx.positional.join(' '); break;
|
|
31
|
+
case '!': value = ''; break;
|
|
32
|
+
case '0': value = 'jsh'; break;
|
|
33
|
+
default: value = /^\d$/.test(seg.name) ? ctx.positional[Number(seg.name) - 1] : ctx.get(seg.name);
|
|
34
|
+
}
|
|
35
|
+
if (!seg.op) return value ?? '';
|
|
36
|
+
const arg = async () => (await expandWord(seg.arg ?? [], ctx, { split: false, glob: false })).join(' ');
|
|
37
|
+
switch (seg.op) {
|
|
38
|
+
case ':-': return value ? value : arg();
|
|
39
|
+
case '-': return value !== undefined ? value : arg();
|
|
40
|
+
case ':+': return value ? arg() : '';
|
|
41
|
+
case ':=': {
|
|
42
|
+
if (value) return value;
|
|
43
|
+
const fallback = await arg();
|
|
44
|
+
ctx.set(seg.name, fallback);
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const GLOB_CHARS = /[*?[]/;
|
|
51
|
+
|
|
52
|
+
function globToRegExp(pattern: string): RegExp {
|
|
53
|
+
let source = '';
|
|
54
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
55
|
+
const c = pattern[i]!;
|
|
56
|
+
if (c === '*') source += '[^/]*';
|
|
57
|
+
else if (c === '?') source += '[^/]';
|
|
58
|
+
else if (c === '[') {
|
|
59
|
+
const end = pattern.indexOf(']', i + 1);
|
|
60
|
+
if (end < 0) source += '\\[';
|
|
61
|
+
else {
|
|
62
|
+
source += '[' + pattern.slice(i + 1, end).replace(/^!/, '^').replace(/\\/g, '\\\\') + ']';
|
|
63
|
+
i = end;
|
|
64
|
+
}
|
|
65
|
+
} else source += c.replace(/[.+^${}()|\\]/g, '\\$&');
|
|
66
|
+
}
|
|
67
|
+
return new RegExp(`^${source}$`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function glob(fs: VirtualFileSystem, cwd: string, pattern: string): string[] {
|
|
71
|
+
const absolute = pattern.startsWith('/');
|
|
72
|
+
const parts = pattern.split('/').filter((p, i) => p || i === 0);
|
|
73
|
+
let matches: string[] = [absolute ? '/' : ''];
|
|
74
|
+
for (const part of absolute ? parts.slice(1) : parts) {
|
|
75
|
+
const next: string[] = [];
|
|
76
|
+
for (const base of matches) {
|
|
77
|
+
const dir = base ? (base.startsWith('/') ? base : join(cwd, base)) : cwd;
|
|
78
|
+
if (!GLOB_CHARS.test(part)) {
|
|
79
|
+
const candidate = base ? (base.endsWith('/') ? base + part : `${base}/${part}`) : part;
|
|
80
|
+
if (fs.exists(candidate.startsWith('/') ? candidate : join(cwd, candidate))) next.push(candidate);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const re = globToRegExp(part);
|
|
84
|
+
for (const name of fs.readdir(normalizePath(dir))) {
|
|
85
|
+
if (name.startsWith('.') && !part.startsWith('.')) continue;
|
|
86
|
+
if (re.test(name)) next.push(base ? (base.endsWith('/') ? base + name : `${base}/${name}`) : name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
matches = next;
|
|
90
|
+
}
|
|
91
|
+
return matches.sort();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Expand one word into zero or more fields. */
|
|
95
|
+
export async function expandWord(word: Word, ctx: ExpandContext, options: { split?: boolean; glob?: boolean } = {}): Promise<string[]> {
|
|
96
|
+
const split = options.split ?? true;
|
|
97
|
+
const fields: string[] = [''];
|
|
98
|
+
let started = false; // a quoted part (even empty) makes the word a field
|
|
99
|
+
let globbable = false;
|
|
100
|
+
const append = (text: string) => {
|
|
101
|
+
fields[fields.length - 1] += text;
|
|
102
|
+
};
|
|
103
|
+
for (const seg of word) {
|
|
104
|
+
if (seg.kind === 'tilde') {
|
|
105
|
+
append(ctx.get('HOME') ?? '/home/user');
|
|
106
|
+
started = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (seg.kind === 'text') {
|
|
110
|
+
append(seg.value);
|
|
111
|
+
if (seg.quoted || seg.value) started = true;
|
|
112
|
+
if (!seg.quoted && GLOB_CHARS.test(seg.value)) globbable = true;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const value = seg.kind === 'var' ? await paramValue(seg, ctx) : (await ctx.substitute(seg.source)).replace(/\n+$/, '');
|
|
116
|
+
if (seg.quoted || !split) {
|
|
117
|
+
append(value);
|
|
118
|
+
started = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const pieces = value.split(/[ \t\n]+/);
|
|
122
|
+
pieces.forEach((piece, index) => {
|
|
123
|
+
if (index > 0 && (fields[fields.length - 1] || started)) fields.push('');
|
|
124
|
+
if (piece) {
|
|
125
|
+
append(piece);
|
|
126
|
+
started = true;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const result = fields.filter((f, i) => f !== '' || (started && i === fields.length - 1 && fields.length === 1));
|
|
131
|
+
if (!started && result.every((f) => !f)) return [];
|
|
132
|
+
if (options.glob === false || !globbable) return result;
|
|
133
|
+
return result.flatMap((field) => {
|
|
134
|
+
if (!GLOB_CHARS.test(field)) return [field];
|
|
135
|
+
const hits = glob(ctx.fs, ctx.cwd, field);
|
|
136
|
+
return hits.length ? hits : [field];
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Expand every word of a command line. */
|
|
141
|
+
export async function expandWords(words: Word[], ctx: ExpandContext): Promise<string[]> {
|
|
142
|
+
const out: string[] = [];
|
|
143
|
+
for (const word of words) out.push(...(await expandWord(word, ctx)));
|
|
144
|
+
return out;
|
|
145
|
+
}
|