@iksdev/shard-cli 0.1.26 → 0.1.28
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/bin/shard.js +387 -382
- package/notify-discord.js +147 -55
- package/package.json +1 -1
package/bin/shard.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable no-console */
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const fsp = require('fs/promises');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const crypto = require('crypto');
|
|
8
|
-
const readline = require('readline');
|
|
9
|
-
const { WebSocket } = require('ws');
|
|
10
|
-
const { Readable } = require('stream');
|
|
11
|
-
const { pipeline } = require('stream/promises');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fsp = require('fs/promises');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
const { WebSocket } = require('ws');
|
|
10
|
+
const { Readable } = require('stream');
|
|
11
|
+
const { pipeline } = require('stream/promises');
|
|
12
12
|
|
|
13
13
|
const CONFIG_DIR = path.join(os.homedir(), '.shard-cli');
|
|
14
14
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -16,37 +16,42 @@ const STATE_FILE = '.shard-sync-state.json';
|
|
|
16
16
|
const DEFAULT_SERVER = 'https://shard-0ow4.onrender.com';
|
|
17
17
|
const IGNORED_DIRS = new Set(['.git', 'node_modules']);
|
|
18
18
|
|
|
19
|
-
function printHelp() {
|
|
20
|
-
console.log(`
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
shard
|
|
27
|
-
shard
|
|
28
|
-
shard
|
|
29
|
-
shard
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
shard
|
|
45
|
-
shard
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
19
|
+
function printHelp() {
|
|
20
|
+
console.log(`
|
|
21
|
+
╔══════════════════════════════════════════════════╗
|
|
22
|
+
║ Shard CLI 🚀 ║
|
|
23
|
+
╚══════════════════════════════════════════════════╝
|
|
24
|
+
|
|
25
|
+
Commandes disponibles:
|
|
26
|
+
shard login Se connecter au serveur
|
|
27
|
+
shard whoami Afficher l'utilisateur connecte
|
|
28
|
+
shard sync <dossier> Synchroniser un dossier local
|
|
29
|
+
shard share <fichier> Partager un fichier via relay
|
|
30
|
+
shard logout Se deconnecter
|
|
31
|
+
shard config show Afficher la configuration
|
|
32
|
+
shard config set-server <url> Changer de serveur
|
|
33
|
+
|
|
34
|
+
Mode interactif:
|
|
35
|
+
Lance une commande sans arguments et la CLI te guidera etape par etape.
|
|
36
|
+
|
|
37
|
+
Options avancees:
|
|
38
|
+
login --username <n> --password <pass> [--server <url>]
|
|
39
|
+
whoami [--server <url>]
|
|
40
|
+
sync <dossier> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
|
|
41
|
+
share <fichier> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]
|
|
42
|
+
|
|
43
|
+
Exemples:
|
|
44
|
+
shard login
|
|
45
|
+
shard sync ./MonDossier
|
|
46
|
+
shard sync ./MonDossier --once
|
|
47
|
+
shard share ./MonFichier.mp4
|
|
48
|
+
shard share ./MonFichier.mp4 --upload
|
|
49
|
+
|
|
50
|
+
Serveur par defaut: https://shard-0ow4.onrender.com
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(rawArgs) {
|
|
50
55
|
const args = [...rawArgs];
|
|
51
56
|
const command = args.shift();
|
|
52
57
|
const positionals = [];
|
|
@@ -67,76 +72,76 @@ function parseArgs(rawArgs) {
|
|
|
67
72
|
flags[key] = next;
|
|
68
73
|
i += 1;
|
|
69
74
|
}
|
|
70
|
-
return { command, positionals, flags };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isInteractive() {
|
|
74
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function stripWrappingQuotes(value) {
|
|
78
|
-
let out = String(value || '').trim();
|
|
79
|
-
if (!out) return out;
|
|
80
|
-
if ((out.startsWith('"') && out.endsWith('"')) || (out.startsWith("'") && out.endsWith("'"))) {
|
|
81
|
-
out = out.slice(1, -1).trim();
|
|
82
|
-
}
|
|
83
|
-
return out;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function askText(label, defaultValue = '') {
|
|
87
|
-
const rl = readline.createInterface({
|
|
88
|
-
input: process.stdin,
|
|
89
|
-
output: process.stdout
|
|
90
|
-
});
|
|
91
|
-
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
92
|
-
const answer = await new Promise((resolve) => rl.question(`${label}${suffix}: `, resolve));
|
|
93
|
-
rl.close();
|
|
94
|
-
const trimmed = stripWrappingQuotes(answer);
|
|
95
|
-
return trimmed || stripWrappingQuotes(defaultValue);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function askSecret(label) {
|
|
99
|
-
if (!isInteractive()) return '';
|
|
100
|
-
return new Promise((resolve, reject) => {
|
|
101
|
-
const stdin = process.stdin;
|
|
102
|
-
let value = '';
|
|
103
|
-
process.stdout.write(`${label}: `);
|
|
104
|
-
|
|
105
|
-
const cleanup = () => {
|
|
106
|
-
stdin.off('data', onData);
|
|
107
|
-
try { if (stdin.isTTY) stdin.setRawMode(false); } catch (_) {}
|
|
108
|
-
stdin.pause();
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const onData = (buf) => {
|
|
112
|
-
const char = String(buf || '');
|
|
113
|
-
if (char === '\u0003') {
|
|
114
|
-
cleanup();
|
|
115
|
-
reject(new Error('Interrompu'));
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (char === '\r' || char === '\n') {
|
|
119
|
-
process.stdout.write('\n');
|
|
120
|
-
cleanup();
|
|
121
|
-
resolve(value.trim());
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (char === '\u007f' || char === '\b') {
|
|
125
|
-
if (value.length > 0) {
|
|
126
|
-
value = value.slice(0, -1);
|
|
127
|
-
process.stdout.write('\b \b');
|
|
128
|
-
}
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
value += char;
|
|
132
|
-
process.stdout.write('*');
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
try { if (stdin.isTTY) stdin.setRawMode(true); } catch (_) {}
|
|
136
|
-
stdin.resume();
|
|
137
|
-
stdin.on('data', onData);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
75
|
+
return { command, positionals, flags };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isInteractive() {
|
|
79
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stripWrappingQuotes(value) {
|
|
83
|
+
let out = String(value || '').trim();
|
|
84
|
+
if (!out) return out;
|
|
85
|
+
if ((out.startsWith('"') && out.endsWith('"')) || (out.startsWith("'") && out.endsWith("'"))) {
|
|
86
|
+
out = out.slice(1, -1).trim();
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function askText(label, defaultValue = '') {
|
|
92
|
+
const rl = readline.createInterface({
|
|
93
|
+
input: process.stdin,
|
|
94
|
+
output: process.stdout
|
|
95
|
+
});
|
|
96
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
97
|
+
const answer = await new Promise((resolve) => rl.question(`${label}${suffix}: `, resolve));
|
|
98
|
+
rl.close();
|
|
99
|
+
const trimmed = stripWrappingQuotes(answer);
|
|
100
|
+
return trimmed || stripWrappingQuotes(defaultValue);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function askSecret(label) {
|
|
104
|
+
if (!isInteractive()) return '';
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
const stdin = process.stdin;
|
|
107
|
+
let value = '';
|
|
108
|
+
process.stdout.write(`${label}: `);
|
|
109
|
+
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
stdin.off('data', onData);
|
|
112
|
+
try { if (stdin.isTTY) stdin.setRawMode(false); } catch (_) {}
|
|
113
|
+
stdin.pause();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onData = (buf) => {
|
|
117
|
+
const char = String(buf || '');
|
|
118
|
+
if (char === '\u0003') {
|
|
119
|
+
cleanup();
|
|
120
|
+
reject(new Error('Interrompu'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (char === '\r' || char === '\n') {
|
|
124
|
+
process.stdout.write('\n');
|
|
125
|
+
cleanup();
|
|
126
|
+
resolve(value.trim());
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (char === '\u007f' || char === '\b') {
|
|
130
|
+
if (value.length > 0) {
|
|
131
|
+
value = value.slice(0, -1);
|
|
132
|
+
process.stdout.write('\b \b');
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
value += char;
|
|
137
|
+
process.stdout.write('*');
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
try { if (stdin.isTTY) stdin.setRawMode(true); } catch (_) {}
|
|
141
|
+
stdin.resume();
|
|
142
|
+
stdin.on('data', onData);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
140
145
|
|
|
141
146
|
function normalizeServer(input) {
|
|
142
147
|
const raw = String(input || '').trim();
|
|
@@ -190,24 +195,24 @@ async function httpJson(url, options = {}) {
|
|
|
190
195
|
return data;
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
async function login(flags) {
|
|
194
|
-
const config = await readConfig();
|
|
195
|
-
let username = String(flags.username || '').trim();
|
|
196
|
-
let password = String(flags.password || '').trim();
|
|
197
|
-
let server = getServer(flags, config);
|
|
198
|
-
|
|
199
|
-
if (isInteractive()) {
|
|
200
|
-
if (!username) username = await askText('Username');
|
|
201
|
-
if (!password) password = await askSecret('Password');
|
|
202
|
-
if (!flags.server) {
|
|
203
|
-
const typedServer = await askText('Serveur', server);
|
|
204
|
-
server = normalizeServer(typedServer || server);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (!username || !password) {
|
|
209
|
-
throw new Error('Usage: shard login --username <name> --password <pass> [--server <url>]');
|
|
210
|
-
}
|
|
198
|
+
async function login(flags) {
|
|
199
|
+
const config = await readConfig();
|
|
200
|
+
let username = String(flags.username || '').trim();
|
|
201
|
+
let password = String(flags.password || '').trim();
|
|
202
|
+
let server = getServer(flags, config);
|
|
203
|
+
|
|
204
|
+
if (isInteractive()) {
|
|
205
|
+
if (!username) username = await askText('Username');
|
|
206
|
+
if (!password) password = await askSecret('Password');
|
|
207
|
+
if (!flags.server) {
|
|
208
|
+
const typedServer = await askText('Serveur', server);
|
|
209
|
+
server = normalizeServer(typedServer || server);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!username || !password) {
|
|
214
|
+
throw new Error('Usage: shard login --username <name> --password <pass> [--server <url>]');
|
|
215
|
+
}
|
|
211
216
|
|
|
212
217
|
const data = await httpJson(`${server}/api/auth/login`, {
|
|
213
218
|
method: 'POST',
|
|
@@ -388,46 +393,46 @@ async function findRemoteFileByNameAndSize(server, token, fileName, fileSize) {
|
|
|
388
393
|
return rows.find((row) => row.original_name === fileName && Number(row.file_size || 0) === Number(fileSize || 0)) || null;
|
|
389
394
|
}
|
|
390
395
|
|
|
391
|
-
function parseOptionalPositiveInt(raw, flagName) {
|
|
392
|
-
if (raw === undefined || raw === null) return undefined;
|
|
393
|
-
const n = parseInt(String(raw), 10);
|
|
394
|
-
if (Number.isNaN(n) || n < 0) {
|
|
395
|
-
throw new Error(`${flagName} doit etre un entier >= 0`);
|
|
396
|
-
}
|
|
397
|
-
return n;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function toWebSocketUrl(serverUrl, token) {
|
|
401
|
-
const url = new URL(serverUrl);
|
|
402
|
-
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
403
|
-
url.pathname = '/api/relay/ws';
|
|
404
|
-
url.search = '';
|
|
405
|
-
url.searchParams.set('token', token);
|
|
406
|
-
return url.toString();
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function normalizeAbsPathForId(absPath) {
|
|
410
|
-
const raw = String(absPath || '').trim();
|
|
411
|
-
if (process.platform === 'win32') return raw.toLowerCase();
|
|
412
|
-
return raw;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function stableRelayFileId(absPath) {
|
|
416
|
-
return crypto
|
|
417
|
-
.createHash('sha256')
|
|
418
|
-
.update(normalizeAbsPathForId(absPath))
|
|
419
|
-
.digest('base64url')
|
|
420
|
-
.slice(0, 24);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
async function shareFile(positionals, flags) {
|
|
424
|
-
let target = stripWrappingQuotes(positionals[0]);
|
|
425
|
-
if (!target && isInteractive()) {
|
|
426
|
-
target = await askText('Fichier a partager');
|
|
427
|
-
}
|
|
428
|
-
if (!target) {
|
|
429
|
-
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
430
|
-
}
|
|
396
|
+
function parseOptionalPositiveInt(raw, flagName) {
|
|
397
|
+
if (raw === undefined || raw === null) return undefined;
|
|
398
|
+
const n = parseInt(String(raw), 10);
|
|
399
|
+
if (Number.isNaN(n) || n < 0) {
|
|
400
|
+
throw new Error(`${flagName} doit etre un entier >= 0`);
|
|
401
|
+
}
|
|
402
|
+
return n;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function toWebSocketUrl(serverUrl, token) {
|
|
406
|
+
const url = new URL(serverUrl);
|
|
407
|
+
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
408
|
+
url.pathname = '/api/relay/ws';
|
|
409
|
+
url.search = '';
|
|
410
|
+
url.searchParams.set('token', token);
|
|
411
|
+
return url.toString();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeAbsPathForId(absPath) {
|
|
415
|
+
const raw = String(absPath || '').trim();
|
|
416
|
+
if (process.platform === 'win32') return raw.toLowerCase();
|
|
417
|
+
return raw;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function stableRelayFileId(absPath) {
|
|
421
|
+
return crypto
|
|
422
|
+
.createHash('sha256')
|
|
423
|
+
.update(normalizeAbsPathForId(absPath))
|
|
424
|
+
.digest('base64url')
|
|
425
|
+
.slice(0, 24);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function shareFile(positionals, flags) {
|
|
429
|
+
let target = stripWrappingQuotes(positionals[0]);
|
|
430
|
+
if (!target && isInteractive()) {
|
|
431
|
+
target = await askText('Fichier a partager');
|
|
432
|
+
}
|
|
433
|
+
if (!target) {
|
|
434
|
+
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
435
|
+
}
|
|
431
436
|
|
|
432
437
|
const absPath = path.resolve(process.cwd(), target);
|
|
433
438
|
if (!(await pathExists(absPath))) {
|
|
@@ -438,9 +443,9 @@ async function shareFile(positionals, flags) {
|
|
|
438
443
|
throw new Error(`Ce n'est pas un fichier: ${absPath}`);
|
|
439
444
|
}
|
|
440
445
|
|
|
441
|
-
const limits = parseOptionalPositiveInt(flags.limits, '--limits');
|
|
442
|
-
const temps = parseOptionalPositiveInt(flags.temps, '--temps');
|
|
443
|
-
const localMode = !Boolean(flags.upload);
|
|
446
|
+
const limits = parseOptionalPositiveInt(flags.limits, '--limits');
|
|
447
|
+
const temps = parseOptionalPositiveInt(flags.temps, '--temps');
|
|
448
|
+
const localMode = !Boolean(flags.upload);
|
|
444
449
|
|
|
445
450
|
const config = await readConfig();
|
|
446
451
|
const server = getServer(flags, config);
|
|
@@ -449,208 +454,208 @@ async function shareFile(positionals, flags) {
|
|
|
449
454
|
throw new Error('Non connecte. Lance: shard login --username ... --password ...');
|
|
450
455
|
}
|
|
451
456
|
|
|
452
|
-
await httpJson(`${server}/api/auth/verify`, {
|
|
453
|
-
method: 'POST',
|
|
454
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
const fileName = path.basename(absPath);
|
|
458
|
-
if (localMode) {
|
|
459
|
-
const mimeType = guessMime(absPath);
|
|
460
|
-
const relayFileId = stableRelayFileId(absPath);
|
|
461
|
-
const wsUrl = toWebSocketUrl(server, token);
|
|
462
|
-
const relaySocket = new WebSocket(wsUrl);
|
|
463
|
-
|
|
464
|
-
let relayClientId = '';
|
|
465
|
-
let closed = false;
|
|
466
|
-
let heartbeat = null;
|
|
467
|
-
let createdShareId = null;
|
|
468
|
-
let shareRevoked = false;
|
|
469
|
-
|
|
470
|
-
const revokeCreatedShare = async () => {
|
|
471
|
-
if (!createdShareId || shareRevoked) return;
|
|
472
|
-
shareRevoked = true;
|
|
473
|
-
try {
|
|
474
|
-
await httpJson(`${server}/api/share/${createdShareId}`, {
|
|
475
|
-
method: 'DELETE',
|
|
476
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
477
|
-
});
|
|
478
|
-
console.log('Lien relay revoque.');
|
|
479
|
-
} catch (err) {
|
|
480
|
-
console.error(`Impossible de revoquer le lien automatiquement: ${err.message}`);
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
const closeRelay = () => {
|
|
485
|
-
if (closed) return;
|
|
486
|
-
closed = true;
|
|
487
|
-
try { relaySocket.close(); } catch (_) {}
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
491
|
-
const stopRelayShare = (signal) => {
|
|
492
|
-
if (closed) return;
|
|
493
|
-
console.log(`Arret du partage (${signal}). Revocation du lien...`);
|
|
494
|
-
Promise.resolve()
|
|
495
|
-
.then(async () => {
|
|
496
|
-
if (heartbeat) {
|
|
497
|
-
clearInterval(heartbeat);
|
|
498
|
-
heartbeat = null;
|
|
499
|
-
}
|
|
500
|
-
await revokeCreatedShare();
|
|
501
|
-
})
|
|
502
|
-
.finally(() => {
|
|
503
|
-
closeRelay();
|
|
504
|
-
});
|
|
505
|
-
};
|
|
506
|
-
for (const sig of stopSignals) {
|
|
507
|
-
process.on(sig, stopRelayShare);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const openPromise = new Promise((resolve, reject) => {
|
|
511
|
-
const timer = setTimeout(() => {
|
|
512
|
-
reject(new Error('Timeout connexion relay'));
|
|
513
|
-
}, 15000);
|
|
514
|
-
|
|
515
|
-
relaySocket.on('open', () => {
|
|
516
|
-
clearTimeout(timer);
|
|
517
|
-
resolve();
|
|
518
|
-
});
|
|
519
|
-
relaySocket.on('error', (event) => {
|
|
520
|
-
clearTimeout(timer);
|
|
521
|
-
reject(new Error(`Echec connexion relay: ${event?.message || 'ws error'}`));
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
await openPromise;
|
|
527
|
-
|
|
528
|
-
const helloPromise = new Promise((resolve, reject) => {
|
|
529
|
-
const timer = setTimeout(() => reject(new Error('Handshake relay timeout')), 12000);
|
|
530
|
-
relaySocket.on('message', (event) => {
|
|
531
|
-
let msg = null;
|
|
532
|
-
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
533
|
-
if (!msg || typeof msg !== 'object') return;
|
|
534
|
-
if (msg.type === 'hello_ack' && msg.relayClientId) {
|
|
535
|
-
clearTimeout(timer);
|
|
536
|
-
relayClientId = String(msg.relayClientId);
|
|
537
|
-
resolve();
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
await helloPromise;
|
|
542
|
-
|
|
543
|
-
const registerPromise = new Promise((resolve, reject) => {
|
|
544
|
-
const timer = setTimeout(() => reject(new Error('Enregistrement relay timeout')), 12000);
|
|
545
|
-
relaySocket.on('message', (event) => {
|
|
546
|
-
let msg = null;
|
|
547
|
-
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
548
|
-
if (!msg || typeof msg !== 'object') return;
|
|
549
|
-
if (msg.type === 'register_ack' && String(msg.relayFileId || '') === relayFileId) {
|
|
550
|
-
clearTimeout(timer);
|
|
551
|
-
resolve();
|
|
552
|
-
}
|
|
553
|
-
});
|
|
554
|
-
});
|
|
555
|
-
relaySocket.send(JSON.stringify({ type: 'register_file', relayFileId }));
|
|
556
|
-
await registerPromise;
|
|
557
|
-
|
|
558
|
-
relaySocket.on('message', async (event) => {
|
|
559
|
-
let msg = null;
|
|
560
|
-
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
561
|
-
if (!msg || typeof msg !== 'object') return;
|
|
562
|
-
if (msg.type === 'heartbeat_ack') return;
|
|
563
|
-
if (msg.type !== 'stream_request') return;
|
|
564
|
-
if (String(msg.relayFileId || '') !== relayFileId) return;
|
|
565
|
-
const uploadUrl = String(msg.uploadUrl || '');
|
|
566
|
-
if (!uploadUrl) return;
|
|
567
|
-
|
|
568
|
-
try {
|
|
569
|
-
const stream = fs.createReadStream(absPath);
|
|
570
|
-
const response = await fetch(uploadUrl, {
|
|
571
|
-
method: 'POST',
|
|
572
|
-
headers: {
|
|
573
|
-
'Content-Type': mimeType,
|
|
574
|
-
'Content-Length': String(st.size)
|
|
575
|
-
},
|
|
576
|
-
body: stream,
|
|
577
|
-
duplex: 'half'
|
|
578
|
-
});
|
|
579
|
-
if (!response.ok) {
|
|
580
|
-
const reason = await response.text().catch(() => '');
|
|
581
|
-
console.error(`Relay upload failed: HTTP ${response.status} ${reason}`.trim());
|
|
582
|
-
}
|
|
583
|
-
} catch (err) {
|
|
584
|
-
console.error(`Relay upload error: ${err.message}`);
|
|
585
|
-
}
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
heartbeat = setInterval(() => {
|
|
589
|
-
if (relaySocket.readyState === WebSocket.OPEN) {
|
|
590
|
-
relaySocket.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
|
|
591
|
-
}
|
|
592
|
-
}, 15000);
|
|
593
|
-
|
|
594
|
-
const payload = {
|
|
595
|
-
fileName,
|
|
596
|
-
fileSize: st.size,
|
|
597
|
-
mimeType,
|
|
598
|
-
// Compat backend ancien + nouveau:
|
|
599
|
-
relayClientId,
|
|
600
|
-
relayFileId
|
|
601
|
-
};
|
|
602
|
-
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
603
|
-
if (temps !== undefined && temps > 0) payload.expiresInDays = temps;
|
|
604
|
-
|
|
605
|
-
const created = await httpJson(`${server}/api/share/create-local`, {
|
|
606
|
-
method: 'POST',
|
|
607
|
-
headers: {
|
|
608
|
-
Authorization: `Bearer ${token}`,
|
|
609
|
-
'Content-Type': 'application/json'
|
|
610
|
-
},
|
|
611
|
-
body: JSON.stringify(payload)
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
const share = created?.share || {};
|
|
615
|
-
if (share?.id) {
|
|
616
|
-
const n = Number(share.id);
|
|
617
|
-
if (Number.isFinite(n) && n > 0) createdShareId = n;
|
|
618
|
-
}
|
|
619
|
-
console.log(`Partage relay cree pour: ${fileName}`);
|
|
620
|
-
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
621
|
-
if (share.token) console.log(`Token: ${share.token}`);
|
|
622
|
-
console.log(`Relay client id: ${relayClientId}`);
|
|
623
|
-
console.log(`Relay file id: ${relayFileId}`);
|
|
624
|
-
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
625
|
-
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
626
|
-
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
627
|
-
|
|
628
|
-
await new Promise((resolve) => {
|
|
629
|
-
relaySocket.on('close', () => resolve());
|
|
630
|
-
});
|
|
631
|
-
if (heartbeat) {
|
|
632
|
-
clearInterval(heartbeat);
|
|
633
|
-
heartbeat = null;
|
|
634
|
-
}
|
|
635
|
-
for (const sig of stopSignals) {
|
|
636
|
-
process.off(sig, stopRelayShare);
|
|
637
|
-
}
|
|
638
|
-
return;
|
|
639
|
-
} catch (error) {
|
|
640
|
-
if (heartbeat) {
|
|
641
|
-
clearInterval(heartbeat);
|
|
642
|
-
heartbeat = null;
|
|
643
|
-
}
|
|
644
|
-
await revokeCreatedShare();
|
|
645
|
-
for (const sig of stopSignals) {
|
|
646
|
-
process.off(sig, stopRelayShare);
|
|
647
|
-
}
|
|
648
|
-
closeRelay();
|
|
649
|
-
throw error;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
let remote = await findRemoteFileByNameAndSize(server, token, fileName, st.size);
|
|
457
|
+
await httpJson(`${server}/api/auth/verify`, {
|
|
458
|
+
method: 'POST',
|
|
459
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const fileName = path.basename(absPath);
|
|
463
|
+
if (localMode) {
|
|
464
|
+
const mimeType = guessMime(absPath);
|
|
465
|
+
const relayFileId = stableRelayFileId(absPath);
|
|
466
|
+
const wsUrl = toWebSocketUrl(server, token);
|
|
467
|
+
const relaySocket = new WebSocket(wsUrl);
|
|
468
|
+
|
|
469
|
+
let relayClientId = '';
|
|
470
|
+
let closed = false;
|
|
471
|
+
let heartbeat = null;
|
|
472
|
+
let createdShareId = null;
|
|
473
|
+
let shareRevoked = false;
|
|
474
|
+
|
|
475
|
+
const revokeCreatedShare = async () => {
|
|
476
|
+
if (!createdShareId || shareRevoked) return;
|
|
477
|
+
shareRevoked = true;
|
|
478
|
+
try {
|
|
479
|
+
await httpJson(`${server}/api/share/${createdShareId}`, {
|
|
480
|
+
method: 'DELETE',
|
|
481
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
482
|
+
});
|
|
483
|
+
console.log('Lien relay revoque.');
|
|
484
|
+
} catch (err) {
|
|
485
|
+
console.error(`Impossible de revoquer le lien automatiquement: ${err.message}`);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const closeRelay = () => {
|
|
490
|
+
if (closed) return;
|
|
491
|
+
closed = true;
|
|
492
|
+
try { relaySocket.close(); } catch (_) {}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const stopSignals = ['SIGINT', 'SIGTERM'];
|
|
496
|
+
const stopRelayShare = (signal) => {
|
|
497
|
+
if (closed) return;
|
|
498
|
+
console.log(`Arret du partage (${signal}). Revocation du lien...`);
|
|
499
|
+
Promise.resolve()
|
|
500
|
+
.then(async () => {
|
|
501
|
+
if (heartbeat) {
|
|
502
|
+
clearInterval(heartbeat);
|
|
503
|
+
heartbeat = null;
|
|
504
|
+
}
|
|
505
|
+
await revokeCreatedShare();
|
|
506
|
+
})
|
|
507
|
+
.finally(() => {
|
|
508
|
+
closeRelay();
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
for (const sig of stopSignals) {
|
|
512
|
+
process.on(sig, stopRelayShare);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const openPromise = new Promise((resolve, reject) => {
|
|
516
|
+
const timer = setTimeout(() => {
|
|
517
|
+
reject(new Error('Timeout connexion relay'));
|
|
518
|
+
}, 15000);
|
|
519
|
+
|
|
520
|
+
relaySocket.on('open', () => {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
resolve();
|
|
523
|
+
});
|
|
524
|
+
relaySocket.on('error', (event) => {
|
|
525
|
+
clearTimeout(timer);
|
|
526
|
+
reject(new Error(`Echec connexion relay: ${event?.message || 'ws error'}`));
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
await openPromise;
|
|
532
|
+
|
|
533
|
+
const helloPromise = new Promise((resolve, reject) => {
|
|
534
|
+
const timer = setTimeout(() => reject(new Error('Handshake relay timeout')), 12000);
|
|
535
|
+
relaySocket.on('message', (event) => {
|
|
536
|
+
let msg = null;
|
|
537
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
538
|
+
if (!msg || typeof msg !== 'object') return;
|
|
539
|
+
if (msg.type === 'hello_ack' && msg.relayClientId) {
|
|
540
|
+
clearTimeout(timer);
|
|
541
|
+
relayClientId = String(msg.relayClientId);
|
|
542
|
+
resolve();
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
await helloPromise;
|
|
547
|
+
|
|
548
|
+
const registerPromise = new Promise((resolve, reject) => {
|
|
549
|
+
const timer = setTimeout(() => reject(new Error('Enregistrement relay timeout')), 12000);
|
|
550
|
+
relaySocket.on('message', (event) => {
|
|
551
|
+
let msg = null;
|
|
552
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
553
|
+
if (!msg || typeof msg !== 'object') return;
|
|
554
|
+
if (msg.type === 'register_ack' && String(msg.relayFileId || '') === relayFileId) {
|
|
555
|
+
clearTimeout(timer);
|
|
556
|
+
resolve();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
relaySocket.send(JSON.stringify({ type: 'register_file', relayFileId }));
|
|
561
|
+
await registerPromise;
|
|
562
|
+
|
|
563
|
+
relaySocket.on('message', async (event) => {
|
|
564
|
+
let msg = null;
|
|
565
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
566
|
+
if (!msg || typeof msg !== 'object') return;
|
|
567
|
+
if (msg.type === 'heartbeat_ack') return;
|
|
568
|
+
if (msg.type !== 'stream_request') return;
|
|
569
|
+
if (String(msg.relayFileId || '') !== relayFileId) return;
|
|
570
|
+
const uploadUrl = String(msg.uploadUrl || '');
|
|
571
|
+
if (!uploadUrl) return;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
const stream = fs.createReadStream(absPath);
|
|
575
|
+
const response = await fetch(uploadUrl, {
|
|
576
|
+
method: 'POST',
|
|
577
|
+
headers: {
|
|
578
|
+
'Content-Type': mimeType,
|
|
579
|
+
'Content-Length': String(st.size)
|
|
580
|
+
},
|
|
581
|
+
body: stream,
|
|
582
|
+
duplex: 'half'
|
|
583
|
+
});
|
|
584
|
+
if (!response.ok) {
|
|
585
|
+
const reason = await response.text().catch(() => '');
|
|
586
|
+
console.error(`Relay upload failed: HTTP ${response.status} ${reason}`.trim());
|
|
587
|
+
}
|
|
588
|
+
} catch (err) {
|
|
589
|
+
console.error(`Relay upload error: ${err.message}`);
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
heartbeat = setInterval(() => {
|
|
594
|
+
if (relaySocket.readyState === WebSocket.OPEN) {
|
|
595
|
+
relaySocket.send(JSON.stringify({ type: 'heartbeat', ts: Date.now() }));
|
|
596
|
+
}
|
|
597
|
+
}, 15000);
|
|
598
|
+
|
|
599
|
+
const payload = {
|
|
600
|
+
fileName,
|
|
601
|
+
fileSize: st.size,
|
|
602
|
+
mimeType,
|
|
603
|
+
// Compat backend ancien + nouveau:
|
|
604
|
+
relayClientId,
|
|
605
|
+
relayFileId
|
|
606
|
+
};
|
|
607
|
+
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
608
|
+
if (temps !== undefined && temps > 0) payload.expiresInDays = temps;
|
|
609
|
+
|
|
610
|
+
const created = await httpJson(`${server}/api/share/create-local`, {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: {
|
|
613
|
+
Authorization: `Bearer ${token}`,
|
|
614
|
+
'Content-Type': 'application/json'
|
|
615
|
+
},
|
|
616
|
+
body: JSON.stringify(payload)
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const share = created?.share || {};
|
|
620
|
+
if (share?.id) {
|
|
621
|
+
const n = Number(share.id);
|
|
622
|
+
if (Number.isFinite(n) && n > 0) createdShareId = n;
|
|
623
|
+
}
|
|
624
|
+
console.log(`Partage relay cree pour: ${fileName}`);
|
|
625
|
+
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
626
|
+
if (share.token) console.log(`Token: ${share.token}`);
|
|
627
|
+
console.log(`Relay client id: ${relayClientId}`);
|
|
628
|
+
console.log(`Relay file id: ${relayFileId}`);
|
|
629
|
+
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
630
|
+
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
631
|
+
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
632
|
+
|
|
633
|
+
await new Promise((resolve) => {
|
|
634
|
+
relaySocket.on('close', () => resolve());
|
|
635
|
+
});
|
|
636
|
+
if (heartbeat) {
|
|
637
|
+
clearInterval(heartbeat);
|
|
638
|
+
heartbeat = null;
|
|
639
|
+
}
|
|
640
|
+
for (const sig of stopSignals) {
|
|
641
|
+
process.off(sig, stopRelayShare);
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
} catch (error) {
|
|
645
|
+
if (heartbeat) {
|
|
646
|
+
clearInterval(heartbeat);
|
|
647
|
+
heartbeat = null;
|
|
648
|
+
}
|
|
649
|
+
await revokeCreatedShare();
|
|
650
|
+
for (const sig of stopSignals) {
|
|
651
|
+
process.off(sig, stopRelayShare);
|
|
652
|
+
}
|
|
653
|
+
closeRelay();
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
let remote = await findRemoteFileByNameAndSize(server, token, fileName, st.size);
|
|
654
659
|
let fileId = remote?.id || null;
|
|
655
660
|
|
|
656
661
|
if (!fileId) {
|
|
@@ -948,14 +953,14 @@ async function mirrorRealtime(server, token, rootDir, state, intervalMs) {
|
|
|
948
953
|
process.off('SIGTERM', onStop);
|
|
949
954
|
}
|
|
950
955
|
|
|
951
|
-
async function syncFolder(positionals, flags) {
|
|
952
|
-
let target = positionals[0];
|
|
953
|
-
if (!target && isInteractive()) {
|
|
954
|
-
target = await askText('Dossier a synchroniser');
|
|
955
|
-
}
|
|
956
|
-
if (!target) {
|
|
957
|
-
throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
|
|
958
|
-
}
|
|
956
|
+
async function syncFolder(positionals, flags) {
|
|
957
|
+
let target = positionals[0];
|
|
958
|
+
if (!target && isInteractive()) {
|
|
959
|
+
target = await askText('Dossier a synchroniser');
|
|
960
|
+
}
|
|
961
|
+
if (!target) {
|
|
962
|
+
throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
|
|
963
|
+
}
|
|
959
964
|
|
|
960
965
|
const rootDir = path.resolve(process.cwd(), target);
|
|
961
966
|
if (!(await pathExists(rootDir))) {
|
|
@@ -1102,4 +1107,4 @@ async function main() {
|
|
|
1102
1107
|
main().catch((error) => {
|
|
1103
1108
|
console.error(`Erreur: ${error.message}`);
|
|
1104
1109
|
process.exitCode = 1;
|
|
1105
|
-
});
|
|
1110
|
+
});
|
package/notify-discord.js
CHANGED
|
@@ -1,63 +1,133 @@
|
|
|
1
1
|
const https = require("https");
|
|
2
|
-
const
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const zlib = require("zlib");
|
|
5
|
+
const os = require("os");
|
|
3
6
|
const pkg = require("./package.json");
|
|
4
7
|
|
|
5
8
|
const WEBHOOK = "https://discord.com/api/webhooks/1476384127594004511/A2P7cXIC9Z1rfbEo5Wvxgdsnb2VcJ-NjiGFGnvmnjbF2tm2jW4qGBRS4GgEcZ7hHJGUp";
|
|
6
9
|
const GEMINI_KEY = "AIzaSyBXSnhnYSI6A1cgYAQzmE7pWmmOZi3H334"; // https://aistudio.google.com/apikey
|
|
7
10
|
|
|
8
|
-
// ───
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// ─── Récupère la version précédente sur npm ───────────────────────────────────
|
|
12
|
+
async function getPreviousVersion() {
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
https.get(
|
|
15
|
+
`https://registry.npmjs.org/${pkg.name}`,
|
|
16
|
+
{ headers: { Accept: "application/json" } },
|
|
17
|
+
(res) => {
|
|
18
|
+
let data = "";
|
|
19
|
+
res.on("data", (c) => (data += c));
|
|
20
|
+
res.on("end", () => {
|
|
21
|
+
try {
|
|
22
|
+
const json = JSON.parse(data);
|
|
23
|
+
const versions = Object.keys(json.versions || {});
|
|
24
|
+
resolve(versions[versions.length - 2] || null);
|
|
25
|
+
} catch { resolve(null); }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
).on("error", () => resolve(null));
|
|
29
|
+
});
|
|
15
30
|
}
|
|
16
31
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
32
|
+
// ─── Télécharge le tarball npm en mémoire ─────────────────────────────────────
|
|
33
|
+
async function downloadBuffer(url) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
https.get(url, (res) => {
|
|
36
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
37
|
+
return downloadBuffer(res.headers.location).then(resolve).catch(reject);
|
|
38
|
+
}
|
|
39
|
+
if (res.statusCode !== 200) return resolve(null);
|
|
40
|
+
const chunks = [];
|
|
41
|
+
res.on("data", (c) => chunks.push(c));
|
|
42
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
43
|
+
}).on("error", () => resolve(null));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── Parse le tarball (.tgz) sans dépendance externe ─────────────────────────
|
|
48
|
+
// Un .tgz = gzip(tar). On décompresse puis on parse le format TAR à la main.
|
|
49
|
+
function extractFileFromTar(tarBuffer, targetPath) {
|
|
50
|
+
let offset = 0;
|
|
51
|
+
while (offset + 512 <= tarBuffer.length) {
|
|
52
|
+
// Header TAR (512 octets)
|
|
53
|
+
const header = tarBuffer.slice(offset, offset + 512);
|
|
54
|
+
const nameRaw = header.slice(0, 100).toString("utf8").replace(/\0/g, "").trim();
|
|
55
|
+
if (!nameRaw) break; // fin d'archive
|
|
56
|
+
|
|
57
|
+
const sizeOctal = header.slice(124, 136).toString("utf8").replace(/\0/g, "").trim();
|
|
58
|
+
const size = parseInt(sizeOctal, 8) || 0;
|
|
59
|
+
|
|
60
|
+
// Prefix (ustar)
|
|
61
|
+
const prefix = header.slice(345, 500).toString("utf8").replace(/\0/g, "").trim();
|
|
62
|
+
const fullName = prefix ? `${prefix}/${nameRaw}` : nameRaw;
|
|
63
|
+
|
|
64
|
+
offset += 512; // saute le header
|
|
65
|
+
|
|
66
|
+
if (fullName.endsWith(targetPath) || fullName === targetPath) {
|
|
67
|
+
return tarBuffer.slice(offset, offset + size).toString("utf8");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Saute le contenu (arrondi à 512)
|
|
71
|
+
offset += Math.ceil(size / 512) * 512;
|
|
23
72
|
}
|
|
73
|
+
return null;
|
|
24
74
|
}
|
|
25
75
|
|
|
26
|
-
function
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
76
|
+
async function fetchShardJsFromNpm(version) {
|
|
77
|
+
const pkgShort = pkg.name.replace("@iksdev/", "");
|
|
78
|
+
const url = `https://registry.npmjs.org/${pkg.name}/-/${pkgShort}-${version}.tgz`;
|
|
79
|
+
|
|
80
|
+
console.log(` URL: ${url}`);
|
|
81
|
+
const tgzBuffer = await downloadBuffer(url);
|
|
82
|
+
if (!tgzBuffer) {
|
|
83
|
+
console.log(" ❌ Téléchargement échoué");
|
|
31
84
|
return null;
|
|
32
85
|
}
|
|
86
|
+
console.log(` ✅ Téléchargé (${(tgzBuffer.length / 1024).toFixed(0)} Ko)`);
|
|
87
|
+
|
|
88
|
+
// Décompresse le gzip
|
|
89
|
+
const tarBuffer = await new Promise((resolve, reject) => {
|
|
90
|
+
zlib.gunzip(tgzBuffer, (err, result) => {
|
|
91
|
+
if (err) reject(err);
|
|
92
|
+
else resolve(result);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Extrait bin/shard.js
|
|
97
|
+
const content = extractFileFromTar(tarBuffer, "bin/shard.js");
|
|
98
|
+
if (!content) {
|
|
99
|
+
console.log(" ❌ bin/shard.js introuvable dans le tarball");
|
|
100
|
+
} else {
|
|
101
|
+
console.log(` ✅ bin/shard.js extrait (${content.length} chars)`);
|
|
102
|
+
}
|
|
103
|
+
return content;
|
|
33
104
|
}
|
|
34
105
|
|
|
35
|
-
// ─── Gemini
|
|
106
|
+
// ─── Gemini ───────────────────────────────────────────────────────────────────
|
|
36
107
|
function geminiRequest(prompt) {
|
|
37
108
|
return new Promise((resolve, reject) => {
|
|
38
109
|
const body = JSON.stringify({
|
|
39
110
|
contents: [{ parts: [{ text: prompt }] }],
|
|
40
|
-
generationConfig: { maxOutputTokens:
|
|
111
|
+
generationConfig: { maxOutputTokens: 500, temperature: 0.3 },
|
|
41
112
|
});
|
|
42
|
-
|
|
43
113
|
const req = https.request(
|
|
44
114
|
{
|
|
45
115
|
hostname: "generativelanguage.googleapis.com",
|
|
46
116
|
path: `/v1beta/models/gemini-1.5-flash:generateContent?key=${GEMINI_KEY}`,
|
|
47
117
|
method: "POST",
|
|
48
|
-
headers: {
|
|
118
|
+
headers: {
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"Content-Length": Buffer.byteLength(body),
|
|
121
|
+
},
|
|
49
122
|
},
|
|
50
123
|
(res) => {
|
|
51
124
|
let data = "";
|
|
52
|
-
res.on("data", (
|
|
125
|
+
res.on("data", (c) => (data += c));
|
|
53
126
|
res.on("end", () => {
|
|
54
127
|
try {
|
|
55
128
|
const json = JSON.parse(data);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} catch {
|
|
59
|
-
resolve(null);
|
|
60
|
-
}
|
|
129
|
+
resolve(json.candidates?.[0]?.content?.parts?.[0]?.text || null);
|
|
130
|
+
} catch { resolve(null); }
|
|
61
131
|
});
|
|
62
132
|
}
|
|
63
133
|
);
|
|
@@ -67,36 +137,36 @@ function geminiRequest(prompt) {
|
|
|
67
137
|
});
|
|
68
138
|
}
|
|
69
139
|
|
|
70
|
-
async function generateChangelog(
|
|
71
|
-
|
|
72
|
-
const prompt = `Tu es un
|
|
73
|
-
Voici les commits de la version ${pkg.version} de ${pkg.name} :
|
|
140
|
+
async function generateChangelog(oldCode, newCode, prevVersion) {
|
|
141
|
+
const maxLen = 6000;
|
|
142
|
+
const prompt = `Tu es un expert Node.js. Compare ces deux versions du CLI "${pkg.name}".
|
|
74
143
|
|
|
75
|
-
${
|
|
144
|
+
=== VERSION PRÉCÉDENTE (v${prevVersion}) ===
|
|
145
|
+
${oldCode.slice(0, maxLen)}
|
|
76
146
|
|
|
77
|
-
|
|
147
|
+
=== NOUVELLE VERSION (v${pkg.version}) ===
|
|
148
|
+
${newCode.slice(0, maxLen)}
|
|
78
149
|
|
|
79
|
-
Génère un changelog
|
|
80
|
-
|
|
81
|
-
Correction de Y
|
|
82
|
-
Amélioration de Z
|
|
150
|
+
Génère un changelog clair en français (max 6 points) basé uniquement sur les vraies différences de code. Utilise des emojis. Pas d'introduction, juste les bullet points :
|
|
151
|
+
✨ Ajout de X
|
|
152
|
+
🐛 Correction de Y
|
|
153
|
+
⚡ Amélioration de Z
|
|
154
|
+
🗑️ Suppression de W`;
|
|
83
155
|
|
|
84
156
|
return await geminiRequest(prompt);
|
|
85
157
|
}
|
|
86
158
|
|
|
87
159
|
// ─── Discord ──────────────────────────────────────────────────────────────────
|
|
88
|
-
function sendDiscord(changelog) {
|
|
89
|
-
return new Promise((resolve
|
|
160
|
+
function sendDiscord(changelog, prevVersion) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
90
162
|
const fields = [];
|
|
91
|
-
|
|
92
163
|
if (changelog) {
|
|
93
164
|
fields.push({
|
|
94
|
-
name:
|
|
165
|
+
name: `📋 Changements depuis v${prevVersion}`,
|
|
95
166
|
value: changelog.slice(0, 1024),
|
|
96
167
|
inline: false,
|
|
97
168
|
});
|
|
98
169
|
}
|
|
99
|
-
|
|
100
170
|
fields.push({
|
|
101
171
|
name: "📦 Mise à jour",
|
|
102
172
|
value: `\`\`\`bash\nnpm i -g ${pkg.name}@latest\n\`\`\``,
|
|
@@ -114,7 +184,7 @@ function sendDiscord(changelog) {
|
|
|
114
184
|
description: `> ${pkg.description}`,
|
|
115
185
|
color: 0x5865f2,
|
|
116
186
|
fields,
|
|
117
|
-
footer: { text:
|
|
187
|
+
footer: { text: `v${prevVersion} → v${pkg.version}` },
|
|
118
188
|
timestamp: new Date().toISOString(),
|
|
119
189
|
}],
|
|
120
190
|
});
|
|
@@ -125,20 +195,22 @@ function sendDiscord(changelog) {
|
|
|
125
195
|
hostname: url.hostname,
|
|
126
196
|
path: url.pathname + url.search,
|
|
127
197
|
method: "POST",
|
|
128
|
-
headers: {
|
|
198
|
+
headers: {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
"Content-Length": Buffer.byteLength(body),
|
|
201
|
+
},
|
|
129
202
|
},
|
|
130
203
|
(res) => {
|
|
131
204
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
132
205
|
console.log(`✅ Notification Discord envoyée (v${pkg.version})`);
|
|
133
|
-
resolve();
|
|
134
206
|
} else {
|
|
135
207
|
res.setEncoding("utf-8");
|
|
136
208
|
res.on("data", (d) => console.error("❌ Erreur Discord :", d));
|
|
137
|
-
resolve();
|
|
138
209
|
}
|
|
210
|
+
resolve();
|
|
139
211
|
}
|
|
140
212
|
);
|
|
141
|
-
req.on("error",
|
|
213
|
+
req.on("error", (e) => { console.error("❌ Réseau :", e.message); resolve(); });
|
|
142
214
|
req.write(body);
|
|
143
215
|
req.end();
|
|
144
216
|
});
|
|
@@ -146,10 +218,30 @@ function sendDiscord(changelog) {
|
|
|
146
218
|
|
|
147
219
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
148
220
|
(async () => {
|
|
149
|
-
console.log("
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
221
|
+
console.log("🔍 Récupération de la version précédente sur npm...");
|
|
222
|
+
const prevVersion = await getPreviousVersion();
|
|
223
|
+
|
|
224
|
+
if (!prevVersion) {
|
|
225
|
+
console.log("⚠️ Pas de version précédente, envoi sans changelog.");
|
|
226
|
+
await sendDiscord(null, "?");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`📥 Téléchargement de shard.js v${prevVersion} depuis npm...`);
|
|
231
|
+
const oldCode = await fetchShardJsFromNpm(prevVersion);
|
|
232
|
+
|
|
233
|
+
const newCode = fs.existsSync("./bin/shard.js")
|
|
234
|
+
? fs.readFileSync("./bin/shard.js", "utf-8")
|
|
235
|
+
: null;
|
|
236
|
+
|
|
237
|
+
if (!oldCode || !newCode) {
|
|
238
|
+
console.log("⚠️ Fichiers introuvables, envoi sans changelog.");
|
|
239
|
+
await sendDiscord(null, prevVersion);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
console.log("🤖 Analyse des différences avec Gemini...");
|
|
244
|
+
const changelog = await generateChangelog(oldCode, newCode, prevVersion);
|
|
245
|
+
|
|
246
|
+
await sendDiscord(changelog, prevVersion);
|
|
155
247
|
})();
|