creepercli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,139 @@
1
+ /*
2
+ * Copyright 2026 DemonZDevelopment
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fsCmds = require('./fs');
20
+ const searchCmds = require('./search');
21
+ const execCmds = require('./exec');
22
+ const monitorCmds = require('./monitor');
23
+ const authCmds = require('./auth');
24
+ const { cpush, cpull, csync } = require('../transfer');
25
+ const { friendly } = require('../protocol');
26
+
27
+ const COMMANDS = [
28
+ { name: 'pwd', run: fsCmds.pwd, desc: 'Print working directory' },
29
+ { name: 'ls', run: fsCmds.ls, desc: 'List directory (flags: -l, -a)' },
30
+ { name: 'cd', run: fsCmds.cd, desc: 'Change directory' },
31
+ { name: 'tree', run: fsCmds.tree, desc: 'Show directory tree (flag: --depth N)' },
32
+ { name: 'cat', run: fsCmds.cat, desc: 'Print file contents' },
33
+ { name: 'head', run: fsCmds.head, desc: 'Print first lines of a file (-n N)' },
34
+ { name: 'tail', run: fsCmds.tail, desc: 'Print last lines of a file (-n N)' },
35
+ { name: 'wc', run: fsCmds.wc, desc: 'Count lines/words/chars of a file' },
36
+ { name: 'touch', run: fsCmds.touch, desc: 'Create or update a file' },
37
+ { name: 'mkdir', run: fsCmds.mkdir, desc: 'Create directory (flag: -p)' },
38
+ { name: 'rm', run: fsCmds.rm, desc: 'Remove file/directory (flag: -r)' },
39
+ { name: 'cp', run: fsCmds.cp, desc: 'Copy file/directory (flag: -r)' },
40
+ { name: 'mv', run: fsCmds.mv, desc: 'Move/rename a file' },
41
+ { name: 'info', run: fsCmds.info, desc: 'Show file info (perms, size, mtime)' },
42
+ { name: 'edit', run: fsCmds.edit, desc: 'Edit a remote file in your local $EDITOR' },
43
+ { name: 'grep', run: searchCmds.grep, desc: 'Search file contents (flags: -i, --max-depth N)' },
44
+ { name: 'find', run: searchCmds.find, desc: 'Find files by glob (flag: --glob "**/*.jar")' },
45
+ { name: 'exec', run: execCmds.exec, desc: 'Run a Minecraft console command (allowlisted)' },
46
+ { name: 'say', run: execCmds.say, desc: 'Shortcut for "exec say ..."' },
47
+ { name: 'restart', run: execCmds.restart, desc: 'Shortcut for "exec restart"' },
48
+ { name: 'stats', run: monitorCmds.stats, desc: 'Server CPU/RAM/disk stats' },
49
+ { name: 'tps', run: monitorCmds.tps, desc: 'Server TPS (1m/5m/15m)' },
50
+ { name: 'log', run: monitorCmds.log, desc: 'Follow the server console (flag: --grep <regex>)' },
51
+ { name: 'top', run: monitorCmds.top, desc: 'Live dashboard (q to quit)' },
52
+ { name: 'cpush', run: cpush, desc: 'Upload a local file to the server' },
53
+ { name: 'cpull', run: cpull, desc: 'Download a server file locally' },
54
+ { name: 'csync', run: csync, desc: 'Synchronize local/remote directories (flag: --yes)' },
55
+ { name: 'login', run: authCmds.login, desc: 'Authenticate and store a session token' },
56
+ { name: 'logout', run: authCmds.logout, desc: 'Invalidate session and clear local credentials' },
57
+ { name: 'passwd', run: authCmds.passwd, desc: 'Change your password' },
58
+ { name: 'totp', run: authCmds.totp, desc: 'Manage 2FA: totp setup | totp disable' },
59
+ { name: 'whoami', run: authCmds.whoami, desc: 'Show current session info' },
60
+ { name: 'ping', run: authCmds.ping, desc: 'Check server connectivity' },
61
+ { name: 'help', run: help, desc: 'Show this help' },
62
+ { name: 'exit', run: exitCmd, desc: 'Leave the REPL' },
63
+ { name: 'quit', run: exitCmd, desc: 'Leave the REPL' },
64
+ { name: 'q', run: exitCmd, desc: 'Leave the REPL' },
65
+ ];
66
+
67
+ function find(name) {
68
+ return COMMANDS.find((c) => c.name === name);
69
+ }
70
+
71
+ function splitArgs(input) {
72
+ const args = [];
73
+ let cur = '';
74
+ let quote = null;
75
+ let escaped = false;
76
+ for (const ch of input) {
77
+ if (escaped) {
78
+ cur += ch;
79
+ escaped = false;
80
+ } else if (ch === '\\') {
81
+ escaped = true;
82
+ } else if (quote) {
83
+ if (ch === quote) quote = null;
84
+ else cur += ch;
85
+ } else if (ch === '"' || ch === "'") {
86
+ quote = ch;
87
+ } else if (/\s/.test(ch)) {
88
+ if (cur) {
89
+ args.push(cur);
90
+ cur = '';
91
+ }
92
+ } else {
93
+ cur += ch;
94
+ }
95
+ }
96
+ if (escaped) cur += '\\';
97
+ if (cur) args.push(cur);
98
+ return args;
99
+ }
100
+
101
+ async function runCommand(ctx, input) {
102
+ let trimmed = input.trim();
103
+ if (!trimmed || trimmed.startsWith('#')) return 0;
104
+
105
+ // Automatically strip redundant "creepercli" or "creepercli.exe" prefix if typed inside the REPL
106
+ if (trimmed.startsWith('creepercli.exe ')) {
107
+ trimmed = trimmed.slice(15).trim();
108
+ } else if (trimmed.startsWith('creepercli ')) {
109
+ trimmed = trimmed.slice(11).trim();
110
+ }
111
+
112
+ const args = splitArgs(trimmed);
113
+ const cmd = find(args[0]);
114
+ if (!cmd) {
115
+ console.error(`Unknown command: ${args[0]} (try "help")`);
116
+ return 1;
117
+ }
118
+ try {
119
+ const rc = await cmd.run(ctx, args.slice(1));
120
+ return rc == null ? 0 : rc;
121
+ } catch (err) {
122
+ console.error(`Error: ${friendly(err)}`);
123
+ return 1;
124
+ }
125
+ }
126
+
127
+ function help() {
128
+ const width = Math.max(...COMMANDS.map((c) => c.name.length)) + 2;
129
+ for (const c of COMMANDS) {
130
+ console.log(c.name.padEnd(width) + c.desc);
131
+ }
132
+ }
133
+
134
+ function exitCmd(ctx) {
135
+ ctx.exit = true;
136
+ return 0;
137
+ }
138
+
139
+ module.exports = { COMMANDS, find, splitArgs, runCommand, help };
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Copyright 2026 DemonZDevelopment
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const { formatStats, formatTps } = require('../formatters');
20
+ const { runTop } = require('../top');
21
+ const { EVENT_LOG_LINE } = require('../protocol');
22
+
23
+ async function stats(ctx) {
24
+ const res = await ctx.client.request('monitor.stats');
25
+ console.log(formatStats(res));
26
+ }
27
+
28
+ async function tps(ctx) {
29
+ const res = await ctx.client.request('monitor.tps');
30
+ console.log(formatTps(res));
31
+ }
32
+
33
+ async function top(ctx) {
34
+ await runTop(ctx.client, ctx.cfg.topRefreshMs);
35
+ }
36
+
37
+ async function log(ctx, args) {
38
+ let grep = null;
39
+ for (let i = 0; i < args.length; i++) {
40
+ if (args[i] === '--grep') grep = args[++i];
41
+ else if (args[i].startsWith('--grep=')) grep = args[i].slice(7);
42
+ }
43
+ const res = await ctx.client.request('monitor.log.start', { grep });
44
+ console.log(`Streaming console log (Ctrl+C to stop)${res.grep ? ' filtered by "' + res.grep + '"' : ''}`);
45
+ if (res.buffer && res.buffer.length) console.log(res.buffer.join('\n'));
46
+ let stop = null;
47
+ try {
48
+ await new Promise((resolve) => {
49
+ const onLine = (event, data) => {
50
+ if (event === EVENT_LOG_LINE) console.log(data.message);
51
+ };
52
+ ctx.client.on('event', onLine);
53
+ stop = () => {
54
+ ctx.client.removeListener('event', onLine);
55
+ ctx.client.request('monitor.log.stop').catch(() => {});
56
+ resolve();
57
+ };
58
+ process.once('SIGINT', stop);
59
+ ctx.interruptHandler = stop;
60
+ });
61
+ } finally {
62
+ if (stop) {
63
+ process.removeListener('SIGINT', stop);
64
+ if (ctx.interruptHandler === stop) ctx.interruptHandler = null;
65
+ }
66
+ }
67
+ }
68
+
69
+ module.exports = { stats, tps, top, log };
@@ -0,0 +1,70 @@
1
+ /*
2
+ * Copyright 2026 DemonZDevelopment
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const { formatMatches } = require('../formatters');
20
+
21
+ async function grep(ctx, args) {
22
+ const opts = {
23
+ caseInsensitive: false,
24
+ recursive: true,
25
+ lineNumbers: true,
26
+ maxDepth: 6,
27
+ maxResults: 1000,
28
+ };
29
+ const rest = [];
30
+ for (let i = 0; i < args.length; i++) {
31
+ const a = args[i];
32
+ if (a === '-i' || a === '--ignore-case') opts.caseInsensitive = true;
33
+ else if (a === '-r' || a === '-R' || a === '--recursive') opts.recursive = true;
34
+ else if (a === '-n' || a === '--line-number') opts.lineNumbers = true;
35
+ else if (a === '--max-depth') opts.maxDepth = parseInt(args[++i], 10) || 6;
36
+ else if (a === '--max-results') opts.maxResults = parseInt(args[++i], 10) || 1000;
37
+ else rest.push(a);
38
+ }
39
+ const pattern = rest[0];
40
+ const path = rest[1] || '.';
41
+ if (!pattern) throw new Error('Usage: grep [options] <pattern> [path]');
42
+ const res = await ctx.client.request('fs.grep', { path, pattern, ...opts });
43
+ if (!res.matches.length) {
44
+ console.log('No matches');
45
+ return;
46
+ }
47
+ console.log(formatMatches(res.matches));
48
+ if (res.truncated) console.error(`(truncated at ${res.matches.length} matches)`);
49
+ }
50
+
51
+ async function find(ctx, args) {
52
+ let glob = '*';
53
+ let path = '.';
54
+ let maxDepth = 12;
55
+ for (let i = 0; i < args.length; i++) {
56
+ const a = args[i];
57
+ if (a === '--glob' || a === '-g') glob = args[++i] || '*';
58
+ else if (a === '--max-depth') maxDepth = parseInt(args[++i], 10) || 12;
59
+ else path = a;
60
+ }
61
+ const res = await ctx.client.request('fs.find', { path, glob, maxDepth });
62
+ if (!res.results.length) {
63
+ console.log('No matches');
64
+ return;
65
+ }
66
+ console.log(res.results.join('\n'));
67
+ if (res.truncated) console.error('(truncated)');
68
+ }
69
+
70
+ module.exports = { grep, find };
package/src/config.js ADDED
@@ -0,0 +1,61 @@
1
+ /*
2
+ * Copyright 2026 DemonZDevelopment
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { DIR, HISTORY_FILE } = require('./session');
22
+
23
+ const CONFIG_FILE = path.join(DIR, 'config.json');
24
+
25
+ const DEFAULTS = {
26
+ host: '127.0.0.1',
27
+ port: 45678,
28
+ editor: process.env.EDITOR || 'vi',
29
+ topRefreshMs: 2000,
30
+ };
31
+
32
+ function load() {
33
+ let file = {};
34
+ try {
35
+ file = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
36
+ } catch {
37
+ }
38
+ const cfg = { ...DEFAULTS, ...file };
39
+ cfg.historyFile = HISTORY_FILE;
40
+ return cfg;
41
+ }
42
+
43
+ function save(newConfig) {
44
+ try {
45
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
46
+ const existing = load();
47
+ const updated = { ...existing, ...newConfig };
48
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), { mode: 0o600 });
49
+ } catch {
50
+ }
51
+ }
52
+
53
+ function applyFlags(cfg, flags) {
54
+ if (flags.host) cfg.host = flags.host;
55
+ if (flags.port) cfg.port = flags.port;
56
+ if (flags.editor) cfg.editor = flags.editor;
57
+ if (flags.refresh) cfg.topRefreshMs = flags.refresh;
58
+ return cfg;
59
+ }
60
+
61
+ module.exports = { load, save, applyFlags, CONFIG_FILE, DEFAULTS };
package/src/editor.js ADDED
@@ -0,0 +1,98 @@
1
+ /*
2
+ * Copyright 2026 DemonZDevelopment
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const crypto = require('crypto');
22
+ const { spawn } = require('child_process');
23
+ const { DIR } = require('./session');
24
+
25
+ function sha256(content) {
26
+ return crypto.createHash('sha256').update(content).digest('hex');
27
+ }
28
+
29
+ async function editFile(ctx, remotePath) {
30
+ if (!remotePath) throw new Error('Usage: edit <path>');
31
+ const tmpRoot = path.join(DIR, 'tmp');
32
+ fs.mkdirSync(tmpRoot, { recursive: true, mode: 0o700 });
33
+ const tmpDir = fs.mkdtempSync(path.join(tmpRoot, 'edit-'));
34
+ const localName = remotePath.split('/').pop() || 'file';
35
+ const tmpFile = path.join(tmpDir, localName);
36
+
37
+ try {
38
+ await ctx.client.request('fs.edit.lock', { path: remotePath });
39
+ } catch (err) {
40
+ if (err.code === 'E_LOCKED') {
41
+ console.error(`Locked: ${err.message}`);
42
+ return 1;
43
+ }
44
+ throw err;
45
+ }
46
+
47
+ try {
48
+ const cat = await ctx.client.request('fs.cat', { path: remotePath });
49
+ fs.writeFileSync(tmpFile, cat.content, { mode: 0o600 });
50
+
51
+ const editor = ctx.cfg.editor;
52
+ const parts = (editor.match(/"[^"]+"|'[^']+'|\S+/g) || []).map((p) => {
53
+ if ((p.startsWith('"') && p.endsWith('"')) || (p.startsWith("'") && p.endsWith("'"))) {
54
+ return p.slice(1, -1);
55
+ }
56
+ return p;
57
+ });
58
+ const execName = parts[0] || editor;
59
+ const args = parts.slice(1);
60
+ await new Promise((resolve, reject) => {
61
+ const child = spawn(execName, [...args, tmpFile], { stdio: 'inherit' });
62
+ child.on('error', (err) => reject(new Error(`Could not start editor "${editor}": ${err.message}`)));
63
+ child.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`Editor exited with code ${code}`))));
64
+ });
65
+
66
+ const newContent = fs.readFileSync(tmpFile, 'utf8');
67
+ if (newContent === cat.content) {
68
+ console.log('No changes.');
69
+ return 0;
70
+ }
71
+ const res = await ctx.client.request('fs.edit.push', {
72
+ path: remotePath,
73
+ content: newContent,
74
+ sha256: sha256(newContent),
75
+ });
76
+ const diff = summarizeDiff(cat.content.split('\n'), newContent.split('\n'));
77
+ console.log(`Pushed ${res.bytes} bytes to ${res.path} (+${diff.added}/-${diff.removed} lines)`);
78
+ return 0;
79
+ } catch (err) {
80
+ console.error(`Edit failed: ${err.message}`);
81
+ return 1;
82
+ } finally {
83
+ await ctx.client.request('fs.edit.unlock', { path: remotePath }).catch(() => {});
84
+ fs.rmSync(tmpDir, { recursive: true, force: true });
85
+ }
86
+ }
87
+
88
+ function summarizeDiff(oldLines, newLines) {
89
+ const a = new Set(oldLines);
90
+ const b = new Set(newLines);
91
+ let added = 0;
92
+ let removed = 0;
93
+ for (const l of newLines) if (!a.has(l)) added++;
94
+ for (const l of oldLines) if (!b.has(l)) removed++;
95
+ return { added, removed };
96
+ }
97
+
98
+ module.exports = { editFile };
@@ -0,0 +1,134 @@
1
+ /*
2
+ * Copyright 2026 DemonZDevelopment
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ function humanSize(bytes) {
20
+ if (bytes == null || bytes < 0) return '-';
21
+ const units = ['B', 'K', 'M', 'G', 'T'];
22
+ let v = bytes;
23
+ let i = 0;
24
+ while (v >= 1024 && i < units.length - 1) {
25
+ v /= 1024;
26
+ i++;
27
+ }
28
+ return (i === 0 ? String(bytes) : v.toFixed(1)) + units[i];
29
+ }
30
+
31
+ function formatDate(ms) {
32
+ if (!ms) return '-';
33
+ const d = new Date(ms);
34
+ const p = (n) => String(n).padStart(2, '0');
35
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
36
+ }
37
+
38
+ function formatLs(entries, { longFormat }) {
39
+ if (!entries.length) return '(empty)';
40
+ if (!longFormat) {
41
+ return wrap(entries.map((e) => e.name + (e.isDir ? '/' : '')));
42
+ }
43
+ return entries.map((e) => {
44
+ const perms = e.perms || '----------';
45
+ const size = e.isDir ? '-' : humanSize(e.size);
46
+ return `${perms} ${String(size).padStart(8)} ${formatDate(e.mtime)} ${e.name}${e.isDir ? '/' : ''}${e.link ? ' -> ' + e.link : ''}`;
47
+ }).join('\n');
48
+ }
49
+
50
+ function wrap(items, width = 100) {
51
+ let line = '';
52
+ const out = [];
53
+ for (const it of items) {
54
+ if (line && line.length + it.length + 2 > width) {
55
+ out.push(line);
56
+ line = '';
57
+ }
58
+ line = line ? line + ' ' + it : it;
59
+ }
60
+ if (line) out.push(line);
61
+ return out.join('\n');
62
+ }
63
+
64
+ function formatTree(nodes, prefix = '') {
65
+ const out = [];
66
+ nodes.forEach((node, i) => {
67
+ const last = i === nodes.length - 1;
68
+ const branch = last ? '└── ' : '├── ';
69
+ out.push(prefix + branch + node.name + (node.isDir ? '/' : ''));
70
+ if (node.children && node.children.length) {
71
+ out.push(...formatTree(node.children, prefix + (last ? ' ' : '│ ')));
72
+ }
73
+ });
74
+ return out.join('\n');
75
+ }
76
+
77
+ function formatUptime(ms) {
78
+ const total = Math.floor(ms / 1000);
79
+ const d = Math.floor(total / 86400);
80
+ const h = Math.floor((total % 86400) / 3600);
81
+ const m = Math.floor((total % 3600) / 60);
82
+ return `${d}d ${h}h ${m}m`;
83
+ }
84
+
85
+ function formatStats(s) {
86
+ const mem = s.memory || {};
87
+ const cpu = s.cpu || {};
88
+ const disk = s.disk || {};
89
+ const load = cpu.load >= 0 ? cpu.load.toFixed(2) : 'n/a';
90
+ const proc = cpu.processCpuPercent >= 0 ? cpu.processCpuPercent.toFixed(1) + '%' : 'n/a';
91
+ const rows = [
92
+ `Uptime: ${formatUptime(s.uptimeMs || 0)}`,
93
+ `CPU load: ${load} process: ${proc}`,
94
+ `Memory heap: ${humanSize(mem.heapUsed)} / ${humanSize(mem.heapTotal)} (max ${humanSize(mem.heapMax)})`,
95
+ `Disk (root): ${humanSize(disk.used)} / ${humanSize(disk.total)} (usable ${humanSize(disk.usable)})`,
96
+ ];
97
+ if (s.jvm && s.jvm.javaVersion) rows.push(`Java: ${s.jvm.javaVersion} (${s.jvm.availableProcessors} cores)`);
98
+ return rows.join('\n');
99
+ }
100
+
101
+ function tpsBar(tps) {
102
+ const filled = Math.max(0, Math.min(20, Math.round(tps)));
103
+ return '█'.repeat(filled) + '░'.repeat(20 - filled);
104
+ }
105
+
106
+ function formatTps(t) {
107
+ return `TPS: 1m ${t.tps1m.toFixed(1)} | 5m ${t.tps5m.toFixed(1)} | 15m ${t.tps15m.toFixed(1)} [${tpsBar(t.tps1m)}] tick ${t.tickMs}ms`;
108
+ }
109
+
110
+ function formatMatches(matches) {
111
+ return matches.map((m) => `${m.path}${m.line ? ':' + m.line : ''}: ${m.text}`).join('\n');
112
+ }
113
+
114
+ function formatInfo(e) {
115
+ return [
116
+ `Path: ${e.path}`,
117
+ `Type: ${e.isDir ? 'directory' : 'file'}${e.link ? ' (symlink -> ' + e.link + ')' : ''}`,
118
+ `Perms: ${e.perms || '-'}`,
119
+ `Size: ${e.isDir ? '-' : humanSize(e.size)}`,
120
+ `Modified: ${formatDate(e.mtime)}`,
121
+ ].join('\n');
122
+ }
123
+
124
+ module.exports = {
125
+ humanSize,
126
+ formatDate,
127
+ formatLs,
128
+ formatTree,
129
+ formatStats,
130
+ formatTps,
131
+ formatMatches,
132
+ formatInfo,
133
+ wrap,
134
+ };