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/LICENSE +190 -0
- package/README.md +85 -117
- package/bin/creepercli.js +12 -25
- package/package.json +48 -48
- package/src/client.js +137 -127
- package/src/commands/auth.js +94 -108
- package/src/commands/exec.js +23 -37
- package/src/commands/fs.js +138 -152
- package/src/commands/index.js +129 -139
- package/src/commands/monitor.js +60 -69
- package/src/commands/search.js +56 -70
- package/src/config.js +47 -61
- package/src/editor.js +84 -98
- package/src/formatters.js +120 -134
- package/src/index.js +175 -186
- package/src/prompts.js +120 -134
- package/src/protocol.js +68 -82
- package/src/repl.js +94 -104
- package/src/session.js +40 -58
- package/src/top.js +39 -53
- package/src/transfer.js +292 -217
- package/src/update.js +73 -87
package/src/transfer.js
CHANGED
|
@@ -1,217 +1,292 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
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
|
+
}
|
|
29
|
+
|
|
30
|
+
function sha256File(file) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const hash = crypto.createHash('sha256');
|
|
33
|
+
const stream = fs.createReadStream(file);
|
|
34
|
+
stream.on('data', (d) => hash.update(d));
|
|
35
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
36
|
+
stream.on('error', reject);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function showProgress(transferred, total) {
|
|
41
|
+
if (!total) return;
|
|
42
|
+
const pct = Math.floor((transferred / total) * 100);
|
|
43
|
+
const kb = Math.floor(transferred / 1024);
|
|
44
|
+
const totalKb = Math.floor(total / 1024);
|
|
45
|
+
process.stdout.write(`\r${pct}% (${kb}KB / ${totalKb}KB) `);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clearProgress() {
|
|
49
|
+
process.stdout.write('\r'.padEnd(70) + '\r');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function cpush(ctx, args) {
|
|
53
|
+
const rest = args.filter((a) => !a.startsWith('--'));
|
|
54
|
+
let [local, remote] = rest;
|
|
55
|
+
if (!local || !remote) throw new Error('Usage: cpush <local-file> <remote-path>');
|
|
56
|
+
remote = remote.replace(/\\/g, '/');
|
|
57
|
+
const stat = fs.statSync(local);
|
|
58
|
+
if (!stat.isFile()) throw new Error('Local path must be a file');
|
|
59
|
+
const sha = await sha256File(local);
|
|
60
|
+
const start = await ctx.client.request('xfer.push.start', {
|
|
61
|
+
path: remote,
|
|
62
|
+
size: stat.size,
|
|
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);
|
|
74
|
+
while (true) {
|
|
75
|
+
const n = fs.readSync(fd, buf, 0, chunkSize, transferred);
|
|
76
|
+
if (n === 0) break;
|
|
77
|
+
await ctx.client.request('xfer.push.chunk', {
|
|
78
|
+
transferId,
|
|
79
|
+
index,
|
|
80
|
+
data: buf.subarray(0, n).toString('base64'),
|
|
81
|
+
}, { timeoutMs: 120000 });
|
|
82
|
+
transferred += n;
|
|
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
|
+
}
|
|
100
|
+
|
|
101
|
+
async function cpull(ctx, args) {
|
|
102
|
+
let [remote, local] = args.filter((a) => !a.startsWith('--'));
|
|
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) {
|
|
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
|
+
}
|
|
124
|
+
fs.writeSync(fd, data, 0, data.length);
|
|
125
|
+
hash.update(data);
|
|
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
|
+
}
|
|
156
|
+
|
|
157
|
+
async function csync(ctx, args) {
|
|
158
|
+
let [remoteDir, localDir] = args.filter((a) => a !== '-y' && !a.startsWith('--'));
|
|
159
|
+
if (!remoteDir || !localDir) throw new Error('Usage: csync <remote-dir> <local-dir> [--yes]');
|
|
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
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (remote.truncated) console.error('(remote listing truncated)');
|
|
179
|
+
|
|
180
|
+
const localMap = walkLocal(localDir);
|
|
181
|
+
|
|
182
|
+
const toDownload = [];
|
|
183
|
+
const toUpload = [];
|
|
184
|
+
for (const [rel, e] of remoteMap) {
|
|
185
|
+
const local = localMap.get(rel);
|
|
186
|
+
if (!local) {
|
|
187
|
+
toDownload.push(rel);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (local.size === e.size && local.mtime === e.mtime) continue;
|
|
191
|
+
if (local.size === e.size) {
|
|
192
|
+
let localSha;
|
|
193
|
+
try {
|
|
194
|
+
localSha = await sha256File(path.join(localDir, rel));
|
|
195
|
+
} catch {
|
|
196
|
+
toUpload.push(rel);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (localSha === e.sha256) continue;
|
|
200
|
+
}
|
|
201
|
+
if (e.mtime > local.mtime) toDownload.push(rel);
|
|
202
|
+
else toUpload.push(rel);
|
|
203
|
+
}
|
|
204
|
+
for (const rel of localMap.keys()) {
|
|
205
|
+
if (!remoteMap.has(rel)) toUpload.push(rel);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!toDownload.length && !toUpload.length) {
|
|
209
|
+
console.log('Directories are in sync.');
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
console.log(`Download: ${toDownload.length} file(s), Upload: ${toUpload.length} file(s)`);
|
|
213
|
+
if (!yes && toUpload.length) {
|
|
214
|
+
const ok = await confirm(`Upload ${toUpload.length} local file(s)?`, false);
|
|
215
|
+
if (!ok) toUpload.length = 0;
|
|
216
|
+
}
|
|
217
|
+
if (!yes && toDownload.length) {
|
|
218
|
+
const ok = await confirm(`Download ${toDownload.length} remote file(s)?`, false);
|
|
219
|
+
if (!ok) toDownload.length = 0;
|
|
220
|
+
}
|
|
221
|
+
for (const rel of toDownload) {
|
|
222
|
+
await cpull(ctx, [joinRemote(remoteDir, rel), path.join(localDir, rel)]);
|
|
223
|
+
}
|
|
224
|
+
for (const rel of toUpload) {
|
|
225
|
+
await cpush(ctx, [path.join(localDir, rel), joinRemote(remoteDir, rel)]);
|
|
226
|
+
}
|
|
227
|
+
console.log('Sync complete.');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function joinRemote(base, rel) {
|
|
231
|
+
const cleanBase = base.replace(/\\/g, '/');
|
|
232
|
+
const cleanRel = rel.replace(/\\/g, '/');
|
|
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
|
+
}
|
|
262
|
+
|
|
263
|
+
function walkLocal(dir) {
|
|
264
|
+
const map = new Map();
|
|
265
|
+
function walk(d, prefix) {
|
|
266
|
+
let names;
|
|
267
|
+
try {
|
|
268
|
+
names = fs.readdirSync(d);
|
|
269
|
+
} catch {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const name of names) {
|
|
273
|
+
const p = path.join(d, name);
|
|
274
|
+
let st;
|
|
275
|
+
try {
|
|
276
|
+
st = fs.lstatSync(p);
|
|
277
|
+
} catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const rel = ((prefix ? prefix + '/' : '') + name).replace(/\\/g, '/');
|
|
281
|
+
if (st.isSymbolicLink()) continue;
|
|
282
|
+
if (st.isDirectory()) walk(p, rel);
|
|
283
|
+
else if (st.isFile()) map.set(rel, { size: st.size, mtime: Math.floor(st.mtimeMs) });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
walk(dir, '');
|
|
287
|
+
return map;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = {
|
|
291
|
+
cpush, cpull, csync, joinRemote, relativeRemotePath, replaceFile, walkLocal, validateTransferStart,
|
|
292
|
+
};
|