// Critical ext module bundle// Generated from /var/www/webx11.duckdns.org/spaces/newos4/modules/ext
/* === 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');
const gutter = document.getElementById('code-editor-gutter');
const statusEl = document.getElementById('code-editor-status');
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();
// Editor enhancements: line numbers, Tab, Ctrl+S
const updateGutter = () => {
const lines = textarea.value.split('\n').length;
let html = '';
for (let i = 1; i <= lines; i++) html += i + '\n';
gutter.textContent = '';
gutter.textContent = html;
};
const syncScroll = () => { gutter.scrollTop = textarea.scrollTop; };
const updateStatus = () => {
const lines = textarea.value.split('\n').length;
const chars = textarea.value.length;
statusEl.textContent = `${lines} lines ยท ${chars} chars`;
};
updateGutter();
updateStatus();
textarea.onscroll = syncScroll;
textarea.oninput = () => { updateGutter(); updateStatus(); };
textarea.onkeydown = (e) => {
if (e.key === 'Tab') { e.preventDefault(); insertTab(textarea, e.shiftKey); updateGutter(); }
if ((e.ctrlKey || e.metaKey) && e.key === 's') { e.preventDefault(); saveBtn.click(); }
};
function insertTab(el, shift) {
const s = el.selectionStart, e2 = el.selectionEnd;
if (shift) {
const before = el.value.substring(0, s);
const lineStart = before.lastIndexOf('\n') + 1;
if (lineStart < s && el.value.substring(lineStart, lineStart + 1) === '\t') {
el.value = el.value.substring(0, lineStart) + el.value.substring(lineStart + 1);
el.selectionStart = el.selectionEnd = s - 1;
}
} else {
el.value = el.value.substring(0, s) + '\t' + el.value.substring(e2);
el.selectionStart = el.selectionEnd = s + 1;
}
}
// Fullscreen toggle
window._editorToggleFullscreen = () => {
const content = modal.querySelector('.modal-content');
if (content.style.maxWidth === '100vw') {
content.style.maxWidth = '90vw'; content.style.maxHeight = '90vh'; content.style.width = '900px'; content.style.height = '80vh';
} else {
content.style.maxWidth = '100vw'; content.style.maxHeight = '100vh'; content.style.width = '100vw'; content.style.height = '100vh';
}
};
// 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);
// Save to command history (preserved from original executeCommand)
const cmdRaw = rawInput.trim();
if (this.history.length === 0 || this.history[this.history.length - 1] !== cmdRaw) {
this.history.push(cmdRaw);
}
if (this.history.length > this._historyCap) {
this.history = this.history.slice(-this._historyCap);
}
this.historyIndex = this.history.length;
this._saveCommandHistory();
// 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 || โ operator now attached to the FOLLOWING segment
const chainParts = self._splitChain(seqPart.trim());
let lastOk = true;
for (const chainPart of chainParts) {
if (!chainPart.cmd.trim()) continue;
const { cmd: chainCmd, operator } = chainPart;
// Check the operator that PRECEDES this command
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 = this.splitPipe(chainCmd);
if (pipeParts.length > 1) {
await this.executePipeChain(pipeParts);
} else {
// Single command โ check for shell built-ins first
const parts = chainCmd.split(/\s+/);
const cmd = parts[0].toLowerCase();
const args = parts.slice(1);
if (self.commands[cmd]) {
try {
this.markDirty();
await self.commands[cmd](args);
lastOk = true;
} catch(e) {
this.print('Error: ' + e.message, 'error');
lastOk = false;
}
} else {
try {
await this.security.checkAndRun(chainCmd);
lastOk = true;
} catch(e) {
this.print('Error: ' + e.message, 'error');
lastOk = false;
}
}
}
}
}
}
};
this.terminal._shellModule = this;
// Modify pipe chain to add pipe-aware injection and security checks
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);
// Security gate โ replicate the original executePipeChain checks
const cls = this.security._classify(cmdText);
if (cls.destructive && this.security.level > 0) {
this.print(`โ ๏ธ Security: Destructive command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
return;
}
if (cls.system && this.security.level > 0) {
this.print(`โ ๏ธ Security: System command "${cmd}" blocked in pipe at level ${this.security.level}`, 'error');
return;
}
if (!cls.known && this.security.level >= 2) {
this.print(`โ ๏ธ Security: Unknown command "${cmd}" blocked in pipe (paranoid mode)`, 'error');
return;
}
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__');
}
}
}
try {
if (i < pipeParts.length - 1) {
pipeInput = await this.capturePipeOutput(cmd, args);
} else {
await this.dispatchCommand(cmd, args);
}
} finally {
// Clean up temp file
if (this.fs.readFile('__pipebuf__') !== null) {
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
// Operator is attached to the FOLLOWING segment (the one it guards)
const parts = [];
let current = '';
let inQuote = false;
let quoteChar = '';
let pendingOp = null; // operator from previous split, applies to next segment
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: pendingOp });
pendingOp = '&&';
current = ''; i += 2;
} else if (!inQuote && input.substring(i, i+2) === '||') {
parts.push({ cmd: current.trim(), operator: pendingOp });
pendingOp = '||';
current = ''; i += 2;
} else {
current += ch; i++;
}
}
parts.push({ cmd: current.trim(), operator: pendingOp });
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()); current = ''; }
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()); current = ''; }
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()); current = ''; }
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');
redirectInfo.exitOk = false;
return;
}
this.terminal._pipeInput = inputContent;
try {
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__');
}
try {
await this.terminal.dispatchCommand(cmd, newArgs);
} finally {
if (fs.readFile('__pipebuf__') !== null) {
delete fs.root.content['__pipebuf__'];
}
}
} else {
await this.terminal.dispatchCommand(cmd, args);
}
redirectInfo.exitOk = true;
} catch(e) {
this.terminal.print('Error: ' + e.message, 'error');
redirectInfo.exitOk = false;
} finally {
this.terminal._pipeInput = null;
}
return;
}
// > or >> โ capture output, write to file
let output = '';
this.terminal._pushPrintCapture(buf => { output += buf; });
let cmdOk = true;
try {
// 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) {
let capt = '';
this.terminal._pushPrintCapture(buf => { capt += buf; });
try {
await this.terminal.dispatchCommand(pCmd, pArgs);
} finally {
this.terminal._popPrintCapture();
}
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);
}
} catch(e) {
cmdOk = false;
// Pop capture before showing the error
this.terminal._popPrintCapture();
this.terminal.print('Error: ' + e.message, 'error');
redirectInfo.exitOk = false;
return;
} finally {
// Always pop capture โ critical: without this, terminal output stays captured
// (empty stack = normal DOM printing, so pop is idempotent-safe when already popped)
if (this.terminal._printCaptureStack && this.terminal._printCaptureStack.length > 0) {
// popped in catch path for error case; success path pops here
this.terminal._popPrintCapture();
}
}
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');
}
}
redirectInfo.exitOk = cmdOk;
}
_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++;
try {
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;
this.terminal.print(`[${path}:${lineNum}] ${line}`, 'command-prompt');
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 a shell built-in command
if (this.commands[cmd]) {
try {
this.terminal.markDirty();
await this.commands[cmd](args);
lastOk = true;
} catch(e) {
this.terminal.print('Error: ' + e.message, 'error');
lastOk = false;
}
} else {
try {
await this.terminal.security.checkAndRun(chainPart.cmd);
lastOk = true;
} catch(e) {
this.terminal.print('Error: ' + e.message, 'error');
lastOk = false;
}
}
}
}
}
}
}
this.terminal.print(`Script ${path} completed (${lineNum} lines)`, 'success');
} finally {
// Always decrement โ even on error or thrown exception
this._scriptDepth--;
}
}
}
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._multiSelect = new Set(); // v4.5.78: multi-select for batch ops
this._clipboard = { mode: null, paths: [] }; // 'copy' or 'cut'
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 โ v4.5.67: sanitize URL to prevent javascript: scheme injection
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#';
return `${text}`;
});
// 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(/