From 1b35dfa9ae70edbac0d474c79578661f973877e6 Mon Sep 17 00:00:00 2001 From: Agando Date: Tue, 29 Sep 2026 13:11:48 +0200 Subject: newOS4 v4.6.12: rebuilt bundles with tail -f and sed --- modules/core/core-bundle.js | 171 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 159 insertions(+), 12 deletions(-) (limited to 'modules/core') diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js index 387afdf..cfdfe11 100644 --- a/modules/core/core-bundle.js +++ b/modules/core/core-bundle.js @@ -2436,7 +2436,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']; this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff']; this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script']; - this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto']; + this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','sed','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto']; this.destructiveCmds = new Set(this._baseDestructive); this.systemCmds = new Set(this._baseSystem); this.suspiciousFlags = new Set(this._baseSuspiciousFlags); @@ -2719,6 +2719,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm this.registerConfigItem('terminal-sysmsg', true, (v) => { this._showSystemMessages = !!v; }); this._pipeInput = null; this._pipeBufferSize = 5000; + this._activeFollow = null; this._historyBuffer = []; this._historyMaxLines = 50; this._historySavePending = false; @@ -2776,6 +2777,15 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm _invalidateCommandCache() { if (this.security) this.security.invalidateCommandCache(); } + stopFollow(msg) { + if (!this._activeFollow) return false; + clearInterval(this._activeFollow.interval); + const f = this._activeFollow.file; + this._activeFollow = null; + if (msg) this.print(msg, 'warning'); + else this.print('^C — stopped following ' + f, 'warning'); + return true; + } initEventListeners() { this.inputField.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { @@ -2838,13 +2848,16 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm // Ctrl+L — clear output (standard terminal) e.preventDefault(); this.clearOutput(); - } else if (e.ctrlKey && (e.key === 'c' || e.key === 'C') && this.inputField.value) { - // Ctrl+C — cancel current input line (standard terminal) + } else if (e.ctrlKey && (e.key === 'c' || e.key === 'C')) { + // Ctrl+C — stop tail -f if active, else cancel current input line (standard terminal) e.preventDefault(); - this.print('^C', 'warning'); - this.inputField.value = ''; - this.inputField.style.height = 'auto'; - this.hideAutocomplete(); + if (this.stopFollow()) return; + if (this.inputField.value) { + this.print('^C', 'warning'); + this.inputField.value = ''; + this.inputField.style.height = 'auto'; + this.hideAutocomplete(); + } return; } else if (e.ctrlKey && (e.key === 'u' || e.key === 'U')) { // Ctrl+U — clear line from cursor to start (standard terminal) @@ -2854,6 +2867,8 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm } else if (e.key === 'Escape') { if (this._aiBusy) { this.executeCommand('ai-stop'); + } else if (this.stopFollow()) { + // tail -f stopped } else { this.hideAutocomplete(); } @@ -6154,7 +6169,8 @@ class FileSystemModule { 'find': 'find — Search files by name pattern', 'grep': 'grep [file] — Search file content for pattern', 'head': 'head [lines] — Show first N lines (default: 10)', - 'tail': 'tail [lines] — Show last N lines (default: 10)', + 'tail': 'tail [-f] [-n N] — Show last N lines; -f follows live changes (Ctrl+C/Esc to stop)', + 'sed': 'sed [file] — Stream editor; s/pat/rep/[g][i][p], /pat/d, Nd, $d; -i write in place; -n quiet mode', 'sort': 'sort — Sort lines of a file alphabetically', 'wc': 'wc — Count lines, words, and bytes', 'touch': 'touch — Create empty file or update timestamp', @@ -6363,11 +6379,142 @@ class FileSystemModule { this.terminal.print(c.split('\n').slice(0, n).join('\n')); }; this.commands['tail'] = (args) => { - if(!args[0]) return this.terminal.print('Usage: tail [lines]', 'warning'); - const c = this.terminal.fs.readFile(args[0]); - if(c === null || c === undefined) return this.terminal.print('tail: ' + args[0] + ': No such file', 'error'); - const n = args[1] ? parseInt(args[1]) : 10; + const self = this; + let follow = false, n = 10; + const positional = []; + for (let i = 0; i < (args || []).length; i++) { + const a = args[i]; + if (a === '-f' || a === '--follow') follow = true; + else if (a === '-n') { + const v = args[++i]; + if (v === undefined || !/^\d+$/.test(v)) return this.terminal.print('tail: -n requires a number', 'error'); + n = parseInt(v, 10); + } + else if (/^-\d+$/.test(a)) n = parseInt(a.slice(1), 10); + else if (a === '-c' || a === '-q' || a === '-v' || a === '--pid') return this.terminal.print('tail: option ' + a + ' not supported', 'error'); + else if (a.startsWith('-')) return this.terminal.print('tail: invalid option: ' + a, 'error'); + else positional.push(a); + } + const file = positional[0]; + if (!file) return this.terminal.print('Usage: tail [-f] [-n N] — last N lines; -f follows changes (Ctrl+C or Esc to stop)', 'warning'); + if (positional[1] && /^\d+$/.test(positional[1])) n = parseInt(positional[1], 10); // legacy back-compat: tail + const c = this.terminal.fs.readFile(file); + if (c === null || c === undefined) return this.terminal.print('tail: ' + file + ': No such file', 'error'); this.terminal.print(c.split('\n').slice(-n).join('\n')); + if (!follow) return; + // stop any previous follow before starting a new one + if (this.terminal._activeFollow && this.terminal._activeFollow.interval) clearInterval(this.terminal._activeFollow.interval); + this.terminal._activeFollow = null; + let offset = c.length; + const interval = setInterval(() => { + const cur = self.terminal.fs.readFile(file); + if (cur === null || cur === undefined) { + self.terminal.stopFollow('tail: ' + file + ': file removed — follow stopped'); + return; + } + if (cur.length < offset) { + self.terminal.print('tail: ' + file + ': file truncated — following from start', 'warning'); + self.terminal.print(cur); + offset = cur.length; + return; + } + if (cur.length > offset) { + self.terminal.print(cur.slice(offset)); + offset = cur.length; + } + }, 1500); + this.terminal._activeFollow = { interval, file }; + this.terminal.print('tail: following ' + file + ' (Ctrl+C or Esc to stop)', 'text-muted'); + }; + this.commands['sed'] = (args) => { + if (!args || args.length === 0) { + return this.terminal.print('Usage: sed [file] — expressions: s/pat/rep/[g][i][p], /pat/d, Nd, $d; flags: -i in place, -n quiet', 'warning'); + } + let inPlace = false, quiet = false, expr = null, file = null; + for (let i = 0; i < args.length; i++) { + const a = args[i]; + if (a === '-i') inPlace = true; + else if (a === '-n') quiet = true; + else if (expr === null) expr = a; + else if (file === null) file = a; + else return this.terminal.print('sed: too many arguments: ' + a, 'error'); + } + if (!expr) return this.terminal.print('sed: missing expression', 'error'); + let content; + if (file) { + content = this.terminal.fs.readFile(file); + if (content === null || content === undefined) return this.terminal.print('sed: ' + file + ': No such file', 'error'); + } else if (this.terminal._pipeInput !== null && this.terminal._pipeInput !== undefined) { + content = this.terminal._pipeInput; // piped input: cat x | sed s/a/b/ + } else { + return this.terminal.print('sed: missing file operand (no piped input)', 'error'); + } + let subst = null, delMatch = null, delLine = null; + if (expr.startsWith('s/') && expr.length > 2) { + const parts = []; let cur = ''; + for (let i = 2; i < expr.length; i++) { + const ch = expr[i]; + if (ch === '\\' && expr[i + 1] === '/') { cur += '/'; i++; } + else if (ch === '/') { parts.push(cur); cur = ''; } + else cur += ch; + } + parts.push(cur); + if (parts.length < 2 || parts.length > 3) return this.terminal.print('sed: malformed expression: ' + expr, 'error'); + const flags = parts[2] || ''; + if ([...flags].some(c => !'gip'.includes(c))) return this.terminal.print('sed: unsupported flags: ' + flags, 'error'); + subst = { pat: parts[0], rep: parts[1], flags }; + } else if (/^\/(.+)\/d$/.test(expr)) { + delMatch = expr.slice(1, -2); + } else if (/^(\d+|\$)d$/.test(expr)) { + delLine = expr.slice(0, -1); + } else { + return this.terminal.print('sed: unsupported expression: ' + expr, 'error'); + } + let lines = content.split('\n'); + let trailingNL = false; + if (lines.length && lines[lines.length - 1] === '') { trailingNL = true; lines.pop(); } + const outLines = []; + let changed = 0; + if (subst) { + let re; + try { + re = new RegExp(subst.pat, (subst.flags.includes('i') ? 'i' : '') + (subst.flags.includes('g') ? 'g' : '')); + } catch (e) { + return this.terminal.print('sed: invalid regex: ' + e.message, 'error'); + } + for (const line of lines) { + const after = line.replace(re, subst.rep); + if (after !== line) changed++; + if (quiet) { + if (subst.flags.includes('p') && after !== line) this.terminal.print(after); + } else { + outLines.push(after); + } + } + } else if (delMatch !== null) { + let re; + try { re = new RegExp(delMatch); } catch (e) { return this.terminal.print('sed: invalid regex: ' + e.message, 'error'); } + for (const line of lines) { + if (re.test(line)) changed++; + else outLines.push(line); + } + } else { + const idx = delLine === '$' ? lines.length - 1 : parseInt(delLine, 10) - 1; + if (idx < 0 || idx >= lines.length) return this.terminal.print('sed: line number out of range: ' + delLine, 'error'); + for (let i = 0; i < lines.length; i++) { + if (i === idx) changed++; + else outLines.push(lines[i]); + } + } + if (inPlace) { + if (!file) return this.terminal.print('sed: -i requires a file operand', 'error'); + this.terminal.fs.writeFile(file, outLines.join('\n') + (trailingNL ? '\n' : '')); + this.terminal.print('sed: updated ' + file + ' (' + changed + ' line' + (changed === 1 ? '' : 's') + ' changed)', 'success'); + } else if (quiet) { + if (changed === 0) this.terminal.print('(no changes)', 'text-muted'); + } else { + this.terminal.print(outLines.join('\n')); + } }; this.commands['sort'] = (args) => { if(!args[0]) return this.terminal.print('Usage: sort ', 'warning'); -- cgit v1.2.3