diff options
Diffstat (limited to 'modules/ext')
| -rw-r--r-- | modules/ext/busybox.js | 2 | ||||
| -rw-r--r-- | modules/ext/editor.js | 11 | ||||
| -rw-r--r-- | modules/ext/mindmap.js | 30 | ||||
| -rw-r--r-- | modules/ext/skill-pack-core.js | 255 | ||||
| -rw-r--r-- | modules/ext/webdav.js | 4 |
5 files changed, 288 insertions, 14 deletions
diff --git a/modules/ext/busybox.js b/modules/ext/busybox.js index 34221f8..53eae06 100644 --- a/modules/ext/busybox.js +++ b/modules/ext/busybox.js @@ -334,7 +334,7 @@ class BusyBoxModule { const ua = navigator.userAgent; const space = 'newos4'; const platform = navigator.platform || 'unknown'; - const version = 'WebX11 v4.2'; + const version = 'WebX11 ' + (window.NEWOS4_VERSION || 'v4.0'); this.terminal.print(`Hostname: ${space}`, 'info'); this.terminal.print(`Platform: ${version} on ${platform}`, 'info'); this.terminal.print(`User Agent: ${ua}`, 'info'); diff --git a/modules/ext/editor.js b/modules/ext/editor.js index 8c3ac0f..3a5f269 100644 --- a/modules/ext/editor.js +++ b/modules/ext/editor.js @@ -44,6 +44,17 @@ class EditorModule { 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); diff --git a/modules/ext/mindmap.js b/modules/ext/mindmap.js index d0cab54..1daa85b 100644 --- a/modules/ext/mindmap.js +++ b/modules/ext/mindmap.js @@ -247,13 +247,18 @@ class MindmapModule { cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0' }; - const layout = { nodes: [], edges: [], width: 0, height: 0, _y: pad }; + const layout = { nodes: [], edges: [], width: 0, height: 0, _y: pad, minX: Infinity, maxX: 0, minY: Infinity, maxY: 0 }; const layoutTree = (node, x) => { const myY = layout._y; const myX = x; const color = colorMap[node.color] || '#818cf8'; layout.nodes.push({ x: myX, y: myY, w: nw, h: nh, text: node.text, color, id: node.id }); + if (myX < layout.minX) layout.minX = myX; + const rightEdge = x + nw; + if (rightEdge > layout.maxX) layout.maxX = rightEdge; + const bottomEdge = myY + nh; + if (bottomEdge > layout.maxY) layout.maxY = bottomEdge; if (node.children && node.children.length > 0) { const totalW = node.children.length * nw + (node.children.length - 1) * hGap; @@ -267,28 +272,33 @@ class MindmapModule { } layout._y -= nh + vGap; } - const rightEdge = x + nw; - if (rightEdge > layout.width) layout.width = rightEdge; }; layout._y = pad; layoutTree(this.root, pad); - layout.height = layout._y + nh + pad; - // Build SVG - let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${layout.width + pad} ${layout.height}" style="max-width:100%;background:#0a0a14;font-family:monospace;border-radius:8px;">`; + // Normalize coordinates — shift so minX becomes pad + const xShift = (layout.minX === Infinity ? 0 : -layout.minX) + pad; + layout.height = layout.maxY + pad; + const svgWidth = (layout.maxX - layout.minX) + pad * 2; + + // Build SVG — apply xShift to all coordinates + let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${svgWidth} ${layout.height}" style="max-width:100%;background:#0a0a14;font-family:monospace;border-radius:8px;">`; // Edges first (behind nodes) for (const e of layout.edges) { - const mx = (e.x1 + e.x2) / 2; - svg += `<path d="M${e.x1},${e.y1} C${mx},${e.y1 + 20} ${mx},${e.y2 - 20} ${e.x2},${e.y2}" fill="none" stroke="#2a2a4a" stroke-width="1.5"/>`; + const sx = e.x1 + xShift, sy = e.y1; + const ex = e.x2 + xShift, ey = e.y2; + const mx = (sx + ex) / 2; + svg += `<path d="M${sx},${sy} C${mx},${sy + 20} ${mx},${ey - 20} ${ex},${ey}" fill="none" stroke="#2a2a4a" stroke-width="1.5"/>`; } // Nodes on top for (const n of layout.nodes) { - svg += `<rect x="${n.x}" y="${n.y}" width="${n.w}" height="${n.h}" rx="6" fill="${n.color}18" stroke="${n.color}" stroke-width="1.5"/>`; + const nx = n.x + xShift; + svg += `<rect x="${nx}" y="${n.y}" width="${n.w}" height="${n.h}" rx="6" fill="${n.color}18" stroke="${n.color}" stroke-width="1.5"/>`; const text = n.text.length > 18 ? n.text.substring(0, 16) + '..' : n.text; - svg += `<text x="${n.x + n.w / 2}" y="${n.y + n.h / 2 + 4}" text-anchor="middle" fill="#e0e0e0" font-size="10" font-family="monospace">${text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</text>`; + svg += `<text x="${nx + n.w / 2}" y="${n.y + n.h / 2 + 4}" text-anchor="middle" fill="#e0e0e0" font-size="10" font-family="monospace">${text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}</text>`; } svg += '</svg>'; diff --git a/modules/ext/skill-pack-core.js b/modules/ext/skill-pack-core.js index f731594..41844bd 100644 --- a/modules/ext/skill-pack-core.js +++ b/modules/ext/skill-pack-core.js @@ -21,6 +21,14 @@ class SkillPackCoreModule { getHelpInfo() { return { 'skill-pack-core': 'Seed and manage built-in core skills (navigation, memory, search, modules, backup, agents, webdav, security, etc.)', + 'skill-list': 'skill-list [category] — List all skills, optionally filtered by category', + 'skill-view': 'skill-view <name> — View a skill\'s full content', + 'skill-create': 'skill-create <name> <desc> "<steps>" [tags] — Create a new skill', + 'skill-patch': 'skill-patch <name> "<content>" — Replace a skill\'s full content', + 'skill-delete': 'skill-delete <name> — Delete a skill', + 'skill-export': 'skill-export <name> — Export a skill as JSON', + 'skill-import': 'skill-import <json> — Import a skill from JSON string', + 'skill-sync': 'skill-sync — Rebuild skills index from /skills/ directory', }; } @@ -427,7 +435,42 @@ class SkillPackCoreModule { 5. **When to use:** - User wants to lock down the terminal → security-level 2 - User wants to allow a blocked command → cmd-allow <cmd> - - Debugging strange behavior → verify security-level (user may have set it higher)`) + - Debugging strange behavior → verify security-level (user may have set it higher)`), + + sk('mindmap-user-guide', 'How to create, view, and manage brainstorming mindmaps using the mindmap terminal module', ['mindmap', 'brainstorming', 'organization', 'planning'], 'productivity', +`## Steps +1. **Create a new mindmap:** + - mindmap new "<topic>" -t tag1,tag2 — creates a root node + - Tags are optional (e.g. planning, research, dev) + - Root gets auto-assigned a color + +2. **Add child nodes:** + - mindmap add <parent-id> "<idea>" -t tag — add a child + - Get the parent-id from mindmap show + - IDs are 4-char random strings (e.g. a3f8, x7k2) + +3. **View the mindmap:** + - mindmap show — full tree with Unicode box-drawing + - mindmap show <id> — subtree starting at that node + +4. **Edit nodes:** + - mindmap rename <id> "<new text>" — change text + - mindmap color <id> <color> — set color (red/green/yellow/blue/cyan/magenta/white) + - mindmap tag <id> <tag> — add a tag + +5. **Persistence:** + - mindmap save <name> — save to localStorage + - mindmap load <name> — restore + - mindmap list — show saved mindmaps + - mindmap delete <name> — remove saved + +6. **Export:** + - mindmap export [id] — Markdown bullet list output + +## When to use +- User wants to brainstorm, organize ideas, plan features +- User says "map out", "diagram", "brainstorm", "organize thoughts" +- Visualizing project structure or task decomposition`) ]; } registerCommands() { @@ -436,7 +479,217 @@ class SkillPackCoreModule { this.terminal.print(`Skill Pack: Core — ${count} skills available`, 'info'); this.terminal.print('Skills will auto-seed on load', 'info'); }; + this.commands['skill-list'] = (args) => this._cmdSkillList(args); + this.commands['skill-view'] = (args) => this._cmdSkillView(args); + this.commands['skill-create'] = (args) => this._cmdSkillCreate(args); + this.commands['skill-patch'] = (args) => this._cmdSkillPatch(args); + this.commands['skill-delete'] = (args) => this._cmdSkillDelete(args); + this.commands['skill-export'] = (args) => this._cmdSkillExport(args); + this.commands['skill-import'] = (args) => this._cmdSkillImport(args); + this.commands['skill-sync'] = () => this._cmdSkillSync(); + } + // ── Skill Commands ── + + _cmdSkillList(args) { + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + const entries = Object.values(index); + const filter = args[0] ? args[0].toLowerCase() : null; + const filtered = filter ? entries.filter(s => s.category === filter || s.category?.toLowerCase() === filter) : entries; + if (filtered.length === 0) { + this.terminal.print(filter ? `No skills in category: ${args[0]}` : 'No skills installed.', 'info'); + return; + } + this.terminal.print(`📚 Skills (${filtered.length} of ${entries.length} total):`, 'info'); + // Group by category + const groups = {}; + for (const s of filtered) { + const cat = s.category || 'uncategorized'; + if (!groups[cat]) groups[cat] = []; + groups[cat].push(s); + } + for (const [cat, skills] of Object.entries(groups)) { + this.terminal.print(` ${cat}:`, ''); + for (const s of skills) { + const tags = s.tags?.length ? ` (${s.tags.join(', ')})` : ''; + this.terminal.print(` 📄 ${s.name} — ${s.description}${tags}`, ''); + } + } + } + + _cmdSkillView(args) { + if (!args[0]) { this.terminal.print('Usage: skill-view <name>', 'warning'); return; } + const name = args[0].replace(/\.md$/, ''); + const fs = this.terminal.fs; + const content = fs.readFile(`/skills/${name}.md`); + if (content === null || content === undefined) { + this.terminal.print(`Skill not found: ${name}`, 'error'); + return; + } + this.terminal.print(`=== ${name} ===`, 'info'); + const lines = content.split('\n'); + for (const line of lines) { + this.terminal.print(line, ''); + } + this.terminal.print(`=== End ${name} ===`, 'info'); + } + + _cmdSkillCreate(args) { + if (args.length < 3) { + this.terminal.print('Usage: skill-create <name> <description> "<markdown content>" [tags]', 'warning'); + return; + } + const name = args[0].replace(/[^a-z0-9-]/g, '').toLowerCase(); + if (!name) { this.terminal.print('Name must be kebab-case (letters, numbers, hyphens only)', 'error'); return; } + const desc = args[1]; + const content = args.slice(2).join(' '); + const tags = []; + if (content.includes('"')) { + // Parse quoted tags at end + } + const fs = this.terminal.fs; + // Check for duplicate + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (index[name]) { + this.terminal.print(`Skill "${name}" already exists. Use skill-patch to update.`, 'warning'); + return; + } + const now = new Date().toISOString(); + const skillContent = `---\nname: ${name}\ndescription: ${desc}\ntags: [${tags.join(', ')}]\ncategory: general\n---\n${content}`; + fs.writeFile(`/skills/${name}.md`, skillContent); + index[name] = { name, description: desc, tags, category: 'general', created: now, updated: now }; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`✅ Skill "${name}" created (${(skillContent.length / 1024).toFixed(1)}KB)`, 'success'); + } + + _cmdSkillPatch(args) { + if (args.length < 2) { + this.terminal.print('Usage: skill-patch <name> "<full content>"', 'warning'); + return; + } + const name = args[0].replace(/\.md$/, ''); + const content = args.slice(1).join(' '); + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (!index[name]) { + this.terminal.print(`Skill not found: ${name}`, 'error'); + return; + } + fs.writeFile(`/skills/${name}.md`, content); + index[name].updated = new Date().toISOString(); + // Re-parse YAML frontmatter to update index + const headerMatch = content.match(/^---\n([\s\S]*?)\n---\n/); + if (headerMatch) { + const header = headerMatch[1]; + const d = header.match(/description:\s*(.+)/); + const c = header.match(/category:\s*(.+)/); + const t = header.match(/tags:\s*\[(.*?)\]/); + if (d) index[name].description = d[1].trim(); + if (c) index[name].category = c[1].trim(); + if (t) index[name].tags = t[1].split(',').map(x => x.trim()).filter(Boolean); + } + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`✅ Skill "${name}" updated`, 'success'); + } + + _cmdSkillDelete(args) { + if (!args[0]) { this.terminal.print('Usage: skill-delete <name>', 'warning'); return; } + const name = args[0].replace(/\.md$/, ''); + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (!index[name]) { + this.terminal.print(`Skill not found: ${name}`, 'error'); + return; + } + fs.deleteFile(`/skills/${name}.md`); + delete index[name]; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`🗑️ Skill "${name}" deleted`, 'success'); + } + + _cmdSkillExport(args) { + if (!args[0]) { this.terminal.print('Usage: skill-export <name>', 'warning'); return; } + const name = args[0].replace(/\.md$/, ''); + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + if (!index[name]) { + this.terminal.print(`Skill not found: ${name}`, 'error'); + return; + } + const content = fs.readFile(`/skills/${name}.md`); + const exportObj = { + ...index[name], + content: content || '' + }; + this.terminal.print(JSON.stringify(exportObj, null, 2), ''); } + + _cmdSkillImport(args) { + if (args.length === 0) { this.terminal.print('Usage: skill-import <json_string> or pipe a JSON object', 'warning'); return; } + const jsonStr = args.join(' '); + let data; + try { data = JSON.parse(jsonStr); } catch (e) { + this.terminal.print('Invalid JSON. Please provide a valid skill object.', 'error'); + return; + } + if (!data.name || !data.content) { + this.terminal.print('JSON must include "name" and "content" fields', 'error'); + return; + } + const fs = this.terminal.fs; + const index = fs.fsReadJSON('/skills/index.json') || {}; + const now = new Date().toISOString(); + const existing = index[data.name]; + fs.writeFile(`/skills/${data.name}.md`, data.content); + index[data.name] = { + name: data.name, + description: data.description || data.name, + tags: data.tags || [], + category: data.category || 'general', + created: existing?.created || now, + updated: now + }; + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`✅ Skill "${data.name}" imported (${existing ? 'updated' : 'created'})`, 'success'); + } + + _cmdSkillSync() { + const fs = this.terminal.fs; + const files = fs.ls('/skills') || []; + const mdFiles = files.filter(f => f.type === 'file' && f.name.endsWith('.md') && f.name !== 'index.json'); + let synced = 0; + const index = {}; + for (const f of mdFiles) { + const content = fs.readFile(`/skills/${f.name}`); + if (!content) continue; + const name = f.name.replace(/\.md$/, ''); + const headerMatch = content.match(/^---\n([\s\S]*?)\n---\n/); + let description = name; + let tags = []; + let category = 'general'; + if (headerMatch) { + const h = headerMatch[1]; + const d = h.match(/description:\s*(.+)/); + const c = h.match(/category:\s*(.+)/); + const t = h.match(/tags:\s*\[(.*?)\]/); + if (d) description = d[1].trim(); + if (c) category = c[1].trim(); + if (t) tags = t[1].split(',').map(x => x.trim()).filter(Boolean); + } + index[name] = { + name, + description, + tags, + category, + created: new Date().toISOString(), + updated: new Date().toISOString() + }; + synced++; + } + fs.fsWriteJSON('/skills/index.json', index); + this.terminal.print(`📚 Skill index rebuilt: ${synced} skills synced`, 'success'); + } + init() { this.seed(); } diff --git a/modules/ext/webdav.js b/modules/ext/webdav.js index 01856ce..a025cb2 100644 --- a/modules/ext/webdav.js +++ b/modules/ext/webdav.js @@ -633,8 +633,8 @@ class WebDAVModule { } const serialized = JSON.stringify(exportData, null, 2); // Size check: abort if >5MB - if (serialized.length > 5 * 1024 * 1024) { - self.terminal.print('Backup too large: ' + self._formatSize(serialized.length) + ' (>5MB limit). Try reducing VFS size first.', 'error'); + if (serialized.length > 52428800) { + self.terminal.print('Backup too large: ' + self._formatSize(serialized.length) + ' (>50MB limit). Try reducing VFS size first.', 'error'); return; } // Gzip payload |