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 ADDED
@@ -0,0 +1,117 @@
1
+ # CreeperCLI (`creepercli`)
2
+
3
+ > Remote Administration CLI for Minecraft (Paper / Spigot) Servers
4
+
5
+ [![npm version](https://img.shields.io/npm/v/creepercli.svg)](https://www.npmjs.com/package/creepercli)
6
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
7
+ [![Discord](https://img.shields.io/badge/chat-Discord-5865F2.svg)](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 };