@iksdev/shard-cli 0.1.10 → 0.1.12
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 +2 -0
- package/bin/shard.js +119 -27
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -48,9 +48,11 @@ shard sync ./MonDossier
|
|
|
48
48
|
|
|
49
49
|
## Notes
|
|
50
50
|
|
|
51
|
+
- Mode interactif: si tu lances `shard login`, `shard sync` ou `shard share` sans arguments, la CLI te pose les questions.
|
|
51
52
|
- Le CLI stocke la config dans `~/.shard-cli/config.json`.
|
|
52
53
|
- Le CLI stocke l'etat de sync dans `<ton-dossier>/.shard-sync-state.json`.
|
|
53
54
|
- Les uploads passent par `POST /api/files/upload` avec token `Bearer`.
|
|
54
55
|
- Par défaut `shard share` utilise le mode relay: le fichier reste sur ton PC, le serveur stocke seulement metadata + token.
|
|
55
56
|
- Aucun tunnel externe à installer: le serveur Shard relaie directement le flux via websocket.
|
|
57
|
+
- Le relay file id est stable par chemin de fichier: relancer `shard share` sur le meme fichier réactive les anciens liens.
|
|
56
58
|
- Utilise `--upload` pour revenir au mode historique (upload serveur).
|
package/bin/shard.js
CHANGED
|
@@ -5,6 +5,8 @@ const fsp = require('fs/promises');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
+
const readline = require('readline');
|
|
9
|
+
const { WebSocket } = require('ws');
|
|
8
10
|
const { Readable } = require('stream');
|
|
9
11
|
const { pipeline } = require('stream/promises');
|
|
10
12
|
|
|
@@ -36,7 +38,7 @@ Examples:
|
|
|
36
38
|
`);
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
function parseArgs(rawArgs) {
|
|
41
|
+
function parseArgs(rawArgs) {
|
|
40
42
|
const args = [...rawArgs];
|
|
41
43
|
const command = args.shift();
|
|
42
44
|
const positionals = [];
|
|
@@ -57,8 +59,67 @@ function parseArgs(rawArgs) {
|
|
|
57
59
|
flags[key] = next;
|
|
58
60
|
i += 1;
|
|
59
61
|
}
|
|
60
|
-
return { command, positionals, flags };
|
|
61
|
-
}
|
|
62
|
+
return { command, positionals, flags };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isInteractive() {
|
|
66
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function askText(label, defaultValue = '') {
|
|
70
|
+
const rl = readline.createInterface({
|
|
71
|
+
input: process.stdin,
|
|
72
|
+
output: process.stdout
|
|
73
|
+
});
|
|
74
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
75
|
+
const answer = await new Promise((resolve) => rl.question(`${label}${suffix}: `, resolve));
|
|
76
|
+
rl.close();
|
|
77
|
+
const trimmed = String(answer || '').trim();
|
|
78
|
+
return trimmed || String(defaultValue || '').trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function askSecret(label) {
|
|
82
|
+
if (!isInteractive()) return '';
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const stdin = process.stdin;
|
|
85
|
+
let value = '';
|
|
86
|
+
process.stdout.write(`${label}: `);
|
|
87
|
+
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
stdin.off('data', onData);
|
|
90
|
+
try { if (stdin.isTTY) stdin.setRawMode(false); } catch (_) {}
|
|
91
|
+
stdin.pause();
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const onData = (buf) => {
|
|
95
|
+
const char = String(buf || '');
|
|
96
|
+
if (char === '\u0003') {
|
|
97
|
+
cleanup();
|
|
98
|
+
reject(new Error('Interrompu'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (char === '\r' || char === '\n') {
|
|
102
|
+
process.stdout.write('\n');
|
|
103
|
+
cleanup();
|
|
104
|
+
resolve(value.trim());
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (char === '\u007f' || char === '\b') {
|
|
108
|
+
if (value.length > 0) {
|
|
109
|
+
value = value.slice(0, -1);
|
|
110
|
+
process.stdout.write('\b \b');
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
value += char;
|
|
115
|
+
process.stdout.write('*');
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
try { if (stdin.isTTY) stdin.setRawMode(true); } catch (_) {}
|
|
119
|
+
stdin.resume();
|
|
120
|
+
stdin.on('data', onData);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
62
123
|
|
|
63
124
|
function normalizeServer(input) {
|
|
64
125
|
const raw = String(input || '').trim();
|
|
@@ -112,15 +173,24 @@ async function httpJson(url, options = {}) {
|
|
|
112
173
|
return data;
|
|
113
174
|
}
|
|
114
175
|
|
|
115
|
-
async function login(flags) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
176
|
+
async function login(flags) {
|
|
177
|
+
const config = await readConfig();
|
|
178
|
+
let username = String(flags.username || '').trim();
|
|
179
|
+
let password = String(flags.password || '').trim();
|
|
180
|
+
let server = getServer(flags, config);
|
|
181
|
+
|
|
182
|
+
if (isInteractive()) {
|
|
183
|
+
if (!username) username = await askText('Username');
|
|
184
|
+
if (!password) password = await askSecret('Password');
|
|
185
|
+
if (!flags.server) {
|
|
186
|
+
const typedServer = await askText('Serveur', server);
|
|
187
|
+
server = normalizeServer(typedServer || server);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!username || !password) {
|
|
192
|
+
throw new Error('Usage: shard login --username <name> --password <pass> [--server <url>]');
|
|
193
|
+
}
|
|
124
194
|
|
|
125
195
|
const data = await httpJson(`${server}/api/auth/login`, {
|
|
126
196
|
method: 'POST',
|
|
@@ -319,8 +389,25 @@ function toWebSocketUrl(serverUrl, token) {
|
|
|
319
389
|
return url.toString();
|
|
320
390
|
}
|
|
321
391
|
|
|
392
|
+
function normalizeAbsPathForId(absPath) {
|
|
393
|
+
const raw = String(absPath || '').trim();
|
|
394
|
+
if (process.platform === 'win32') return raw.toLowerCase();
|
|
395
|
+
return raw;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function stableRelayFileId(absPath) {
|
|
399
|
+
return crypto
|
|
400
|
+
.createHash('sha256')
|
|
401
|
+
.update(normalizeAbsPathForId(absPath))
|
|
402
|
+
.digest('base64url')
|
|
403
|
+
.slice(0, 24);
|
|
404
|
+
}
|
|
405
|
+
|
|
322
406
|
async function shareFile(positionals, flags) {
|
|
323
|
-
|
|
407
|
+
let target = positionals[0];
|
|
408
|
+
if (!target && isInteractive()) {
|
|
409
|
+
target = await askText('Fichier a partager');
|
|
410
|
+
}
|
|
324
411
|
if (!target) {
|
|
325
412
|
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
326
413
|
}
|
|
@@ -353,7 +440,7 @@ async function shareFile(positionals, flags) {
|
|
|
353
440
|
const fileName = path.basename(absPath);
|
|
354
441
|
if (localMode) {
|
|
355
442
|
const mimeType = guessMime(absPath);
|
|
356
|
-
const relayFileId =
|
|
443
|
+
const relayFileId = stableRelayFileId(absPath);
|
|
357
444
|
const wsUrl = toWebSocketUrl(server, token);
|
|
358
445
|
const relaySocket = new WebSocket(wsUrl);
|
|
359
446
|
|
|
@@ -375,11 +462,11 @@ async function shareFile(positionals, flags) {
|
|
|
375
462
|
reject(new Error('Timeout connexion relay'));
|
|
376
463
|
}, 15000);
|
|
377
464
|
|
|
378
|
-
relaySocket.
|
|
465
|
+
relaySocket.on('open', () => {
|
|
379
466
|
clearTimeout(timer);
|
|
380
467
|
resolve();
|
|
381
468
|
});
|
|
382
|
-
relaySocket.
|
|
469
|
+
relaySocket.on('error', (event) => {
|
|
383
470
|
clearTimeout(timer);
|
|
384
471
|
reject(new Error(`Echec connexion relay: ${event?.message || 'ws error'}`));
|
|
385
472
|
});
|
|
@@ -390,9 +477,9 @@ async function shareFile(positionals, flags) {
|
|
|
390
477
|
|
|
391
478
|
const helloPromise = new Promise((resolve, reject) => {
|
|
392
479
|
const timer = setTimeout(() => reject(new Error('Handshake relay timeout')), 12000);
|
|
393
|
-
relaySocket.
|
|
480
|
+
relaySocket.on('message', (event) => {
|
|
394
481
|
let msg = null;
|
|
395
|
-
try { msg = JSON.parse(String(event
|
|
482
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
396
483
|
if (!msg || typeof msg !== 'object') return;
|
|
397
484
|
if (msg.type === 'hello_ack' && msg.relayClientId) {
|
|
398
485
|
clearTimeout(timer);
|
|
@@ -403,9 +490,11 @@ async function shareFile(positionals, flags) {
|
|
|
403
490
|
});
|
|
404
491
|
await helloPromise;
|
|
405
492
|
|
|
406
|
-
relaySocket.
|
|
493
|
+
relaySocket.send(JSON.stringify({ type: 'register_file', relayFileId }));
|
|
494
|
+
|
|
495
|
+
relaySocket.on('message', async (event) => {
|
|
407
496
|
let msg = null;
|
|
408
|
-
try { msg = JSON.parse(String(event
|
|
497
|
+
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
409
498
|
if (!msg || typeof msg !== 'object') return;
|
|
410
499
|
if (msg.type === 'heartbeat_ack') return;
|
|
411
500
|
if (msg.type !== 'stream_request') return;
|
|
@@ -443,7 +532,6 @@ async function shareFile(positionals, flags) {
|
|
|
443
532
|
fileName,
|
|
444
533
|
fileSize: st.size,
|
|
445
534
|
mimeType,
|
|
446
|
-
relayClientId,
|
|
447
535
|
relayFileId
|
|
448
536
|
};
|
|
449
537
|
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
@@ -463,12 +551,13 @@ async function shareFile(positionals, flags) {
|
|
|
463
551
|
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
464
552
|
if (share.token) console.log(`Token: ${share.token}`);
|
|
465
553
|
console.log(`Relay client id: ${relayClientId}`);
|
|
554
|
+
console.log(`Relay file id: ${relayFileId}`);
|
|
466
555
|
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
467
556
|
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
468
557
|
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
469
558
|
|
|
470
559
|
await new Promise((resolve) => {
|
|
471
|
-
relaySocket.
|
|
560
|
+
relaySocket.on('close', () => resolve());
|
|
472
561
|
});
|
|
473
562
|
clearInterval(heartbeat);
|
|
474
563
|
for (const sig of stopSignals) {
|
|
@@ -782,11 +871,14 @@ async function mirrorRealtime(server, token, rootDir, state, intervalMs) {
|
|
|
782
871
|
process.off('SIGTERM', onStop);
|
|
783
872
|
}
|
|
784
873
|
|
|
785
|
-
async function syncFolder(positionals, flags) {
|
|
786
|
-
|
|
787
|
-
if (!target) {
|
|
788
|
-
|
|
789
|
-
}
|
|
874
|
+
async function syncFolder(positionals, flags) {
|
|
875
|
+
let target = positionals[0];
|
|
876
|
+
if (!target && isInteractive()) {
|
|
877
|
+
target = await askText('Dossier a synchroniser');
|
|
878
|
+
}
|
|
879
|
+
if (!target) {
|
|
880
|
+
throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
|
|
881
|
+
}
|
|
790
882
|
|
|
791
883
|
const rootDir = path.resolve(process.cwd(), target);
|
|
792
884
|
if (!(await pathExists(rootDir))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iksdev/shard-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.12",
|
|
4
4
|
"description": "CLI pour synchroniser un dossier local avec Shard",
|
|
5
5
|
"bin": {
|
|
6
6
|
"shard": "bin/shard.js"
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"check": "node --check bin/shard.js"
|
|
11
11
|
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"ws": "^8.18.3"
|
|
14
|
+
},
|
|
12
15
|
"engines": {
|
|
13
16
|
"node": ">=20.0.0"
|
|
14
17
|
},
|