From de713efd619360e29b9bfc4b99d30d2a86b96caf Mon Sep 17 00:00:00 2001
From: Agando
Date: Sat, 13 Jun 2026 18:00:58 +0200
Subject: auto: 2026-06-13_18:00 changes
---
modules/ext/critical-ext-bundle.js | 2363 ++++++++++++++++++++++++++++++++++++
1 file changed, 2363 insertions(+)
create mode 100644 modules/ext/critical-ext-bundle.js
(limited to 'modules/ext/critical-ext-bundle.js')
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 )',
+ };
+ }
+
+ registerCommands() {
+ this.commands['edit'] = (args) => this.edit(args);
+ }
+ edit(args) {
+ if (!args[0]) return this.terminal.print('Usage: edit ', '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 = — Set a shell variable',
+ 'unset': 'unset — Remove a shell variable',
+ 'script': 'script — Run a shell script from VFS',
+ '$': '$ — Execute with shell parsing (default: auto-detected)',
+ 'sh': '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 =', '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 ', '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 ', '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 )',
+ };
+ }
+
+ 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 <name>=<value>', '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 <name>', '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 )',
+ };
+ }
+
+ registerCommands() {
+ this.commands['find'] = (args) => this.find(args);
+ }
+ find(args) {
+ if (!args[0]) return this.terminal.print('Usage: find — 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 — 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 ', '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, '&')
+ .replace(//g, '>');
+ // code blocks first
+ html = html.replace(/```([\s\S]*?)```/g, '$1
');
+ // inline code
+ html = html.replace(/`([^`]+)`/g, '$1');
+ // headers
+ html = html.replace(/^### (.+)$/gm, '$1
');
+ html = html.replace(/^## (.+)$/gm, '$1
');
+ html = html.replace(/^# (.+)$/gm, '$1
');
+ // bold
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1');
+ // italic
+ html = html.replace(/\*(.+?)\*/g, '$1');
+ // links
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1');
+ // horizontal rules
+ html = html.replace(/^---$/gm, '
');
+ // unordered lists
+ html = html.replace(/^[\s]*[-*]\s+(.+)$/gm, '$1');
+ // paragraphs - double newlines
+ html = html.replace(/\n\n/g, '
');
+ html = '
' + html + '
';
+ return html;
+ } catch (e) {
+ return '' + text.replace(/&/g, '&').replace(//g, '>') + '
';
+ }
+ }
+
+ _getPreviewHtml(path, content) {
+ if (content === null || content === false) {
+ return 'File not found: ' + this._escapeHtml(path) + '
';
+ }
+
+ 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 'Cannot preview: non-string content
';
+ }
+ 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 '' + this._renderMarkdown(content) + '
';
+ }
+
+ if (type === 'html') {
+ // Render HTML in an inline sandbox frame
+ const safeHtml = content
+ .replace(/