creepercli 1.1.0 → 1.1.1

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 CHANGED
@@ -1,12 +1,12 @@
1
1
  # CreeperCLI
2
2
 
3
- > Remote administration CLI for Paper/Spigot servers and BungeeCord/Waterfall or Velocity proxies.
3
+ > Remote administration CLI for Paper/Spigot servers and BungeeCord/Waterfall or Velocity proxies.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/creepercli.svg)](https://www.npmjs.com/package/creepercli)
6
6
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
7
7
  [![Discord](https://img.shields.io/badge/chat-Discord-5865F2.svg)](https://discord.com/invite/zCkE44hsBR)
8
8
 
9
- `creepercli` is the client half of CreeperCLI. It talks to the CreeperCLI server/proxy plugin over a TCP socket and gives you a remote shell: sandboxed filesystem access, console commands through an allowlist, live logs, verified transfers, and monitoring.
9
+ `creepercli` is the client half of CreeperCLI. It talks to the CreeperCLI server/proxy plugin over a TCP socket and gives you a remote shell: sandboxed filesystem access, console commands through an allowlist, live logs, verified transfers, and monitoring.
10
10
 
11
11
  The plugin is required. Install it on the server, then create a user from the server console:
12
12
 
@@ -70,7 +70,7 @@ One-shot mode works for scripts: `creepercli ls /plugins`, `creepercli exec list
70
70
  - The server-side sandbox blocks `..`, symlink, and absolute-path escapes.
71
71
  - Passwords are bcrypt-hashed (cost 12). Sessions expire after 15 idle minutes and bind to your IP.
72
72
  - Only allowlisted console commands run (`list`, `say *`, `whitelist *`, `restart` by default).
73
- - The plugin binds `127.0.0.1` by default. Connect through an SSH tunnel, VPN, or private network; do not expose the plaintext port directly to the internet.
73
+ - The plugin binds `127.0.0.1` by default. Connect through an SSH tunnel, VPN, or private network; do not expose the plaintext port directly to the internet.
74
74
 
75
75
  The full threat model lives in the docs: [demonz-development.github.io/creepercli](https://demonz-development.github.io/creepercli/).
76
76
 
@@ -82,4 +82,4 @@ The full threat model lives in the docs: [demonz-development.github.io/creepercl
82
82
 
83
83
  ## License
84
84
 
85
- Apache License 2.0. See [LICENSE](LICENSE).
85
+ Apache License 2.0. See [LICENSE](LICENSE).
package/bin/creepercli.js CHANGED
@@ -1,15 +1,14 @@
1
-
2
-
3
-
1
+ #!/usr/bin/env node
2
+
4
3
  'use strict';
5
4
 
6
5
  const { main } = require('../src/index');
7
6
 
8
7
  main(process.argv.slice(2)).then((code) => {
9
8
  if (typeof code === 'number') process.exitCode = code;
10
- }).catch((err) => {
11
- if (!err || !err.creepercliReported) {
12
- console.error(err && err.message ? err.message : String(err));
13
- }
14
- process.exitCode = 1;
15
- });
9
+ }).catch((err) => {
10
+ if (!err || !err.creepercliReported) {
11
+ console.error(err && err.message ? err.message : String(err));
12
+ }
13
+ process.exitCode = 1;
14
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "creepercli",
3
- "version": "1.1.0",
4
- "description": "Remote administration CLI for Minecraft servers and proxies running CreeperCLI",
3
+ "version": "1.1.1",
4
+ "description": "Remote administration CLI for Minecraft servers and proxies running CreeperCLI",
5
5
  "author": "DemonZDevelopment",
6
6
  "license": "Apache-2.0",
7
7
  "type": "commonjs",
package/src/client.js CHANGED
@@ -6,9 +6,9 @@ const net = require('net');
6
6
  const crypto = require('crypto');
7
7
  const { EventEmitter } = require('events');
8
8
  const { StringDecoder } = require('string_decoder');
9
- const { ProtocolError, ERR, PROTOCOL_VERSION } = require('./protocol');
10
-
11
- const MAX_INCOMING_BUFFER_BYTES = 16 * 1024 * 1024;
9
+ const { ProtocolError, ERR, PROTOCOL_VERSION } = require('./protocol');
10
+
11
+ const MAX_INCOMING_BUFFER_BYTES = 16 * 1024 * 1024;
12
12
 
13
13
  class CreeperClient extends EventEmitter {
14
14
  constructor({ host, port, timeoutMs = 300000 }) {
@@ -29,47 +29,47 @@ class CreeperClient extends EventEmitter {
29
29
  socket.setNoDelay(true);
30
30
  socket.on('connect', () => resolve());
31
31
  socket.on('error', (err) => reject(err));
32
- socket.on('close', () => {
33
- if (this.socket === socket) this.socket = null;
34
- this._onClose();
35
- });
32
+ socket.on('close', () => {
33
+ if (this.socket === socket) this.socket = null;
34
+ this._onClose();
35
+ });
36
36
  socket.on('data', (chunk) => this._onData(chunk));
37
37
  });
38
38
  }
39
39
 
40
- _onData(chunk) {
41
- this.buffer += this._decoder.write(chunk);
42
- if (Buffer.byteLength(this.buffer, 'utf8') > MAX_INCOMING_BUFFER_BYTES) {
43
- this._failAll(new ProtocolError(ERR.PAYLOAD_TOO_LARGE, 'Server frame exceeded client buffer limit'));
44
- this.close();
45
- return;
46
- }
40
+ _onData(chunk) {
41
+ this.buffer += this._decoder.write(chunk);
42
+ if (Buffer.byteLength(this.buffer, 'utf8') > MAX_INCOMING_BUFFER_BYTES) {
43
+ this._failAll(new ProtocolError(ERR.PAYLOAD_TOO_LARGE, 'Server frame exceeded client buffer limit'));
44
+ this.close();
45
+ return;
46
+ }
47
47
  let idx;
48
48
  while ((idx = this.buffer.indexOf('\n')) !== -1) {
49
49
  const line = this.buffer.slice(0, idx).trim();
50
50
  this.buffer = this.buffer.slice(idx + 1);
51
51
  if (!line) continue;
52
52
  this._handleLine(line);
53
- }
54
- }
53
+ }
54
+ }
55
55
 
56
56
  _handleLine(line) {
57
57
  let frame;
58
58
  try {
59
59
  frame = JSON.parse(line);
60
- } catch {
61
- return;
62
- }
63
- if (frame.v !== PROTOCOL_VERSION) {
64
- const entry = this.pending.get(frame.id);
65
- if (entry) {
66
- this.pending.delete(frame.id);
67
- clearTimeout(entry.timer);
68
- entry.reject(new ProtocolError(ERR.BAD_REQUEST,
69
- `Unsupported protocol version ${frame.v} (expected ${PROTOCOL_VERSION})`));
70
- }
71
- return;
72
- }
60
+ } catch {
61
+ return;
62
+ }
63
+ if (frame.v !== PROTOCOL_VERSION) {
64
+ const entry = this.pending.get(frame.id);
65
+ if (entry) {
66
+ this.pending.delete(frame.id);
67
+ clearTimeout(entry.timer);
68
+ entry.reject(new ProtocolError(ERR.BAD_REQUEST,
69
+ `Unsupported protocol version ${frame.v} (expected ${PROTOCOL_VERSION})`));
70
+ }
71
+ return;
72
+ }
73
73
  if (frame.type === 'response') {
74
74
  const entry = this.pending.get(frame.id);
75
75
  if (!entry) return;
@@ -92,7 +92,7 @@ class CreeperClient extends EventEmitter {
92
92
  }
93
93
 
94
94
  request(action, params = {}, { timeoutMs } = {}) {
95
- if (!this.socket || this.socket.destroyed === true || this.socket.writable === false) {
95
+ if (!this.socket || this.socket.destroyed === true || this.socket.writable === false) {
96
96
  return Promise.reject(new ProtocolError(ERR.INTERNAL, 'Connection closed'));
97
97
  }
98
98
  const id = crypto.randomUUID();
@@ -102,7 +102,7 @@ class CreeperClient extends EventEmitter {
102
102
  reject(new ProtocolError(ERR.TIMEOUT, `Request timed out (${action})`));
103
103
  }, timeoutMs || this.timeoutMs);
104
104
  this.pending.set(id, { resolve, reject, timer });
105
- const frame = JSON.stringify({ v: PROTOCOL_VERSION, type: 'request', id, action, params });
105
+ const frame = JSON.stringify({ v: PROTOCOL_VERSION, type: 'request', id, action, params });
106
106
  try {
107
107
  this.socket.write(frame + '\n');
108
108
  } catch (err) {
@@ -113,16 +113,16 @@ class CreeperClient extends EventEmitter {
113
113
  });
114
114
  }
115
115
 
116
- _onClose() {
117
- this._failAll(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
118
- }
119
-
120
- _failAll(error) {
121
- for (const { reject, timer } of this.pending.values()) {
122
- clearTimeout(timer);
123
- reject(error);
124
- }
125
- this.pending.clear();
116
+ _onClose() {
117
+ this._failAll(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
118
+ }
119
+
120
+ _failAll(error) {
121
+ for (const { reject, timer } of this.pending.values()) {
122
+ clearTimeout(timer);
123
+ reject(error);
124
+ }
125
+ this.pending.clear();
126
126
  }
127
127
 
128
128
  close() {
@@ -54,21 +54,21 @@ function find(name) {
54
54
  return COMMANDS.find((c) => c.name === name);
55
55
  }
56
56
 
57
- function splitArgs(input) {
57
+ function splitArgs(input) {
58
58
  const args = [];
59
59
  let cur = '';
60
60
  let quote = null;
61
61
  let escaped = false;
62
- for (let i = 0; i < input.length; i++) {
63
- const ch = input[i];
62
+ for (let i = 0; i < input.length; i++) {
63
+ const ch = input[i];
64
64
  if (escaped) {
65
65
  cur += ch;
66
66
  escaped = false;
67
- } else if (ch === '\\') {
68
- const next = input[i + 1];
69
- const escapable = next != null && (next === '\\' || next === '"' || next === "'" || /\s/.test(next));
70
- if (escapable) escaped = true;
71
- else cur += ch;
67
+ } else if (ch === '\\') {
68
+ const next = input[i + 1];
69
+ const escapable = next != null && (next === '\\' || next === '"' || next === "'" || /\s/.test(next));
70
+ if (escapable) escaped = true;
71
+ else cur += ch;
72
72
  } else if (quote) {
73
73
  if (ch === quote) quote = null;
74
74
  else cur += ch;
@@ -29,18 +29,18 @@ async function log(ctx, args) {
29
29
  const res = await ctx.client.request('monitor.log.start', { grep });
30
30
  console.log(`Streaming console log (Ctrl+C to stop)${res.grep ? ' filtered by "' + res.grep + '"' : ''}`);
31
31
  if (res.buffer && res.buffer.length) console.log(res.buffer.join('\n'));
32
- let stop = null;
33
- let stopped = false;
32
+ let stop = null;
33
+ let stopped = false;
34
34
  try {
35
35
  await new Promise((resolve) => {
36
36
  const onLine = (event, data) => {
37
37
  if (event === EVENT_LOG_LINE) console.log(data.message);
38
38
  };
39
- ctx.client.on('event', onLine);
40
- stop = () => {
41
- if (stopped) return;
42
- stopped = true;
43
- ctx.client.removeListener('event', onLine);
39
+ ctx.client.on('event', onLine);
40
+ stop = () => {
41
+ if (stopped) return;
42
+ stopped = true;
43
+ ctx.client.removeListener('event', onLine);
44
44
  ctx.client.request('monitor.log.stop').catch(() => {});
45
45
  resolve();
46
46
  };
package/src/index.js CHANGED
@@ -9,17 +9,17 @@ const { startRepl } = require('./repl');
9
9
  const { runCommand } = require('./commands');
10
10
  const { doLogin } = require('./commands/auth');
11
11
  const { friendly } = require('./protocol');
12
- const { checkUpdate } = require('./update');
13
- const pkg = require('../package.json');
12
+ const { checkUpdate } = require('./update');
13
+ const pkg = require('../package.json');
14
14
 
15
- async function main(argv) {
16
- const flags = parseFlags(argv);
17
- if (flags.help) {
18
- printUsage();
19
- return 0;
20
- }
21
- checkUpdate();
22
- const cfg = applyFlags(loadConfig(), flags);
15
+ async function main(argv) {
16
+ const flags = parseFlags(argv);
17
+ if (flags.help) {
18
+ printUsage();
19
+ return 0;
20
+ }
21
+ checkUpdate();
22
+ const cfg = applyFlags(loadConfig(), flags);
23
23
  const args = flags.args;
24
24
  const cmd = args[0];
25
25
 
@@ -33,10 +33,10 @@ async function main(argv) {
33
33
  startRepl(ctx);
34
34
  return 0;
35
35
  }
36
- } catch (err) {
37
- console.error(`\nError: ${friendly(err)}\n`);
38
- client.close();
39
- return 1;
36
+ } catch (err) {
37
+ console.error(`\nError: ${friendly(err)}\n`);
38
+ client.close();
39
+ return 1;
40
40
  } finally {
41
41
  if (!ctx.exit && !process.stdout.isTTY) {
42
42
  client.close();
@@ -66,10 +66,10 @@ async function main(argv) {
66
66
  if (creds) {
67
67
  ctx.username = creds.username;
68
68
  try {
69
- const resume = await client.request('auth.resume', { token: creds.token }, { timeoutMs: 15000 });
70
- ctx.username = resume.username;
71
- ctx.cwd = resume.cwd || '/';
72
- session.save({ ...creds, expiresAt: Date.now() + (resume.expiresInSeconds || 0) * 1000 });
69
+ const resume = await client.request('auth.resume', { token: creds.token }, { timeoutMs: 15000 });
70
+ ctx.username = resume.username;
71
+ ctx.cwd = resume.cwd || '/';
72
+ session.save({ ...creds, expiresAt: Date.now() + (resume.expiresInSeconds || 0) * 1000 });
73
73
  } catch (err) {
74
74
  if (err.code === 'E_SESSION_EXPIRED' || err.code === 'E_UNAUTHORIZED') {
75
75
  session.clear();
@@ -134,7 +134,7 @@ async function connect(cfg) {
134
134
  console.error(`CREEPER CLI SETUP & TROUBLESHOOTING GUIDE`);
135
135
  console.error(`--------------------------------------------------------------------------------\n`);
136
136
  console.error(`1. INSTALL THE PLUGIN ON YOUR MINECRAFT SERVER:`);
137
- console.error(` * Place CreeperCLI-${pkg.version}.jar inside your server's 'plugins/' folder.`);
137
+ console.error(` * Place CreeperCLI-${pkg.version}.jar inside your server's 'plugins/' folder.`);
138
138
  console.error(` * Start or restart your server (Paper / Spigot 1.21+).\n`);
139
139
  console.error(`2. CREATE YOUR ADMIN USER IN SERVER CONSOLE:`);
140
140
  console.error(` * Open your server console (or run in-game as OP):`);
@@ -147,13 +147,13 @@ async function connect(cfg) {
147
147
  console.error(` ssh -L ${cfg.port}:127.0.0.1:${cfg.port} user@<your-server-ip>\n`);
148
148
  console.error(`--------------------------------------------------------------------------------`);
149
149
  console.error(`Documentation: https://github.com/DemonZ-Development/creepercli\n`);
150
- err.creepercliReported = true;
151
- throw err;
152
- }
153
- }
150
+ err.creepercliReported = true;
151
+ throw err;
152
+ }
153
+ }
154
154
 
