From a065a4c30be748b0c8fc6dd0b5ee1dc4d3de4b1c Mon Sep 17 00:00:00 2001
From: Agando
Date: Mon, 9 Nov 2026 15:36:23 +0100
Subject: chore(newos4): commit accumulated maintenance output
The daily collector and the feedback pipeline had left 57 modified files uncommitted (skills,
modules-index, CHANGELOG, feedback decisions, debug counters). The public mirror publishes git
HEAD, so without this the published repo stayed behind the tree - and it blocked the publisher
outright once uncommitted subtrees started failing loudly.
---
.bkp-fix-20261023_143429/filemgr.js | 1150 +++++++++++++++++++++++++++++++++++
1 file changed, 1150 insertions(+)
create mode 100644 .bkp-fix-20261023_143429/filemgr.js
(limited to '.bkp-fix-20261023_143429/filemgr.js')
diff --git a/.bkp-fix-20261023_143429/filemgr.js b/.bkp-fix-20261023_143429/filemgr.js
new file mode 100644
index 0000000..01d1da9
--- /dev/null
+++ b/.bkp-fix-20261023_143429/filemgr.js
@@ -0,0 +1,1150 @@
+class FileManagerModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'files': { argType: 'file' }, 'preview': { argType: 'file' },
+ };
+ this.terminal = null;
+ this.currentPath = [];
+ this._overlay = null;
+ this._selectedFile = null;
+ this._multiSelect = new Set(); // v4.5.78: multi-select for batch ops
+ this._selectMode = false; // v4.6.35: tap-to-select mode (touch devices)
+ this._itemCount = 0; // items rendered in the current directory/filter
+ this._clipboard = { mode: null, paths: [] }; // 'copy' or 'cut'
+ this._searchFilter = '';
+ this._rawMode = false;
+ this._inlineHtml = false;
+ }
+
+ requirements() {
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'files': 'files — Open the file manager overlay with VFS directory listing, preview, encrypt, zip',
+ 'preview': 'preview — Preview a specific file',
+ };
+ }
+
+ registerCommands() {
+ this.commands['files'] = () => this._openOverlay();
+ this.commands['preview'] = (args) => this._previewCommand(args);
+ }
+
+ _previewCommand(args) {
+ if (!args[0]) return this.terminal.print('Usage: preview ', 'warning');
+ const path = args.join(' ');
+ this._openOverlay();
+ this._navigateToPathFromParts(path);
+ const content = this.terminal.fs.readFile(path);
+ if (content === null || content === false) {
+ this._renderPreviewError('File not found: ' + path);
+ return;
+ }
+ this._selectedFile = path;
+ this._renderPreview(path, content);
+ this._highlightSelected();
+ }
+
+ _navigateToPathFromParts(path) {
+ const parts = path.split('/').filter(p => p);
+ if (parts.length <= 1) {
+ this.currentPath = [];
+ return;
+ }
+ this.currentPath = parts.slice(0, -1);
+ }
+
+ _detectType(filePath) {
+ const ext = filePath.split('.').pop().toLowerCase();
+ if (!ext || ext === filePath) return 'other';
+ const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'ico'];
+ const videoExts = ['mp4', 'webm', 'avi', 'mov', 'mkv'];
+ const audioExts = ['mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a'];
+ const codeExts = ['js', 'py', 'css', 'yaml', 'yml', 'toml'];
+ if (ext === 'md') return 'md';
+ if (imageExts.includes(ext)) return 'image';
+ if (videoExts.includes(ext)) return 'video';
+ if (audioExts.includes(ext)) return 'audio';
+ if (codeExts.includes(ext)) return 'code';
+ if (ext === 'html') return 'html';
+ if (ext === 'json') return 'json';
+ if (ext === 'csv') return 'csv';
+ if (ext === 'pdf') return 'pdf';
+ if (ext === 'svg') return 'svg';
+ if (ext === 'txt') return 'text';
+ return 'other';
+ }
+
+ _renderMarkdown(text) {
+ if (!text) return '';
+ try {
+ let html = text
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ // code blocks first
+ html = html.replace(/```([\s\S]*?)```/g, '$1
');
+ // inline code
+ html = html.replace(/`([^`]+)`/g, '$1');
+ // headers
+ html = html.replace(/^### (.+)$/gm, '$1
');
+ html = html.replace(/^## (.+)$/gm, '$1
');
+ html = html.replace(/^# (.+)$/gm, '$1
');
+ // bold
+ html = html.replace(/\*\*(.+?)\*\*/g, '$1');
+ // italic
+ html = html.replace(/\*(.+?)\*/g, '$1');
+ // links — v4.5.67: sanitize URL to prevent javascript: scheme injection
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
+ const safeUrl = /^(https?:\/\/|mailto:|\/|#)/.test(url) ? url : '#';
+ return `${text}`;
+ });
+ // horizontal rules
+ html = html.replace(/^---$/gm, '
');
+ // unordered lists
+ html = html.replace(/^[\s]*[-*]\s+(.+)$/gm, '$1');
+ // paragraphs - double newlines
+ html = html.replace(/\n\n/g, '
');
+ html = '
' + html + '
';
+ return html;
+ } catch (e) {
+ return '' + text.replace(/&/g, '&').replace(//g, '>') + '
';
+ }
+ }
+
+ _getPreviewHtml(path, content) {
+ if (content === null || content === false) {
+ return 'File not found: ' + this._escapeHtml(path) + '
';
+ }
+
+ const type = this._detectType(path);
+ const ext = path.split('.').pop().toLowerCase();
+ const fileName = path.split('/').pop() || path;
+
+ // Handle binary detection for media types
+ if (typeof content !== 'string') {
+ return 'Cannot preview: non-string content
';
+ }
+ const isLikelyBinary = content.length > 0 && !content.startsWith('\n') && !content.startsWith('\r') && !content.startsWith(' ') && !content.startsWith('<') && !content.startsWith('{') && !content.startsWith('[') && content.charCodeAt(0) < 32 && content.charCodeAt(0) !== 9 && content.charCodeAt(0) !== 10 && content.charCodeAt(0) !== 13;
+
+ const isTextContent = typeof content === 'string' && (content.startsWith('{') || content.startsWith('[') || content.startsWith('<') || content.startsWith('#') || /^[\w\s\n\r\t.,;:!?()\-=+*@#$%^&"'\/\\|`~]/.test(content.substring(0, 100)));
+
+ if (type === 'md' && isTextContent) {
+ return '' + this._renderMarkdown(content) + '
';
+ }
+
+ if (type === 'html') {
+ // Render HTML in an inline sandbox frame
+ const safeHtml = content
+ .replace(/