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/README.md +117 -0
- package/bin/creepercli.js +27 -0
- package/package.json +48 -0
- package/src/client.js +127 -0
- package/src/commands/auth.js +108 -0
- package/src/commands/exec.js +37 -0
- package/src/commands/fs.js +152 -0
- package/src/commands/index.js +139 -0
- package/src/commands/monitor.js +69 -0
- package/src/commands/search.js +70 -0
- package/src/config.js +61 -0
- package/src/editor.js +98 -0
- package/src/formatters.js +134 -0
- package/src/index.js +186 -0
- package/src/prompts.js +134 -0
- package/src/protocol.js +82 -0
- package/src/repl.js +104 -0
- package/src/session.js +58 -0
- package/src/top.js +53 -0
- package/src/transfer.js +217 -0
- package/src/update.js +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# CreeperCLI (`creepercli`)
|
|
2
|
+
|
|
3
|
+
> Remote Administration CLI for Minecraft (Paper / Spigot) Servers
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/creepercli)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://discord.com/invite/zCkE44hsBR)
|
|
8
|
+
|
|
9
|
+
**CreeperCLI** is a zero-dependency, ultra-fast Node.js command-line interface for managing Paper and Spigot Minecraft servers remotely. It pairs with the official **CreeperCLI Paper Plugin** to deliver hardened TCP remote control, real-time log streaming, live performance metrics, and sandboxed file management.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ⚡ Quick Start
|
|
14
|
+
|
|
15
|
+
### Installation
|
|
16
|
+
|
|
17
|
+
Install globally via `npm`:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g creepercli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or run instantly without installing using `npx`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx creepercli login --host <your-server-ip> --port <your-port>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 🚀 Usage Guide
|
|
32
|
+
|
|
33
|
+
### 1. Authenticate & Launch REPL
|
|
34
|
+
|
|
35
|
+
Log in to your Minecraft server and automatically enter the interactive REPL shell:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
creepercli login --host 89.106.64.70 --port 30087
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
You will be prompted for your username and password. On successful authentication, CreeperCLI drops into an interactive shell:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
Authenticated as admin. Session valid for 15 minutes.
|
|
45
|
+
Entering interactive REPL shell (type "help" for commands, "exit" or "q" to quit)...
|
|
46
|
+
|
|
47
|
+
admin@89.106.64.70:/>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 2. Interactive REPL Shell
|
|
51
|
+
|
|
52
|
+
Inside the REPL, commands are typed directly without `creepercli` prefixes:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
admin@89.106.64.70:/> stats
|
|
56
|
+
Uptime: 1d 4h 12m
|
|
57
|
+
CPU load: 0.15 process: 1.0%
|
|
58
|
+
Memory heap: 494.1M / 680.0M (max 8.0G)
|
|
59
|
+
Disk (root): 40.3G / 192.7G (usable 152.4G)
|
|
60
|
+
Java: 25.0.3 (2 cores)
|
|
61
|
+
|
|
62
|
+
admin@89.106.64.70:/> tps
|
|
63
|
+
TPS: 1m 20.0 | 5m 20.0 | 15m 20.0 [████████████████████] tick 50ms
|
|
64
|
+
|
|
65
|
+
admin@89.106.64.70:/> ls /plugins
|
|
66
|
+
banned-ips.json bukkit.yml config/ logs/ plugins/ server.properties
|
|
67
|
+
|
|
68
|
+
admin@89.106.64.70:/> cd plugins
|
|
69
|
+
Changed directory to /plugins
|
|
70
|
+
|
|
71
|
+
admin@89.106.64.70:/plugins> q
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🛠️ Complete CLI Command Reference
|
|
77
|
+
|
|
78
|
+
| Command | Usage | Description |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `login` | `creepercli login [--host H] [--port P]` | Log in and auto-launch the interactive REPL shell |
|
|
81
|
+
| `logout` | `creepercli logout` | Invalidate active session and clear local credentials |
|
|
82
|
+
| `whoami` | `creepercli whoami` | Display active session token info, IP, and permissions |
|
|
83
|
+
| `stats` | `creepercli stats` | View live server CPU, RAM heap, disk usage, and Java runtime stats |
|
|
84
|
+
| `tps` | `creepercli tps` | View 1m/5m/15m TPS breakdown with tick health bar |
|
|
85
|
+
| `top` | `creepercli top` | Real-time live dashboard monitoring TPS, RAM, and online players |
|
|
86
|
+
| `log` | `creepercli log [--grep regex]` | Stream live server console logs directly to terminal |
|
|
87
|
+
| `exec` | `creepercli exec <command>` | Run allowlisted Minecraft console commands (`list`, `say`, `restart`) |
|
|
88
|
+
| `ls` | `creepercli ls [path] [-l] [-a]` | List files in server directory |
|
|
89
|
+
| `cd` | `creepercli cd <path>` | Change working directory (`cd ~` or `cd /` goes to jail root) |
|
|
90
|
+
| `cat` | `creepercli cat <file>` | Read file contents from server |
|
|
91
|
+
| `edit` | `creepercli edit <file>` | Pull, edit in local `$EDITOR`, and push back under exclusive lock |
|
|
92
|
+
| `cpush` | `creepercli cpush <local> <remote> [--force]` | Upload a file with SHA-256 integrity verification |
|
|
93
|
+
| `cpull` | `creepercli cpull <remote> <local>` | Download a file with SHA-256 integrity verification |
|
|
94
|
+
| `csync` | `creepercli csync <remote> <local> [-y]` | Two-way directory sync comparing mtimes and checksums |
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 🔒 Security Architecture
|
|
99
|
+
|
|
100
|
+
- **Path Jailing**: All file operations are jailed inside the server working directory (`server-root`). Symlinks and `..` path escapes are blocked (`E_PATH_ESCAPE`).
|
|
101
|
+
- **BCrypt & TOTP 2FA**: User credentials are hashed with BCrypt (cost 12). Optional 2FA with 6-digit TOTP tokens supported.
|
|
102
|
+
- **Fail2Ban Protection**: Automatic IP ban after 3 failed login attempts.
|
|
103
|
+
- **Allowlist Execution**: Only console commands explicitly allowed in `config.yml` can be executed remotely.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 🔗 Links & Resources
|
|
108
|
+
|
|
109
|
+
- **GitHub Repository**: [DemonZDevelopment/creepercli](https://github.com/DemonZDevelopment/creepercli)
|
|
110
|
+
- **Discord Community**: [Join Support Discord](https://discord.com/invite/zCkE44hsBR)
|
|
111
|
+
- **Modrinth Project**: [CreeperCLI on Modrinth](https://modrinth.com/project/creepercli)
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 📄 License
|
|
116
|
+
|
|
117
|
+
Licensed under the [Apache License, Version 2.0](LICENSE).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Copyright 2026 DemonZDevelopment
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { main } = require('../src/index');
|
|
21
|
+
|
|
22
|
+
main(process.argv.slice(2)).then((code) => {
|
|
23
|
+
if (typeof code === 'number') process.exitCode = code;
|
|
24
|
+
}).catch((err) => {
|
|
25
|
+
console.error(err && err.message ? err.message : String(err));
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "creepercli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remote administration CLI for Minecraft (Paper/Spigot) servers running the CreeperCLI plugin",
|
|
5
|
+
"author": "DemonZDevelopment",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"creepercli": "bin/creepercli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test",
|
|
23
|
+
"start": "node bin/creepercli.js"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"qrcode-terminal": "^0.12.0"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"minecraft",
|
|
30
|
+
"spigot",
|
|
31
|
+
"paper",
|
|
32
|
+
"bukkit",
|
|
33
|
+
"admin",
|
|
34
|
+
"cli",
|
|
35
|
+
"remote",
|
|
36
|
+
"server",
|
|
37
|
+
"creeper",
|
|
38
|
+
"creepercli"
|
|
39
|
+
],
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/DemonZDevelopment/creepercli.git"
|
|
43
|
+
},
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/DemonZDevelopment/creepercli/issues"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/DemonZDevelopment/creepercli"
|
|
48
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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 net = require('net');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
const { EventEmitter } = require('events');
|
|
22
|
+
const { StringDecoder } = require('string_decoder');
|
|
23
|
+
const { ProtocolError, ERR } = require('./protocol');
|
|
24
|
+
|
|
25
|
+
class CreeperClient extends EventEmitter {
|
|
26
|
+
constructor({ host, port, timeoutMs = 300000 }) {
|
|
27
|
+
super();
|
|
28
|
+
this.host = host;
|
|
29
|
+
this.port = port;
|
|
30
|
+
this.timeoutMs = timeoutMs;
|
|
31
|
+
this.socket = null;
|
|
32
|
+
this.buffer = '';
|
|
33
|
+
this._decoder = new StringDecoder('utf8');
|
|
34
|
+
this.pending = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
connect() {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const socket = net.connect({ host: this.host, port: this.port });
|
|
40
|
+
this.socket = socket;
|
|
41
|
+
socket.setNoDelay(true);
|
|
42
|
+
socket.on('connect', () => resolve());
|
|
43
|
+
socket.on('error', (err) => reject(err));
|
|
44
|
+
socket.on('close', () => this._onClose());
|
|
45
|
+
socket.on('data', (chunk) => this._onData(chunk));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_onData(chunk) {
|
|
50
|
+
this.buffer += this._decoder.write(chunk);
|
|
51
|
+
let idx;
|
|
52
|
+
while ((idx = this.buffer.indexOf('\n')) !== -1) {
|
|
53
|
+
const line = this.buffer.slice(0, idx).trim();
|
|
54
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
55
|
+
if (!line) continue;
|
|
56
|
+
this._handleLine(line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_handleLine(line) {
|
|
61
|
+
let frame;
|
|
62
|
+
try {
|
|
63
|
+
frame = JSON.parse(line);
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (frame.type === 'response') {
|
|
68
|
+
const entry = this.pending.get(frame.id);
|
|
69
|
+
if (!entry) return;
|
|
70
|
+
this.pending.delete(frame.id);
|
|
71
|
+
clearTimeout(entry.timer);
|
|
72
|
+
if (frame.ok) {
|
|
73
|
+
entry.resolve(frame.data || {});
|
|
74
|
+
} else {
|
|
75
|
+
entry.reject(new ProtocolError(frame.error && frame.error.code, frame.error && frame.error.message));
|
|
76
|
+
}
|
|
77
|
+
} else if (frame.type === 'event') {
|
|
78
|
+
this.emit('event', frame.event, frame.data || {});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async flush() {
|
|
83
|
+
while (this.pending.size > 0) {
|
|
84
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
request(action, params = {}, { timeoutMs } = {}) {
|
|
89
|
+
if (!this.socket) {
|
|
90
|
+
return Promise.reject(new ProtocolError(ERR.INTERNAL, 'Connection closed'));
|
|
91
|
+
}
|
|
92
|
+
const id = crypto.randomUUID();
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
this.pending.delete(id);
|
|
96
|
+
reject(new ProtocolError(ERR.TIMEOUT, `Request timed out (${action})`));
|
|
97
|
+
}, timeoutMs || this.timeoutMs);
|
|
98
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
99
|
+
const frame = JSON.stringify({ v: 1, type: 'request', id, action, params });
|
|
100
|
+
try {
|
|
101
|
+
this.socket.write(frame + '\n');
|
|
102
|
+
} catch (err) {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
this.pending.delete(id);
|
|
105
|
+
reject(new ProtocolError(ERR.INTERNAL, `Connection closed (${err.code || err.message})`));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_onClose() {
|
|
111
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
|
|
114
|
+
}
|
|
115
|
+
this.pending.clear();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
close() {
|
|
119
|
+
if (this.socket) {
|
|
120
|
+
this.socket.end();
|
|
121
|
+
this.socket.destroy();
|
|
122
|
+
this.socket = null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = { CreeperClient };
|
|
@@ -0,0 +1,108 @@
|
|
|
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 { question, readSecret } = require('../prompts');
|
|
20
|
+
const session = require('../session');
|
|
21
|
+
const config = require('../config');
|
|
22
|
+
|
|
23
|
+
function saveCreds(cfg, res) {
|
|
24
|
+
session.save({
|
|
25
|
+
host: cfg.host,
|
|
26
|
+
port: cfg.port,
|
|
27
|
+
token: res.token,
|
|
28
|
+
username: res.username,
|
|
29
|
+
expiresAt: Date.now() + res.expiresInMinutes * 60000,
|
|
30
|
+
});
|
|
31
|
+
config.save({ host: cfg.host, port: cfg.port });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function doLogin(ctx) {
|
|
35
|
+
const user = await question('Username: ');
|
|
36
|
+
const pass = await readSecret('Password: ');
|
|
37
|
+
let res;
|
|
38
|
+
try {
|
|
39
|
+
res = await ctx.client.request('auth.login', { username: user, password: pass }, { timeoutMs: 30000 });
|
|
40
|
+
} catch (err) {
|
|
41
|
+
if (err.code === 'E_TOTP_REQUIRED') {
|
|
42
|
+
const code = await question('TOTP code: ');
|
|
43
|
+
res = await ctx.client.request('auth.login', { username: user, password: pass, totp: code }, { timeoutMs: 30000 });
|
|
44
|
+
} else {
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
saveCreds(ctx.cfg, res);
|
|
49
|
+
ctx.username = res.username;
|
|
50
|
+
ctx.cwd = '/';
|
|
51
|
+
console.log(`Authenticated as ${res.username}. Session valid for ${res.expiresInMinutes} minutes.`);
|
|
52
|
+
return res;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function login(ctx) {
|
|
56
|
+
await doLogin(ctx);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function logout(ctx) {
|
|
60
|
+
const creds = session.load(ctx.cfg.host, ctx.cfg.port);
|
|
61
|
+
await ctx.client.request('auth.logout', { token: creds ? creds.token : null }).catch(() => {});
|
|
62
|
+
session.clear();
|
|
63
|
+
ctx.username = null;
|
|
64
|
+
console.log('Logged out. Local credentials cleared.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function whoami(ctx) {
|
|
68
|
+
const res = await ctx.client.request('auth.whoami');
|
|
69
|
+
console.log(`User ${res.username} (IP ${res.ip}), cwd ${res.cwd}, session expires in ${Math.floor(res.expiresInSeconds / 60)}m${res.totpEnabled ? ', 2FA enabled' : ', 2FA disabled'}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function ping(ctx) {
|
|
73
|
+
const res = await ctx.client.request('ping', {}, { timeoutMs: 15000 });
|
|
74
|
+
console.log(`pong (protocol v${res.protocol}, plugin v${res.pluginVersion}, server time ${new Date(res.serverTime).toISOString()})`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function passwd(ctx) {
|
|
78
|
+
const oldP = await readSecret('Current password: ');
|
|
79
|
+
const newP = await readSecret('New password: ');
|
|
80
|
+
if (newP.length < 8) throw new Error('New password must be at least 8 characters');
|
|
81
|
+
await ctx.client.request('auth.passwd', { oldPassword: oldP, newPassword: newP });
|
|
82
|
+
console.log('Password changed. Other sessions were invalidated.');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function totp(ctx, args) {
|
|
86
|
+
const sub = args[0];
|
|
87
|
+
if (sub === 'setup') {
|
|
88
|
+
const pass = await readSecret('Password: ');
|
|
89
|
+
const res = await ctx.client.request('auth.totp.setup', { password: pass });
|
|
90
|
+
console.log('Scan the QR below with your authenticator app, or enter the secret manually:');
|
|
91
|
+
console.log(`Secret: ${res.secret}`);
|
|
92
|
+
console.log(`(setup expires in ${Math.floor((res.expiresInSeconds || 600) / 60)} minutes)`);
|
|
93
|
+
const qr = require('qrcode-terminal');
|
|
94
|
+
qr.generate(res.otpauthUrl, { small: true });
|
|
95
|
+
const code = await question('Enter the 6-digit code from your authenticator app: ');
|
|
96
|
+
await ctx.client.request('auth.totp.verify', { code });
|
|
97
|
+
console.log('TOTP enabled.');
|
|
98
|
+
} else if (sub === 'disable') {
|
|
99
|
+
const pass = await readSecret('Password: ');
|
|
100
|
+
const code = await question('TOTP code: ');
|
|
101
|
+
await ctx.client.request('auth.totp.disable', { password: pass, code });
|
|
102
|
+
console.log('TOTP disabled.');
|
|
103
|
+
} else {
|
|
104
|
+
throw new Error('Usage: totp <setup|disable>');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = { login, logout, whoami, ping, passwd, totp, doLogin };
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
async function exec(ctx, args) {
|
|
20
|
+
if (!args.length) throw new Error('Usage: exec <minecraft command>');
|
|
21
|
+
const command = args.join(' ');
|
|
22
|
+
const res = await ctx.client.request('exec.run', { command }, { timeoutMs: 150000 });
|
|
23
|
+
if (res.lines && res.lines.length) console.log(res.lines.join('\n'));
|
|
24
|
+
if (!res.success) console.error('Command reported failure');
|
|
25
|
+
if (res.elapsedMs != null) console.error(`(completed in ${res.elapsedMs}ms)`);
|
|
26
|
+
return res.success ? 0 : 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function say(ctx, args) {
|
|
30
|
+
return exec(ctx, ['say', ...args]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function restart(ctx, args) {
|
|
34
|
+
return exec(ctx, ['restart', ...args]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { exec, say, restart };
|
|
@@ -0,0 +1,152 @@
|
|
|
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 { formatLs, formatTree, humanSize, formatInfo } = require('../formatters');
|
|
20
|
+
const { editFile } = require('../editor');
|
|
21
|
+
|
|
22
|
+
async function pwd(ctx) {
|
|
23
|
+
const res = await ctx.client.request('fs.pwd');
|
|
24
|
+
ctx.cwd = res.cwd;
|
|
25
|
+
console.log(res.cwd);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function ls(ctx, args) {
|
|
29
|
+
const flags = args.filter((a) => a.startsWith('-') && a !== '-');
|
|
30
|
+
const longFormat = flags.some((f) => f.includes('l'));
|
|
31
|
+
const all = flags.some((f) => f.includes('a'));
|
|
32
|
+
const path = args.find((a) => !a.startsWith('-')) || '.';
|
|
33
|
+
const res = await ctx.client.request('fs.ls', { path, long: longFormat, all });
|
|
34
|
+
ctx.cwd = res.cwd;
|
|
35
|
+
console.log(formatLs(res.entries, { longFormat }));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function cd(ctx, args) {
|
|
39
|
+
let path = args[0] || '/';
|
|
40
|
+
if (path === '~' || path.startsWith('~/')) {
|
|
41
|
+
path = '/' + path.slice(1).replace(/^\/+/, '');
|
|
42
|
+
}
|
|
43
|
+
const res = await ctx.client.request('fs.cd', { path });
|
|
44
|
+
ctx.cwd = res.cwd;
|
|
45
|
+
console.log(`Changed directory to ${res.cwd}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function tree(ctx, args) {
|
|
49
|
+
let depth = 3;
|
|
50
|
+
const pathArgs = [];
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
if (args[i] === '-d' || args[i] === '--depth') {
|
|
53
|
+
depth = parseInt(args[++i], 10) || 3;
|
|
54
|
+
} else {
|
|
55
|
+
pathArgs.push(args[i]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const res = await ctx.client.request('fs.tree', { path: pathArgs[0] || '.', depth });
|
|
59
|
+
console.log(formatTree(res.children || []));
|
|
60
|
+
if (res.truncated) console.log('(tree truncated)');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function cat(ctx, args) {
|
|
64
|
+
if (!args[0]) throw new Error('Usage: cat <path>');
|
|
65
|
+
const res = await ctx.client.request('fs.cat', { path: args[0] });
|
|
66
|
+
process.stdout.write(res.content);
|
|
67
|
+
if (!res.content.endsWith('\n')) process.stdout.write('\n');
|
|
68
|
+
if (res.truncated) console.error(`(truncated: file is ${humanSize(res.size)})`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function head(ctx, args) {
|
|
72
|
+
const { lines, path } = parseTailArgs(args, 10);
|
|
73
|
+
const res = await ctx.client.request('fs.head', { path, lines });
|
|
74
|
+
console.log(res.lines.join('\n'));
|
|
75
|
+
if (res.truncated) console.error('(truncated)');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function tail(ctx, args) {
|
|
79
|
+
if (args.includes('-f') || args.includes('--follow')) {
|
|
80
|
+
throw new Error('Live follow is available via the "log" command: creepercli log [--grep <regex>]');
|
|
81
|
+
}
|
|
82
|
+
const { lines, path } = parseTailArgs(args, 10);
|
|
83
|
+
const res = await ctx.client.request('fs.tail', { path, lines });
|
|
84
|
+
console.log(res.lines.join('\n'));
|
|
85
|
+
if (res.truncated) console.error('(truncated)');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseTailArgs(args, def) {
|
|
89
|
+
let n = def;
|
|
90
|
+
let path = null;
|
|
91
|
+
for (let i = 0; i < args.length; i++) {
|
|
92
|
+
if (args[i] === '-n') n = parseInt(args[++i], 10) || def;
|
|
93
|
+
else path = args[i];
|
|
94
|
+
}
|
|
95
|
+
if (!path) throw new Error('Usage: <command> [-n N] <path>');
|
|
96
|
+
return { lines: n, path };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function wc(ctx, args) {
|
|
100
|
+
if (!args[0]) throw new Error('Usage: wc <path>');
|
|
101
|
+
const res = await ctx.client.request('fs.wc', { path: args[0] });
|
|
102
|
+
console.log(`${res.lines} ${res.words} ${res.chars} ${humanSize(res.bytes)} ${args[0]}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function touch(ctx, args) {
|
|
106
|
+
if (!args[0]) throw new Error('Usage: touch <path>');
|
|
107
|
+
const res = await ctx.client.request('fs.touch', { path: args[0] });
|
|
108
|
+
console.log(`touched ${res.path}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function mkdir(ctx, args) {
|
|
112
|
+
const parents = args.includes('-p') || args.includes('--parents');
|
|
113
|
+
const path = args.find((a) => !a.startsWith('-'));
|
|
114
|
+
if (!path) throw new Error('Usage: mkdir [-p] <path>');
|
|
115
|
+
const res = await ctx.client.request('fs.mkdir', { path, parents });
|
|
116
|
+
console.log(`created ${res.path}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function rm(ctx, args) {
|
|
120
|
+
const recursive = args.includes('-r') || args.includes('-R') || args.includes('--recursive');
|
|
121
|
+
const path = args.find((a) => !a.startsWith('-'));
|
|
122
|
+
if (!path) throw new Error('Usage: rm [-r] <path>');
|
|
123
|
+
const res = await ctx.client.request('fs.rm', { path, recursive });
|
|
124
|
+
console.log(`removed ${res.path}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function cp(ctx, args) {
|
|
128
|
+
const recursive = args.includes('-r') || args.includes('-R') || args.includes('--recursive');
|
|
129
|
+
const rest = args.filter((a) => !a.startsWith('-'));
|
|
130
|
+
if (rest.length < 2) throw new Error('Usage: cp [-r] <src> <dst>');
|
|
131
|
+
const res = await ctx.client.request('fs.cp', { src: rest[0], dst: rest[1], recursive });
|
|
132
|
+
console.log(`copied ${res.src} -> ${res.dst}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function mv(ctx, args) {
|
|
136
|
+
if (args.length < 2) throw new Error('Usage: mv <src> <dst>');
|
|
137
|
+
const res = await ctx.client.request('fs.mv', { src: args[0], dst: args[1] });
|
|
138
|
+
console.log(`moved ${res.src} -> ${res.dst}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function info(ctx, args) {
|
|
142
|
+
if (!args[0]) throw new Error('Usage: info <path>');
|
|
143
|
+
const res = await ctx.client.request('fs.info', { path: args[0] });
|
|
144
|
+
console.log(formatInfo(res));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function edit(ctx, args) {
|
|
148
|
+
if (!args[0]) throw new Error('Usage: edit <path>');
|
|
149
|
+
return editFile(ctx, args[0]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { pwd, ls, cd, tree, cat, head, tail, wc, touch, mkdir, rm, cp, mv, info, edit };
|