// 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 — re-fetch, re-parse, re-register
this.commands['module-reload'] = async (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 mm = this.terminal.moduleManager;
// Find the module's URL from registry
const regEntry = mm.moduleRegistry.find(m => m.name === name);
const url = regEntry ? regEntry.url : null;
if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
// Unregister old instance
mm.unregisterModule(name);
// Re-fetch and re-register
const success = await mm.loadModuleFromURL(url);
if (success) {
this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
} else {
this.terminal.print(`❌ Failed to reload '${name}'`, 'error');
}
};
// 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);
};
}
}