aboutsummaryrefslogtreecommitdiff
path: root/modules/ext/critical-ext-bundle.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/ext/critical-ext-bundle.js')
-rw-r--r--modules/ext/critical-ext-bundle.js2363
1 files changed, 2363 insertions, 0 deletions
diff --git a/modules/ext/critical-ext-bundle.js b/modules/ext/critical-ext-bundle.js
new file mode 100644
index 0000000..f2cefbb
--- /dev/null
+++ b/modules/ext/critical-ext-bundle.js
@@ -0,0 +1,2363 @@
+// Critical ext module bundle — generated for v4.5.49// Loaded synchronously in main.html before module-loader.js.
+/* === editor.js === */
+class EditorModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._vfsPath = null;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'edit': 'Open the code editor for a VFS file (usage: edit <path>)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['edit'] = (args) => this.edit(args);
+ }
+ edit(args) {
+ if (!args[0]) return this.terminal.print('Usage: edit <path>', 'warning');
+ const path = args.join(' ');
+ const modal = document.getElementById('code-editor');
+ const title = document.getElementById('code-editor-title');
+ const textarea = document.getElementById('code-editor-textarea');
+ if (!modal || !textarea) return this.terminal.print('Editor modal not found', 'error');
+ // Read existing file content or use empty for new files
+ const content = this.terminal.fs.readFile(path);
+ const fileContent = (content !== null && content !== false) ? content : '';
+ // Set up modal
+ this._vfsPath = path;
+ title.textContent = '📝 Editing: ' + path;
+ textarea.value = fileContent;
+ modal.style.display = 'flex';
+ textarea.focus();
+ // Override save button to write back to VFS
+ const saveBtn = modal.querySelector('.modal-footer .btn:last-child');
+ const cancelBtn = modal.querySelector('.modal-footer .btn-danger');
+ if (saveBtn) {
+ saveBtn._origOnclick = saveBtn.onclick;
+ saveBtn.onclick = async () => {
+ const code = textarea.value;
+ const ok = this.terminal.fs.writeFile(this._vfsPath, code);
+ if (ok) {
+ this.terminal.print('✅ Saved to ' + this._vfsPath, 'success');
+ // Mirror to _mods/ for test_module compatibility
+ const modMatch = this._vfsPath.match(/(?:^|\/)(\w+)\.js$/);
+ if (modMatch) {
+ const modName = modMatch[1];
+ this.terminal.fs.mkdirRecursive('_mods');
+ this.terminal.fs.writeFile('_mods/' + modName + '.js', code);
+ if (!this.terminal.fs.readFile('_mods/' + modName + '.orig.js')) {
+ this.terminal.fs.writeFile('_mods/' + modName + '.orig.js', 'placeholder');
+ }
+ this.terminal.print('📋 Mirrored to _mods/' + modName + '.js for live testing', 'info');
+ }
+ // Notify tutorial module for quest 4 (edit + save)
+ if (this.terminal.tutorialModule) {
+ this.terminal.tutorialModule.onFileSaved(this._vfsPath);
+ }
+ } else {
+ this.terminal.print('❌ Failed to save ' + this._vfsPath, 'error');
+ }
+ modal.style.display = 'none';
+ this._vfsPath = null;
+ // Restore original onclick handler
+ if (saveBtn._origOnclick) saveBtn.onclick = saveBtn._origOnclick;
+ delete saveBtn._origOnclick;
+ };
+ }
+ if (cancelBtn) {
+ cancelBtn._origOnclick = cancelBtn.onclick;
+ cancelBtn.onclick = () => {
+ modal.style.display = 'none';
+ this._vfsPath = null;
+ if (cancelBtn._origOnclick) cancelBtn.onclick = cancelBtn._origOnclick;
+ delete cancelBtn._origOnclick;
+ };
+ }
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new EditorModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/editor.js');
+}
+
+/* === shell.js === */
+class ShellModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this._vars = {
+ HOME: '/',
+ USER: 'darkbot',
+ SHELL: 'webx11-shell',
+ TERM: 'xterm-256color',
+ PWD: '/'
+ };
+ this._scriptDepth = 0;
+ this._maxScriptDepth = 3;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'export': 'export <var>=<value> — Set a shell variable',
+ 'unset': 'unset <var> — Remove a shell variable',
+ 'script': 'script <path.sh> — Run a shell script from VFS',
+ '$': '$ <command> — Execute with shell parsing (default: auto-detected)',
+ 'sh': 'sh <path.sh> — Run a shell script from VFS (alias for script)',
+ 'set': 'set — List all shell variables',
+ };
+ }
+
+ registerCommands() {
+ this.commands['export'] = (args) => this._cmdExport(args);
+ this.commands['unset'] = (args) => this._cmdUnset(args);
+ this.commands['script'] = (args) => this._cmdScript(args);
+ this.commands['sh'] = (args) => this._cmdScript(args);
+ this.commands['set'] = () => this._cmdSet();
+ }
+
+ // ── Hook into terminal.executeCommand ──
+
+ init() {
+ // Guard: prevent double-wrapping if init is called multiple times
+ if (this._initialized) return;
+ this._initialized = true;
+
+ // Wrap executeCommand to add shell parsing — only for compound commands
+ const origExecute = this.terminal.executeCommand.bind(this.terminal);
+ const self = this;
+ this.terminal.executeCommand = async function(rawInput) {
+ if (!rawInput.trim()) return;
+ this.markDirty();
+ this.printUserPrompt(rawInput);
+
+ // For simple single-word commands without special chars, skip shell parsing
+ if (/^[a-zA-Z0-9._\/-]+$/.test(rawInput.trim()) && !rawInput.includes(' ') && !rawInput.includes('$')) {
+ try {
+ return await this.security.checkAndRun(rawInput);
+ } catch(e) {
+ this.print('Error: ' + e.message, 'error');
+ return;
+ }
+ }
+
+ // Substitute variables in the raw input
+ const substituted = self._substituteVars(rawInput);
+ // Split on ; first (sequence)
+ const seqParts = self._splitSequence(substituted);
+ for (const seqPart of seqParts) {
+ if (!seqPart.trim()) continue;
+ // Split on && and ||
+ const chainParts = self._splitChain(seqPart.trim());
+ let lastOk = true;
+ for (const chainPart of chainParts) {
+ if (!chainPart.cmd.trim()) continue;
+ const { cmd: chainCmd, operator } = chainPart;
+ if (operator === '&&' && !lastOk) continue;
+ if (operator === '||' && lastOk) continue;
+ // Check for redirects
+ const redirectInfo = self._extractRedirects(chainCmd);
+ if (redirectInfo.hasRedirect) {
+ // Run, capture, write to file
+ await self._executeWithRedirect(redirectInfo);
+ lastOk = redirectInfo.exitOk;
+ } else {
+ // Normal or piped command
+ const pipeParts = self.terminal.splitPipe(chainCmd);
+ if (pipeParts.length > 1) {
+ await self.terminal.executePipeChain(pipeParts);
+ } else {
+ // Single command
+ const parts = chainCmd.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ try {
+ await this.security.checkAndRun(chainCmd);
+ } catch(e) {
+ this.terminal && this.terminal.print ? this.terminal.print('Error: ' + e.message, 'error') : console.error(e);
+ }
+ }
+ lastOk = true;
+ }
+ }
+ }
+ };
+ this.terminal._shellModule = this;
+
+ // Modify pipe chain to read _pipeInput for commands that support it
+ const origPipeChain = this.terminal.executePipeChain.bind(this.terminal);
+ this.terminal.executePipeChain = async function(pipeParts) {
+ // Write pipe input from previous segment as temp file for next segment
+ const shellMod = this._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);
+ this._pipeInput = pipeInput;
+
+ if (pipeInput !== null) {
+ // Write pipe input as temp file so file-reading commands can use it
+ this.fs.writeFile('__pipebuf__', pipeInput);
+ // Inject __pipebuf__ as file arg if command is pipe-aware (reads files for input)
+ if (shellMod && shellMod._isPipeAware(cmd)) {
+ // Don't inject if command already has enough file args
+ if (!args.some(a => !a.startsWith('-') && a !== '|' && a !== '>')) {
+ args = args.concat('__pipebuf__');
+ }
+ }
+ }
+
+ if (i < pipeParts.length - 1) {
+ pipeInput = await this.capturePipeOutput(cmd, args);
+ } else {
+ await this.dispatchCommand(cmd, args);
+ }
+
+ // Clean up temp file
+ if (this.fs.readFile('__pipebuf__') !== null) {
+ this.fs.root.content['__pipebuf__'] = undefined;
+ delete this.fs.root.content['__pipebuf__'];
+ }
+ }
+ this._pipeInput = null;
+ };
+ }
+
+ // ── Tokenizer helpers ──
+
+ _splitSequence(input) {
+ // Split on ; (not inside quotes)
+ const parts = [];
+ let current = '';
+ let inQuote = false;
+ let quoteChar = '';
+ for (const ch of input) {
+ if ((ch === '"' || ch === "'") && !inQuote) {
+ inQuote = true; quoteChar = ch; current += ch;
+ } else if (ch === quoteChar && inQuote) {
+ inQuote = false; quoteChar = ''; current += ch;
+ } else if (ch === ';' && !inQuote) {
+ parts.push(current.trim());
+ current = '';
+ } else {
+ current += ch;
+ }
+ }
+ parts.push(current.trim());
+ return parts;
+ }
+
+ _splitChain(input) {
+ // Split on && and ||, preserving operator info
+ const parts = [];
+ let current = '';
+ let inQuote = false;
+ let quoteChar = '';
+ let i = 0;
+ while (i < input.length) {
+ const ch = input[i];
+ if ((ch === '"' || ch === "'") && !inQuote) {
+ inQuote = true; quoteChar = ch; current += ch; i++;
+ } else if (ch === quoteChar && inQuote) {
+ inQuote = false; quoteChar = ''; current += ch; i++;
+ } else if (!inQuote && input.substring(i, i+2) === '&&') {
+ parts.push({ cmd: current.trim(), operator: '&&' });
+ current = ''; i += 2;
+ } else if (!inQuote && input.substring(i, i+2) === '||') {
+ parts.push({ cmd: current.trim(), operator: '||' });
+ current = ''; i += 2;
+ } else {
+ current += ch; i++;
+ }
+ }
+ parts.push({ cmd: current.trim(), operator: null });
+ return parts;
+ }
+
+ _extractRedirects(input) {
+ // Find >, >>, < outside quotes
+ let cmd = '';
+ let hasRedirect = false;
+ let redirectOp = '';
+ let redirectFile = '';
+ let append = false;
+ let inQuote = false, quoteChar = '';
+ let i = 0;
+ const parts = [];
+ let current = '';
+ while (i < input.length) {
+ const ch = input[i];
+ if ((ch === '"' || ch === "'") && !inQuote) {
+ inQuote = true; quoteChar = ch; current += ch; i++;
+ } else if (ch === quoteChar && inQuote) {
+ inQuote = false; quoteChar = ''; current += ch; i++;
+ } else if (!inQuote) {
+ if (input.substring(i, i+2) === '>>') {
+ if (current.trim()) parts.push(current.trim());
+ hasRedirect = true;
+ redirectOp = '>>';
+ append = true;
+ i += 2;
+ // Read until next redirect or end
+ let file = '';
+ while (i < input.length && input[i] === ' ') i++;
+ while (i < input.length && input[i] !== '>' && input[i] !== '<' && input[i] !== '|') {
+ file += input[i]; i++;
+ }
+ redirectFile = file.trim();
+ if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
+ break;
+ } else if (input.substring(i, i+1) === '>') {
+ if (current.trim()) parts.push(current.trim());
+ hasRedirect = true;
+ redirectOp = '>';
+ append = false;
+ i += 1;
+ let file = '';
+ while (i < input.length && input[i] === ' ') i++;
+ while (i < input.length && input[i] !== '>' && input[i] !== '<' && input[i] !== '|') {
+ file += input[i]; i++;
+ }
+ redirectFile = file.trim();
+ if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
+ break;
+ } else if (input.substring(i, i+1) === '<') {
+ if (current.trim()) parts.push(current.trim());
+ hasRedirect = true;
+ redirectOp = '<';
+ i += 1;
+ let file = '';
+ while (i < input.length && input[i] === ' ') i++;
+ while (i < input.length && input[i] !== '>' && input[i] !== '<' && input[i] !== '|') {
+ file += input[i]; i++;
+ }
+ redirectFile = file.trim();
+ if (i < input.length && (input[i] === '>' || input[i] === '<')) continue;
+ break;
+ } else {
+ current += ch; i++;
+ }
+ } else {
+ current += ch; i++;
+ }
+ }
+ if (current.trim()) parts.push(current.trim());
+
+ const cmdText = parts.join(' ');
+ return {
+ hasRedirect,
+ redirectOp,
+ redirectFile,
+ append,
+ cmdText,
+ exitOk: true
+ };
+ }
+
+ async _executeWithRedirect(redirectInfo) {
+ const { redirectOp, redirectFile, append, cmdText } = redirectInfo;
+ const fs = this.terminal.fs;
+
+ if (redirectOp === '<') {
+ // Read file content as stdin
+ const inputContent = fs.readFile(redirectFile);
+ if (inputContent === null) {
+ this.terminal.print(`shell: ${redirectFile}: No such file`, 'error');
+ return;
+ }
+ this.terminal._pipeInput = inputContent;
+ // Execute the command with pipe input available
+ const parts = cmdText.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // If command reads files, inject pipe content as temp file
+ if (this._isPipeAware(cmd)) {
+ fs.writeFile('__pipebuf__', inputContent);
+ const newArgs = [...args];
+ if (!newArgs.some(a => !a.startsWith('-'))) {
+ newArgs.push('__pipebuf__');
+ }
+ await this.terminal.dispatchCommand(cmd, newArgs);
+ if (fs.readFile('__pipebuf__') !== null) {
+ delete fs.root.content['__pipebuf__'];
+ }
+ } else {
+ await this.terminal.dispatchCommand(cmd, args);
+ }
+ this.terminal._pipeInput = null;
+ return;
+ }
+
+ // > or >> — capture output, write to file
+ const originalPrint = this.terminal.print.bind(this.terminal);
+ let output = '';
+ this.terminal.print = (text, type) => { output += text + '\n'; };
+
+ // Check for pipes within the command before redirect
+ const pipeParts = this.terminal.splitPipe(cmdText);
+ if (pipeParts.length > 1) {
+ // Execute pipe chain manually to capture all output
+ 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);
+ if (pipeInput !== null) {
+ fs.writeFile('__pipebuf__', pipeInput);
+ }
+ if (i < pipeParts.length - 1) {
+ const captPrint = this.terminal.print.bind(this.terminal);
+ let capt = '';
+ this.terminal.print = (t, ty) => { capt += t + '\n'; };
+ await this.terminal.dispatchCommand(pCmd, pArgs);
+ this.terminal.print = captPrint;
+ pipeInput = capt.trim();
+ } else {
+ await this.terminal.dispatchCommand(pCmd, pArgs);
+ }
+ 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);
+ }
+
+ this.terminal.print = originalPrint;
+ const captured = output.trimEnd();
+
+ if (captured) {
+ if (append) {
+ const existing = fs.readFile(redirectFile) || '';
+ fs.writeFile(redirectFile, existing + (existing ? '\n' : '') + captured);
+ } else {
+ fs.writeFile(redirectFile, captured);
+ }
+ this.terminal.print(`Redirected ${captured.split('\n').length} line(s) to ${redirectFile}`, 'success');
+ } else {
+ // Write empty file
+ if (!append) {
+ fs.writeFile(redirectFile, '');
+ this.terminal.print(`Redirected (empty) to ${redirectFile}`, 'success');
+ }
+ }
+ }
+
+ _isPipeAware(cmd) {
+ // Commands that read from files and should use pipe buffer
+ return ['grep', 'head', 'tail', 'sort', 'wc', 'cut', 'uniq', 'expand'].includes(cmd);
+ }
+
+ // ── Variable substitution ──
+
+ _substituteVars(input) {
+ // Replace $VAR and ${VAR} (not inside single quotes)
+ let result = '';
+ let inSingle = false;
+ let i = 0;
+ while (i < input.length) {
+ const ch = input[i];
+ if (ch === "'") {
+ inSingle = !inSingle;
+ result += ch;
+ i++;
+ } else if (ch === '$' && !inSingle && i + 1 < input.length) {
+ // Check for $(
+ if (input[i+1] === '(') {
+ result += ch;
+ i++;
+ continue;
+ }
+ // Check for ${VAR}
+ if (input[i+1] === '{') {
+ let end = input.indexOf('}', i);
+ if (end !== -1) {
+ const varName = input.substring(i+2, end);
+ const val = this._getVar(varName);
+ result += val !== undefined ? val : '$' + varName;
+ i = end + 1;
+ continue;
+ }
+ }
+ // Check for $VAR (alphanumeric/underscore)
+ let j = i + 1;
+ if (j < input.length && /[A-Za-z_]/.test(input[j])) {
+ while (j < input.length && /\w/.test(input[j])) j++;
+ const varName = input.substring(i+1, j);
+ const val = this._getVar(varName);
+ result += val !== undefined ? val : '';
+ i = j;
+ continue;
+ }
+ // $ followed by digit — not a var
+ result += ch;
+ i++;
+ } else {
+ result += ch;
+ i++;
+ }
+ }
+ return result;
+ }
+
+ _getVar(name) {
+ if (name === 'PWD') return '/' + this.terminal.fs.currentPath.join('/');
+ return this._vars[name] !== undefined ? this._vars[name] : undefined;
+ }
+
+ _setVar(name, value) {
+ this._vars[name] = value;
+ }
+
+ // ── Commands ──
+
+ _cmdExport(args) {
+ if (!args[0] || !args[0].includes('=')) {
+ return this.terminal.print('Usage: export <var>=<value>', 'warning');
+ }
+ const eqIdx = args[0].indexOf('=');
+ const name = args[0].substring(0, eqIdx);
+ let value = args[0].substring(eqIdx + 1);
+ // Support quoted values: export FOO="hello world"
+ if (args.length > 1) {
+ const rest = args.slice(1).join(' ');
+ if (value.endsWith('"') || value.endsWith("'")) {
+ // First part ended quote — fine
+ } else if (value.startsWith('"') || value.startsWith("'")) {
+ value = value + (rest ? ' ' + rest : '');
+ }
+ }
+ // Strip surrounding quotes
+ if ((value.startsWith('"') && value.endsWith('"')) ||
+ (value.startsWith("'") && value.endsWith("'"))) {
+ value = value.substring(1, value.length - 1);
+ }
+ this._setVar(name, value);
+ this.terminal.print(`Shell ${name}=${value}`, 'success');
+ }
+
+ _cmdUnset(args) {
+ if (!args[0]) return this.terminal.print('Usage: unset <var>', 'warning');
+ delete this._vars[args[0]];
+ this.terminal.print(`Shell var removed: ${args[0]}`, 'info');
+ }
+
+ _cmdSet() {
+ const fs = this.terminal.fs;
+ this.terminal.print('Shell variables:', 'info');
+ for (const [k, v] of Object.entries(this._vars)) {
+ this.terminal.print(`${k}=${v}`, 'info');
+ }
+ this.terminal.print(`PWD=/${fs.currentPath.join('/')}`, 'info');
+ }
+
+ async _cmdScript(args) {
+ if (!args[0]) return this.terminal.print('Usage: script <path.sh>', 'warning');
+ if (this._scriptDepth >= this._maxScriptDepth) {
+ return this.terminal.print(`script: max recursion depth (${this._maxScriptDepth}) reached`, 'error');
+ }
+ const path = args[0];
+ const content = this.terminal.fs.readFile(path);
+ if (content === null) return this.terminal.print(`script: ${path}: No such file`, 'error');
+ this._scriptDepth++;
+ this.terminal.print(`Running script: ${path}`, 'info');
+ const lines = content.split('\n');
+ let lineNum = 0;
+ for (const rawLine of lines) {
+ lineNum++;
+ const line = rawLine.trim();
+ if (!line || line.startsWith('#')) continue;
+ // Run each line through the shell (by calling executeCommand directly)
+ // We need to manually call the wrapped executeCommand
+ this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt');
+ // Parse inline: handle pipe, redirect, chain, sequence
+ const substituted = this._substituteVars(line);
+ const seqParts = this._splitSequence(substituted);
+ for (const seqPart of seqParts) {
+ if (!seqPart.trim()) continue;
+ const chainParts = this._splitChain(seqPart.trim());
+ let lastOk = true;
+ for (const chainPart of chainParts) {
+ if (!chainPart.cmd.trim()) continue;
+ if (chainPart.operator === '&&' && !lastOk) continue;
+ if (chainPart.operator === '||' && lastOk) continue;
+ const redirectInfo = this._extractRedirects(chainPart.cmd);
+ if (redirectInfo.hasRedirect) {
+ await this._executeWithRedirect(redirectInfo);
+ lastOk = redirectInfo.exitOk;
+ } else {
+ const pipeParts = this.terminal.splitPipe(chainPart.cmd);
+ if (pipeParts.length > 1) {
+ await this.terminal.executePipeChain(pipeParts);
+ } else {
+ const parts = chainPart.cmd.split(/\s+/);
+ const cmd = parts[0].toLowerCase();
+ const args = parts.slice(1);
+ // Check if it's our built-in command
+ if (this.commands[cmd]) {
+ this.terminal.markDirty();
+ await this.commands[cmd](args);
+ } else {
+ await this.terminal.security.checkAndRun(chainPart.cmd);
+ }
+ }
+ lastOk = true;
+ }
+ }
+ }
+ }
+ this._scriptDepth--;
+ this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success');
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new ShellModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/shell.js');
+}
+
+/* === alias.js === */
+class AliasModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ this.key = 'webx11_aliases';
+ this.defaults = { 'll': 'ls', 'h': 'help', 'cls': 'clear', 'sh': 'script' };
+ this.aliases = this.load();
+ this._aliasDepth = 0;
+ this._aliasMaxDepth = 10;
+ }
+
+ getHelpInfo() {
+ return {
+ 'alias': 'List, set, or show aliases (usage: alias [name=value])',
+ 'unalias': 'Remove a user-defined alias (usage: unalias <name>)',
+ };
+ }
+
+ load() {
+ try {
+ const data = localStorage.getItem(this.key);
+ return data ? { ...this.defaults, ...JSON.parse(data) } : { ...this.defaults };
+ } catch (e) { return { ...this.defaults }; }
+ }
+ save() {
+ const custom = {};
+ Object.keys(this.aliases).forEach(k => {
+ if (!this.defaults[k]) custom[k] = this.aliases[k];
+ });
+ localStorage.setItem(this.key, JSON.stringify(custom));
+ }
+ registerCommands() {
+ const self = this;
+ // Register management commands
+ this.commands['alias'] = (args) => self.handleAlias(args);
+ this.commands['unalias'] = (args) => self.handleUnalias(args);
+ // Register each alias as a delegating command
+ Object.keys(this.aliases).forEach(name => {
+ if (name === 'alias' || name === 'unalias') return;
+ const target = this.aliases[name];
+ this.commands[name] = (args) => {
+ if (this._aliasDepth >= this._aliasMaxDepth) { this.terminal.print(`alias: max depth (${this._aliasMaxDepth}) reached`, 'error'); return; }
+ this._aliasDepth++;
+ try { this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); }
+ finally { this._aliasDepth--; }
+ };
+ });
+ }
+ handleAlias(args) {
+ if (!args[0]) {
+ // List all aliases
+ this.terminal.print('=== Aliases ===', 'info');
+ const keys = Object.keys(this.aliases);
+ if (keys.length === 0) {
+ this.terminal.print(' No aliases defined', 'warning');
+ } else {
+ keys.forEach(name => {
+ const mark = this.defaults[name] ? ' (built-in)' : '';
+ this.terminal.print(` ${name}=${this.aliases[name]}${mark}`);
+ });
+ }
+ this.terminal.print('Usage: alias &lt;name&gt;=&lt;value&gt;', 'info');
+ return;
+ }
+ // Parse name=value
+ const input = args.join(' ');
+ const eqIdx = input.indexOf('=');
+ if (eqIdx === -1) {
+ // Show specific alias value
+ const name = args[0];
+ if (this.aliases[name]) {
+ const mark = this.defaults[name] ? ' (built-in)' : '';
+ this.terminal.print(`${name}=${this.aliases[name]}${mark}`, 'info');
+ } else {
+ this.terminal.print(`Alias '${name}' not found`, 'error');
+ }
+ return;
+ }
+ const name = input.substring(0, eqIdx).trim();
+ const value = input.substring(eqIdx + 1).trim();
+ if (!name) return this.terminal.print('Invalid alias name', 'error');
+ if (!value) return this.terminal.print('Invalid alias value', 'error');
+ if (name === 'alias' || name === 'unalias') {
+ return this.terminal.print('Cannot alias reserved command name', 'error');
+ }
+ if (this.defaults[name]) {
+ return this.terminal.print(`Cannot alias built-in alias '${name}'`, 'warning');
+ }
+ this.aliases[name] = value;
+ this.save();
+ // Register the new alias as a command immediately
+ const target = value;
+ this.commands[name] = (args) => {
+ if (this._aliasDepth >= this._aliasMaxDepth) { this.terminal.print(`alias: max depth (${this._aliasMaxDepth}) reached`, 'error'); return; }
+ this._aliasDepth++;
+ try { this.terminal.executeCommand(target + (args.length ? ' ' + args.join(' ') : '')); }
+ finally { this._aliasDepth--; }
+ };
+ this.terminal.print(`Alias set: ${name}=${value}`, 'success');
+ }
+ handleUnalias(args) {
+ if (!args[0]) return this.terminal.print('Usage: unalias &lt;name&gt;', 'warning');
+ const name = args[0];
+ if (this.defaults[name]) {
+ return this.terminal.print(`Cannot unalias built-in alias '${name}'`, 'warning');
+ }
+ if (!this.aliases[name]) {
+ return this.terminal.print(`Alias '${name}' not found`, 'error');
+ }
+ delete this.aliases[name];
+ delete this.commands[name];
+ this.save();
+ this.terminal.print(`Alias removed: ${name}`, 'success');
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new AliasModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/alias.js');
+}
+
+/* === finder.js === */
+class FinderModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'find': 'Recursively search VFS files/directories by name (usage: find <query>)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['find'] = (args) => this.find(args);
+ }
+ find(args) {
+ if (!args[0]) return this.terminal.print('Usage: find <query> — recursively searches VFS files/dirs matching query', 'warning');
+ const query = args.join(' ').toLowerCase();
+ const results = [];
+ this._recurse(this.terminal.fs.root, [], query, results);
+ if (results.length === 0) return this.terminal.print(`No matches for "${query}"`, 'warning');
+ this.terminal.print(`=== Found ${results.length} match(es) for "${query}" ===`, 'info');
+ results.forEach(r => {
+ const icon = r.type === 'dir' ? '📁' : '📄';
+ this.terminal.print(` ${icon} ${r.path}`);
+ });
+ }
+ _recurse(node, pathParts, query, results) {
+ if (!node || !node.content) return;
+ for (const [name, child] of Object.entries(node.content)) {
+ const fullPath = '/' + [...pathParts, name].join('/');
+ if (name.toLowerCase().includes(query)) {
+ results.push({ name, path: fullPath, type: child.type || (child.content ? 'dir' : 'file') });
+ }
+ if (child.content && typeof child.content === 'object') {
+ this._recurse(child, [...pathParts, name], query, results);
+ }
+ }
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new FinderModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/finder.js');
+}
+
+/* === filemgr.js === */
+class FileManagerModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'files': { argType: 'file' }, 'preview': { argType: 'file' },
+ };
+ this.terminal = null;
+ this.currentPath = [];
+ this._overlay = null;
+ this._selectedFile = null;
+ this._searchFilter = '';
+ this._rawMode = false;
+ this._inlineHtml = false;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'files': 'files — Open the file manager overlay with VFS directory listing, preview, encrypt, zip',
+ 'preview': 'preview <path> — Preview a specific file',
+ };
+ }
+
+ registerCommands() {
+ this.commands['files'] = () => this._openOverlay();
+ this.commands['preview'] = (args) => this._previewCommand(args);
+ }
+
+ _previewCommand(args) {
+ if (!args[0]) return this.terminal.print('Usage: preview <path>', 'warning');
+ const path = args.join(' ');
+ this._openOverlay();
+ this._navigateToPathFromParts(path);
+ const content = this.terminal.fs.readFile(path);
+ if (content === null || content === false) {
+ this._renderPreviewError('File not found: ' + path);
+ return;
+ }
+ this._selectedFile = path;
+ this._renderPreview(path, content);
+ this._highlightSelected();
+ }
+
+ _navigateToPathFromParts(path) {
+ const parts = path.split('/').filter(p => p);
+ if (parts.length <= 1) {
+ this.currentPath = [];
+ return;
+ }
+ this.currentPath = parts.slice(0, -1);
+ }
+
+ _detectType(filePath) {
+ const ext = filePath.split('.').pop().toLowerCase();
+ if (!ext || ext === filePath) return 'other';
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'];
+ const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv'];
+ const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
+ const codeExts = ['js', 'py', 'css', 'yaml', 'yml', 'toml'];
+ if (ext === 'md') return 'md';
+ if (imageExts.includes(ext)) return 'image';
+ if (videoExts.includes(ext)) return 'video';
+ if (audioExts.includes(ext)) return 'audio';
+ if (codeExts.includes(ext)) return 'code';
+ if (ext === 'html') return 'html';
+ if (ext === 'json') return 'json';
+ if (ext === 'csv') return 'csv';
+ if (ext === 'pdf') return 'pdf';
+ if (ext === 'svg') return 'svg';
+ if (ext === 'txt') return 'text';
+ return 'other';
+ }
+
+ _renderMarkdown(text) {
+ if (!text) return '';
+ try {
+ let html = text
+ .replace(/&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;');
+ // code blocks first
+ html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
+ // inline code
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
+ // headers
+ html = html.replace(/^### (.+)$/gm, '<h3 style="color:var(--accent-color);margin:8px 0 4px">$1</h3>');
+ html = html.replace(/^## (.+)$/gm, '<h2 style="color:var(--accent-color);margin:10px 0 4px">$1</h2>');
+ html = html.replace(/^# (.+)$/gm, '<h1 style="color:var(--accent-color);margin:12px 0 6px">$1</h1>');
+ // bold
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
+ // italic
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
+ // links
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--info-color)">$1</a>');
+ // horizontal rules
+ html = html.replace(/^---$/gm, '<hr style="border-color:var(--border-color);margin:8px 0">');
+ // unordered lists
+ html = html.replace(/^[\s]*[-*]\s+(.+)$/gm, '<li style="margin-left:16px;list-style:disc">$1</li>');
+ // paragraphs - double newlines
+ html = html.replace(/\n\n/g, '</p><p style="margin:6px 0">');
+ html = '<p style="margin:6px 0">' + html + '</p>';
+ return html;
+ } catch (e) {
+ return '<pre style="white-space:pre-wrap">' + text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</pre>';
+ }
+ }
+
+ _getPreviewHtml(path, content) {
+ if (content === null || content === false) {
+ return '<div style="color:var(--error-color);padding:20px">File not found: ' + this._escapeHtml(path) + '</div>';
+ }
+
+ const type = this._detectType(path);
+ const ext = path.split('.').pop().toLowerCase();
+ const fileName = path.split('/').pop() || path;
+
+ // Handle binary detection for media types
+ if (typeof content !== 'string') {
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">Cannot preview: non-string content</div>';
+ }
+ const isLikelyBinary = content.length > 0 && !content.startsWith('\n') && !content.startsWith('\r') && !content.startsWith(' ') && !content.startsWith('<') && !content.startsWith('{') && !content.startsWith('[') && content.charCodeAt(0) < 32 && content.charCodeAt(0) !== 9 && content.charCodeAt(0) !== 10 && content.charCodeAt(0) !== 13;
+
+ const isTextContent = typeof content === 'string' && (content.startsWith('{') || content.startsWith('[') || content.startsWith('<') || content.startsWith('#') || /^[\w\s\n\r\t.,;:!?()\-=+*@#$%^&"'\/\\|`~]/.test(content.substring(0, 100)));
+
+ if (type === 'md' && isTextContent) {
+ return '<div class="markdown-preview" style="padding:12px;font-family:var(--font-primary);line-height:1.5">' + this._renderMarkdown(content) + '</div>';
+ }
+
+ if (type === 'html') {
+ // Render HTML in an inline sandbox frame
+ const safeHtml = content
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
+ .replace(/on\w+\s*=\s*"[^"]*"/gi, '')
+ .replace(/on\w+\s*=\s*'[^']*'/gi, '');
+ return '<div style="padding:0;width:100%;height:60vh;border:none;overflow:hidden">' +
+ '<iframe srcdoc="' + this._escapeHtml(safeHtml.replace(/"/g, '&quot;').replace(/'/g, '&#39;')) + '" ' +
+ 'style="width:100%;height:100%;border:none;background:#fff" sandbox="allow-same-origin"></iframe></div>';
+ }
+
+ if (type === 'text') {
+ const truncated = content.substring(0, 2000);
+ const extra = content.length > 2000 ? '<div style="color:var(--text-muted);margin-top:8px">(truncated to 2000 chars)</div>' : '';
+ return '<pre style="white-space:pre-wrap;padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(truncated) + extra + '</pre>';
+ }
+
+ if (type === 'json') {
+ try {
+ const parsed = JSON.parse(content);
+ const formatted = JSON.stringify(parsed, null, 2);
+ const html = this._syntaxHighlightJson(formatted);
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto;line-height:1.4">' + html + '</pre>';
+ } catch (e) {
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>';
+ }
+ }
+
+ if (type === 'code') {
+ const lines = content.split('\n');
+ const totalLines = lines.length;
+ const maxLines = 150;
+ const displayLines = lines.slice(0, maxLines);
+ let html = '<table style="border-collapse:collapse;width:100%;font-family:var(--font-primary);font-size:0.85em;margin:0"><tbody>';
+ displayLines.forEach((line, i) => {
+ const lineNum = i + 1;
+ const escaped = this._escapeHtml(line);
+ html += '<tr><td style="color:var(--text-muted);padding:0 8px 0 4px;text-align:right;user-select:none;border-right:1px solid var(--border-color);vertical-align:top;width:40px">' + lineNum + '</td>';
+ html += '<td style="padding:0 8px;white-space:pre;width:100%">' + (escaped || '&nbsp;') + '</td></tr>';
+ });
+ html += '</tbody></table>';
+ if (totalLines > maxLines) {
+ html += '<div style="color:var(--text-muted);padding:8px;border-top:1px solid var(--border-color)">(showing ' + maxLines + ' of ' + totalLines + ' lines)</div>';
+ }
+ return html;
+ }
+
+ if (type === 'image') {
+ // VFS stores images as base64 strings — detect and render inline
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ const mimeTypes = {jpg:'image/jpeg',jpeg:'image/jpeg',png:'image/png',gif:'image/gif',webp:'image/webp',bmp:'image/bmp',ico:'image/x-icon'};
+ const mime = mimeTypes[ext] || 'image/png';
+ return '<div style="padding:12px;text-align:center;max-width:100%"><img src="data:' + mime + ';base64,' + content + '" style="max-width:100%;max-height:70vh;border-radius:4px;border:1px solid var(--border-color)" alt="' + this._escapeHtml(fileName) + '"></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📷 Image — cannot render, file may be stored as binary</div>';
+ }
+
+ if (type === 'svg') {
+ try {
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(content, 'text/html');
+ const scripts = doc.querySelectorAll('script');
+ scripts.forEach(s => s.remove());
+ // Remove event handlers from svg elements
+ const allEls = doc.querySelectorAll('*');
+ allEls.forEach(el => {
+ for (let i = el.attributes.length - 1; i >= 0; i--) {
+ const attr = el.attributes[i].name;
+ if (attr.startsWith('on')) el.removeAttribute(attr);
+ }
+ });
+ const cleanSvg = doc.body.innerHTML;
+ return '<div style="padding:12px;text-align:center;max-width:100%;overflow:auto">' + cleanSvg + '</div>';
+ } catch (e) {
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>';
+ }
+ }
+
+ if (type === 'video') {
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ const mimeTypes = {mp4:'video/mp4',webm:'video/webm',avi:'video/x-msvideo',mov:'video/quicktime',mkv:'video/x-matroska'};
+ const mime = mimeTypes[ext] || 'video/mp4';
+ return '<div style="padding:12px;text-align:center"><video controls style="max-width:90%;max-height:65vh;border-radius:4px"><source src="data:' + mime + ';base64,' + content + '" type="' + mime + '"></video></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎬 Video — cannot render</div>';
+ }
+
+ if (type === 'audio') {
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ const mimeTypes = {mp3:'audio/mpeg',wav:'audio/wav',ogg:'audio/ogg',flac:'audio/flac',aac:'audio/aac',m4a:'audio/mp4'};
+ const mime = mimeTypes[ext] || 'audio/mpeg';
+ return '<div style="padding:12px;text-align:center"><audio controls style="max-width:90%"><source src="data:' + mime + ';base64,' + content + '" type="' + mime + '"></audio></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">🎵 Audio — cannot render</div>';
+ }
+
+ if (type === 'pdf') {
+ const isBase64 = /^[A-Za-z0-9+/=]+$/.test(content.substring(0, 100));
+ if (isBase64) {
+ return '<div style="padding:12px;text-align:center;height:70vh"><embed src="data:application/pdf;base64,' + content + '" type="application/pdf" style="width:100%;height:100%;border:1px solid var(--border-color);border-radius:4px"></div>';
+ }
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📄 PDF — cannot render</div>';
+ }
+
+ if (type === 'csv') {
+ try {
+ const lines = content.split('\n').filter(l => l.trim());
+ const maxRows = 50;
+ const rows = lines.slice(0, maxRows).map(l => l.split(',').map(c => c.trim().replace(/^"|"$/g, '')));
+ let html = '<table class="csv-table" style="border-collapse:collapse;width:100%;font-family:var(--font-primary);font-size:0.85em;margin:0;background:var(--bg-secondary)">';
+ if (rows.length > 0) {
+ html += '<thead><tr>';
+ rows[0].forEach(cell => {
+ html += '<th style="border:1px solid var(--border-color);padding:6px 8px;text-align:left;background:var(--bg-tertiary);color:var(--accent-color);font-weight:bold">' + this._escapeHtml(cell) + '</th>';
+ });
+ html += '</tr></thead><tbody>';
+ rows.slice(1).forEach(row => {
+ html += '<tr>';
+ row.forEach(cell => {
+ html += '<td style="border:1px solid var(--border-color);padding:4px 8px">' + this._escapeHtml(cell) + '</td>';
+ });
+ html += '</tr>';
+ });
+ html += '</tbody></table>';
+ if (lines.length > maxRows) {
+ html += '<div style="color:var(--text-muted);padding:8px">(showing ' + maxRows + ' of ' + lines.length + ' rows)</div>';
+ }
+ }
+ return html;
+ } catch (e) {
+ return '<pre style="padding:12px;font-family:var(--font-primary);font-size:0.9em;overflow-x:auto">' + this._escapeHtml(content.substring(0, 5000)) + '</pre>';
+ }
+ }
+
+ // 'other' — raw text fallback
+ if (typeof content === 'string' && isTextContent) {
+ const lines = content.split('\n');
+ const maxLines = 80;
+ const displayLines = lines.slice(0, maxLines);
+ const truncated = lines.length > maxLines;
+ let html = '<pre style="white-space:pre-wrap;padding:12px;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">';
+ displayLines.forEach((line, i) => {
+ html += this._escapeHtml(line) + '\n';
+ });
+ html += '</pre>';
+ if (truncated) {
+ html += '<div style="color:var(--text-muted);padding:8px;border-top:1px solid var(--border-color)">(showing ' + maxLines + ' of ' + lines.length + ' lines)</div>';
+ }
+ return html;
+ }
+
+ return '<div style="padding:20px;text-align:center;color:var(--text-muted)">📁 Preview not available for this file type</div>';
+ }
+
+ _syntaxHighlightJson(jsonStr) {
+ return jsonStr.replace(/("(?:\\.|[^"\\])*")\s*:/g, '<span style="color:var(--info-color)">$1</span>:')
+ .replace(/:\s*("(?:\\.|[^"\\])*")/g, ': <span style="color:var(--success-color)">$1</span>')
+ .replace(/:\s*(\d+\.?\d*)/g, ': <span style="color:var(--warning-color)">$1</span>')
+ .replace(/:\s*(true|false|null)/g, ': <span style="color:var(--error-color)">$1</span>');
+ }
+
+ _escapeHtml(str) {
+ if (typeof str !== 'string') return '';
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
+ }
+
+ _formatSize(size) {
+ if (!size && size !== 0) return '';
+ if (size < 1024) return size + ' B';
+ if (size < 1024 * 1024) return (size / 1024).toFixed(1) + ' KB';
+ return (size / (1024 * 1024)).toFixed(1) + ' MB';
+ }
+
+ _createOverlay() {
+ if (this._overlay) return;
+
+ const overlay = document.createElement('div');
+ overlay.id = 'filemgr-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:1800;display:flex;flex-direction:column;background:var(--bg-color);color:var(--text-color);font-family:var(--font-primary);font-size:14px;height:100vh;';
+
+ // Inject responsive styles for the overlay
+ const styleId = 'filemgr-styles';
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ #filemgr-overlay.responsive .filemgr-main {
+ flex-direction: column !important;
+ }
+ #filemgr-overlay.responsive #filemgr-sidebar {
+ width: 100% !important;
+ max-height: 40vh;
+ }
+ #filemgr-overlay.responsive #filemgr-preview {
+ width: 100% !important;
+ }
+ #filemgr-overlay .filemgr-main {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+ }
+ @media (max-width: 600px) {
+ #filemgr-overlay .filemgr-main {
+ flex-direction: column !important;
+ }
+ #filemgr-overlay #filemgr-sidebar {
+ width: 100% !important;
+ max-height: 40vh;
+ border-right: none !important;
+ border-bottom: 1px solid var(--border-color);
+ }
+ #filemgr-overlay #filemgr-preview {
+ width: 100% !important;
+ flex: 1 !important;
+ min-height: 0;
+ }
+ #filemgr-action-bar {
+ flex-wrap: wrap;
+ gap: 6px;
+ }
+ #filemgr-action-bar button {
+ min-width: 44px;
+ min-height: 44px;
+ font-size: 0.9em;
+ }
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ // Toolbar
+ const toolbar = document.createElement('div');
+ toolbar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-bottom:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;';
+
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = '✕';
+ closeBtn.style.cssText = 'background:none;border:1px solid var(--border-color);color:var(--text-color);cursor:pointer;padding:4px 10px;border-radius:4px;font-family:inherit;font-size:1em;min-height:44px;touch-action:manipulation;';
+ closeBtn.onclick = () => this._closeOverlay();
+ toolbar.appendChild(closeBtn);
+
+ const titleSpan = document.createElement('span');
+ titleSpan.textContent = '📁 File Manager';
+ titleSpan.style.cssText = 'font-weight:bold;margin-right:8px;';
+ toolbar.appendChild(titleSpan);
+
+ // Breadcrumbs
+ const breadcrumbs = document.createElement('div');
+ breadcrumbs.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;font-size:0.9em;';
+ breadcrumbs.id = 'filemgr-breadcrumbs';
+
+ // Root breadcrumb
+ const rootSeg = document.createElement('span');
+ rootSeg.textContent = '/';
+ rootSeg.style.cssText = 'color:var(--accent-color);cursor:pointer;padding:2px 4px;border-radius:3px;';
+ rootSeg.onmouseenter = () => { rootSeg.style.background = 'var(--bg-tertiary)'; };
+ rootSeg.onmouseleave = () => { rootSeg.style.background = 'transparent'; };
+ rootSeg.onclick = () => this._navigateTo([]);
+ breadcrumbs.appendChild(rootSeg);
+
+ toolbar.appendChild(breadcrumbs);
+
+ // Search input
+ const searchInput = document.createElement('input');
+ searchInput.type = 'text';
+ searchInput.placeholder = '🔍 Filter files...';
+ searchInput.style.cssText = 'flex:1;min-width:120px;max-width:300px;padding:6px 10px;background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:4px;color:var(--text-color);font-family:var(--font-primary);font-size:0.9em;outline:none;';
+ searchInput.oninput = (e) => {
+ this._searchFilter = e.target.value.toLowerCase();
+ this._refreshFileList();
+ };
+ toolbar.appendChild(searchInput);
+
+ overlay.appendChild(toolbar);
+
+ // Main area — wraps sidebar + preview in a responsive flex container
+ const mainArea = document.createElement('div');
+ mainArea.id = 'filemgr-main-container';
+ mainArea.className = 'filemgr-main';
+ mainArea.style.cssText = 'flex:1;overflow:hidden;display:flex;min-height:0;';
+
+ // Sidebar
+ const sidebar = document.createElement('div');
+ sidebar.id = 'filemgr-sidebar';
+ sidebar.style.cssText = 'width:25%;min-width:150px;overflow-y:auto;background:var(--bg-secondary);border-right:1px solid var(--border-color);';
+
+ mainArea.appendChild(sidebar);
+
+ // Preview pane
+ const preview = document.createElement('div');
+ preview.id = 'filemgr-preview';
+ preview.style.cssText = 'flex:1;overflow-y:auto;padding:0;';
+
+ mainArea.appendChild(preview);
+
+ overlay.appendChild(mainArea);
+
+ // Action bar
+ const actionBar = document.createElement('div');
+ actionBar.id = 'filemgr-action-bar';
+ actionBar.style.cssText = 'display:flex;align-items:center;padding:8px 12px;border-top:1px solid var(--border-color);background:var(--bg-secondary);flex-shrink:0;gap:8px;flex-wrap:wrap;';
+
+ const statusSpan = document.createElement('span');
+ statusSpan.id = 'filemgr-status';
+ statusSpan.style.cssText = 'flex:1;color:var(--text-muted);font-size:0.85em;';
+ statusSpan.textContent = 'No file selected';
+ actionBar.appendChild(statusSpan);
+
+ const encryptBtn = document.createElement('button');
+ encryptBtn.textContent = '🔐 Encrypt';
+ encryptBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--warning-color);color:var(--warning-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ encryptBtn.onclick = () => this._actionEncrypt();
+ actionBar.appendChild(encryptBtn);
+
+ const compressBtn = document.createElement('button');
+ compressBtn.textContent = '📦 Zip';
+ compressBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--info-color);color:var(--info-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ compressBtn.onclick = () => this._actionCompress();
+ actionBar.appendChild(compressBtn);
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.textContent = '🗑 Delete';
+ deleteBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--error-color);color:var(--error-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ deleteBtn.onclick = () => this._actionDelete();
+ actionBar.appendChild(deleteBtn);
+
+ const downloadBtn = document.createElement('button');
+ downloadBtn.textContent = '⬇ Download';
+ downloadBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ downloadBtn.onclick = () => this._actionDownload();
+ actionBar.appendChild(downloadBtn);
+
+ const editBtn = document.createElement('button');
+ editBtn.textContent = '✏ Edit';
+ editBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--accent-color);color:var(--accent-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ editBtn.onclick = () => this._actionEdit();
+ actionBar.appendChild(editBtn);
+
+ const newFileBtn = document.createElement('button');
+ newFileBtn.textContent = '📄 New';
+ newFileBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--success-color);color:var(--success-color);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ newFileBtn.onclick = () => this._actionNewFile();
+ actionBar.appendChild(newFileBtn);
+
+ const previewToggleBtn = document.createElement('button');
+ previewToggleBtn.id = 'filemgr-toggle-raw';
+ previewToggleBtn.textContent = '📄 Raw';
+ previewToggleBtn.style.cssText = 'padding:6px 14px;background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-muted);border-radius:4px;cursor:pointer;font-family:inherit;font-size:0.9em;min-height:44px;touch-action:manipulation;';
+ previewToggleBtn.onclick = () => this._togglePreviewMode();
+ actionBar.appendChild(previewToggleBtn);
+
+ overlay.appendChild(actionBar);
+
+ this._overlay = overlay;
+ return overlay;
+ }
+
+ _openOverlay() {
+ if (this._overlay) {
+ this._overlay.style.display = 'flex';
+ this._refreshFileList();
+ return;
+ }
+ const overlay = this._createOverlay();
+ document.body.appendChild(overlay);
+ this._navigateTo(this.currentPath);
+ }
+
+ _closeOverlay() {
+ if (this._overlay) {
+ this._overlay.style.display = 'none';
+ }
+ }
+
+ _clearOverlay() {
+ if (this._overlay && this._overlay.parentNode) {
+ this._overlay.parentNode.removeChild(this._overlay);
+ }
+ this._overlay = null;
+ this._selectedFile = null;
+ this._searchFilter = '';
+ this._rawMode = false;
+ }
+
+ _navigateTo(pathParts) {
+ this.currentPath = pathParts || [];
+ this._selectedFile = null;
+ this._searchFilter = '';
+ const searchInput = this._overlay?.querySelector('input[type="text"]');
+ if (searchInput) searchInput.value = '';
+ this._updateBreadcrumbs();
+ this._refreshFileList();
+ this._clearPreview();
+ this._updateStatus('No file selected');
+ }
+
+ _updateBreadcrumbs() {
+ const breadcrumbs = document.getElementById('filemgr-breadcrumbs');
+ if (!breadcrumbs) return;
+
+ // Keep root segment, remove rest
+ while (breadcrumbs.children.length > 1) {
+ breadcrumbs.removeChild(breadcrumbs.lastChild);
+ }
+
+ let accumulated = '';
+ this.currentPath.forEach((part, i) => {
+ const sep = document.createElement('span');
+ sep.textContent = ' › ';
+ sep.style.cssText = 'color:var(--text-muted);margin:0 2px;';
+ breadcrumbs.appendChild(sep);
+
+ const seg = document.createElement('span');
+ seg.textContent = part;
+ seg.style.cssText = 'cursor:pointer;padding:2px 4px;border-radius:3px;color:var(--text-color);';
+ if (i === this.currentPath.length - 1) {
+ seg.style.color = 'var(--accent-color)';
+ seg.style.fontWeight = 'bold';
+ }
+ seg.onmouseenter = () => { seg.style.background = 'var(--bg-tertiary)'; };
+ seg.onmouseleave = () => { seg.style.background = 'transparent'; };
+ const pathUpTo = this.currentPath.slice(0, i + 1);
+ seg.onclick = () => this._navigateTo(pathUpTo);
+ breadcrumbs.appendChild(seg);
+ });
+ }
+
+ _refreshFileList() {
+ const sidebar = document.getElementById('filemgr-sidebar');
+ if (!sidebar) return;
+
+ // Read directory
+ const pathStr = '/' + this.currentPath.join('/');
+ const entries = this.terminal.fs.ls(pathStr) || [];
+
+ // Separate dirs and files
+ const dirs = entries.filter(e => e.type === 'dir');
+ const files = entries.filter(e => e.type !== 'dir');
+
+ // Sort alphabetically
+ dirs.sort((a, b) => a.name.localeCompare(b.name));
+ files.sort((a, b) => a.name.localeCompare(b.name));
+
+ // Apply search filter
+ let filteredDirs = dirs;
+ let filteredFiles = files;
+ if (this._searchFilter) {
+ const q = this._searchFilter;
+ filteredDirs = dirs.filter(e => e.name.toLowerCase().includes(q));
+ filteredFiles = files.filter(e => e.name.toLowerCase().includes(q));
+ }
+
+ sidebar.innerHTML = '';
+
+ if (filteredDirs.length === 0 && filteredFiles.length === 0) {
+ const emptyMsg = document.createElement('div');
+ emptyMsg.style.cssText = 'padding:20px;text-align:center;color:var(--text-muted);';
+ emptyMsg.textContent = this._searchFilter ? 'No matching files' : '(empty)';
+ sidebar.appendChild(emptyMsg);
+ this._updateStatus('0 files');
+ return;
+ }
+
+ // Add directory items
+ filteredDirs.forEach(entry => {
+ const item = document.createElement('div');
+ const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name;
+ item.dataset.path = fullPath;
+ item.dataset.type = 'dir';
+ item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;';
+ item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; };
+ item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; };
+ item.onclick = () => {
+ const newPath = [...this.currentPath, entry.name];
+ this._navigateTo(newPath);
+ };
+
+ const icon = document.createElement('span');
+ icon.textContent = '📁';
+ item.appendChild(icon);
+
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = entry.name + '/';
+ nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+ item.appendChild(nameSpan);
+
+ sidebar.appendChild(item);
+ });
+
+ // Add file items
+ filteredFiles.forEach(entry => {
+ const item = document.createElement('div');
+ const fullPath = pathStr === '/' ? '/' + entry.name : pathStr + '/' + entry.name;
+ const isSelected = this._selectedFile === fullPath;
+ item.dataset.path = fullPath;
+ item.dataset.type = 'file';
+ item.dataset.selected = isSelected ? 'true' : 'false';
+ const bgColor = isSelected ? 'var(--bg-tertiary)' : 'transparent';
+ item.style.cssText = 'display:flex;align-items:center;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--border-color);gap:6px;background:' + bgColor + ';';
+ if (isSelected) {
+ item.style.borderLeft = '3px solid var(--accent-color)';
+ }
+ item.onmouseenter = () => { if (!item.dataset.selected) item.style.background = 'var(--bg-tertiary)'; };
+ item.onmouseleave = () => { if (!item.dataset.selected) item.style.background = 'transparent'; };
+ item.onclick = () => {
+ this._selectedFile = fullPath;
+ this._highlightSelected();
+ const content = this.terminal.fs.readFile(fullPath);
+ this._renderPreview(fullPath, content);
+ };
+
+ const icon = document.createElement('span');
+ icon.textContent = '📄';
+ item.appendChild(icon);
+
+ const nameSpan = document.createElement('span');
+ nameSpan.textContent = entry.name;
+ nameSpan.style.cssText = 'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
+ item.appendChild(nameSpan);
+
+ const sizeSpan = document.createElement('span');
+ sizeSpan.textContent = this._formatSize(entry.size);
+ sizeSpan.style.cssText = 'color:var(--text-muted);font-size:0.8em;white-space:nowrap;margin-left:4px;';
+ item.appendChild(sizeSpan);
+
+ sidebar.appendChild(item);
+ });
+
+ const totalCount = entries.length;
+ this._updateStatus(totalCount + ' item' + (totalCount !== 1 ? 's' : ''));
+ }
+
+ _highlightSelected() {
+ const sidebar = document.getElementById('filemgr-sidebar');
+ if (!sidebar) return;
+ const items = sidebar.querySelectorAll('[data-type="file"]');
+ items.forEach(item => {
+ const selected = item.dataset.path === this._selectedFile;
+ item.dataset.selected = selected ? 'true' : 'false';
+ item.style.background = selected ? 'var(--bg-tertiary)' : 'transparent';
+ item.style.borderLeft = selected ? '3px solid var(--accent-color)' : '3px solid transparent';
+ });
+ }
+
+ _renderPreview(path, content) {
+ const preview = document.getElementById('filemgr-preview');
+ if (!preview) return;
+
+ // Re-read content on each preview call
+ if (content === undefined) {
+ content = this.terminal.fs.readFile(path);
+ }
+
+ // For raw mode, show raw text regardless of type
+ if (this._rawMode && typeof content === 'string') {
+ preview.innerHTML = '<div style="padding:12px"><div style="color:var(--text-muted);font-size:0.85em;margin-bottom:8px">RAW: ' + this._escapeHtml(path) + '</div><pre style="white-space:pre-wrap;font-family:var(--font-primary);font-size:0.9em;margin:0;overflow-x:auto">' + this._escapeHtml(content.substring(0, 10000)) + '</pre></div>';
+ this._updateStatus('Preview: ' + path);
+ return;
+ }
+
+ // Check file size for text files
+ const type = this._detectType(path);
+ const textTypes = ['md', 'text', 'json', 'code', 'csv', 'other'];
+ if (textTypes.includes(type) && typeof content === 'string' && content.length > 51200) {
+ content = content.substring(0, 51200);
+ preview.innerHTML = this._getPreviewHtml(path, content);
+ const truncatedMsg = document.createElement('div');
+ truncatedMsg.style.cssText = 'color:var(--warning-color);padding:8px 12px;border-top:1px solid var(--border-color);font-size:0.85em;';
+ truncatedMsg.textContent = '(truncated — file exceeds 50KB)';
+ preview.appendChild(truncatedMsg);
+ this._updateStatus('Preview: ' + path);
+ return;
+ }
+
+ preview.innerHTML = this._getPreviewHtml(path, content);
+ this._updateStatus('Preview: ' + path);
+ }
+
+ _renderPreviewError(msg) {
+ const preview = document.getElementById('filemgr-preview');
+ if (!preview) return;
+ preview.innerHTML = '<div style="color:var(--error-color);padding:20px;font-size:1em">' + this._escapeHtml(msg) + '</div>';
+ this._updateStatus('Error');
+ }
+
+ _clearPreview() {
+ const preview = document.getElementById('filemgr-preview');
+ if (preview) preview.innerHTML = '';
+ }
+
+ _updateStatus(msg) {
+ const status = document.getElementById('filemgr-status');
+ if (status) status.textContent = msg;
+ }
+
+ _actionDelete() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected to delete');
+ return;
+ }
+ if (!confirm('Delete ' + this._selectedFile + '?')) return;
+ const ok = this.terminal.fs.deleteFile(this._selectedFile);
+ if (ok) {
+ this._selectedFile = null;
+ this._refreshFileList();
+ this._clearPreview();
+ this._updateStatus('✅ Deleted');
+ } else {
+ this._updateStatus('❌ Failed to delete');
+ }
+ }
+
+ _actionDownload() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected to download');
+ return;
+ }
+ const content = this.terminal.fs.readFile(this._selectedFile);
+ if (content === null || content === false) {
+ this._updateStatus('❌ File not found');
+ return;
+ }
+ const fileName = this._selectedFile.split('/').pop() || 'file';
+ const blob = new Blob([content], { type: 'application/octet-stream' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ this._updateStatus('⬇ Downloaded: ' + fileName);
+ }
+
+ _actionEdit() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected to edit');
+ return;
+ }
+ const path = this._selectedFile;
+ // Close file manager first so code editor modal (z-index:1000) is visible above
+ this._closeOverlay();
+ if (typeof webx11 !== 'undefined' && webx11 && webx11.dispatchCommand) {
+ setTimeout(() => {
+ webx11.dispatchCommand('edit', [path]);
+ }, 100);
+ this._updateStatus('✏ Opened in editor: ' + path);
+ } else {
+ this._updateStatus('❌ Editor not available');
+ }
+ }
+
+ _togglePreviewMode() {
+ this._rawMode = !this._rawMode;
+ const btn = document.getElementById('filemgr-toggle-raw');
+ if (btn) {
+ btn.textContent = this._rawMode ? '📄 Rendered' : '📄 Raw';
+ btn.style.borderColor = this._rawMode ? 'var(--accent-color)' : 'var(--border-color)';
+ btn.style.color = this._rawMode ? 'var(--accent-color)' : 'var(--text-muted)';
+ }
+ if (this._selectedFile) {
+ const content = this.terminal.fs.readFile(this._selectedFile);
+ this._renderPreview(this._selectedFile, content);
+ }
+ }
+
+ _actionEncrypt() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected');
+ return;
+ }
+ if (this._selectedFile.endsWith('.enc')) {
+ this._updateStatus('⚠ Already encrypted (.enc)');
+ return;
+ }
+ const passphrase = prompt('Enter passphrase for AES-256-GCM encryption:');
+ if (!passphrase || passphrase.length < 4) {
+ this._updateStatus('❌ Passphrase too short (min 4 chars)');
+ return;
+ }
+ // Delegate to cryptogzip module's encrypt command
+ if (typeof webx11 !== 'undefined' && webx11.executeCommand) {
+ webx11.executeCommand('encrypt ' + this._selectedFile + ' ' + passphrase);
+ this._updateStatus('🔐 Encrypted: ' + this._selectedFile + '.enc');
+ this._refreshFileList();
+ }
+ }
+
+ _actionCompress() {
+ if (!this._selectedFile) {
+ this._updateStatus('No file selected');
+ return;
+ }
+ if (this._selectedFile.endsWith('.gz')) {
+ this._updateStatus('⚠ Already compressed (.gz)');
+ return;
+ }
+ if (typeof webx11 !== 'undefined' && webx11.executeCommand) {
+ webx11.executeCommand('gzip ' + this._selectedFile);
+ this._updateStatus('📦 Compressed: ' + this._selectedFile + '.gz');
+ this._refreshFileList();
+ }
+ }
+
+ _actionNewFile() {
+ const name = prompt('New file name (e.g. myfile.txt):');
+ if (!name) return;
+ const pathStr = '/' + this.currentPath.join('/');
+ const cleanPath = pathStr === '/' ? '/' + name : pathStr + '/' + name;
+ const content = prompt('Enter file content (optional):') || '';
+ const ok = this.terminal.fs.writeFile(cleanPath, content);
+ if (ok) {
+ this._updateStatus('✅ Created: ' + cleanPath + ' (' + content.length + ' chars)');
+ this._refreshFileList();
+ } else {
+ this._updateStatus('❌ Failed to create file');
+ }
+ }
+}
+
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new FileManagerModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/filemgr.js');
+}
+
+/* === diff.js === */
+class DiffModule {
+ constructor() {
+ this.commands = {};
+ this.terminal = null;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'diff': 'Show differences between two VFS files or compare module source (usage: diff <file1> <file2> or diff --module <name>)',
+ };
+ }
+
+ registerCommands() {
+ this.commands['diff'] = (args) => this.diff(args);
+ }
+ async diff(args) {
+ if (!args.length) return this.terminal.print('Usage: diff <file1> <file2> or diff --module <ModuleName>', 'warning');
+
+ if (args[0] === '--module') {
+ if (!args[1]) return this.terminal.print('Usage: diff --module <ModuleName>', 'warning');
+ return this._diffModule(args[1]);
+ }
+
+ return this._diffFiles(args[0], args[1]);
+ }
+ _diffFiles(path1, path2) {
+ let content1, content2;
+ try {
+ content1 = this.terminal.fs.readFile(path1);
+ content2 = this.terminal.fs.readFile(path2);
+ } catch (e) {
+ return this.terminal.print(`Error reading files: ${e.message}`, 'error');
+ }
+ if (content1 === null || content1 === undefined) {
+ return this.terminal.print(`File not found: ${path1}`, 'error');
+ }
+ if (content2 === null || content2 === undefined) {
+ return this.terminal.print(`File not found: ${path2}`, 'error');
+ }
+ const lines1 = String(content1).split('\n');
+ const lines2 = String(content2).split('\n');
+
+ // Strip trailing empty line if file ends with newline
+ if (lines1.length > 0 && lines1[lines1.length - 1] === '') lines1.pop();
+ if (lines2.length > 0 && lines2[lines2.length - 1] === '') lines2.pop();
+
+ if (lines1.length === 0 && lines2.length === 0) {
+ return this.terminal.print('Both files are empty — no differences', 'info');
+ }
+
+ // Compute LCS-based diff
+ const result = this._computeDiff(lines1, lines2);
+
+ // Render output
+ this.terminal.print(`--- ${path1}`, 'info');
+ this.terminal.print(`+++ ${path2}`, 'info');
+
+ if (result.length === 0) {
+ return this.terminal.print('Files are identical', 'success');
+ }
+
+ const maxLineNum = Math.max(lines1.length, lines2.length);
+ const lineNumWidth = String(maxLineNum).length;
+
+ result.forEach(part => {
+ if (part.type === 'same') {
+ // Print up to 3 unchanged lines for context, then collapse
+ if (part.lines.length <= 3) {
+ part.lines.forEach((line, i) => {
+ const ln = String(part.aStart + i + 1).padStart(lineNumWidth);
+ this.terminal.print(` ${ln} ${line}`, 'dim');
+ });
+ } else {
+ // Show first line
+ this.terminal.print(` ${String(part.aStart + 1).padStart(lineNumWidth)} ${part.lines[0]}`, 'dim');
+ this.terminal.print(` ${' '.repeat(lineNumWidth)} ... ${part.lines.length - 2} more unchanged lines`, 'dim');
+ // Show last line
+ const lastIdx = part.lines.length - 1;
+ const ln = String(part.aStart + lastIdx + 1).padStart(lineNumWidth);
+ this.terminal.print(` ${ln} ${part.lines[lastIdx]}`, 'dim');
+ }
+ } else if (part.type === 'removed') {
+ part.lines.forEach((line, i) => {
+ const ln = String(part.aStart + i + 1).padStart(lineNumWidth);
+ this.terminal.print(`-${ln} ${line}`, 'error');
+ });
+ } else if (part.type === 'added') {
+ part.lines.forEach((line, i) => {
+ const ln = String(part.bStart + i + 1).padStart(lineNumWidth);
+ this.terminal.print(`+${ln} ${line}`, 'success');
+ });
+ }
+ });
+ }
+ _computeDiff(a, b) {
+ // Build LCS table
+ const m = a.length;
+ const n = b.length;
+ const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1));
+
+ for (let i = 1; i <= m; i++) {
+ for (let j = 1; j <= n; j++) {
+ if (a[i - 1] === b[j - 1]) {
+ dp[i][j] = dp[i - 1][j - 1] + 1;
+ } else {
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
+ }
+ }
+ }
+
+ // Backtrack to build diff parts
+ const parts = [];
+ let i = m, j = n;
+ let removed = [], added = [], same = [];
+ const aStart = 0, bStart = 0;
+
+ const flush = () => {
+ if (same.length > 0) {
+ parts.push({
+ type: 'same',
+ aStart: aStart + (parts.length > 0 ? parts.reduce((sum, p) => {
+ if (p.type === 'same') return sum + p.lines.length;
+ if (p.type === 'removed') return sum + p.lines.length;
+ return sum;
+ }, 0) : 0),
+ lines: same.splice(0)
+ });
+ }
+ if (removed.length > 0 || added.length > 0) {
+ if (removed.length > 0) {
+ const aIdx = parts.reduce((sum, p) => {
+ if (p.type === 'same') return sum + p.lines.length;
+ if (p.type === 'removed') return sum + p.lines.length;
+ return sum;
+ }, 0);
+ parts.push({ type: 'removed', aStart: aIdx, lines: removed.splice(0) });
+ }
+ if (added.length > 0) {
+ const bIdx = parts.reduce((sum, p) => {
+ if (p.type === 'same') return sum;
+ if (p.type === 'added') return sum + p.lines.length;
+ return sum;
+ }, 0);
+ parts.push({ type: 'added', bStart: bIdx, lines: added.splice(0) });
+ }
+ }
+ };
+
+ // Trace back through DP table
+ const trace = [];
+ while (i > 0 || j > 0) {
+ if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) {
+ trace.push({ type: 'same', lineA: a[i - 1], lineB: b[j - 1] });
+ i--; j--;
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
+ trace.push({ type: 'added', lineB: b[j - 1] });
+ j--;
+ } else {
+ trace.push({ type: 'removed', lineA: a[i - 1] });
+ i--;
+ }
+ }
+
+ // Reverse trace to get forward order
+ trace.reverse();
+
+ // Build parts from trace
+ let aIdx = 0, bIdx = 0;
+ for (const entry of trace) {
+ if (entry.type === 'same') {
+ same.push(entry.lineA);
+ aIdx++;
+ bIdx++;
+ } else if (entry.type === 'removed') {
+ flush();
+ removed.push(entry.lineA);
+ aIdx++;
+ } else if (entry.type === 'added') {
+ flush();
+ added.push(entry.lineB);
+ bIdx++;
+ }
+ }
+ flush();
+
+ return parts;
+ }
+ async _diffModule(modName) {
+ // Find module in registry
+ const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName);
+ if (!entry) {
+ return this.terminal.print(`Module "${modName}" not found in registry`, 'error');
+ }
+
+ const url = entry.url;
+ if (!url) {
+ return this.terminal.print(`No source URL for module "${modName}"`, 'warning');
+ }
+
+ // Fetch original source
+ let originalSource;
+ try {
+ const resp = await fetch(url);
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
+ originalSource = await resp.text();
+ } catch (e) {
+ return this.terminal.print(`Failed to fetch original source: ${e.message}`, 'error');
+ }
+
+ // Read local edited version
+ const localPath = `_mods/${modName}.js`;
+ let localSource;
+ let hasLocalEdit = false;
+ try {
+ localSource = this.terminal.fs.readFile(localPath);
+ if (localSource === null || localSource === undefined) {
+ hasLocalEdit = false;
+ } else {
+ hasLocalEdit = true;
+ }
+ } catch (e) {
+ hasLocalEdit = false;
+ }
+
+ if (!hasLocalEdit) {
+ // Try to read the module's currently loaded source
+ const loadedModule = this.terminal.moduleManager.modules[modName];
+ if (!loadedModule) {
+ return this.terminal.print(`Module "${modName}" is not loaded and has no local edits`, 'info');
+ }
+ return this.terminal.print(`Module "${modName}" has no local edits — source matches original`, 'success');
+ }
+
+ // Compare original source vs local edit
+ const lines1 = String(originalSource).split('\n');
+ const lines2 = String(localSource).split('\n');
+
+ // Strip trailing empty line if file ends with newline
+ if (lines1.length > 0 && lines1[lines1.length - 1] === '') lines1.pop();
+ if (lines2.length > 0 && lines2[lines2.length - 1] === '') lines2.pop();
+
+ this.terminal.print(`--- ${url}`, 'info');
+ this.terminal.print(`+++ ${localPath}`, 'info');
+
+ if (lines1.length === lines2.length && lines1.every((l, i) => l === lines2[i])) {
+ return this.terminal.print('No differences — local edit matches original', 'success');
+ }
+
+ const result = this._computeDiff(lines1, lines2);
+ const maxLineNum = Math.max(lines1.length, lines2.length);
+ const lineNumWidth = String(maxLineNum).length;
+
+ result.forEach(part => {
+ if (part.type === 'same') {
+ if (part.lines.length <= 3) {
+ part.lines.forEach((line, i) => {
+ const ln = String(part.aStart + i + 1).padStart(lineNumWidth);
+ this.terminal.print(` ${ln} ${line}`, 'dim');
+ });
+ } else {
+ this.terminal.print(` ${String(part.aStart + 1).padStart(lineNumWidth)} ${part.lines[0]}`, 'dim');
+ this.terminal.print(` ${' '.repeat(lineNumWidth)} ... ${part.lines.length - 2} more unchanged lines`, 'dim');
+ const lastIdx = part.lines.length - 1;
+ const ln = String(part.aStart + lastIdx + 1).padStart(lineNumWidth);
+ this.terminal.print(` ${ln} ${part.lines[lastIdx]}`, 'dim');
+ }
+ } else if (part.type === 'removed') {
+ part.lines.forEach((line, i) => {
+ const ln = String(part.aStart + i + 1).padStart(lineNumWidth);
+ this.terminal.print(`-${ln} ${line}`, 'error');
+ });
+ } else if (part.type === 'added') {
+ part.lines.forEach((line, i) => {
+ const ln = String(part.bStart + i + 1).padStart(lineNumWidth);
+ this.terminal.print(`+${ln} ${line}`, 'success');
+ });
+ }
+ });
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new DiffModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/diff.js');
+}
+
+/* === busybox.js === */
+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]) {
+ let foundMod = null;
+ for (const modName in this.terminal.moduleManager.modules) {
+ const mod = this.terminal.moduleManager.modules[modName];
+ if (mod.commands && mod.commands[query]) { foundMod = mod; break; }
+ }
+ const src = foundMod ? ` (from ${foundMod.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 ' + (window.NEWOS4_VERSION || 'v4.0');
+ 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');
+ }
+}
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new BusyBoxModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/busybox.js');
+}