155
155
  function printUsage() {
156
- console.log(`CreeperCLI v${pkg.version} — remote Minecraft administration
156
+ console.log(`CreeperCLI v${pkg.version} — remote Minecraft administration
157
157
 
158
158
  Usage:
159
159
  creepercli login Authenticate with the server
package/src/repl.js CHANGED
@@ -10,9 +10,9 @@ function prompt(ctx) {
10
10
  return `${ctx.username || '?'}@${ctx.cfg.host}:${ctx.cwd || '/'}> `;
11
11
  }
12
12
 
13
- function startRepl(ctx, io = {}) {
14
- const input = io.input || process.stdin;
15
- const output = io.output || process.stdout;
13
+ function startRepl(ctx, io = {}) {
14
+ const input = io.input || process.stdin;
15
+ const output = io.output || process.stdout;
16
16
  const completions = COMMANDS.map((c) => c.name);
17
17
 
18
18
  function completer(line) {
@@ -22,9 +22,9 @@ function startRepl(ctx, io = {}) {
22
22
  }
23
23
 
24
24
  const rl = readline.createInterface({
25
- input,
26
- output,
27
- terminal: Boolean(output.isTTY),
25
+ input,
26
+ output,
27
+ terminal: Boolean(output.isTTY),
28
28
  historySize: 1000,
29
29
  completer,
30
30
  });
@@ -47,11 +47,11 @@ function startRepl(ctx, io = {}) {
47
47
  running = true;
48
48
  while (queue.length) {
49
49
  const line = queue.shift();
50
- await runCommand(ctx, line);
51
- if (ctx.exit) {
52
- queue.length = 0;
53
- rl.close();
54
- return;
50
+ await runCommand(ctx, line);
51
+ if (ctx.exit) {
52
+ queue.length = 0;
53
+ rl.close();
54
+ return;
55
55
  }
56
56
  rl.setPrompt(prompt(ctx));
57
57
  rl.prompt();
@@ -83,12 +83,12 @@ function startRepl(ctx, io = {}) {
83
83
  });
84
84
  });
85
85
 
86
- rl.on('close', async () => {
87
- await ctx.client.flush();
88
- ctx.client.close();
89
- });
90
-
91
- return rl;
92
- }
86
+ rl.on('close', async () => {
87
+ await ctx.client.flush();
88
+ ctx.client.close();
89
+ });
90
+
91
+ return rl;
92
+ }
93
93
 
94
94
  module.exports = { startRepl, prompt };
package/src/session.js CHANGED
@@ -24,7 +24,7 @@ function load(host, port) {
24
24
  const raw = fs.readFileSync(CREDS_FILE, 'utf8');
25
25
  const creds = JSON.parse(raw);
26
26
  if (creds.host !== host || creds.port !== port) return null;
27
- return creds;
27
+ return creds;
28
28
  } catch {
29
29
  return null;
30
30
  }
package/src/transfer.js CHANGED
@@ -4,28 +4,28 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const crypto = require('crypto');
8
- const { confirm } = require('./prompts');
9
-
10
- const MAX_TRANSFER_CHUNK_BYTES = 1024 * 1024;
11
-
12
- function validateTransferStart(start, expectFileMetadata = false) {
13
- if (!start || typeof start.transferId !== 'string' || !start.transferId) {
14
- throw new Error('Server returned an invalid transfer id');
15
- }
16
- if (!Number.isInteger(start.chunkSize) || start.chunkSize < 1024
17
- || start.chunkSize > MAX_TRANSFER_CHUNK_BYTES) {
18
- throw new Error('Server returned an invalid transfer chunk size');
19
- }
20
- if (expectFileMetadata) {
21
- if (!Number.isSafeInteger(start.size) || start.size < 0) {
22
- throw new Error('Server returned an invalid file size');
23
- }
24
- if (typeof start.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(start.sha256)) {
25
- throw new Error('Server returned an invalid file checksum');
26
- }
27
- }
28
- }
7
+ const crypto = require('crypto');
8
+ const { confirm } = require('./prompts');
9
+
10
+ const MAX_TRANSFER_CHUNK_BYTES = 1024 * 1024;
11
+
12
+ function validateTransferStart(start, expectFileMetadata = false) {
13
+ if (!start || typeof start.transferId !== 'string' || !start.transferId) {
14
+ throw new Error('Server returned an invalid transfer id');
15
+ }
16
+ if (!Number.isInteger(start.chunkSize) || start.chunkSize < 1024
17
+ || start.chunkSize > MAX_TRANSFER_CHUNK_BYTES) {
18
+ throw new Error('Server returned an invalid transfer chunk size');
19
+ }
20
+ if (expectFileMetadata) {
21
+ if (!Number.isSafeInteger(start.size) || start.size < 0) {
22
+ throw new Error('Server returned an invalid file size');
23
+ }
24
+ if (typeof start.sha256 !== 'string' || !/^[0-9a-f]{64}$/i.test(start.sha256)) {
25
+ throw new Error('Server returned an invalid file checksum');
26
+ }
27
+ }
28
+ }
29
29
 
30
30
  function sha256File(file) {
31
31
  return new Promise((resolve, reject) => {
@@ -49,7 +49,7 @@ function clearProgress() {
49
49
  process.stdout.write('\r'.padEnd(70) + '\r');
50
50
  }
51
51
 
52
- async function cpush(ctx, args) {
52
+ async function cpush(ctx, args) {
53
53
  const rest = args.filter((a) => !a.startsWith('--'));
54
54
  let [local, remote] = rest;
55
55
  if (!local || !remote) throw new Error('Usage: cpush <local-file> <remote-path>');
@@ -57,20 +57,20 @@ async function cpush(ctx, args) {
57
57
  const stat = fs.statSync(local);
58
58
  if (!stat.isFile()) throw new Error('Local path must be a file');
59
59
  const sha = await sha256File(local);
60
- const start = await ctx.client.request('xfer.push.start', {
60
+ const start = await ctx.client.request('xfer.push.start', {
61
61
  path: remote,
62
62
  size: stat.size,
63
63
  sha256: sha,
64
- force: args.includes('--force'),
65
- }, { timeoutMs: 60000 });
66
- const { transferId, chunkSize } = start;
67
- let fd = null;
68
- let transferred = 0;
69
- let index = 0;
70
- try {
71
- validateTransferStart(start);
72
- fd = fs.openSync(local, 'r');
73
- const buf = Buffer.alloc(chunkSize);
64
+ force: args.includes('--force'),
65
+ }, { timeoutMs: 60000 });
66
+ const { transferId, chunkSize } = start;
67
+ let fd = null;
68
+ let transferred = 0;
69
+ let index = 0;
70
+ try {
71
+ validateTransferStart(start);
72
+ fd = fs.openSync(local, 'r');
73
+ const buf = Buffer.alloc(chunkSize);
74
74
  while (true) {
75
75
  const n = fs.readSync(fd, buf, 0, chunkSize, transferred);
76
76
  if (n === 0) break;
@@ -81,99 +81,99 @@ async function cpush(ctx, args) {
81
81
  }, { timeoutMs: 120000 });
82
82
  transferred += n;
83
83
  index++;
84
- showProgress(transferred, stat.size);
85
- }
86
- const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
87
- if (!finish || typeof finish.sha256 !== 'string' || finish.sha256.toLowerCase() !== sha) {
88
- throw new Error('Server reported an unexpected upload checksum');
89
- }
90
- clearProgress();
91
- console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
92
- } catch (err) {
93
- await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
94
- clearProgress();
95
- throw err;
96
- } finally {
97
- if (fd !== null) fs.closeSync(fd);
98
- }
99
- }
84
+ showProgress(transferred, stat.size);
85
+ }
86
+ const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
87
+ if (!finish || typeof finish.sha256 !== 'string' || finish.sha256.toLowerCase() !== sha) {
88
+ throw new Error('Server reported an unexpected upload checksum');
89
+ }
90
+ clearProgress();
91
+ console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
92
+ } catch (err) {
93
+ await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
94
+ clearProgress();
95
+ throw err;
96
+ } finally {
97
+ if (fd !== null) fs.closeSync(fd);
98
+ }
99
+ }
100
100
 
101
101
  async function cpull(ctx, args) {
102
102
  let [remote, local] = args.filter((a) => !a.startsWith('--'));
103
103
  if (!remote || !local) throw new Error('Usage: cpull <remote-path> <local-file>');
104
- remote = remote.replace(/\\/g, '/');
105
- const start = await ctx.client.request('xfer.pull.start', { path: remote });
106
- const { transferId, size, sha256: expectedSha, chunkSize } = start;
107
- const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
108
- const hash = crypto.createHash('sha256');
109
- let fd = null;
110
- let transferred = 0;
111
- let index = 0;
112
- try {
113
- validateTransferStart(start, true);
114
- const parent = path.dirname(path.resolve(local));
115
- fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
116
- fd = fs.openSync(part, 'w');
117
- while (true) {
104
+ remote = remote.replace(/\\/g, '/');
105
+ const start = await ctx.client.request('xfer.pull.start', { path: remote });
106
+ const { transferId, size, sha256: expectedSha, chunkSize } = start;
107
+ const part = `${local}.creepercli-part-${process.pid}-${Date.now()}`;
108
+ const hash = crypto.createHash('sha256');
109
+ let fd = null;
110
+ let transferred = 0;
111
+ let index = 0;
112
+ try {
113
+ validateTransferStart(start, true);
114
+ const parent = path.dirname(path.resolve(local));
115
+ fs.mkdirSync(parent, { recursive: true, mode: 0o700 });
116
+ fd = fs.openSync(part, 'w');
117
+ while (true) {
118
118
  const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
119
- const data = Buffer.from(res.data || '', 'base64');
120
- if (data.length === 0) break;
121
- if (data.length > chunkSize || transferred + data.length > size) {
122
- throw new Error('Server sent an invalid transfer chunk');
123
- }
119
+ const data = Buffer.from(res.data || '', 'base64');
120
+ if (data.length === 0) break;
121
+ if (data.length > chunkSize || transferred + data.length > size) {
122
+ throw new Error('Server sent an invalid transfer chunk');
123
+ }
124
124
  fs.writeSync(fd, data, 0, data.length);
125
125
  hash.update(data);
126
126
  transferred += data.length;
127
- index++;
128
- showProgress(transferred, size);
129
- }
130
- fs.closeSync(fd);
131
- fd = null;
132
- if (transferred !== size) {
133
- throw new Error(`Transfer ended early: expected ${size} bytes, received ${transferred}`);
134
- }
135
- const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
136
- clearProgress();
137
- if (!actual || typeof actual.sha256 !== 'string'
138
- || actual.sha256.toLowerCase() !== expectedSha.toLowerCase()) {
139
- throw new Error('Checksum mismatch: transfer aborted');
140
- }
141
- replaceFile(part, local);
142
- console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
143
- } catch (err) {
144
- if (fd !== null) {
145
- fs.closeSync(fd);
146
- fd = null;
147
- }
148
- await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
149
- try { fs.unlinkSync(part); } catch {}
150
- clearProgress();
151
- throw err;
152
- } finally {
153
- if (fd !== null) fs.closeSync(fd);
154
- }
155
- }
127
+ index++;
128
+ showProgress(transferred, size);
129
+ }
130
+ fs.closeSync(fd);
131
+ fd = null;
132
+ if (transferred !== size) {
133
+ throw new Error(`Transfer ended early: expected ${size} bytes, received ${transferred}`);
134
+ }
135
+ const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
136
+ clearProgress();
137
+ if (!actual || typeof actual.sha256 !== 'string'
138
+ || actual.sha256.toLowerCase() !== expectedSha.toLowerCase()) {
139
+ throw new Error('Checksum mismatch: transfer aborted');
140
+ }
141
+ replaceFile(part, local);
142
+ console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
143
+ } catch (err) {
144
+ if (fd !== null) {
145
+ fs.closeSync(fd);
146
+ fd = null;
147
+ }
148
+ await ctx.client.request('xfer.abort', { transferId }).catch(() => {});
149
+ try { fs.unlinkSync(part); } catch {}
150
+ clearProgress();
151
+ throw err;
152
+ } finally {
153
+ if (fd !== null) fs.closeSync(fd);
154
+ }
155
+ }
156
156
 
157
157
  async function csync(ctx, args) {
158
158
  let [remoteDir, localDir] = args.filter((a) => a !== '-y' && !a.startsWith('--'));
159
159
  if (!remoteDir || !localDir) throw new Error('Usage: csync <remote-dir> <local-dir> [--yes]');
160
160
  remoteDir = remoteDir.replace(/\\/g, '/');
161
- const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
162
- await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
163
-
164
- const remoteInfo = await ctx.client.request('fs.info', { path: remoteDir });
165
- if (!remoteInfo.isDir || typeof remoteInfo.path !== 'string' || !remoteInfo.path.startsWith('/')) {
166
- throw new Error('Server returned an invalid sync directory');
167
- }
168
- remoteDir = remoteInfo.path;
169
-
170
- const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
171
- const remoteMap = new Map();
172
- for (const e of remote.entries) {
173
- if (!e.isDir) {
174
- const rel = relativeRemotePath(remoteDir, e.path);
175
- if (rel) remoteMap.set(rel, e);
176
- }
161
+ const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
162
+ await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
163
+
164
+ const remoteInfo = await ctx.client.request('fs.info', { path: remoteDir });
165
+ if (!remoteInfo.isDir || typeof remoteInfo.path !== 'string' || !remoteInfo.path.startsWith('/')) {
166
+ throw new Error('Server returned an invalid sync directory');
167
+ }
168
+ remoteDir = remoteInfo.path;
169
+
170
+ const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
171
+ const remoteMap = new Map();
172
+ for (const e of remote.entries) {
173
+ if (!e.isDir) {
174
+ const rel = relativeRemotePath(remoteDir, e.path);
175
+ if (rel) remoteMap.set(rel, e);
176
+ }
177
177
  }
178
178
  if (remote.truncated) console.error('(remote listing truncated)');
179
179
 
@@ -227,38 +227,38 @@ async function csync(ctx, args) {
227
227
  console.log('Sync complete.');
228
228
  }
229
229
 
230
- function joinRemote(base, rel) {
230
+ function joinRemote(base, rel) {
231
231
  const cleanBase = base.replace(/\\/g, '/');
232
232
  const cleanRel = rel.replace(/\\/g, '/');
233
233
  return (cleanBase.endsWith('/') ? cleanBase : cleanBase + '/') + cleanRel;
234
- }
235
-
236
- function relativeRemotePath(base, absolutePath) {
237
- const cleanBase = ('/' + base.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).replace(/^\/$/, '');
238
- const cleanPath = '/' + absolutePath.replace(/\\/g, '/').replace(/^\/+/, '');
239
- if (!cleanBase) return cleanPath.replace(/^\//, '');
240
- if (cleanPath === cleanBase) return '';
241
- if (!cleanPath.startsWith(cleanBase + '/')) {
242
- throw new Error(`Server returned path outside sync root: ${absolutePath}`);
243
- }
244
- return cleanPath.slice(cleanBase.length + 1);
245
- }
246
-
247
- function replaceFile(part, destination) {
248
- if (!fs.existsSync(destination)) {
249
- fs.renameSync(part, destination);
250
- return;
251
- }
252
- const backup = `${destination}.creepercli-backup-${process.pid}-${Date.now()}`;
253
- fs.renameSync(destination, backup);
254
- try {
255
- fs.renameSync(part, destination);
256
- } catch (err) {
257
- try { fs.renameSync(backup, destination); } catch {}
258
- throw err;
259
- }
260
- try { fs.rmSync(backup, { force: true }); } catch {}
261
- }
234
+ }
235
+
236
+ function relativeRemotePath(base, absolutePath) {
237
+ const cleanBase = ('/' + base.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '')).replace(/^\/$/, '');
238
+ const cleanPath = '/' + absolutePath.replace(/\\/g, '/').replace(/^\/+/, '');
239
+ if (!cleanBase) return cleanPath.replace(/^\//, '');
240
+ if (cleanPath === cleanBase) return '';
241
+ if (!cleanPath.startsWith(cleanBase + '/')) {
242
+ throw new Error(`Server returned path outside sync root: ${absolutePath}`);
243
+ }
244
+ return cleanPath.slice(cleanBase.length + 1);
245
+ }
246
+
247
+ function replaceFile(part, destination) {
248
+ if (!fs.existsSync(destination)) {
249
+ fs.renameSync(part, destination);
250
+ return;
251
+ }
252
+ const backup = `${destination}.creepercli-backup-${process.pid}-${Date.now()}`;
253
+ fs.renameSync(destination, backup);
254
+ try {
255
+ fs.renameSync(part, destination);
256
+ } catch (err) {
257
+ try { fs.renameSync(backup, destination); } catch {}
258
+ throw err;
259
+ }
260
+ try { fs.rmSync(backup, { force: true }); } catch {}
261
+ }
262
262
 
263
263
  function walkLocal(dir) {
264
264
  const map = new Map();
@@ -273,13 +273,13 @@ function walkLocal(dir) {
273
273
  const p = path.join(d, name);
274
274
  let st;
275
275
  try {
276
- st = fs.lstatSync(p);
276
+ st = fs.lstatSync(p);
277
277
  } catch {
278
278
  continue;
279
279
  }
280
280
  const rel = ((prefix ? prefix + '/' : '') + name).replace(/\\/g, '/');
281
- if (st.isSymbolicLink()) continue;
282
- if (st.isDirectory()) walk(p, rel);
281
+ if (st.isSymbolicLink()) continue;
282
+ if (st.isDirectory()) walk(p, rel);
283
283
  else if (st.isFile()) map.set(rel, { size: st.size, mtime: Math.floor(st.mtimeMs) });
284
284
  }
285
285
  }
@@ -287,6 +287,6 @@ function walkLocal(dir) {
287
287
  return map;
288
288
  }
289
289
 
290
- module.exports = {
291
- cpush, cpull, csync, joinRemote, relativeRemotePath, replaceFile, walkLocal, validateTransferStart,
292
- };
290
+ module.exports = {
291
+ cpush, cpull, csync, joinRemote, relativeRemotePath, replaceFile, walkLocal, validateTransferStart,
292
+ };
package/src/update.js CHANGED
@@ -22,14 +22,14 @@ function checkUpdate() {
22
22
 
23
23
  if (cache.lastCheck && now - cache.lastCheck < CHECK_INTERVAL_MS) {
24
24
  if (cache.latest && isNewerVersion(pkg.version, cache.latest)) {
25
- console.log(`[Notice] A new version of creepercli (v${cache.latest}) is available! Run "npm install -g creepercli" to update.\n`);
25
+ console.log(`[Notice] A new version of creepercli (v${cache.latest}) is available! Run "npm install -g creepercli" to update.\n`);
26
26
  }
27
27
  return;
28
28
  }
29
29
 
30
- const req = https.get('https://registry.npmjs.org/creepercli/latest', {
30
+ const req = https.get('https://registry.npmjs.org/creepercli/latest', {
31
31
  timeout: 2500,
32
- headers: { 'User-Agent': `creepercli/${pkg.version}` }
32
+ headers: { 'User-Agent': `creepercli/${pkg.version}` }
33
33
  }, (res) => {
34
34
  if (res.statusCode !== 200) return;
35
35
  let body = '';
@@ -42,7 +42,7 @@ function checkUpdate() {
42
42
  fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
43
43
  fs.writeFileSync(UPDATE_FILE, JSON.stringify({ lastCheck: now, latest }), { mode: 0o600 });
44
44
  if (isNewerVersion(pkg.version, latest)) {
45
- console.log(`[Notice] A new version of creepercli (v${latest}) is available! Run "npm install -g creepercli" to update.\n`);
45
+ console.log(`[Notice] A new version of creepercli (v${latest}) is available! Run "npm install -g creepercli" to update.\n`);
46
46
  }
47
47
  }
48
48
  } catch {
@@ -50,9 +50,9 @@ function checkUpdate() {
50
50
  });
51
51
  });
52
52
 
53
- req.on('error', () => {});
54
- req.on('socket', (socket) => socket.unref());
55
- } catch {
53
+ req.on('error', () => {});
54
+ req.on('socket', (socket) => socket.unref());
55
+ } catch {
56
56
  }
57
57
  }
58
58