aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/busybox.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/busybox.js')
-rw-r--r--modules/ext/busybox.js407
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');
+ }
+}