diff options
| -rw-r--r-- | CHANGELOG.md | 53 | ||||
| -rw-r--r-- | main.html | 6 | ||||
| -rw-r--r-- | modules/core/module-loader.js | 2 | ||||
| -rw-r--r-- | modules/ext/critical-ext-bundle.js | 73 | ||||
| -rw-r--r-- | modules/ext/shell.js | 73 | ||||
| -rw-r--r-- | modules/test/test-pipe-guard.js | 10 |
6 files changed, 166 insertions, 51 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d8e3f..c978a56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,16 +74,67 @@ Generated from git history by `scripts/build-changelog.py` — do not edit by ha Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + +# newOS4 changelog + +Generated from git history by `scripts/build-changelog.py` — do not edit by hand between the markers. + <!-- generated:start --> +## v4.6.131 — 2026-10-29 + +_Touches modules (8), scripts (3), help.html (1), main.html (1) · fix 1_ + +- `Pipe stage quoting, maintain ordering, help cost row, plan status (v4.6.131)` `2c6c218` + +## v4.6.130 — 2026-10-29 + +_Touches modules (5), main.html (1) · fix 1_ + +- `Js keeps its string literals — dispatcher forwards the raw command line (v4.6.130)` `161372f` + +## v4.6.128 — 2026-10-29 + +_Touches modules (2), help.html (1), main.html (1) · fix 1_ + +- `Js console handles promise rejections + Node-builtin hint (v4.6.128)` `75c5113` + ## v4.6.119 — 2026-10-27 -_Touches modules (4), scripts (4), CHANGELOG.md (2), main.html (1) · release 3, fix 1_ +_Touches scripts (6), modules (4), CHANGELOG.md (3), guide.html (2) · release 3, fix 2_ - `if test …` was always false and `for` items never globbed` `e575ee8` - `Generate the changelog from git and keep it current in maintenance` `22bb505` - `Link the changelog in and deep-link each board fix to its build` `b26bfb8` - `Tag builds, feed the changelog to machines, fail maintenance when it ages` `782b3db` +- `Apply the GLM-5.3 review to the changelog pipeline` `f3b2664` ## v4.6.118 — 2026-10-27 @@ -1349,13 +1349,13 @@ </script> <!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix --> -<script src="modules/core/core-bundle.js?v=v4.6.131"></script> +<script src="modules/core/core-bundle.js?v=v4.6.132"></script> <!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules --> -<script src="modules/core/module-loader.js?v=v4.6.131"></script> +<script src="modules/core/module-loader.js?v=v4.6.132"></script> <!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work --> -<script src="modules/ext/critical-ext-bundle.js?v=v4.6.131"></script> +<script src="modules/ext/critical-ext-bundle.js?v=v4.6.132"></script> <script> // ═══ ONE TRUE VERSION SOURCE ═══ diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js index 3e50c3e..0d39123 100644 --- a/modules/core/module-loader.js +++ b/modules/core/module-loader.js @@ -4,7 +4,7 @@ // Core module bundle loaded synchronously in main.html. // This loader only handles version cache-busting, splash, terminal init, and optional ext modules. - const LATEST_VER = 'v4.6.131'; + const LATEST_VER = 'v4.6.132'; const CACHE = '?v=' + LATEST_VER; // Exposed so ext modules can register their own URL with the same cache-busting version // instead of an unversioned path the browser may already have cached (shell.js did). diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js index 00d7345..0f2e1b9 100644 --- a/modules/ext/critical-ext-bundle.js +++ b/modules/ext/critical-ext-bundle.js @@ -244,12 +244,14 @@ class ShellModule { await this.executePipeChain(pipeParts); } else { // Single command — check for shell built-ins first - const parts = chainCmd.split(/\s+/); - const cmd = parts[0].toLowerCase(); + // v4.6.132: quote-aware (split(/\s+/) broke `grep "a b" f; ls`) + const argv = self._tokenize(chainCmd); + if (!argv.length) continue; + const cmd = argv[0].toLowerCase(); // Globs expand at the prompt too — the script runner did it and the // interactive path did not, so `ls *.txt` printed (empty) but the same // line inside a script worked. - const args = self._expandGlobs(parts.slice(1), (this.fs && this.fs.currentPath) || []); + const args = self._expandGlobs(argv.slice(1), (this.fs && this.fs.currentPath) || []); const expandedCmd = [cmd].concat(args).join(' '); if (self.commands[cmd]) { try { @@ -284,9 +286,14 @@ class ShellModule { let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const cmdText = pipeParts[i]; - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - let args = parts.slice(1); + // v4.6.132: quote-aware tokenisation. split(/\s+/) turned `grep "a b" f | wc -l` + // into args ['"a','b"','f'], and `cmd | | cmd2` into a stage with cmd '' that was + // still dispatched. The security tokeniser keeps a quoted argument whole and + // returns [] for an empty stage, so the guard below can skip it. + const argv = this.security._parseArgs(String(cmdText || '').trim()); + if (argv.length === 0) continue; + const cmd = argv[0].toLowerCase(); + let args = argv.slice(1); // Security gate — replicate the original executePipeChain checks const cls = this.security._classify(cmdText); @@ -339,6 +346,20 @@ class ShellModule { // ── Tokenizer helpers ── + /** + * Split a command line into argv, honouring quotes — the same parser the dispatcher uses + * (`security._parseArgs`), so the shell paths and the bare-command path cannot drift. + * v4.6.132: every `.split(/\s+/)` call site in this file turned a quoted argument into several, + * and dispatched an empty stage as a command named ''. + */ + _tokenize(cmdText) { + const src = String(cmdText || '').trim(); + if (!src) return []; + const sec = (this.terminal && this.terminal.security) || this.security; + if (sec && typeof sec._parseArgs === 'function') return sec._parseArgs(src); + return src.split(/\s+/); // defensive: the terminal always exposes security + } + _splitSequence(input) { // Split on ; (not inside quotes) const parts = []; @@ -486,9 +507,11 @@ class ShellModule { } this.terminal._pipeInput = inputContent; try { - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); + // v4.6.132: quote-aware — `cat < "/my file.txt"` and `grep "a b" f < x` lost the + // quoted argument to split(/\s+/). + const argv = this._tokenize(cmdText); + const cmd = (argv[0] || '').toLowerCase(); + const args = argv.slice(1); // If command reads files, inject pipe content as temp file if (this._isPipeAware(cmd)) { fs.writeFile('__pipebuf__', inputContent); @@ -529,9 +552,12 @@ class ShellModule { let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const pText = pipeParts[i]; - const pParts = pText.split(/\s+/); - const pCmd = pParts[0].toLowerCase(); - const pArgs = pParts.slice(1); + // v4.6.132: tokenise like the interactive path (and keep the raw text, so a + // `js` stage in a script keeps its string literals too). + const pArgv = this._tokenize(pText); + if (!pArgv.length) continue; + const pCmd = pArgv[0].toLowerCase(); + const pArgs = pArgv.slice(1); if (pipeInput !== null) { fs.writeFile('__pipebuf__', pipeInput); } @@ -539,23 +565,24 @@ class ShellModule { let capt = ''; this.terminal._pushPrintCapture(buf => { capt += buf; }); try { - await this.terminal.dispatchCommand(pCmd, pArgs); + await this.terminal.dispatchCommand(pCmd, pArgs, pText); } finally { this.terminal._popPrintCapture(); } pipeInput = capt.trim(); } else { - await this.terminal.dispatchCommand(pCmd, pArgs); + await this.terminal.dispatchCommand(pCmd, pArgs, pText); } if (fs.readFile('__pipebuf__') !== null) { delete fs.root.content['__pipebuf__']; } } } else { - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); - await this.terminal.dispatchCommand(cmd, args); + // v4.6.132: quote-aware, and pass the raw text so `js`/`node -e` keep literals here too + const argv = this._tokenize(cmdText); + const cmd = (argv[0] || '').toLowerCase(); + const args = argv.slice(1); + await this.terminal.dispatchCommand(cmd, args, cmdText); } } catch(e) { cmdOk = false; @@ -884,7 +911,8 @@ class ShellModule { .trim(); if (!t) return false; const fs = this.terminal.fs; - const parts = t.split(/\s+/); + // v4.6.132: quote-aware — `[ -f "/my file.txt" ]` was false because the path was split. + const parts = this._tokenize(t); const p = parts[1]; switch (parts[0]) { case '-f': return typeof fs.readFile(p) === 'string'; @@ -982,10 +1010,11 @@ class ShellModule { } async _dispatchOne(cmdText, env, ctx) { - const parts = String(cmdText).trim().split(/\s+/); - const cmd = (parts[0] || '').toLowerCase(); + // v4.6.132: quote-aware tokenisation (split(/\s+/) split `grep "a b" f` into three args) + const argv = this._tokenize(cmdText); + const cmd = (argv[0] || '').toLowerCase(); const cwd = (this.terminal.fs && this.terminal.fs.currentPath) || []; - const args = this._expandGlobs(parts.slice(1), cwd); + const args = this._expandGlobs(argv.slice(1), cwd); if (this.commands[cmd]) { try { this.terminal.markDirty(); await this.commands[cmd](args); return true; } catch (e) { this.terminal.print('Error: ' + e.message, 'error'); return false; } diff --git a/modules/ext/shell.js b/modules/ext/shell.js index 8640630..cb26484 100644 --- a/modules/ext/shell.js +++ b/modules/ext/shell.js @@ -108,12 +108,14 @@ class ShellModule { await this.executePipeChain(pipeParts); } else { // Single command — check for shell built-ins first - const parts = chainCmd.split(/\s+/); - const cmd = parts[0].toLowerCase(); + // v4.6.132: quote-aware (split(/\s+/) broke `grep "a b" f; ls`) + const argv = self._tokenize(chainCmd); + if (!argv.length) continue; + const cmd = argv[0].toLowerCase(); // Globs expand at the prompt too — the script runner did it and the // interactive path did not, so `ls *.txt` printed (empty) but the same // line inside a script worked. - const args = self._expandGlobs(parts.slice(1), (this.fs && this.fs.currentPath) || []); + const args = self._expandGlobs(argv.slice(1), (this.fs && this.fs.currentPath) || []); const expandedCmd = [cmd].concat(args).join(' '); if (self.commands[cmd]) { try { @@ -148,9 +150,14 @@ class ShellModule { let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const cmdText = pipeParts[i]; - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - let args = parts.slice(1); + // v4.6.132: quote-aware tokenisation. split(/\s+/) turned `grep "a b" f | wc -l` + // into args ['"a','b"','f'], and `cmd | | cmd2` into a stage with cmd '' that was + // still dispatched. The security tokeniser keeps a quoted argument whole and + // returns [] for an empty stage, so the guard below can skip it. + const argv = this.security._parseArgs(String(cmdText || '').trim()); + if (argv.length === 0) continue; + const cmd = argv[0].toLowerCase(); + let args = argv.slice(1); // Security gate — replicate the original executePipeChain checks const cls = this.security._classify(cmdText); @@ -203,6 +210,20 @@ class ShellModule { // ── Tokenizer helpers ── + /** + * Split a command line into argv, honouring quotes — the same parser the dispatcher uses + * (`security._parseArgs`), so the shell paths and the bare-command path cannot drift. + * v4.6.132: every `.split(/\s+/)` call site in this file turned a quoted argument into several, + * and dispatched an empty stage as a command named ''. + */ + _tokenize(cmdText) { + const src = String(cmdText || '').trim(); + if (!src) return []; + const sec = (this.terminal && this.terminal.security) || this.security; + if (sec && typeof sec._parseArgs === 'function') return sec._parseArgs(src); + return src.split(/\s+/); // defensive: the terminal always exposes security + } + _splitSequence(input) { // Split on ; (not inside quotes) const parts = []; @@ -350,9 +371,11 @@ class ShellModule { } this.terminal._pipeInput = inputContent; try { - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); + // v4.6.132: quote-aware — `cat < "/my file.txt"` and `grep "a b" f < x` lost the + // quoted argument to split(/\s+/). + const argv = this._tokenize(cmdText); + const cmd = (argv[0] || '').toLowerCase(); + const args = argv.slice(1); // If command reads files, inject pipe content as temp file if (this._isPipeAware(cmd)) { fs.writeFile('__pipebuf__', inputContent); @@ -393,9 +416,12 @@ class ShellModule { let pipeInput = null; for (let i = 0; i < pipeParts.length; i++) { const pText = pipeParts[i]; - const pParts = pText.split(/\s+/); - const pCmd = pParts[0].toLowerCase(); - const pArgs = pParts.slice(1); + // v4.6.132: tokenise like the interactive path (and keep the raw text, so a + // `js` stage in a script keeps its string literals too). + const pArgv = this._tokenize(pText); + if (!pArgv.length) continue; + const pCmd = pArgv[0].toLowerCase(); + const pArgs = pArgv.slice(1); if (pipeInput !== null) { fs.writeFile('__pipebuf__', pipeInput); } @@ -403,23 +429,24 @@ class ShellModule { let capt = ''; this.terminal._pushPrintCapture(buf => { capt += buf; }); try { - await this.terminal.dispatchCommand(pCmd, pArgs); + await this.terminal.dispatchCommand(pCmd, pArgs, pText); } finally { this.terminal._popPrintCapture(); } pipeInput = capt.trim(); } else { - await this.terminal.dispatchCommand(pCmd, pArgs); + await this.terminal.dispatchCommand(pCmd, pArgs, pText); } if (fs.readFile('__pipebuf__') !== null) { delete fs.root.content['__pipebuf__']; } } } else { - const parts = cmdText.split(/\s+/); - const cmd = parts[0].toLowerCase(); - const args = parts.slice(1); - await this.terminal.dispatchCommand(cmd, args); + // v4.6.132: quote-aware, and pass the raw text so `js`/`node -e` keep literals here too + const argv = this._tokenize(cmdText); + const cmd = (argv[0] || '').toLowerCase(); + const args = argv.slice(1); + await this.terminal.dispatchCommand(cmd, args, cmdText); } } catch(e) { cmdOk = false; @@ -748,7 +775,8 @@ class ShellModule { .trim(); if (!t) return false; const fs = this.terminal.fs; - const parts = t.split(/\s+/); + // v4.6.132: quote-aware — `[ -f "/my file.txt" ]` was false because the path was split. + const parts = this._tokenize(t); const p = parts[1]; switch (parts[0]) { case '-f': return typeof fs.readFile(p) === 'string'; @@ -846,10 +874,11 @@ class ShellModule { } async _dispatchOne(cmdText, env, ctx) { - const parts = String(cmdText).trim().split(/\s+/); - const cmd = (parts[0] || '').toLowerCase(); + // v4.6.132: quote-aware tokenisation (split(/\s+/) split `grep "a b" f` into three args) + const argv = this._tokenize(cmdText); + const cmd = (argv[0] || '').toLowerCase(); const cwd = (this.terminal.fs && this.terminal.fs.currentPath) || []; - const args = this._expandGlobs(parts.slice(1), cwd); + const args = this._expandGlobs(argv.slice(1), cwd); if (this.commands[cmd]) { try { this.terminal.markDirty(); await this.commands[cmd](args); return true; } catch (e) { this.terminal.print('Error: ' + e.message, 'error'); return false; } diff --git a/modules/test/test-pipe-guard.js b/modules/test/test-pipe-guard.js index 7851613..e689e60 100644 --- a/modules/test/test-pipe-guard.js +++ b/modules/test/test-pipe-guard.js @@ -43,10 +43,16 @@ const shellBody = si >= 0 ? shellSrc.slice(si, shellSrc.indexOf('_splitSequence( const shellForwardsRaw = si >= 0 && shellBody.indexOf('this.dispatchCommand(cmd, args, cmdText)') > 0 && shellBody.indexOf('this.capturePipeOutput(cmd, args, cmdText)') > 0; +// 5. v4.6.132: that override must tokenise with the quote-aware parser, not split(/\s+/), and must +// skip empty stages instead of dispatching cmd ''. +const shellTokensQuoted = si >= 0 + && shellBody.indexOf('this.security._parseArgs(String(cmdText') > 0 + && shellBody.indexOf('if (argv.length === 0) continue') > 0 + && shellBody.indexOf('cmdText.split(') < 0; -const report = { empty, blank, classifyGuarded, pipeGuarded, shellForwardsRaw }; +const report = { empty, blank, classifyGuarded, pipeGuarded, shellForwardsRaw, shellTokensQuoted }; console.log(JSON.stringify(report)); const ok = Array.isArray(empty) && empty.length === 0 && Array.isArray(blank) && blank.length === 0 - && classifyGuarded && pipeGuarded && shellForwardsRaw; + && classifyGuarded && pipeGuarded && shellForwardsRaw && shellTokensQuoted; process.exit(ok ? 0 : 1); |