aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-fs.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/core/module-fs.js')
-rw-r--r--modules/core/module-fs.js100
1 files changed, 83 insertions, 17 deletions
diff --git a/modules/core/module-fs.js b/modules/core/module-fs.js
index 0dea0d5..b6cf272 100644
--- a/modules/core/module-fs.js
+++ b/modules/core/module-fs.js
@@ -730,17 +730,52 @@ class FileSystemModule {
};
this.commands['vfs-size'] = () => {
try {
- const json = JSON.stringify(this.terminal.fs.root);
- const totalSize = (json.length / 1024 / 1024).toFixed(2);
+ // Every number this command prints is STORED BYTES: the UTF-8 length of each
+ // file's content. It used to report JSON.stringify(root).length, which is UTF-16
+ // string units plus every structural character of the serialisation — roughly 2x
+ // the real size of a plain-text tree, and wrong the other way for non-ASCII.
+ const utf8Len = (s) => {
+ if (typeof s !== 'string' || s.length === 0) return 0;
+ if (typeof Blob !== 'undefined') {
+ try { return new Blob([s]).size; } catch (e) { /* fall through */ }
+ }
+ if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(s).length;
+ return s.length; // last resort only: no Blob and no TextEncoder
+ };
+ const fmtSize = (bytes) => {
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / 1024 / 1024).toFixed(2) + ' MB';
+ };
+ // Recursively total one subtree. Each file is visited exactly once, so a nested
+ // tree is never double counted and no directory node adds bytes of its own.
+ const measure = (node) => {
+ const out = { bytes: 0, files: 0 };
+ if (!node || !node.content) return out;
+ for (const child of Object.values(node.content)) {
+ if (!child) continue;
+ if (child.type === 'file') {
+ out.bytes += utf8Len(child.content);
+ out.files += 1;
+ } else if (child.type === 'dir') {
+ const sub = measure(child);
+ out.bytes += sub.bytes;
+ out.files += sub.files;
+ }
+ }
+ return out;
+ };
const root = this.terminal.fs.root;
+ const total = measure(root);
+ // Per-file table, same byte metric.
const sizeMap = [];
function walk(node, path) {
if (!node || !node.content) return;
for (const [name, child] of Object.entries(node.content)) {
+ if (!child) continue;
const fullPath = path + '/' + name;
if (child.type === 'file') {
- const size = (child.content ? child.content.length : 0);
- sizeMap.push({ path: fullPath, size, sizeKB: (size / 1024).toFixed(1) });
+ sizeMap.push({ path: fullPath, size: utf8Len(child.content) });
} else if (child.type === 'dir') {
walk(child, fullPath);
}
@@ -748,23 +783,54 @@ class FileSystemModule {
}
walk(root, '');
sizeMap.sort((a, b) => b.size - a.size);
+ // Top-level groups, so the table accounts for the whole tree: one row per
+ // top-level directory plus a row for files stored directly at root.
+ const groups = [];
+ const rootFiles = { bytes: 0, files: 0 };
+ for (const [name, child] of Object.entries((root && root.content) || {})) {
+ if (!child) continue;
+ if (child.type === 'dir') {
+ const m = measure(child);
+ groups.push({ name: '/' + name, bytes: m.bytes, files: m.files });
+ } else if (child.type === 'file') {
+ rootFiles.bytes += utf8Len(child.content);
+ rootFiles.files += 1;
+ }
+ }
+ if (rootFiles.files > 0) groups.push({ name: '/(root files)', bytes: rootFiles.bytes, files: rootFiles.files });
+ groups.sort((a, b) => b.bytes - a.bytes);
const totalFiles = sizeMap.length;
- const top5 = sizeMap.slice(0, 5);
this.terminal.print('📊 VFS Size Report', 'text-muted');
- this.terminal.print(' Total: ' + totalSize + ' MB across ' + totalFiles + ' files');
- this.terminal.print(' Largest:');
- top5.forEach((f, i) => {
- this.terminal.print(' ' + (i + 1) + '. ' + f.path + ' — ' + f.sizeKB + ' KB');
- });
- // Show system dir sizes
- const sysDirs = ['/_snapshots', '/_sessions', '/.trash'];
+ this.terminal.print(' Total: ' + fmtSize(total.bytes) + ' (' + total.bytes + ' bytes) across ' + totalFiles + (totalFiles === 1 ? ' file' : ' files'));
+ if (totalFiles > 0) {
+ this.terminal.print(' Largest:');
+ const shownFiles = sizeMap.slice(0, 10);
+ shownFiles.forEach((f, i) => {
+ this.terminal.print(' ' + (i + 1) + '. ' + f.path + ' — ' + fmtSize(f.size));
+ });
+ // The rows must add up to the total above, so the tail is folded into one
+ // row instead of dropped: this table used to be top-5 only, and the rest of
+ // the tree was missing from the report entirely.
+ if (totalFiles > shownFiles.length) {
+ let restBytes = 0;
+ for (const f of sizeMap.slice(shownFiles.length)) restBytes += f.size;
+ this.terminal.print(' (+' + (totalFiles - shownFiles.length) + ' more files — ' + fmtSize(restBytes) + ')');
+ }
+ }
+ // Every top-level group is listed (root breadth, not depth), so these rows
+ // always sum to the total above.
+ this.terminal.print(' Top directories by stored bytes:', 'info');
+ for (const g of groups) {
+ this.terminal.print(' ' + g.name + ' — ' + fmtSize(g.bytes) + ' (' + g.files + (g.files === 1 ? ' file)' : ' files)'));
+ }
+ // Show system dir sizes — real bytes, not JSON.stringify(list of entries).length
+ const sysDirs = ['/_snapshots', '/_sessions', '/.trash', '/_mods', '/.semantic_index'];
for (const dir of sysDirs) {
- const dirNode = this.terminal.fs.readDir(dir);
+ const name = dir.replace(/^\//, '');
+ const dirNode = (root && root.content && root.content[name]) || null;
if (dirNode) {
- const cnt = dirNode.length;
- // estimate dir size
- const dirSize = JSON.stringify(dirNode).length;
- this.terminal.print(' ' + dir + ': ' + cnt + ' files, ~' + (dirSize / 1024).toFixed(1) + ' KB', dirSize > 102400 ? 'warning' : 'text-muted');
+ const m = measure(dirNode);
+ this.terminal.print(' ' + dir + ': ' + m.files + (m.files === 1 ? ' file, ' : ' files, ') + fmtSize(m.bytes), m.bytes > 102400 ? 'warning' : 'text-muted');
}
}
} catch (e) {