diff options
| author | Agando <roduccomm@gmail.com> | 2026-05-27 06:00:43 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-05-27 06:00:43 +0200 |
| commit | 212708b464c2bdbdd9670c00804623dca0729d2c (patch) | |
| tree | ee190cbb0d1524350baea641df65aa19817744b5 /modules/ext/busybox.js | |
| parent | f4d934d341056142da1212c845eeeb170fd452c2 (diff) | |
auto: 2026-05-27_06:00 changes
Diffstat (limited to 'modules/ext/busybox.js')
| -rw-r--r-- | modules/ext/busybox.js | 407 |
1 files changed, 407 insertions, 0 deletions
diff --git a/modules/ext/busybox.js b/modules/ext/busybox.js new file mode 100644 index 0000000..f143087 --- /dev/null +++ b/modules/ext/busybox.js @@ -0,0 +1,407 @@ +class BusyBoxModule { + constructor() { + this.commands = {}; + this.terminal = null; + } + + requirements() { + return { met: true, missing: [] }; + } + + getHelpInfo() { + return { + 'which': 'which <name> — Locate a command in the module registry', + 'env': 'env — List environment variables (webx11_ keys + user profile)', + 'date': 'date — Show current date/time (ISO, local, UTC, timestamp)', + 'cal': 'cal [month] [year] — Display an ASCII calendar', + 'yes': 'yes [string] — Output repeated string (default "y", capped at 50)', + 'seq': 'seq <last> or seq <first> <last> — Generate sequence of numbers', + 'base64': 'base64 [-d] <string> — Base64 encode/decode', + 'printf': 'printf <format> [args...] — Formatted print (%s, %d, %f, \\n, \\t)', + 'cut': 'cut -f<N> [-d<delim>] <file> — Extract column from file', + 'uniq': 'uniq <file> — Filter adjacent duplicate lines from a file', + 'hostname': 'hostname — Show host/user-agent info', + 'whoami': 'whoami — Show current user name', + 'basename': 'basename <path> — Strip directory from path', + 'dirname': 'dirname <path> — Strip filename from path', + 'sleep': 'sleep <seconds> — Delay execution with countdown', + 'expand': 'expand <file> — Convert tabs to spaces (default 4) in file', + }; + } + + registerCommands() { + this.commands['which'] = (args) => this._cmdWhich(args); + this.commands['env'] = () => this._cmdEnv(); + this.commands['date'] = (args) => this._cmdDate(args); + this.commands['cal'] = (args) => this._cmdCal(args); + this.commands['yes'] = (args) => this._cmdYes(args); + this.commands['seq'] = (args) => this._cmdSeq(args); + this.commands['base64'] = (args) => this._cmdBase64(args); + this.commands['printf'] = (args) => this._cmdPrintf(args); + this.commands['cut'] = (args) => this._cmdCut(args); + this.commands['uniq'] = (args) => this._cmdUniq(args); + this.commands['hostname'] = () => this._cmdHostname(); + this.commands['whoami'] = () => this._cmdWhoami(); + this.commands['basename'] = (args) => this._cmdBasename(args); + this.commands['dirname'] = (args) => this._cmdDirname(args); + this.commands['sleep'] = (args) => this._cmdSleep(args); + this.commands['expand'] = (args) => this._cmdExpand(args); + } + + // ── which ── + + _cmdWhich(args) { + if (!args[0]) return this.terminal.print('Usage: which <name>', 'warning'); + const query = args[0].toLowerCase(); + const all = this.terminal.moduleManager.getAllCommands(); + const keys = Object.keys(all); + + if (all[query]) { + const mod = this.terminal.moduleManager._findModuleForCommand(query); + const src = mod ? ` (from ${mod.constructor.name})` : ''; + return this.terminal.print(`${query}${src}`, 'info'); + } + + // Fuzzy match: check Levenshtein distance up to 2 + let best = null, bestDist = 3; + for (const k of keys) { + const d = this._levenshtein(query, k); + if (d < bestDist) { best = k; bestDist = d; } + } + if (best) { + this.terminal.print(`which: no '${query}' in registry`, 'error'); + this.terminal.print(`Did you mean '${best}'?`, 'info'); + } else { + this.terminal.print(`which: no '${query}' in registry`, 'error'); + } + } + + _levenshtein(a, b) { + const m = a.length, n = b.length; + const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); + for (let i = 0; i <= m; i++) dp[i][0] = i; + for (let j = 0; j <= n; j++) dp[0][j] = j; + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i][j] = a[i-1] === b[j-1] ? dp[i-1][j-1] + : 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]); + } + } + return dp[m][n]; + } + + // ── env ── + + _cmdEnv() { + const lines = []; + // webx11_ localStorage keys + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (k && k.startsWith('webx11_')) { + let v = localStorage.getItem(k) || ''; + if (v.length > 80) v = v.substring(0, 77) + '...'; + lines.push(`${k}=${v}`); + } + } + // Runtime info + if (this.terminal.ollamaManager) { + lines.push(`OLLAMA_MODEL=${this.terminal.ollamaManager.currentModel || 'none'}`); + lines.push(`OLLAMA_CONNECTED=${this.terminal.ollamaManager.connected}`); + } + lines.push(`PWD=/${this.terminal.fs.currentPath.join('/')}`); + lines.push(`SHELL=webx11-terminal`); + lines.push(`USER=darkbot`); + lines.push(`HOME=/`); + lines.push(`TERM=xterm-256color`); + const prof = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + if (prof && prof._userProfile) { + const facts = prof._userProfile.knownFacts; + if (facts) { + for (const [k, v] of Object.entries(facts)) { + lines.push(`USER_${k.toUpperCase()}=${v}`); + } + } + } + this.terminal.print(lines.join('\n'), 'info'); + } + + // ── date ── + + _cmdDate(args) { + const n = new Date(); + if (args.length > 0) { + const flag = args[0].toLowerCase(); + if (flag === '-u' || flag === '--utc' || flag === 'utc') + return this.terminal.print(n.toUTCString(), 'info'); + if (flag === '-i' || flag === '--iso' || flag === 'iso') + return this.terminal.print(n.toISOString(), 'info'); + if (flag === '-t' || flag === '--ts' || flag === 'timestamp') + return this.terminal.print(String(n.getTime()), 'info'); + if (flag === '-r' || flag === '--rfc' || flag === 'rfc') + return this.terminal.print(n.toUTCString(), 'info'); + } + const local = n.toLocaleString('en-US', { + weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', + hour: '2-digit', minute: '2-digit', second: '2-digit', + timeZoneName: 'short' + }); + this.terminal.print(local, 'info'); + this.terminal.print(`ISO: ${n.toISOString()}`, 'info'); + this.terminal.print(`UTC: ${n.toUTCString()}`, 'info'); + this.terminal.print(`Unix: ${Math.floor(n.getTime() / 1000)}`, 'info'); + } + + // ── cal ── + + _cmdCal(args) { + const now = new Date(); + let year = now.getFullYear(); + let month = now.getMonth() + 1; + + if (args.length >= 2) { + month = parseInt(args[0]); + year = parseInt(args[1]); + } else if (args.length === 1) { + // If only one arg, could be year (4 digits) or month + const v = parseInt(args[0]); + if (args[0].length === 4 || v > 12) { + year = v; + } else { + month = v; + } + } + + if (isNaN(month) || isNaN(year) || month < 1 || month > 12) { + return this.terminal.print('cal: invalid month or year', 'error'); + } + + const monthNames = ['January','February','March','April','May','June', + 'July','August','September','October','November','December']; + + const first = new Date(year, month - 1, 1); + const last = new Date(year, month, 0); + const daysInMonth = last.getDate(); + const startDay = first.getDay(); // 0=Sun + + const header = ` ${monthNames[month-1]} ${year}`; + const weekdays = 'Su Mo Tu We Th Fr Sa'; + + let cal = header + '\n' + weekdays + '\n'; + + let line = ' '.repeat(startDay); + for (let d = 1; d <= daysInMonth; d++) { + line += String(d).padStart(2) + ' '; + if ((startDay + d) % 7 === 0 || d === daysInMonth) { + cal += line.trimEnd() + '\n'; + line = ''; + } + } + + this.terminal.print(cal, 'info'); + } + + // ── yes ── + + _cmdYes(args) { + const text = args.length > 0 ? args.join(' ') : 'y'; + const count = 50; + for (let i = 0; i < count; i++) { + this.terminal.print(text); + } + this.terminal.print(`(repeated ${count} times, capped)`, 'text-muted'); + } + + // ── seq ── + + _cmdSeq(args) { + if (args.length === 0) return this.terminal.print('Usage: seq <last> or seq <first> <last>', 'warning'); + let first = 1, last; + if (args.length === 1) { + last = parseInt(args[0]); + } else { + first = parseInt(args[0]); + last = parseInt(args[1]); + } + if (isNaN(first) || isNaN(last)) return this.terminal.print('seq: non-integer argument', 'error'); + const max = 1000; + if (last - first > max) return this.terminal.print(`seq: range too large (max ${max})`, 'error'); + const output = []; + if (first <= last) { + for (let i = first; i <= last; i++) output.push(String(i)); + } else { + for (let i = first; i >= last; i--) output.push(String(i)); + } + this.terminal.print(output.join('\n'), 'info'); + } + + // ── base64 ── + + _cmdBase64(args) { + if (args.length === 0) return this.terminal.print('Usage: base64 [-d] <string>', 'warning'); + if (args[0] === '-d' || args[0] === '--decode') { + if (args.length < 2) return this.terminal.print('Usage: base64 -d <encoded>', 'warning'); + const encoded = args.slice(1).join(' '); + try { + const decoded = atob(encoded); + this.terminal.print(decoded, 'info'); + } catch (e) { + this.terminal.print('base64: invalid input (not valid base64)', 'error'); + } + } else { + const input = args.join(' '); + this.terminal.print(btoa(input), 'info'); + } + } + + // ── printf ── + + _cmdPrintf(args) { + if (args.length === 0) return this.terminal.print('Usage: printf <format> [args...]', 'warning'); + let fmt = args[0]; + const vals = args.slice(1); + let vi = 0; + // Replace format specifiers step by step + const result = fmt + .replace(/\\n/g, '\n') + .replace(/\\t/g, '\t') + .replace(/\\\\/g, '\\') + .replace(/%s/g, () => vals[vi++] || '') + .replace(/%d/g, () => { + const v = parseInt(vals[vi++]); + return isNaN(v) ? '0' : String(v); + }) + .replace(/%f/g, () => { + const v = parseFloat(vals[vi++]); + return isNaN(v) ? '0.000000' : v.toFixed(6); + }) + .replace(/%[^sdf%]/g, (m) => `(unsupported: ${m})`); + this.terminal.print(result, 'info'); + } + + // ── cut ── + + _cmdCut(args) { + if (args.length < 2) return this.terminal.print('Usage: cut -f<N> [-d<delim>] <file>', 'warning'); + let field = null, delim = '\t', file = ''; + for (const a of args) { + if (a.startsWith('-f')) field = parseInt(a.substring(2)); + else if (a.startsWith('-d')) delim = a.substring(2); + else file = a; + } + if (!field || isNaN(field)) return this.terminal.print('cut: -f flag required', 'error'); + if (!file) return this.terminal.print('cut: file argument required', 'error'); + + const content = this.terminal.fs.readFile(file); + if (content === null) return this.terminal.print(`cut: ${file}: No such file`, 'error'); + + const lines = content.split('\n').filter(l => l); + const output = []; + for (const line of lines) { + const parts = line.split(delim); + const idx = field - 1; + if (idx >= 0 && idx < parts.length) { + output.push(parts[idx]); + } + } + this.terminal.print(output.join('\n'), 'info'); + } + + // ── uniq ── + + _cmdUniq(args) { + if (!args[0]) return this.terminal.print('Usage: uniq <file>', 'warning'); + const content = this.terminal.fs.readFile(args[0]); + if (content === null) return this.terminal.print(`uniq: ${args[0]}: No such file`, 'error'); + const lines = content.split('\n'); + let last = null; + const output = []; + for (const line of lines) { + if (line !== last) { + output.push(line); + last = line; + } + } + this.terminal.print(output.join('\n'), 'info'); + } + + // ── hostname ── + + _cmdHostname() { + const ua = navigator.userAgent; + const space = 'newos4'; + const platform = navigator.platform || 'unknown'; + const version = 'WebX11 v4.2'; + this.terminal.print(`Hostname: ${space}`, 'info'); + this.terminal.print(`Platform: ${version} on ${platform}`, 'info'); + this.terminal.print(`User Agent: ${ua}`, 'info'); + } + + // ── whoami ── + + _cmdWhoami() { + // Try to get from user profile + let name = 'darkbot'; + try { + const prof = this.terminal.moduleManager.modules['WebX11AIAssistantModule']; + if (prof && prof._userProfile && prof._userProfile.knownFacts && prof._userProfile.knownFacts.name) { + name = prof._userProfile.knownFacts.name; + } + } catch (e) {} + this.terminal.print(name, 'info'); + } + + // ── basename ── + + _cmdBasename(args) { + if (!args[0]) return this.terminal.print('Usage: basename <path>', 'warning'); + const parts = args[0].split('/').filter(p => p); + const result = parts.pop() || '/'; + this.terminal.print(result, 'info'); + } + + // ── dirname ── + + _cmdDirname(args) { + if (!args[0]) return this.terminal.print('Usage: dirname <path>', 'warning'); + const parts = args[0].split('/').filter(p => p); + if (parts.length <= 1) { + this.terminal.print('.', 'info'); + return; + } + parts.pop(); + const result = args[0].startsWith('/') ? '/' + parts.join('/') : parts.join('/'); + this.terminal.print(result || '.', 'info'); + } + + // ── sleep ── + + _cmdSleep(args) { + if (!args[0]) return this.terminal.print('Usage: sleep <seconds>', 'warning'); + const secs = parseInt(args[0]); + if (isNaN(secs) || secs < 1) return this.terminal.print('sleep: invalid duration', 'error'); + const max = 30; + const actual = Math.min(secs, max); + if (actual !== secs) this.terminal.print(`sleep: capped to ${max}s (from ${secs}s)`, 'warning'); + this.terminal.print(`Sleeping ${actual}s...`, 'info'); + let remaining = actual; + const interval = setInterval(() => { + remaining--; + if (remaining > 0) { + this.terminal.print(`${remaining}s...`, 'info'); + } else { + clearInterval(interval); + this.terminal.print('Done', 'success'); + } + }, 1000); + } + + // ── expand ── + + _cmdExpand(args) { + if (!args[0]) return this.terminal.print('Usage: expand <file>', 'warning'); + const content = this.terminal.fs.readFile(args[0]); + if (content === null) return this.terminal.print(`expand: ${args[0]}: No such file`, 'error'); + const expanded = content.replace(/\t/g, ' '); + this.terminal.print(expanded, 'info'); + } +} |