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.
package/src/index.js ADDED
@@ -0,0 +1,186 @@
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 { CreeperClient } = require('./client');
20
+ const { load: loadConfig, applyFlags } = require('./config');
21
+ const session = require('./session');
22
+ const { startRepl } = require('./repl');
23
+ const { runCommand } = require('./commands');
24
+ const { doLogin } = require('./commands/auth');
25
+ const { friendly } = require('./protocol');
26
+ const { checkUpdate } = require('./update');
27
+
28
+ async function main(argv) {
29
+ checkUpdate();
30
+ const flags = parseFlags(argv);
31
+ const cfg = applyFlags(loadConfig(), flags);
32
+ if (flags.help) {
33
+ printUsage();
34
+ return 0;
35
+ }
36
+ const args = flags.args;
37
+ const cmd = args[0];
38
+
39
+ if (cmd === 'login') {
40
+ const client = await connect(cfg);
41
+ const ctx = { client, cfg, username: null, cwd: '/', exit: false, flags };
42
+ try {
43
+ const res = await doLogin(ctx);
44
+ if (res && res.token && process.stdout.isTTY) {
45
+ console.log('Entering interactive REPL shell (type "help" for commands, "exit" or "q" to quit)...\n');
46
+ startRepl(ctx);
47
+ return 0;
48
+ }
49
+ } catch (err) {
50
+ console.error(`\nError: ${friendly(err)}\n`);
51
+ client.close();
52
+ process.exit(1);
53
+ } finally {
54
+ if (!ctx.exit && !process.stdout.isTTY) {
55
+ client.close();
56
+ }
57
+ }
58
+ return 0;
59
+ }
60
+
61
+ if (cmd === 'logout') {
62
+ const client = await connect(cfg);
63
+ try {
64
+ const creds = session.load(cfg.host, cfg.port);
65
+ if (creds) {
66
+ await runCommand({ client, cfg, username: creds.username, cwd: '/' }, 'logout');
67
+ } else {
68
+ console.log('No local session to clear.');
69
+ }
70
+ } finally {
71
+ client.close();
72
+ }
73
+ return 0;
74
+ }
75
+
76
+ const client = await connect(cfg);
77
+ const ctx = { client, cfg, username: null, cwd: '/', exit: false, flags };
78
+ let creds = session.load(cfg.host, cfg.port);
79
+ if (creds) {
80
+ ctx.username = creds.username;
81
+ try {
82
+ const resume = await client.request('auth.resume', { token: creds.token }, { timeoutMs: 15000 });
83
+ ctx.username = resume.username;
84
+ ctx.cwd = resume.cwd || '/';
85
+ } catch (err) {
86
+ if (err.code === 'E_SESSION_EXPIRED' || err.code === 'E_UNAUTHORIZED') {
87
+ session.clear();
88
+ creds = null;
89
+ console.log('Session expired, please log in.');
90
+ } else {
91
+ throw err;
92
+ }
93
+ }
94
+ }
95
+ if (!creds) {
96
+ if (!process.stdout.isTTY) {
97
+ console.error('Not authenticated and stdin is not a TTY. Run "creepercli login" first.');
98
+ client.close();
99
+ return 1;
100
+ }
101
+ await doLogin(ctx);
102
+ }
103
+ if (!cmd || cmd === 'repl' || cmd === 'shell') {
104
+ startRepl(ctx);
105
+ return 0;
106
+ }
107
+ try {
108
+ return await runCommand(ctx, args.join(' '));
109
+ } finally {
110
+ client.close();
111
+ }
112
+ }
113
+
114
+ function parseFlags(argv) {
115
+ const flags = { args: [], host: null, port: null, editor: null, refresh: null, yes: false, help: false };
116
+ for (let i = 0; i < argv.length; i++) {
117
+ const a = argv[i];
118
+ if (a === '--host') {
119
+ if (i + 1 < argv.length) flags.host = argv[++i];
120
+ } else if (a === '--port') {
121
+ if (i + 1 < argv.length) flags.port = parseInt(argv[++i], 10);
122
+ } else if (a === '--editor') {
123
+ if (i + 1 < argv.length) flags.editor = argv[++i];
124
+ } else if (a === '--refresh') {
125
+ if (i + 1 < argv.length) flags.refresh = parseInt(argv[++i], 10);
126
+ } else if (a === '--yes' || a === '-y') {
127
+ flags.yes = true;
128
+ } else if (a === '--help' || a === '-h') {
129
+ flags.help = true;
130
+ } else {
131
+ flags.args.push(a);
132
+ }
133
+ }
134
+ return flags;
135
+ }
136
+
137
+ async function connect(cfg) {
138
+ const client = new CreeperClient({ host: cfg.host, port: cfg.port });
139
+ try {
140
+ await client.connect();
141
+ return client;
142
+ } catch (err) {
143
+ const codeStr = err.code || err.message;
144
+ console.error(`\nError: Cannot connect to server at ${cfg.host}:${cfg.port} (${codeStr})\n`);
145
+ console.error(`--------------------------------------------------------------------------------`);
146
+ console.error(`CREEPER CLI SETUP & TROUBLESHOOTING GUIDE`);
147
+ console.error(`--------------------------------------------------------------------------------\n`);
148
+ console.error(`1. INSTALL THE PLUGIN ON YOUR MINECRAFT SERVER:`);
149
+ console.error(` * Place CreeperCLI-1.0.0.jar inside your server's 'plugins/' folder.`);
150
+ console.error(` * Start or restart your server (Paper / Spigot 1.21+).\n`);
151
+ console.error(`2. CREATE YOUR ADMIN USER IN SERVER CONSOLE:`);
152
+ console.error(` * Open your server console (or run in-game as OP):`);
153
+ console.error(` /creepercli user add <username> <password>\n`);
154
+ console.error(`3. VERIFY NETWORK & PORT FORWARDING:`);
155
+ console.error(` * CreeperCLI listens on TCP port ${cfg.port} by default.`);
156
+ console.error(` * If connecting to a remote server, specify host & port flags:`);
157
+ console.error(` creepercli login --host <your-server-ip> --port ${cfg.port}`);
158
+ console.error(` * Or set up an SSH tunnel:`);
159
+ console.error(` ssh -L ${cfg.port}:127.0.0.1:${cfg.port} user@<your-server-ip>\n`);
160
+ console.error(`--------------------------------------------------------------------------------`);
161
+ console.error(`Documentation: https://github.com/DemonZDevelopment/creepercli\n`);
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ function printUsage() {
167
+ console.log(`CreeperCLI v1.0.0 — remote Minecraft administration
168
+
169
+ Usage:
170
+ creepercli login Authenticate with the server
171
+ creepercli logout Clear the local session
172
+ creepercli <command> [args...] Run a single command
173
+ creepercli repl Interactive shell (default)
174
+
175
+ Options:
176
+ --host <ip> server address (default 127.0.0.1)
177
+ --port <port> server port (default 45678)
178
+ --editor <cmd> editor for "edit" (default $EDITOR or vi)
179
+ --refresh <ms> dashboard refresh rate for "top"
180
+ --yes / -y auto-confirm transfers (csync)
181
+ --help / -h show this help
182
+
183
+ Run "help" inside the REPL for the full command reference.`);
184
+ }
185
+
186
+ module.exports = { main };
package/src/prompts.js ADDED
@@ -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
+ const readline = require('readline');
20
+
21
+ let pipeBuffer = '';
22
+ let pipeWaiters = [];
23
+ let pipeStarted = false;
24
+ let pipeEnded = false;
25
+
26
+ function startPipeReader() {
27
+ pipeStarted = true;
28
+ process.stdin.on('data', (chunk) => {
29
+ pipeBuffer += chunk.toString();
30
+ drainPipe();
31
+ });
32
+ process.stdin.on('end', () => {
33
+ pipeEnded = true;
34
+ drainPipe();
35
+ });
36
+ }
37
+
38
+ function drainPipe() {
39
+ while (pipeWaiters.length > 0) {
40
+ const idx = pipeBuffer.indexOf('\n');
41
+ if (idx === -1) {
42
+ if (pipeEnded && pipeBuffer.length > 0) {
43
+ const cb = pipeWaiters.shift();
44
+ const last = pipeBuffer;
45
+ pipeBuffer = '';
46
+ cb(last.trim());
47
+ continue;
48
+ }
49
+ break;
50
+ }
51
+ const line = pipeBuffer.slice(0, idx);
52
+ pipeBuffer = pipeBuffer.slice(idx + 1);
53
+ const cb = pipeWaiters.shift();
54
+ cb(line.replace(/\r$/, '').trim());
55
+ }
56
+ if (pipeEnded && pipeWaiters.length > 0 && pipeBuffer === '') {
57
+ const cb = pipeWaiters.shift();
58
+ cb('');
59
+ }
60
+ }
61
+
62
+ function pipeQuestion(prompt) {
63
+ if (!pipeStarted) startPipeReader();
64
+ process.stdout.write(prompt);
65
+ return new Promise((resolve) => {
66
+ pipeWaiters.push(resolve);
67
+ drainPipe();
68
+ });
69
+ }
70
+
71
+ function question(prompt) {
72
+ if (!process.stdin.isTTY) {
73
+ return pipeQuestion(prompt);
74
+ }
75
+ return new Promise((resolve) => {
76
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
77
+ rl.question(prompt, (answer) => {
78
+ rl.close();
79
+ resolve(answer.trim());
80
+ });
81
+ });
82
+ }
83
+
84
+ function readSecret(prompt) {
85
+ if (!process.stdin.isTTY) {
86
+ return pipeQuestion(prompt);
87
+ }
88
+ return new Promise((resolve, reject) => {
89
+ const input = process.stdin;
90
+ const output = process.stdout;
91
+ input.setRawMode(true);
92
+ input.resume();
93
+ output.write(prompt);
94
+ let value = '';
95
+ const onData = (chunk) => {
96
+ for (const ch of chunk.toString()) {
97
+ if (ch === '\u0003') {
98
+ cleanup();
99
+ reject(new Error('Aborted'));
100
+ return;
101
+ }
102
+ if (ch === '\r' || ch === '\n') {
103
+ output.write('\n');
104
+ cleanup();
105
+ resolve(value);
106
+ return;
107
+ }
108
+ if (ch === '\u007f' || ch === '\b') {
109
+ if (value.length > 0) {
110
+ value = value.slice(0, -1);
111
+ output.write('\b \b');
112
+ }
113
+ } else {
114
+ value += ch;
115
+ output.write('*');
116
+ }
117
+ }
118
+ };
119
+ const cleanup = () => {
120
+ input.removeListener('data', onData);
121
+ input.setRawMode(false);
122
+ input.pause();
123
+ };
124
+ input.on('data', onData);
125
+ });
126
+ }
127
+
128
+ async function confirm(text, defYes = false) {
129
+ const answer = await question(text + (defYes ? ' [Y/n] ' : ' [y/N] '));
130
+ if (!answer) return defYes;
131
+ return /^y(es)?$/i.test(answer.trim());
132
+ }
133
+
134
+ module.exports = { question, readSecret, confirm };
@@ -0,0 +1,82 @@
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 ERR = {
20
+ UNAUTHORIZED: 'E_UNAUTHORIZED',
21
+ AUTH_FAILED: 'E_AUTH_FAILED',
22
+ TOTP_REQUIRED: 'E_TOTP_REQUIRED',
23
+ BANNED: 'E_BANNED',
24
+ RATE_LIMITED: 'E_RATE_LIMITED',
25
+ SESSION_EXPIRED: 'E_SESSION_EXPIRED',
26
+ INVALID_PARAMS: 'E_INVALID_PARAMS',
27
+ BAD_REQUEST: 'E_BAD_REQUEST',
28
+ PATH_ESCAPE: 'E_PATH_ESCAPE',
29
+ NOT_FOUND: 'E_NOT_FOUND',
30
+ IS_DIRECTORY: 'E_IS_DIRECTORY',
31
+ NOT_DIRECTORY: 'E_NOT_DIRECTORY',
32
+ ALREADY_EXISTS: 'E_ALREADY_EXISTS',
33
+ FORBIDDEN: 'E_FORBIDDEN',
34
+ IO: 'E_IO',
35
+ LOCKED: 'E_LOCKED',
36
+ ALLOWLIST_DENIED: 'E_ALLOWLIST_DENIED',
37
+ TIMEOUT: 'E_TIMEOUT',
38
+ PAYLOAD_TOO_LARGE: 'E_PAYLOAD_TOO_LARGE',
39
+ CHECKSUM_MISMATCH: 'E_CHECKSUM_MISMATCH',
40
+ NO_TRANSFER: 'E_NO_TRANSFER',
41
+ INTERNAL: 'E_INTERNAL',
42
+ SERVER_FULL: 'E_SERVER_FULL',
43
+ };
44
+
45
+ const PROTOCOL_VERSION = 1;
46
+
47
+ const EVENT_LOG_LINE = 'log.line';
48
+
49
+ class ProtocolError extends Error {
50
+ constructor(code, message) {
51
+ super(message || code);
52
+ this.code = code;
53
+ }
54
+ }
55
+
56
+ function friendly(err) {
57
+ if (err instanceof ProtocolError) {
58
+ const hints = {
59
+ [ERR.UNAUTHORIZED]: 'run "creepercli login" first',
60
+ [ERR.AUTH_FAILED]: 'check your username/password or add user in server console: /creepercli user add <user> <pass>',
61
+ [ERR.TOTP_REQUIRED]: 'provide your 6-digit TOTP code',
62
+ [ERR.BANNED]: 'too many failures, this IP is temporarily banned by Fail2Ban',
63
+ [ERR.RATE_LIMITED]: 'too many requests in a short window, please slow down',
64
+ [ERR.SESSION_EXPIRED]: 'session expired, run "creepercli login"',
65
+ [ERR.PATH_ESCAPE]: 'path blocked by server PathSanitizer sandbox',
66
+ [ERR.ALLOWLIST_DENIED]: 'command restricted by server allowlist config',
67
+ [ERR.LOCKED]: 'file currently locked by another active session',
68
+ [ERR.CHECKSUM_MISMATCH]: 'data integrity check failed, partial file cleaned up',
69
+ [ERR.SERVER_FULL]: 'server connection limit reached',
70
+ [ERR.TIMEOUT]: 'request timed out, check server load',
71
+ [ERR.NOT_FOUND]: 'file or directory not found on remote server',
72
+ [ERR.IS_DIRECTORY]: 'target path is a directory, not a file',
73
+ [ERR.NOT_DIRECTORY]: 'target path is a file, not a directory',
74
+ [ERR.FORBIDDEN]: 'permission denied by server policy',
75
+ };
76
+ const hint = hints[err.code];
77
+ return `${err.message}${hint ? ` [${hint}]` : ''} (${err.code})`;
78
+ }
79
+ return err && err.message ? err.message : String(err);
80
+ }
81
+
82
+ module.exports = { ERR, PROTOCOL_VERSION, EVENT_LOG_LINE, ProtocolError, friendly };
package/src/repl.js ADDED
@@ -0,0 +1,104 @@
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 readline = require('readline');
21
+ const { COMMANDS, runCommand } = require('./commands');
22
+
23
+ function prompt(ctx) {
24
+ return `${ctx.username || '?'}@${ctx.cfg.host}:${ctx.cwd || '/'}> `;
25
+ }
26
+
27
+ function startRepl(ctx) {
28
+ const completions = COMMANDS.map((c) => c.name);
29
+
30
+ function completer(line) {
31
+ const trimmed = line.trimStart();
32
+ const hits = completions.filter((c) => c.startsWith(trimmed));
33
+ return [hits.length ? hits : completions, trimmed];
34
+ }
35
+
36
+ const rl = readline.createInterface({
37
+ input: process.stdin,
38
+ output: process.stdout,
39
+ terminal: true,
40
+ historySize: 1000,
41
+ completer,
42
+ });
43
+ try {
44
+ const history = fs.readFileSync(ctx.cfg.historyFile, 'utf8').split('\n').filter(Boolean).reverse();
45
+ rl.history = history;
46
+ } catch {
47
+ }
48
+ rl.on('history', (history) => {
49
+ try {
50
+ fs.writeFileSync(ctx.cfg.historyFile, history.filter(Boolean).join('\n') + '\n', { mode: 0o600 });
51
+ } catch {
52
+ }
53
+ });
54
+
55
+ const queue = [];
56
+ let running = false;
57
+ async function pump() {
58
+ if (running) return;
59
+ running = true;
60
+ while (queue.length) {
61
+ const line = queue.shift();
62
+ await runCommand(ctx, line);
63
+ if (ctx.exit) {
64
+ queue.length = 0;
65
+ break;
66
+ }
67
+ rl.setPrompt(prompt(ctx));
68
+ rl.prompt();
69
+ }
70
+ running = false;
71
+ }
72
+
73
+ rl.setPrompt(prompt(ctx));
74
+ rl.prompt();
75
+
76
+ rl.on('line', (line) => {
77
+ queue.push(line);
78
+ pump();
79
+ });
80
+
81
+ rl.on('SIGINT', () => {
82
+ if (running) {
83
+ if (ctx.interruptHandler) {
84
+ console.log('\n(interrupted)');
85
+ ctx.interruptHandler();
86
+ } else {
87
+ console.log('\n(interrupt)');
88
+ }
89
+ return;
90
+ }
91
+ rl.question('Really quit? [y/N] ', (answer) => {
92
+ if (/^y/i.test(answer.trim())) rl.close();
93
+ else rl.prompt();
94
+ });
95
+ });
96
+
97
+ rl.on('close', async () => {
98
+ await ctx.client.flush();
99
+ ctx.client.close();
100
+ process.exit(0);
101
+ });
102
+ }
103
+
104
+ module.exports = { startRepl, prompt };
package/src/session.js ADDED
@@ -0,0 +1,58 @@
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 os = require('os');
21
+ const path = require('path');
22
+
23
+ const DIR = path.join(os.homedir(), '.creepercli');
24
+ const CREDS_FILE = path.join(DIR, 'creds');
25
+ const HISTORY_FILE = path.join(DIR, 'history');
26
+
27
+ function ensureDir() {
28
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
29
+ }
30
+
31
+ function save(creds) {
32
+ ensureDir();
33
+ fs.writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2), { mode: 0o600 });
34
+ }
35
+
36
+ function load(host, port) {
37
+ try {
38
+ const raw = fs.readFileSync(CREDS_FILE, 'utf8');
39
+ const creds = JSON.parse(raw);
40
+ if (creds.host !== host || creds.port !== port) return null;
41
+ if (creds.expiresAt && Date.now() > creds.expiresAt) {
42
+ clear();
43
+ return null;
44
+ }
45
+ return creds;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function clear() {
52
+ try {
53
+ fs.unlinkSync(CREDS_FILE);
54
+ } catch {
55
+ }
56
+ }
57
+
58
+ module.exports = { DIR, CREDS_FILE, HISTORY_FILE, save, load, clear };
package/src/top.js ADDED
@@ -0,0 +1,53 @@
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 readline = require('readline');
20
+ const { formatStats, formatTps } = require('./formatters');
21
+
22
+ function sleep(ms) {
23
+ return new Promise((r) => setTimeout(r, ms));
24
+ }
25
+
26
+ async function runTop(client, refreshMs) {
27
+ if (process.stdin.isTTY) {
28
+ readline.emitKeypressEvents(process.stdin);
29
+ process.stdin.setRawMode(true);
30
+ }
31
+ let stopped = false;
32
+ const onKey = (str, key) => {
33
+ if (key.name === 'q' || key.name === 'escape' || (key.ctrl && key.name === 'c')) stopped = true;
34
+ };
35
+ process.stdin.on('keypress', onKey);
36
+ try {
37
+ while (!stopped) {
38
+ const res = await client.request('monitor.top', {});
39
+ readline.cursorTo(process.stdout, 0, 0);
40
+ readline.clearScreenDown(process.stdout);
41
+ console.log('CreeperCLI top (q to quit)\n');
42
+ console.log(formatStats(res.stats));
43
+ console.log();
44
+ console.log(formatTps(res.tps));
45
+ await sleep(refreshMs);
46
+ }
47
+ } finally {
48
+ process.stdin.removeListener('keypress', onKey);
49
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
50
+ }
51
+ }
52
+
53
+ module.exports = { runTop };