aboutsummaryrefslogtreecommitdiff
path: root/modules/core/terminal.js
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
committerAgando <roduccomm@gmail.com>2026-07-09 06:00:32 +0200
commit2538c91876455fe96febf441fc8dde20f2606023 (patch)
treeb74c949cdf4296d10103dd57ef8178ba427a5b39 /modules/core/terminal.js
parentea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (diff)
auto: 2026-07-09_06:00 changes
Diffstat (limited to 'modules/core/terminal.js')
-rw-r--r--modules/core/terminal.js291
1 files changed, 233 insertions, 58 deletions
diff --git a/modules/core/terminal.js b/modules/core/terminal.js
index 3abb074..9896bee 100644
--- a/modules/core/terminal.js
+++ b/modules/core/terminal.js
@@ -7,19 +7,71 @@
this.lastRouteTime = 0;
this.lastRouteCmd = '';
this.blockedCount = 0;
- this.destructiveCmds = new Set(['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot']);
- this.systemCmds = new Set(['reboot','shutdown','format','kill','halt','poweroff']);
- this.suspiciousFlags = new Set(['--dump','--execute','--eval','--exec','--shell','--script']);
- this.whitelist = new Set(['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze']);
- // Load persisted overrides from localStorage
- try { const d = localStorage.getItem('webx11_security_destructive'); if(d){JSON.parse(d).forEach(c=>this.destructiveCmds.add(c));} }catch(e){}
- try { const s = localStorage.getItem('webx11_security_system'); if(s){JSON.parse(s).forEach(c=>this.systemCmds.add(c));} }catch(e){}
- try { const w = localStorage.getItem('webx11_security_whitelist'); if(w){JSON.parse(w).forEach(c=>this.whitelist.add(c));} }catch(e){}
- try { const f = localStorage.getItem('webx11_security_flags'); if(f){JSON.parse(f).forEach(c=>this.suspiciousFlags.add(c));} }catch(e){}
+ this._baseDestructive = ['delete','del','write','save','overwrite','format','kill','stop','shutdown','reboot'];
+ this._baseSystem = ['reboot','shutdown','format','kill','halt','poweroff'];
+ this._baseSuspiciousFlags = ['--dump','--execute','--eval','--exec','--shell','--script'];
+ this._baseWhitelist = ['help','clear','ls','cat','pwd','whoami','df','free','top','ps','echo','date','env','history','find','grep','tree','head','tail','wc','sort','du','diff','which','hostname','cal','basename','dirname','yes','seq','cut','uniq','expand','printf','base64','sleep','ll','h','mem','status','health-check','system-health','alias','unalias','modules','listmods','modules-list','nvfs-index','nvfs-status','nvfs-resolve','nvfs-tree','nvfs-config','nvfs-alias','nvfs-sync','floating-add','floating-remove','floating-list','floating-pos','floating-clear','floating-toggle','mem-debug','reset-mem','reset','factory-reset','reset-vfs','js','js-run','node','debug','debug-ai','debug-stats','debug-history','debug-analyze','debug-trends','debug-fix','debug-health','session-new','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard','context-auto'];
+ this.destructiveCmds = new Set(this._baseDestructive);
+ this.systemCmds = new Set(this._baseSystem);
+ this.suspiciousFlags = new Set(this._baseSuspiciousFlags);
+ this.whitelist = new Set(this._baseWhitelist);
+ // Load persisted user deltas (add/remove) so hardcoded defaults can be demoted.
+ this._loadSecurityDeltas();
this._registerBuiltIn();
}
+ _loadSecurityDeltas() {
+ try {
+ const schema = this.terminal._getDataSchemaVersion ? this.terminal._getDataSchemaVersion() : 1;
+ localStorage.setItem('webx11_data_schema_version', String(schema));
+ const raw = localStorage.getItem('webx11_security_deltas');
+ if (!raw) {
+ // One-time migration from legacy flat keys to delta format.
+ const legacy = {};
+ try { legacy.destructive = JSON.parse(localStorage.getItem('webx11_security_destructive') || '[]'); } catch (e) {}
+ try { legacy.system = JSON.parse(localStorage.getItem('webx11_security_system') || '[]'); } catch (e) {}
+ try { legacy.whitelist = JSON.parse(localStorage.getItem('webx11_security_whitelist') || '[]'); } catch (e) {}
+ try { legacy.flags = JSON.parse(localStorage.getItem('webx11_security_flags') || '[]'); } catch (e) {}
+ const delta = { add: {}, remove: {} };
+ for (const c of (legacy.destructive || [])) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of (legacy.system || [])) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of (legacy.whitelist || [])) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const f of (legacy.flags || [])) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ localStorage.removeItem('webx11_security_destructive');
+ localStorage.removeItem('webx11_security_system');
+ localStorage.removeItem('webx11_security_whitelist');
+ localStorage.removeItem('webx11_security_flags');
+ return;
+ }
+ const d = JSON.parse(raw);
+ if (d.remove) {
+ for (const c of (d.remove.destructive || [])) this.destructiveCmds.delete(c);
+ for (const c of (d.remove.system || [])) this.systemCmds.delete(c);
+ for (const c of (d.remove.whitelist || [])) this.whitelist.delete(c);
+ for (const f of (d.remove.flags || [])) this.suspiciousFlags.delete(f);
+ }
+ if (d.add) {
+ for (const c of (d.add.destructive || [])) this.destructiveCmds.add(c);
+ for (const c of (d.add.system || [])) this.systemCmds.add(c);
+ for (const c of (d.add.whitelist || [])) this.whitelist.add(c);
+ for (const f of (d.add.flags || [])) this.suspiciousFlags.add(f);
+ }
+ } catch (e) {}
+ }
+ _saveSecurityDeltas() {
+ const delta = { add: {}, remove: {} };
+ for (const c of this.destructiveCmds) if (!this._baseDestructive.includes(c)) { if (!delta.add.destructive) delta.add.destructive = []; delta.add.destructive.push(c); }
+ for (const c of this._baseDestructive) if (!this.destructiveCmds.has(c)) { if (!delta.remove.destructive) delta.remove.destructive = []; delta.remove.destructive.push(c); }
+ for (const c of this.systemCmds) if (!this._baseSystem.includes(c)) { if (!delta.add.system) delta.add.system = []; delta.add.system.push(c); }
+ for (const c of this._baseSystem) if (!this.systemCmds.has(c)) { if (!delta.remove.system) delta.remove.system = []; delta.remove.system.push(c); }
+ for (const c of this.whitelist) if (!this._baseWhitelist.includes(c)) { if (!delta.add.whitelist) delta.add.whitelist = []; delta.add.whitelist.push(c); }
+ for (const c of this._baseWhitelist) if (!this.whitelist.has(c)) { if (!delta.remove.whitelist) delta.remove.whitelist = []; delta.remove.whitelist.push(c); }
+ for (const f of this.suspiciousFlags) if (!this._baseSuspiciousFlags.includes(f)) { if (!delta.add.flags) delta.add.flags = []; delta.add.flags.push(f); }
+ for (const f of this._baseSuspiciousFlags) if (!this.suspiciousFlags.has(f)) { if (!delta.remove.flags) delta.remove.flags = []; delta.remove.flags.push(f); }
+ localStorage.setItem('webx11_security_deltas', JSON.stringify(delta));
+ }
_registerBuiltIn() {
- const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','cat','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze'];
+ const internals = ['clear','help','ls','cat','df','free','env','history','cd','pwd','touch','mkdir','rm','cp','mv','edit','run','font-size','load-local','loadlocal','mem','security','security-level','security-destructive','security-system','security-whitelist','security-flag','config','ai-config','startup-modules','smart-prompt','cmd-allow','cmd-deny','system-health','server','servers','server-check','monitor','vfs','vfs-clean','vfs-size','empty-trash','expand','sysmsg','orchestrate','orchestrate-parallel','orchestrate-decompose','orchestrate-result','orchestrator-status','orchestrator-spawn','orchestrator-kill','orchestrator-purge','orchestrator-config','orchestrator-list','orchestrator-sleep','orchestrator-wake','orchestrator-restart-policy','a2a-send','a2a-list','a2a-post','a2a-poll','a2a-ack','a2a-reject','a2a-status','a2a-prune','consensus','consensus-status','consensus-history','pipeline','agent-list','agent-register','agent-status','agent-unregister','contract-list','contract-register','voice','speak','analyze','dashboard'];
internals.forEach(c => this.registerCommand(c, { category: 'builtin' }));
}
setLevel(n) {
@@ -38,8 +90,13 @@
}
registerCommand(name, meta = {}) {
this.commandMeta.set(name, { destructive: false, category: 'user', ...meta });
+ this.invalidateCommandCache();
+ }
+ invalidateCommandCache() {
+ this._commandCacheDirty = true;
}
_collectAllCommands() {
+ if (!this._commandCacheDirty && this._commandCache) return this._commandCache;
const cmds = new Set([...this.commandMeta.keys()]);
for (const modName in this.terminal.moduleManager.modules) {
const mod = this.terminal.moduleManager.modules[modName];
@@ -49,7 +106,9 @@
}
const builtins = ['clear','help','ls','df','security','security-level'];
builtins.forEach(c => cmds.add(c));
- return [...cmds];
+ this._commandCache = [...cmds];
+ this._commandCacheDirty = false;
+ return this._commandCache;
}
_levenshtein(a, b) {
a = a.toLowerCase(); b = b.toLowerCase();
@@ -278,6 +337,19 @@
// Apply config overrides after everything is initialized
this._applyTerminalConfig();
}
+ /* ---------- helpers ---------- */
+ static _escapeHtml(str) {
+ if (str == null) return '';
+ return String(str).replace(/[&<>'"]/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[m]));
+ }
+ _escapeHtml(str) { return WebX11Terminal._escapeHtml(str); }
+ _getDataSchemaVersion() { return 1; }
+ _escapeYamlString(str) {
+ return '"' + String(str == null ? '' : str).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
+ }
+ _invalidateCommandCache() {
+ if (this.security) this.security.invalidateCommandCache();
+ }
initEventListeners() {
this.inputField.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
@@ -1195,6 +1267,7 @@
this._safeRegister('ServerMonitorModule', 'server monitor');
this._safeRegister('SmartContextModule', 'smart context');
this._safeRegister('SimpleGUIModule', 'simple GUI builder');
+ this._safeRegister('DebugSupervisorModule', 'debug supervisor');
// v4.5.47: Critical ext modules promoted to boot path
this._safeRegister('EditorModule', 'editor');
this._safeRegister('ShellModule', 'shell');
@@ -1242,6 +1315,7 @@
DiffModule: typeof DiffModule !== 'undefined' ? DiffModule : undefined,
BusyBoxModule: typeof BusyBoxModule !== 'undefined' ? BusyBoxModule : undefined,
SmartContextModule: typeof SmartContextModule !== 'undefined' ? SmartContextModule : undefined,
+ DebugSupervisorModule: typeof DebugSupervisorModule !== 'undefined' ? DebugSupervisorModule : undefined,
};
const cls = registry[className];
if (typeof cls === 'undefined') {
@@ -1371,10 +1445,12 @@
splitPipe(input) {
const parts = [];
let current = '';
- let inQuote = false;
+ let inSingle = false;
+ let inDouble = false;
for (const ch of input) {
- if (ch === '"' || ch === "'") { inQuote = !inQuote; current += ch; }
- else if (ch === '|' && !inQuote) { parts.push(current.trim()); current = ''; }
+ if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; }
+ else if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; }
+ else if (ch === '|' && !inSingle && !inDouble) { parts.push(current.trim()); current = ''; }
else { current += ch; }
}
parts.push(current.trim());
@@ -1414,8 +1490,11 @@
const originalPrint = this.print.bind(this);
let output = '';
this.print = (text, type) => { output += text + '\n'; };
- await this.dispatchCommand(cmd, args);
- this.print = originalPrint;
+ try {
+ await this.dispatchCommand(cmd, args);
+ } finally {
+ this.print = originalPrint;
+ }
const trimmed = output.trim();
if (trimmed.length > this._pipeBufferSize) {
return trimmed.slice(0, this._pipeBufferSize);
@@ -1431,6 +1510,21 @@
if (cmd === 'load-local' || cmd === 'loadlocal') { this.loadLocalModule(args[0]); return; }
if (cmd === 'smart-prompt') { const val = args[0] && args[0] === 'off' ? 'off' : 'on'; localStorage.setItem('webx11_smart_prompt', val); this.print('Smart prompt ' + val, 'success'); this.updatePrompt(); return; }
if (cmd === 'mem') { this.printMemoryStats(); return; }
+ if (cmd === 'dashboard') {
+ // Collect dashboard data and open the dashboard page
+ const aiMod = this.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && typeof aiMod._collectDashboardData === 'function') {
+ aiMod._collectDashboardData().then(function() {
+ this.print('📊 Dashboard data collected — opening dashboard...', 'success');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }.bind(this));
+ } else {
+ // Fallback: just open the page (it will try localStorage)
+ this.print('📊 Opening dashboard...', 'info');
+ window.open('/spaces/newos4/downloads/dashboard.html', 'newOS4 Dashboard', 'width=1200,height=800');
+ }
+ return;
+ }
if (cmd === 'config') {
if (args.length === 0) {
this.print('=== Terminal Config ===', 'info');
@@ -1493,7 +1587,7 @@
if (action === 'add') { this.security.destructiveCmds.add(args[1]); }
else if (action === 'remove') { this.security.destructiveCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_destructive', JSON.stringify([...this.security.destructiveCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated destructive set (${this.security.destructiveCmds.size} commands)`, 'success');
return;
}
@@ -1509,7 +1603,7 @@
if (action === 'add') { this.security.systemCmds.add(args[1]); }
else if (action === 'remove') { this.security.systemCmds.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_system', JSON.stringify([...this.security.systemCmds]));
+ this.security._saveSecurityDeltas();
this.print(`Updated system set (${this.security.systemCmds.size} commands)`, 'success');
return;
}
@@ -1525,7 +1619,7 @@
if (action === 'add') { this.security.whitelist.add(args[1]); }
else if (action === 'remove') { this.security.whitelist.delete(args[1]); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_whitelist', JSON.stringify([...this.security.whitelist]));
+ this.security._saveSecurityDeltas();
this.print(`Updated whitelist (${this.security.whitelist.size} commands)`, 'success');
return;
}
@@ -1542,15 +1636,15 @@
if (action === 'add') { this.security.suspiciousFlags.add(flag); }
else if (action === 'remove') { this.security.suspiciousFlags.delete(flag); }
else { this.print('Action must be: add, remove, or list', 'warning'); return; }
- localStorage.setItem('webx11_security_flags', JSON.stringify([...this.security.suspiciousFlags]));
+ this.security._saveSecurityDeltas();
this.print(`Updated suspicious flags (${this.security.suspiciousFlags.size} flags)`, 'success');
return;
}
if (cmd === 'cmd-allow') { this._resolveCmdConfirm(true); return; }
if (cmd === 'cmd-deny') { this._resolveCmdConfirm(false); return; }
if (cmd === 'vfs' && args[0] === 'reset') {
- if (args[1] !== '--force') {
- this.print('⚠ This will permanently delete all files! Use vfs reset --force to confirm.', 'warning');
+ if (args[1] !== '--confirm' && args[1] !== '--force') {
+ this.print('⚠ This will permanently delete all files! Use vfs reset --confirm to confirm.', 'warning');
return;
}
this.fs.reset();
@@ -1592,8 +1686,8 @@
return;
}
if (cmd === 'empty-trash') {
- if (args[0] !== '--force') {
- this.print('⚠ This will permanently delete all trashed files! Use empty-trash --force to confirm.', 'warning');
+ if (args[0] !== '--confirm' && args[0] !== '--force') {
+ this.print('⚠ This will permanently delete all trashed files! Use empty-trash --confirm to confirm.', 'warning');
return;
}
if (this.fs.emptyTrash()) {
@@ -1710,7 +1804,11 @@
} catch (_e) {}
}
return; }
- catch (err) { this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error'); return; }
+ catch (err) {
+ this.print(`Error in ${modName}.${cmd}: ${err.message}`, 'error');
+ console.error(`[Terminal] Error in ${modName}.${cmd}:`, err);
+ return;
+ }
}
}
// Enhanced: check fuzzy suggestions before showing "not found"
@@ -1747,7 +1845,17 @@
files.forEach(f => {
const div = document.createElement('div');
div.className = `file-item ${f.type === 'dir' ? 'dir' : ''}`;
- div.innerHTML = `<span>${f.type === 'dir' ? '📁' : '📄'} ${f.name}</span> <span style="font-size:0.8em">${f.size}b</span>`;
+ const icon = document.createElement('span');
+ icon.textContent = f.type === 'dir' ? '📁' : '📄';
+ const name = document.createElement('span');
+ name.textContent = ' ' + f.name;
+ const size = document.createElement('span');
+ size.style.fontSize = '0.8em';
+ size.textContent = ' ' + f.size + 'b';
+ div.appendChild(icon);
+ div.appendChild(name);
+ div.appendChild(document.createTextNode(' '));
+ div.appendChild(size);
div.onclick = () => {
if (f.type === 'file') {
const content = this.fs.readFile(f.name);
@@ -1786,14 +1894,29 @@
modules.forEach(mod => {
const div = document.createElement('div');
div.className = `module-item ${mod.loaded ? '' : 'disabled'}`;
- const badge = mod.url ? '' : '<span class="module-badge builtin">Built-in</span>';
- div.innerHTML = `
- <div>
- <strong>${mod.name}</strong>${badge}
- <div style="font-size:0.8em; color:#666">${mod.url || 'Built-in'}</div>
- </div>
- ${mod.url ? `<button class="btn btn-danger" onclick="webx11.removeModule('${mod.name}')" style="padding:4px 8px;">✕</button>` : ''}
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = mod.name;
+ info.appendChild(strong);
+ if (!mod.url) {
+ const badge = document.createElement('span');
+ badge.className = 'module-badge builtin';
+ badge.textContent = 'Built-in';
+ info.appendChild(badge);
+ }
+ const sub = document.createElement('div');
+ sub.style.cssText = 'font-size:0.8em; color:#666';
+ sub.textContent = mod.url || 'Built-in';
+ info.appendChild(sub);
+ div.appendChild(info);
+ if (mod.url) {
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.padding = '4px 8px';
+ btn.textContent = '✕';
+ btn.onclick = () => this.removeModule(mod.name);
+ div.appendChild(btn);
+ }
list.appendChild(div);
});
}
@@ -1905,6 +2028,7 @@
renderConfigList() {
const list = document.getElementById('config-list');
if (!list) return;
+ list.innerHTML = '';
const prefix = 'webx11_config_';
const entries = [];
for (let i = 0; i < localStorage.length; i++) {
@@ -1917,14 +2041,28 @@
list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">No configuration saved.</div>';
return;
}
- list.innerHTML = entries.map(e => {
- const displayVal = e.key.toLowerCase().includes('password') || e.key.toLowerCase().includes('token') || e.key.toLowerCase().includes('secret')
- ? '****' : e.value;
- return `<div class="module-item">
- <div><strong>${e.key}</strong> <span style="color:var(--text-muted)">${displayVal}</span></div>
- <button class="btn btn-danger" onclick="webx11.deleteConfigFromUI('${e.key.replace(/'/g, "\\'")}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>`;
- }).join('');
+ entries.forEach(e => {
+ const item = document.createElement('div');
+ item.className = 'module-item';
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.textContent = e.key;
+ info.appendChild(strong);
+ const val = document.createElement('span');
+ val.style.color = 'var(--text-muted)';
+ const sensitive = /password|token|secret|key|private|credential|auth/i.test(e.key);
+ val.textContent = sensitive ? '****' : e.value;
+ info.appendChild(document.createTextNode(' '));
+ info.appendChild(val);
+ const btn = document.createElement('button');
+ btn.className = 'btn btn-danger';
+ btn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ btn.textContent = '✕';
+ btn.onclick = () => this.deleteConfigFromUI(e.key);
+ item.appendChild(info);
+ item.appendChild(btn);
+ list.appendChild(item);
+ });
}
addConfigFromUI() {
const key = document.getElementById('config-key-input').value.trim();
@@ -1977,10 +2115,24 @@
// Stats
if (statsEl) {
const cats = new Set(Object.values(index).map(s => s.category || 'general'));
- statsEl.innerHTML = `<span>📚 ${names.length} skills</span><span>📂 ${cats.size} categories</span>${query ? `<span>🔍 ${filtered.length} match '${query}'</span>` : ''}`;
+ statsEl.innerHTML = '';
+ const totalSpan = document.createElement('span');
+ totalSpan.textContent = '📚 ' + names.length + ' skills';
+ statsEl.appendChild(totalSpan);
+ const catSpan = document.createElement('span');
+ catSpan.textContent = '📂 ' + cats.size + ' categories';
+ statsEl.appendChild(catSpan);
+ if (query) {
+ const matchSpan = document.createElement('span');
+ matchSpan.textContent = '🔍 ' + filtered.length + ' match';
+ statsEl.appendChild(matchSpan);
+ }
}
if (filtered.length === 0) {
- list.innerHTML = '<div style="padding:20px; text-align:center; color:#666">' + (query ? 'No skills match your search.' : 'No skills yet. Create one below!') + '</div>';
+ const empty = document.createElement('div');
+ empty.style.cssText = 'padding:20px; text-align:center; color:#666';
+ empty.textContent = query ? 'No skills match your search.' : 'No skills yet. Create one below!';
+ list.appendChild(empty);
return;
}
// Group by category
@@ -2006,18 +2158,41 @@
const tags = (s.tags || []).join(', ');
const div = document.createElement('div');
div.className = 'module-item';
- div.innerHTML = `
- <div>
- <strong style="color:var(--success-color)">${name}</strong>
- <div style="font-size:0.85em; color:#aaa">${s.description || ''}</div>
- <div style="font-size:0.75em; color:#666">${tags ? '🏷 ' + tags : ''}</div>
- </div>
- <div style="display:flex;gap:4px">
- <button class="btn" onclick="webx11.viewSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">👁</button>
- <button class="btn" onclick="webx11.editSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✏️</button>
- <button class="btn btn-danger" onclick="webx11.deleteSkillUI('${name}')" style="padding:4px 8px; font-size:0.8em">✕</button>
- </div>
- `;
+ const info = document.createElement('div');
+ const strong = document.createElement('strong');
+ strong.style.color = 'var(--success-color)';
+ strong.textContent = name;
+ info.appendChild(strong);
+ const desc = document.createElement('div');
+ desc.style.cssText = 'font-size:0.85em; color:#aaa';
+ desc.textContent = s.description || '';
+ info.appendChild(desc);
+ const tagsDiv = document.createElement('div');
+ tagsDiv.style.cssText = 'font-size:0.75em; color:#666';
+ tagsDiv.textContent = tags ? '🏷 ' + tags : '';
+ info.appendChild(tagsDiv);
+ const actions = document.createElement('div');
+ actions.style.cssText = 'display:flex;gap:4px';
+ const viewBtn = document.createElement('button');
+ viewBtn.className = 'btn';
+ viewBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ viewBtn.textContent = '👁';
+ viewBtn.onclick = () => this.viewSkillUI(name);
+ const editBtn = document.createElement('button');
+ editBtn.className = 'btn';
+ editBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ editBtn.textContent = '✏️';
+ editBtn.onclick = () => this.editSkillUI(name);
+ const delBtn = document.createElement('button');
+ delBtn.className = 'btn btn-danger';
+ delBtn.style.cssText = 'padding:4px 8px; font-size:0.8em';
+ delBtn.textContent = '✕';
+ delBtn.onclick = () => this.deleteSkillUI(name);
+ actions.appendChild(viewBtn);
+ actions.appendChild(editBtn);
+ actions.appendChild(delBtn);
+ div.appendChild(info);
+ div.appendChild(actions);
body.appendChild(div);
}
section.appendChild(body);
@@ -2081,9 +2256,9 @@
alert(`Skill '${name}' already exists. Delete it first or use skill-patch command.`);
return;
}
- let content = contentInput || `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n## Steps\n1. (to be filled)\n`;
+ let content = contentInput || `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n## Steps\n1. (to be filled)\n`;
if (!content.startsWith('---')) {
- content = `---\nname: ${name}\ndescription: ${description}\ntags: [${tags.join(', ')}]\ncategory: ${category}\n---\n${content}`;
+ content = `---\nname: ${this._escapeYamlString(name)}\ndescription: ${this._escapeYamlString(description)}\ntags: [${tags.map(t => this._escapeYamlString(t)).join(', ')}]\ncategory: ${this._escapeYamlString(category)}\n---\n${content}`;
}
fs.writeFile(`/skills/${name}.md`, content);
const index = fs.fsReadJSON('/skills/index.json') || {};