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.
@@ -0,0 +1,217 @@
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 path = require('path');
21
+ const crypto = require('crypto');
22
+ const { confirm } = require('./prompts');
23
+
24
+ function sha256File(file) {
25
+ return new Promise((resolve, reject) => {
26
+ const hash = crypto.createHash('sha256');
27
+ const stream = fs.createReadStream(file);
28
+ stream.on('data', (d) => hash.update(d));
29
+ stream.on('end', () => resolve(hash.digest('hex')));
30
+ stream.on('error', reject);
31
+ });
32
+ }
33
+
34
+ function showProgress(transferred, total) {
35
+ if (!total) return;
36
+ const pct = Math.floor((transferred / total) * 100);
37
+ const kb = Math.floor(transferred / 1024);
38
+ const totalKb = Math.floor(total / 1024);
39
+ process.stdout.write(`\r${pct}% (${kb}KB / ${totalKb}KB) `);
40
+ }
41
+
42
+ function clearProgress() {
43
+ process.stdout.write('\r'.padEnd(70) + '\r');
44
+ }
45
+
46
+ async function cpush(ctx, args) {
47
+ const rest = args.filter((a) => !a.startsWith('--'));
48
+ let [local, remote] = rest;
49
+ if (!local || !remote) throw new Error('Usage: cpush <local-file> <remote-path>');
50
+ remote = remote.replace(/\\/g, '/');
51
+ const stat = fs.statSync(local);
52
+ if (!stat.isFile()) throw new Error('Local path must be a file');
53
+ const sha = await sha256File(local);
54
+ const start = await ctx.client.request('xfer.push.start', {
55
+ path: remote,
56
+ size: stat.size,
57
+ sha256: sha,
58
+ force: args.includes('--force'),
59
+ }, { timeoutMs: 60000 });
60
+ const { transferId, chunkSize } = start;
61
+ const fd = fs.openSync(local, 'r');
62
+ let transferred = 0;
63
+ let index = 0;
64
+ try {
65
+ const buf = Buffer.alloc(chunkSize);
66
+ while (true) {
67
+ const n = fs.readSync(fd, buf, 0, chunkSize, transferred);
68
+ if (n === 0) break;
69
+ await ctx.client.request('xfer.push.chunk', {
70
+ transferId,
71
+ index,
72
+ data: buf.subarray(0, n).toString('base64'),
73
+ }, { timeoutMs: 120000 });
74
+ transferred += n;
75
+ index++;
76
+ showProgress(transferred, stat.size);
77
+ }
78
+ } finally {
79
+ fs.closeSync(fd);
80
+ }
81
+ clearProgress();
82
+ const finish = await ctx.client.request('xfer.push.finish', { transferId, sha256: sha });
83
+ console.log(`Pushed ${local} -> ${finish.target} (${finish.size} bytes, sha256 ${finish.sha256})`);
84
+ }
85
+
86
+ async function cpull(ctx, args) {
87
+ let [remote, local] = args.filter((a) => !a.startsWith('--'));
88
+ if (!remote || !local) throw new Error('Usage: cpull <remote-path> <local-file>');
89
+ remote = remote.replace(/\\/g, '/');
90
+ const start = await ctx.client.request('xfer.pull.start', { path: remote });
91
+ const { transferId, size, sha256: expectedSha, chunkSize } = start;
92
+ const part = local + '.creepercli-part';
93
+ const hash = crypto.createHash('sha256');
94
+ const fd = fs.openSync(part, 'w');
95
+ let transferred = 0;
96
+ let index = 0;
97
+ try {
98
+ while (true) {
99
+ const res = await ctx.client.request('xfer.pull.chunk', { transferId, index }, { timeoutMs: 120000 });
100
+ const data = Buffer.from(res.data || '', 'base64');
101
+ if (data.length === 0) break;
102
+ fs.writeSync(fd, data, 0, data.length);
103
+ hash.update(data);
104
+ transferred += data.length;
105
+ index++;
106
+ showProgress(transferred, size);
107
+ }
108
+ } finally {
109
+ fs.closeSync(fd);
110
+ }
111
+ const actual = await ctx.client.request('xfer.pull.finish', { transferId, sha256: hash.digest('hex') });
112
+ clearProgress();
113
+ if (actual.sha256 !== expectedSha) {
114
+ fs.unlinkSync(part);
115
+ throw new Error('Checksum mismatch: partial file removed, transfer aborted');
116
+ }
117
+ fs.renameSync(part, local);
118
+ console.log(`Pulled ${remote} -> ${local} (${transferred} bytes, sha256 ${actual.sha256})`);
119
+ }
120
+
121
+ async function csync(ctx, args) {
122
+ let [remoteDir, localDir] = args.filter((a) => a !== '-y' && !a.startsWith('--'));
123
+ if (!remoteDir || !localDir) throw new Error('Usage: csync <remote-dir> <local-dir> [--yes]');
124
+ remoteDir = remoteDir.replace(/\\/g, '/');
125
+ const yes = args.includes('--yes') || args.includes('-y') || !!(ctx.flags && ctx.flags.yes);
126
+ await ctx.client.request('fs.mkdir', { path: remoteDir, parents: true }).catch(() => {});
127
+
128
+ const remote = await ctx.client.request('xfer.list', { path: remoteDir }, { timeoutMs: 600000 });
129
+ const remoteMap = new Map();
130
+ for (const e of remote.entries) {
131
+ if (!e.isDir) remoteMap.set(e.path.replace(/^\//, ''), e);
132
+ }
133
+ if (remote.truncated) console.error('(remote listing truncated)');
134
+
135
+ const localMap = walkLocal(localDir);
136
+
137
+ const toDownload = [];
138
+ const toUpload = [];
139
+ for (const [rel, e] of remoteMap) {
140
+ const local = localMap.get(rel);
141
+ if (!local) {
142
+ toDownload.push(rel);
143
+ continue;
144
+ }
145
+ if (local.size === e.size && local.mtime === e.mtime) continue;
146
+ if (local.size === e.size) {
147
+ let localSha;
148
+ try {
149
+ localSha = await sha256File(path.join(localDir, rel));
150
+ } catch {
151
+ toUpload.push(rel);
152
+ continue;
153
+ }
154
+ if (localSha === e.sha256) continue;
155
+ }
156
+ if (e.mtime > local.mtime) toDownload.push(rel);
157
+ else toUpload.push(rel);
158
+ }
159
+ for (const rel of localMap.keys()) {
160
+ if (!remoteMap.has(rel)) toUpload.push(rel);
161
+ }
162
+
163
+ if (!toDownload.length && !toUpload.length) {
164
+ console.log('Directories are in sync.');
165
+ return;
166
+ }
167
+ console.log(`Download: ${toDownload.length} file(s), Upload: ${toUpload.length} file(s)`);
168
+ if (!yes && toUpload.length) {
169
+ const ok = await confirm(`Upload ${toUpload.length} local file(s)?`, false);
170
+ if (!ok) toUpload.length = 0;
171
+ }
172
+ if (!yes && toDownload.length) {
173
+ const ok = await confirm(`Download ${toDownload.length} remote file(s)?`, false);
174
+ if (!ok) toDownload.length = 0;
175
+ }
176
+ for (const rel of toDownload) {
177
+ await cpull(ctx, [joinRemote(remoteDir, rel), path.join(localDir, rel)]);
178
+ }
179
+ for (const rel of toUpload) {
180
+ await cpush(ctx, [path.join(localDir, rel), joinRemote(remoteDir, rel)]);
181
+ }
182
+ console.log('Sync complete.');
183
+ }
184
+
185
+ function joinRemote(base, rel) {
186
+ const cleanBase = base.replace(/\\/g, '/');
187
+ const cleanRel = rel.replace(/\\/g, '/');
188
+ return (cleanBase.endsWith('/') ? cleanBase : cleanBase + '/') + cleanRel;
189
+ }
190
+
191
+ function walkLocal(dir) {
192
+ const map = new Map();
193
+ function walk(d, prefix) {
194
+ let names;
195
+ try {
196
+ names = fs.readdirSync(d);
197
+ } catch {
198
+ return;
199
+ }
200
+ for (const name of names) {
201
+ const p = path.join(d, name);
202
+ let st;
203
+ try {
204
+ st = fs.statSync(p);
205
+ } catch {
206
+ continue;
207
+ }
208
+ const rel = ((prefix ? prefix + '/' : '') + name).replace(/\\/g, '/');
209
+ if (st.isDirectory()) walk(p, rel);
210
+ else if (st.isFile()) map.set(rel, { size: st.size, mtime: Math.floor(st.mtimeMs) });
211
+ }
212
+ }
213
+ walk(dir, '');
214
+ return map;
215
+ }
216
+
217
+ module.exports = { cpush, cpull, csync, joinRemote, walkLocal };
package/src/update.js ADDED
@@ -0,0 +1,87 @@
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 path = require('path');
21
+ const https = require('https');
22
+ const { DIR } = require('./session');
23
+ const pkg = require('../package.json');
24
+
25
+ const UPDATE_FILE = path.join(DIR, 'update.json');
26
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
27
+
28
+ function checkUpdate() {
29
+ try {
30
+ const now = Date.now();
31
+ let cache = {};
32
+ try {
33
+ cache = JSON.parse(fs.readFileSync(UPDATE_FILE, 'utf8'));
34
+ } catch {
35
+ }
36
+
37
+ if (cache.lastCheck && now - cache.lastCheck < CHECK_INTERVAL_MS) {
38
+ if (cache.latest && isNewerVersion(pkg.version, cache.latest)) {
39
+ console.log(`[Notice] A new version of creepercli (v${cache.latest}) is available! Run "npm install -g creeper-cli" to update.\n`);
40
+ }
41
+ return;
42
+ }
43
+
44
+ const req = https.get('https://registry.npmjs.org/creeper-cli/latest', {
45
+ timeout: 2500,
46
+ headers: { 'User-Agent': `creeper-cli/${pkg.version}` }
47
+ }, (res) => {
48
+ if (res.statusCode !== 200) return;
49
+ let body = '';
50
+ res.on('data', (chunk) => body += chunk);
51
+ res.on('end', () => {
52
+ try {
53
+ const data = JSON.parse(body);
54
+ if (data && data.version) {
55
+ const latest = data.version;
56
+ fs.mkdirSync(DIR, { recursive: true, mode: 0o700 });
57
+ fs.writeFileSync(UPDATE_FILE, JSON.stringify({ lastCheck: now, latest }), { mode: 0o600 });
58
+ if (isNewerVersion(pkg.version, latest)) {
59
+ console.log(`[Notice] A new version of creepercli (v${latest}) is available! Run "npm install -g creeper-cli" to update.\n`);
60
+ }
61
+ }
62
+ } catch {
63
+ }
64
+ });
65
+ });
66
+
67
+ req.on('error', () => {});
68
+ req.end();
69
+ } catch {
70
+ }
71
+ }
72
+
73
+ function isNewerVersion(current, latest) {
74
+ if (!current || !latest) return false;
75
+ const c = current.split('-')[0].split('.').map((x) => parseInt(x, 10) || 0);
76
+ const l = latest.split('-')[0].split('.').map((x) => parseInt(x, 10) || 0);
77
+ const max = Math.max(c.length, l.length);
78
+ for (let i = 0; i < max; i++) {
79
+ const cv = c[i] || 0;
80
+ const lv = l[i] || 0;
81
+ if (lv > cv) return true;
82
+ if (lv < cv) return false;
83
+ }
84
+ return false;
85
+ }
86
+
87
+ module.exports = { checkUpdate };