// Extracted from newOS4 index.html — ModuleManagerModule // v4.5.43: Enhanced with module-info, module-list, module-stats, module-disable/enable, // module-reload, module-reset. Categories: CORE/EXT/FOUNDATION. class ModuleManagerModule { constructor() { this.commands = {}; this.terminal = null; } getHelpInfo() { return { 'modules': 'modules — Open the module manager GUI', 'listmods': 'listmods — List all loaded modules with status', 'modules-list': 'modules-list [core|ext] — List all available modules from the module index', 'module-list': 'module-list [flags] — List loaded modules grouped by category. Flags: --flat, --core, --ext, --foundation, --lazy, --eager, --loaded, --unloaded, --search=, --failed, --disabled', 'module-info': 'module-info — Show module details: path, status, size, type, source, commands list', 'module-stats': 'module-stats — Show aggregate module statistics (loaded/unloaded/lazy/failed/disabled + total size)', 'module-load': 'module-load — Mark a lazy module as loaded', 'module-unload': 'module-unload — Mark a module as unloaded', 'module-reload': 'module-reload — Unload then reload a module (shows diff)', 'module-reset': 'module-reset — Reset a module to a clean state (clears its state, keeps commands)', 'module-disable': 'module-disable — Disable a module from auto-loading on refresh (persists)', 'module-enable': 'module-enable — Re-enable a previously disabled module', 'loadmod': 'loadmod — Load a module from a URL', 'removemod': 'removemod — Remove a loaded module by name', }; } // Helper: read disabled list from localStorage _getDisabled() { try { return JSON.parse(localStorage.getItem('webx11_module_disabled') || '[]'); } catch { return []; } } _setDisabled(list) { localStorage.setItem('webx11_module_disabled', JSON.stringify(list)); } // Helper: get category for a module (CORE / EXTENSION / FOUNDATION / UNKNOWN) _getCategory(mod) { if (!mod) return 'UNKNOWN'; if (mod.path && mod.path.includes('/modules/ext/')) return 'EXTENSION'; if (mod.path && mod.path.includes('/modules/core/')) return 'CORE'; if (mod.type === 'foundation' || (mod.path && mod.path.includes('FOUNDATION'))) return 'FOUNDATION'; return 'CORE'; // default } // Helper: collect all modules (from registry) into an array of {name, mod} _collectAll() { const mm = this.terminal.moduleManager; const result = []; const allMods = mm.modules || new Map(); // moduleManager.modules is a Map if (allMods instanceof Map) { for (const [name, mod] of allMods) { result.push({ name, mod }); } } else { for (const name of Object.keys(allMods)) { result.push({ name, mod: allMods[name] }); } } return result; } // Helper: get a single module by name _getMod(name) { const mm = this.terminal.moduleManager; if (mm.modules instanceof Map) return mm.modules.get(name); return mm.modules[name]; } registerCommands() { this.commands['modules'] = () => { this.terminal.openModuleManager(); }; this.commands['loadmod'] = async (args) => { if(!args[0]) return this.terminal.print("Usage: loadmod ", 'warning'); await this.terminal.moduleManager.loadModuleFromURL(args[0]); this.terminal.renderModuleList(); }; this.commands['removemod'] = (args) => { if(!args[0]) return this.terminal.print("Usage: removemod ", 'warning'); if(this.terminal.moduleManager.unregisterModule(args[0])) { this.terminal.print(`Module '${args[0]}' removed`, 'success'); } else { this.terminal.print(`Module not found: ${args[0]}`, 'error'); } }; this.commands['listmods'] = () => { const mods = this.terminal.moduleManager.getModuleList(); if(mods.length === 0) return this.terminal.print("No modules loaded"); mods.forEach(m => { const status = m.loaded ? '✓' : '✗'; const badge = m.url ? '' : ' [Built-in]'; this.terminal.print(`${status} ${m.name}${badge}`); }); }; this.commands['modules-list'] = async (args) => { try { const filter = (args[0] || '').toLowerCase(); const resp = await fetch('/spaces/newos4/modules-index.json?v=' + (localStorage.getItem('webx11_cache_ver') || '')); if (!resp.ok) return this.terminal.print('Failed to load module index', 'error'); const idx = await resp.json(); let mods = Object.values(idx.modules); if (filter === 'core' || filter === 'ext') mods = mods.filter(m => m.type === filter); if (filter === 'infra') mods = mods.filter(m => m.layer === 'infrastructure'); const loaded = this.terminal.moduleManager.modules || {}; const loadedCount = Object.keys(loaded).length; const byLayer = {}; mods.forEach(m => { const l = m.layer || m.type || 'other'; if (!byLayer[l]) byLayer[l] = []; byLayer[l].push(m); }); const layerSummary = Object.entries(byLayer).map(([l, ms]) => `${ms.length} ${l}`).join(', '); this.terminal.print(`📦 Module Index (${mods.length} modules: ${layerSummary} — ${loadedCount} loaded)`, 'header'); for (const [layer, layerMods] of Object.entries(byLayer)) { this.terminal.print(` ── ${layer.toUpperCase()} (${layerMods.length}) ──`, 'info'); for (const m of layerMods) { const isLoaded = loaded[m.class] ? '✓' : ' '; const layerTag = m.layer === 'infrastructure' ? '[infra]' : m.type === 'core' ? '[core]' : '[ext]'; const cmdHint = m.commands.length > 0 ? ` ${m.commands.slice(0, 4).join(', ')}${m.commands.length > 4 ? '...' : ''}` : ''; const desc = m.description ? ` — ${m.description.substring(0, 60)}` : ''; this.terminal.print(` ${isLoaded} ${layerTag} ${m.name}${cmdHint}${desc}`, ''); } } } catch (e) { this.terminal.print(`modules-list error: ${e.message}`, 'error'); } }; // ── v4.5.43 enhanced module commands ───────────────────── // module-list [flags] — grouped by category with filters this.commands['module-list'] = (args) => { const flags = args.filter(a => a.startsWith('--')); const allMods = this._collectAll(); const disabled = this._getDisabled(); // Apply filters let filtered = allMods; if (flags.includes('--core')) { filtered = filtered.filter(({ mod }) => this._getCategory(mod) === 'CORE'); } if (flags.includes('--ext')) { filtered = filtered.filter(({ mod }) => this._getCategory(mod) === 'EXTENSION'); } if (flags.includes('--foundation')) { filtered = filtered.filter(({ mod }) => this._getCategory(mod) === 'FOUNDATION'); } if (flags.includes('--lazy')) { filtered = filtered.filter(({ mod }) => mod.lazy); } if (flags.includes('--eager')) { filtered = filtered.filter(({ mod }) => !mod.lazy); } if (flags.includes('--loaded')) { filtered = filtered.filter(({ mod }) => mod.loaded !== false); } if (flags.includes('--unloaded')) { filtered = filtered.filter(({ mod }) => mod.loaded === false); } if (flags.includes('--failed')) { filtered = filtered.filter(({ mod }) => mod.status === 'failed' || mod.error); } if (flags.includes('--disabled')) { filtered = filtered.filter(({ name }) => disabled.includes(name)); } const searchFlag = flags.find(f => f.startsWith('--search=')); if (searchFlag) { const pattern = searchFlag.split('=')[1].toLowerCase(); filtered = filtered.filter(({ name, mod }) => name.toLowerCase().includes(pattern) || (mod.description || '').toLowerCase().includes(pattern) ); } if (filtered.length === 0) { this.terminal.print('No modules match filters', 'info'); return; } const flat = flags.includes('--flat'); if (flat) { // Old format: wall of text this.terminal.print(`📦 Modules (${filtered.length} of ${allMods.length})`, 'header'); for (const { name, mod } of filtered) { const size = mod.size ? `${(mod.size / 1024).toFixed(1)}KB` : '?KB'; const lazy = mod.lazy ? ' [L]' : ''; const loaded = mod.loaded !== false ? '✓' : '✗'; const isDisabled = disabled.includes(name) ? ' [DISABLED]' : ''; this.terminal.print(` ${loaded} ${name.padEnd(28)} ${size.padStart(8)}${lazy}${isDisabled}`); } } else { // Grouped by category const groups = { CORE: [], EXTENSION: [], FOUNDATION: [], UNKNOWN: [] }; for (const { name, mod } of filtered) { const cat = this._getCategory(mod); groups[cat].push({ name, mod }); } for (const cat of ['CORE', 'EXTENSION', 'FOUNDATION', 'UNKNOWN']) { if (groups[cat].length === 0) continue; this.terminal.print(`── ${cat} (${groups[cat].length}) ──`, 'info'); for (const { name, mod } of groups[cat]) { const size = mod.size ? `${(mod.size / 1024).toFixed(1)}KB` : '?KB'; const lazy = mod.lazy ? ' [L]' : ''; const loaded = mod.loaded !== false ? '✓' : '✗'; const isDisabled = disabled.includes(name) ? ' [DISABLED]' : ''; const errMark = (mod.status === 'failed' || mod.error) ? ' ⚠️' : ''; this.terminal.print(` ${loaded} ${name.padEnd(28)} ${size.padStart(8)}${lazy}${isDisabled}${errMark}`); } } } // Aggregate summary const totalSize = filtered.reduce((sum, { mod }) => sum + (mod.size || 0), 0); const lazyCount = filtered.filter(({ mod }) => mod.lazy).length; const failedCount = filtered.filter(({ mod }) => mod.status === 'failed' || mod.error).length; const disabledCount = filtered.filter(({ name }) => disabled.includes(name)).length; this.terminal.print('─'.repeat(50), 'text-muted'); this.terminal.print( `Total: ${filtered.length} modules, ${(totalSize / 1024).toFixed(1)}KB, ` + `${lazyCount} lazy, ${failedCount} failed, ${disabledCount} disabled`, failedCount > 0 ? 'warning' : 'info' ); }; // module-info — detailed view including commands this.commands['module-info'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-info ', 'warning'); const mod = this._getMod(args[0]); if (!mod) return this.terminal.print(`Module not found: ${args[0]}`, 'error'); const disabled = this._getDisabled(); const isDisabled = disabled.includes(args[0]); const cat = this._getCategory(mod); const sizeKB = mod.size ? (mod.size / 1024).toFixed(1) : '?'; const commandCount = mod.commands ? Object.keys(mod.commands).length : 0; const status = mod.status || (mod.loaded !== false ? 'loaded' : 'unloaded'); this.terminal.print(`📋 Module: ${args[0]}`, 'header'); this.terminal.print(` Status: ${status}${isDisabled ? ' [DISABLED]' : ''}`); this.terminal.print(` Category: ${cat}`); this.terminal.print(` Type: ${mod.type || 'unknown'}`); this.terminal.print(` Path: ${mod.path || 'unknown'}`); this.terminal.print(` Size: ${sizeKB} KB`); this.terminal.print(` Lazy: ${mod.lazy ? 'yes' : 'no'}`); this.terminal.print(` Commands: ${commandCount}`); if (mod.description) { this.terminal.print(` Description: ${mod.description}`); } // List the commands if (mod.commands && commandCount > 0) { this.terminal.print(` ── Commands (${commandCount}) ──`, 'info'); const cmds = Object.keys(mod.commands).sort(); // Print in 2 columns const half = Math.ceil(cmds.length / 2); for (let i = 0; i < half; i++) { const left = cmds[i] || ''; const right = cmds[i + half] || ''; this.terminal.print(` ${left.padEnd(24)} ${right}`); } } if (mod.error) { this.terminal.print(` ⚠️ Error: ${mod.error}`, 'error'); } }; // module-stats — aggregate view this.commands['module-stats'] = () => { const all = this._collectAll(); const disabled = this._getDisabled(); const loaded = all.filter(({ mod }) => mod.loaded !== false).length; const unloaded = all.filter(({ mod }) => mod.loaded === false).length; const lazy = all.filter(({ mod }) => mod.lazy).length; const eager = all.length - lazy; const failed = all.filter(({ mod }) => mod.status === 'failed' || mod.error).length; const disabledCount = all.filter(({ name }) => disabled.includes(name)).length; const totalSize = all.reduce((sum, { mod }) => sum + (mod.size || 0), 0); const totalCmds = all.reduce((sum, { mod }) => sum + (mod.commands ? Object.keys(mod.commands).length : 0), 0); this.terminal.print('📊 Module Stats', 'header'); this.terminal.print(` Total: ${all.length} modules`); this.terminal.print(` Loaded: ${loaded}`); this.terminal.print(` Unloaded: ${unloaded}`); this.terminal.print(` Lazy: ${lazy}`); this.terminal.print(` Eager: ${eager}`); this.terminal.print(` Failed: ${failed}`, failed > 0 ? 'warning' : ''); this.terminal.print(` Disabled: ${disabledCount}`, disabledCount > 0 ? 'warning' : ''); this.terminal.print(` Total size: ${(totalSize / 1024).toFixed(1)} KB`); this.terminal.print(` Total cmds: ${totalCmds}`); }; // module-disable — persists in localStorage this.commands['module-disable'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-disable ', 'warning'); const name = args[0]; if (!this._getMod(name)) return this.terminal.print(`Module not found: ${name}`, 'error'); const list = this._getDisabled(); if (list.includes(name)) { return this.terminal.print(`Module '${name}' is already disabled`, 'info'); } list.push(name); this._setDisabled(list); this.terminal.print(`✅ Module '${name}' disabled (will not auto-load on next refresh)`, 'success'); this.terminal.print(` To re-enable: module-enable ${name}`, 'info'); }; // module-enable — removes from disabled list this.commands['module-enable'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-enable ', 'warning'); const name = args[0]; const list = this._getDisabled(); if (!list.includes(name)) { return this.terminal.print(`Module '${name}' is not disabled`, 'info'); } const newList = list.filter(n => n !== name); this._setDisabled(newList); this.terminal.print(`✅ Module '${name}' re-enabled (will auto-load on next refresh)`, 'success'); }; // module-load — mark a lazy module as loaded this.commands['module-load'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-load ', 'warning'); const name = args[0]; const mod = this._getMod(name); if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error'); if (mod.loaded !== false) { return this.terminal.print(`Module '${name}' is already loaded`, 'info'); } mod.loaded = true; this.terminal.print(`✅ Module '${name}' marked as loaded`, 'success'); }; // module-unload — mark as unloaded this.commands['module-unload'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-unload ', 'warning'); const name = args[0]; const mod = this._getMod(name); if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error'); if (mod.loaded === false) { return this.terminal.print(`Module '${name}' is already unloaded`, 'info'); } mod.loaded = false; this.terminal.print(`✅ Module '${name}' marked as unloaded (commands will not be available)`, 'success'); }; // module-reload — unload + reload with diff this.commands['module-reload'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-reload ', 'warning'); const name = args[0]; const mod = this._getMod(name); if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error'); const wasLoaded = mod.loaded !== false; const wasLazy = mod.lazy; this.terminal.print(`🔄 Reloading '${name}' (was: ${wasLoaded ? 'loaded' : 'unloaded'}, lazy=${wasLazy})`, 'info'); mod.loaded = false; setTimeout(() => { mod.loaded = true; this.terminal.print(`✅ Module '${name}' reloaded`, 'success'); const cmdCount = mod.commands ? Object.keys(mod.commands).length : 0; this.terminal.print(` Now: loaded=true, lazy=${mod.lazy}, ${cmdCount} commands`, 'info'); }, 100); }; // module-reset — clear module state and reload this.commands['module-reset'] = (args) => { if (!args[0]) return this.terminal.print('Usage: module-reset ', 'warning'); const name = args[0]; const mod = this._getMod(name); if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error'); this.terminal.print(`🔄 Resetting module '${name}' state...`, 'info'); // Unload mod.loaded = false; // Clear any state-related localStorage keys for this module let clearedKeys = 0; try { for (let i = localStorage.length - 1; i >= 0; i--) { const key = localStorage.key(i); // Match module-specific keys (e.g., "webx11__..." or "webx11_") if (key && (key === `webx11_${name}` || key.startsWith(`webx11_${name}_`) || key.startsWith(`${name}_`))) { localStorage.removeItem(key); clearedKeys++; } } } catch (e) {} // Reload after a tick setTimeout(() => { mod.loaded = true; this.terminal.print(`✅ Module '${name}' reset (cleared ${clearedKeys} localStorage keys)`, 'success'); }, 100); }; } }