class DiffModule { constructor() { this.commands = {}; this.terminal = null; } requirements() { return { met: true, missing: [] }; } getHelpInfo() { return { 'diff': 'Show differences between two VFS files or compare module source (usage: diff or diff --module )', }; } registerCommands() { this.commands['diff'] = (args) => this.diff(args); } async diff(args) { if (!args.length) return this.terminal.print('Usage: diff or diff --module ', 'warning'); if (args[0] === '--module') { if (!args[1]) return this.terminal.print('Usage: diff --module ', 'warning'); return this._diffModule(args[1]); } return this._diffFiles(args[0], args[1]); } _diffFiles(path1, path2) { let content1, content2; try { content1 = this.terminal.fs.readFile(path1); content2 = this.terminal.fs.readFile(path2); } catch (e) { return this.terminal.print(`Error reading files: ${e.message}`, 'error'); } if (content1 === null || content1 === undefined) { return this.terminal.print(`File not found: ${path1}`, 'error'); } if (content2 === null || content2 === undefined) { return this.terminal.print(`File not found: ${path2}`, 'error'); } const lines1 = String(content1).split('\n'); const lines2 = String(content2).split('\n'); // Strip trailing empty line if file ends with newline if (lines1.length > 0 && lines1[lines1.length - 1] === '') lines1.pop(); if (lines2.length > 0 && lines2[lines2.length - 1] === '') lines2.pop(); if (lines1.length === 0 && lines2.length === 0) { return this.terminal.print('Both files are empty — no differences', 'info'); } // Compute LCS-based diff const result = this._computeDiff(lines1, lines2); // Render output this.terminal.print(`--- ${path1}`, 'info'); this.terminal.print(`+++ ${path2}`, 'info'); if (result.length === 0) { return this.terminal.print('Files are identical', 'success'); } const maxLineNum = Math.max(lines1.length, lines2.length); const lineNumWidth = String(maxLineNum).length; result.forEach(part => { if (part.type === 'same') { // Print up to 3 unchanged lines for context, then collapse if (part.lines.length <= 3) { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${line}`, 'dim'); }); } else { // Show first line this.terminal.print(` ${String(part.aStart + 1).padStart(lineNumWidth)} ${part.lines[0]}`, 'dim'); this.terminal.print(` ${' '.repeat(lineNumWidth)} ... ${part.lines.length - 2} more unchanged lines`, 'dim'); // Show last line const lastIdx = part.lines.length - 1; const ln = String(part.aStart + lastIdx + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${part.lines[lastIdx]}`, 'dim'); } } else if (part.type === 'removed') { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(`-${ln} ${line}`, 'error'); }); } else if (part.type === 'added') { part.lines.forEach((line, i) => { const ln = String(part.bStart + i + 1).padStart(lineNumWidth); this.terminal.print(`+${ln} ${line}`, 'success'); }); } }); } _computeDiff(a, b) { // Build LCS table const m = a.length; const n = b.length; const dp = Array.from({ length: m + 1 }, () => new Int32Array(n + 1)); for (let i = 1; i <= m; i++) { for (let j = 1; j <= n; j++) { if (a[i - 1] === b[j - 1]) { dp[i][j] = dp[i - 1][j - 1] + 1; } else { dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); } } } // Backtrack to build diff parts const parts = []; let i = m, j = n; let removed = [], added = [], same = []; const aStart = 0, bStart = 0; const flush = () => { if (same.length > 0) { parts.push({ type: 'same', aStart: aStart + (parts.length > 0 ? parts.reduce((sum, p) => { if (p.type === 'same') return sum + p.lines.length; if (p.type === 'removed') return sum + p.lines.length; return sum; }, 0) : 0), lines: same.splice(0) }); } if (removed.length > 0 || added.length > 0) { if (removed.length > 0) { const aIdx = parts.reduce((sum, p) => { if (p.type === 'same') return sum + p.lines.length; if (p.type === 'removed') return sum + p.lines.length; return sum; }, 0); parts.push({ type: 'removed', aStart: aIdx, lines: removed.splice(0) }); } if (added.length > 0) { const bIdx = parts.reduce((sum, p) => { if (p.type === 'same') return sum; if (p.type === 'added') return sum + p.lines.length; return sum; }, 0); parts.push({ type: 'added', bStart: bIdx, lines: added.splice(0) }); } } }; // Trace back through DP table const trace = []; while (i > 0 || j > 0) { if (i > 0 && j > 0 && a[i - 1] === b[j - 1]) { trace.push({ type: 'same', lineA: a[i - 1], lineB: b[j - 1] }); i--; j--; } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) { trace.push({ type: 'added', lineB: b[j - 1] }); j--; } else { trace.push({ type: 'removed', lineA: a[i - 1] }); i--; } } // Reverse trace to get forward order trace.reverse(); // Build parts from trace let aIdx = 0, bIdx = 0; for (const entry of trace) { if (entry.type === 'same') { same.push(entry.lineA); aIdx++; bIdx++; } else if (entry.type === 'removed') { flush(); removed.push(entry.lineA); aIdx++; } else if (entry.type === 'added') { flush(); added.push(entry.lineB); bIdx++; } } flush(); return parts; } async _diffModule(modName) { // Find module in registry const entry = this.terminal.moduleManager.moduleRegistry.find(m => m.name === modName); if (!entry) { return this.terminal.print(`Module "${modName}" not found in registry`, 'error'); } const url = entry.url; if (!url) { return this.terminal.print(`No source URL for module "${modName}"`, 'warning'); } // Fetch original source let originalSource; try { const resp = await fetch(url); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); originalSource = await resp.text(); } catch (e) { return this.terminal.print(`Failed to fetch original source: ${e.message}`, 'error'); } // Read local edited version const localPath = `_mods/${modName}.js`; let localSource; let hasLocalEdit = false; try { localSource = this.terminal.fs.readFile(localPath); if (localSource === null || localSource === undefined) { hasLocalEdit = false; } else { hasLocalEdit = true; } } catch (e) { hasLocalEdit = false; } if (!hasLocalEdit) { // Try to read the module's currently loaded source const loadedModule = this.terminal.moduleManager.modules[modName]; if (!loadedModule) { return this.terminal.print(`Module "${modName}" is not loaded and has no local edits`, 'info'); } return this.terminal.print(`Module "${modName}" has no local edits — source matches original`, 'success'); } // Compare original source vs local edit const lines1 = String(originalSource).split('\n'); const lines2 = String(localSource).split('\n'); // Strip trailing empty line if file ends with newline if (lines1.length > 0 && lines1[lines1.length - 1] === '') lines1.pop(); if (lines2.length > 0 && lines2[lines2.length - 1] === '') lines2.pop(); this.terminal.print(`--- ${url}`, 'info'); this.terminal.print(`+++ ${localPath}`, 'info'); if (lines1.length === lines2.length && lines1.every((l, i) => l === lines2[i])) { return this.terminal.print('No differences — local edit matches original', 'success'); } const result = this._computeDiff(lines1, lines2); const maxLineNum = Math.max(lines1.length, lines2.length); const lineNumWidth = String(maxLineNum).length; result.forEach(part => { if (part.type === 'same') { if (part.lines.length <= 3) { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${line}`, 'dim'); }); } else { this.terminal.print(` ${String(part.aStart + 1).padStart(lineNumWidth)} ${part.lines[0]}`, 'dim'); this.terminal.print(` ${' '.repeat(lineNumWidth)} ... ${part.lines.length - 2} more unchanged lines`, 'dim'); const lastIdx = part.lines.length - 1; const ln = String(part.aStart + lastIdx + 1).padStart(lineNumWidth); this.terminal.print(` ${ln} ${part.lines[lastIdx]}`, 'dim'); } } else if (part.type === 'removed') { part.lines.forEach((line, i) => { const ln = String(part.aStart + i + 1).padStart(lineNumWidth); this.terminal.print(`-${ln} ${line}`, 'error'); }); } else if (part.type === 'added') { part.lines.forEach((line, i) => { const ln = String(part.bStart + i + 1).padStart(lineNumWidth); this.terminal.print(`+${ln} ${line}`, 'success'); }); } }); } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new DiffModule(), 'https://webx11.duckdns.org/spaces/chatstudio/newos4/modules/diff.js'); }