creepercli 1.0.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/src/prompts.js CHANGED
@@ -1,134 +1,120 @@
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 };
1
+
2
+
3
+ 'use strict';
4
+
5
+ const readline = require('readline');
6
+
7
+ let pipeBuffer = '';
8
+ let pipeWaiters = [];
9
+ let pipeStarted = false;
10
+ let pipeEnded = false;
11
+
12
+ function startPipeReader() {
13
+ pipeStarted = true;
14
+ process.stdin.on('data', (chunk) => {
15
+ pipeBuffer += chunk.toString();
16
+ drainPipe();
17
+ });
18
+ process.stdin.on('end', () => {
19
+ pipeEnded = true;
20
+ drainPipe();
21
+ });
22
+ }
23
+
24
+ function drainPipe() {
25
+ while (pipeWaiters.length > 0) {
26
+ const idx = pipeBuffer.indexOf('\n');
27
+ if (idx === -1) {
28
+ if (pipeEnded && pipeBuffer.length > 0) {
29
+ const cb = pipeWaiters.shift();
30
+ const last = pipeBuffer;
31
+ pipeBuffer = '';
32
+ cb(last.trim());
33
+ continue;
34
+ }
35
+ break;
36
+ }
37
+ const line = pipeBuffer.slice(0, idx);
38
+ pipeBuffer = pipeBuffer.slice(idx + 1);
39
+ const cb = pipeWaiters.shift();
40
+ cb(line.replace(/\r$/, '').trim());
41
+ }
42
+ if (pipeEnded && pipeWaiters.length > 0 && pipeBuffer === '') {
43
+ const cb = pipeWaiters.shift();
44
+ cb('');
45
+ }
46
+ }
47
+
48
+ function pipeQuestion(prompt) {
49
+ if (!pipeStarted) startPipeReader();
50
+ process.stdout.write(prompt);
51
+ return new Promise((resolve) => {
52
+ pipeWaiters.push(resolve);
53
+ drainPipe();
54
+ });
55
+ }
56
+
57
+ function question(prompt) {
58
+ if (!process.stdin.isTTY) {
59
+ return pipeQuestion(prompt);
60
+ }
61
+ return new Promise((resolve) => {
62
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
63
+ rl.question(prompt, (answer) => {
64
+ rl.close();
65
+ resolve(answer.trim());
66
+ });
67
+ });
68
+ }
69
+
70
+ function readSecret(prompt) {
71
+ if (!process.stdin.isTTY) {
72
+ return pipeQuestion(prompt);
73
+ }
74
+ return new Promise((resolve, reject) => {
75
+ const input = process.stdin;
76
+ const output = process.stdout;
77
+ input.setRawMode(true);
78
+ input.resume();
79
+ output.write(prompt);
80
+ let value = '';
81
+ const onData = (chunk) => {
82
+ for (const ch of chunk.toString()) {
83
+ if (ch === '\u0003') {
84
+ cleanup();
85
+ reject(new Error('Aborted'));
86
+ return;
87
+ }
88
+ if (ch === '\r' || ch === '\n') {
89
+ output.write('\n');
90
+ cleanup();
91
+ resolve(value);
92
+ return;
93
+ }
94
+ if (ch === '\u007f' || ch === '\b') {
95
+ if (value.length > 0) {
96
+ value = value.slice(0, -1);
97
+ output.write('\b \b');
98
+ }
99
+ } else {
100
+ value += ch;
101
+ output.write('*');
102
+ }
103
+ }
104
+ };
105
+ const cleanup = () => {
106
+ input.removeListener('data', onData);
107
+ input.setRawMode(false);
108
+ input.pause();
109
+ };
110
+ input.on('data', onData);
111
+ });
112
+ }
113
+
114
+ async function confirm(text, defYes = false) {
115
+ const answer = await question(text + (defYes ? ' [Y/n] ' : ' [y/N] '));
116
+ if (!answer) return defYes;
117
+ return /^y(es)?$/i.test(answer.trim());
118
+ }
119
+
120
+ module.exports = { question, readSecret, confirm };
package/src/protocol.js CHANGED
@@ -1,82 +1,68 @@
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 };
1
+
2
+
3
+ 'use strict';
4
+
5
+ const ERR = {
6
+ UNAUTHORIZED: 'E_UNAUTHORIZED',
7
+ AUTH_FAILED: 'E_AUTH_FAILED',
8
+ TOTP_REQUIRED: 'E_TOTP_REQUIRED',
9
+ BANNED: 'E_BANNED',
10
+ RATE_LIMITED: 'E_RATE_LIMITED',
11
+ SESSION_EXPIRED: 'E_SESSION_EXPIRED',
12
+ INVALID_PARAMS: 'E_INVALID_PARAMS',
13
+ BAD_REQUEST: 'E_BAD_REQUEST',
14
+ PATH_ESCAPE: 'E_PATH_ESCAPE',
15
+ NOT_FOUND: 'E_NOT_FOUND',
16
+ IS_DIRECTORY: 'E_IS_DIRECTORY',
17
+ NOT_DIRECTORY: 'E_NOT_DIRECTORY',
18
+ ALREADY_EXISTS: 'E_ALREADY_EXISTS',
19
+ FORBIDDEN: 'E_FORBIDDEN',
20
+ IO: 'E_IO',
21
+ LOCKED: 'E_LOCKED',
22
+ ALLOWLIST_DENIED: 'E_ALLOWLIST_DENIED',
23
+ TIMEOUT: 'E_TIMEOUT',
24
+ PAYLOAD_TOO_LARGE: 'E_PAYLOAD_TOO_LARGE',
25
+ CHECKSUM_MISMATCH: 'E_CHECKSUM_MISMATCH',
26
+ NO_TRANSFER: 'E_NO_TRANSFER',
27
+ INTERNAL: 'E_INTERNAL',
28
+ SERVER_FULL: 'E_SERVER_FULL',
29
+ };
30
+
31
+ const PROTOCOL_VERSION = 1;
32
+
33
+ const EVENT_LOG_LINE = 'log.line';
34
+
35
+ class ProtocolError extends Error {
36
+ constructor(code, message) {
37
+ super(message || code);
38
+ this.code = code;
39
+ }
40
+ }
41
+
42
+ function friendly(err) {
43
+ if (err instanceof ProtocolError) {
44
+ const hints = {
45
+ [ERR.UNAUTHORIZED]: 'run "creepercli login" first',
46
+ [ERR.AUTH_FAILED]: 'check your username/password or add user in server console: /creepercli user add <user> <pass>',
47
+ [ERR.TOTP_REQUIRED]: 'provide your 6-digit TOTP code',
48
+ [ERR.BANNED]: 'too many failures, this IP is temporarily banned by Fail2Ban',
49
+ [ERR.RATE_LIMITED]: 'too many requests in a short window, please slow down',
50
+ [ERR.SESSION_EXPIRED]: 'session expired, run "creepercli login"',
51
+ [ERR.PATH_ESCAPE]: 'path blocked by server PathSanitizer sandbox',
52
+ [ERR.ALLOWLIST_DENIED]: 'command restricted by server allowlist config',
53
+ [ERR.LOCKED]: 'file currently locked by another active session',
54
+ [ERR.CHECKSUM_MISMATCH]: 'data integrity check failed, partial file cleaned up',
55
+ [ERR.SERVER_FULL]: 'server connection limit reached',
56
+ [ERR.TIMEOUT]: 'request timed out, check server load',
57
+ [ERR.NOT_FOUND]: 'file or directory not found on remote server',
58
+ [ERR.IS_DIRECTORY]: 'target path is a directory, not a file',
59
+ [ERR.NOT_DIRECTORY]: 'target path is a file, not a directory',
60
+ [ERR.FORBIDDEN]: 'permission denied by server policy',
61
+ };
62
+ const hint = hints[err.code];
63
+ return `${err.message}${hint ? ` [${hint}]` : ''} (${err.code})`;
64
+ }
65
+ return err && err.message ? err.message : String(err);
66
+ }
67
+
68
+ module.exports = { ERR, PROTOCOL_VERSION, EVENT_LOG_LINE, ProtocolError, friendly };
package/src/repl.js CHANGED
@@ -1,104 +1,94 @@
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 };
1
+
2
+
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const readline = require('readline');
7
+ const { COMMANDS, runCommand } = require('./commands');
8
+
9
+ function prompt(ctx) {
10
+ return `${ctx.username || '?'}@${ctx.cfg.host}:${ctx.cwd || '/'}> `;
11
+ }
12
+
13
+ function startRepl(ctx, io = {}) {
14
+ const input = io.input || process.stdin;
15
+ const output = io.output || process.stdout;
16
+ const completions = COMMANDS.map((c) => c.name);
17
+
18
+ function completer(line) {
19
+ const trimmed = line.trimStart();
20
+ const hits = completions.filter((c) => c.startsWith(trimmed));
21
+ return [hits.length ? hits : completions, trimmed];
22
+ }
23
+
24
+ const rl = readline.createInterface({
25
+ input,
26
+ output,
27
+ terminal: Boolean(output.isTTY),
28
+ historySize: 1000,
29
+ completer,
30
+ });
31
+ try {
32
+ const history = fs.readFileSync(ctx.cfg.historyFile, 'utf8').split('\n').filter(Boolean).reverse();
33
+ rl.history = history;
34
+ } catch {
35
+ }
36
+ rl.on('history', (history) => {
37
+ try {
38
+ fs.writeFileSync(ctx.cfg.historyFile, history.filter(Boolean).join('\n') + '\n', { mode: 0o600 });
39
+ } catch {
40
+ }
41
+ });
42
+
43
+ const queue = [];
44
+ let running = false;
45
+ async function pump() {
46
+ if (running) return;
47
+ running = true;
48
+ while (queue.length) {
49
+ const line = queue.shift();
50
+ await runCommand(ctx, line);
51
+ if (ctx.exit) {
52
+ queue.length = 0;
53
+ rl.close();
54
+ return;
55
+ }
56
+ rl.setPrompt(prompt(ctx));
57
+ rl.prompt();
58
+ }
59
+ running = false;
60
+ }
61
+
62
+ rl.setPrompt(prompt(ctx));
63
+ rl.prompt();
64
+
65
+ rl.on('line', (line) => {
66
+ queue.push(line);
67
+ pump();
68
+ });
69
+
70
+ rl.on('SIGINT', () => {
71
+ if (running) {
72
+ if (ctx.interruptHandler) {
73
+ console.log('\n(interrupted)');
74
+ ctx.interruptHandler();
75
+ } else {
76
+ console.log('\n(interrupt)');
77
+ }
78
+ return;
79
+ }
80
+ rl.question('Really quit? [y/N] ', (answer) => {
81
+ if (/^y/i.test(answer.trim())) rl.close();
82
+ else rl.prompt();
83
+ });
84
+ });
85
+
86
+ rl.on('close', async () => {
87
+ await ctx.client.flush();
88
+ ctx.client.close();
89
+ });
90
+
91
+ return rl;
92
+ }
93
+
94
+ module.exports = { startRepl, prompt };