@iksdev/shard-cli 0.1.11 → 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 +111 -20
- package/package.json +1 -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,7 @@ 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');
|
|
8
9
|
const { WebSocket } = require('ws');
|
|
9
10
|
const { Readable } = require('stream');
|
|
10
11
|
const { pipeline } = require('stream/promises');
|
|
@@ -37,7 +38,7 @@ Examples:
|
|
|
37
38
|
`);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function parseArgs(rawArgs) {
|
|
41
|
+
function parseArgs(rawArgs) {
|
|
41
42
|
const args = [...rawArgs];
|
|
42
43
|
const command = args.shift();
|
|
43
44
|
const positionals = [];
|
|
@@ -58,8 +59,67 @@ function parseArgs(rawArgs) {
|
|
|
58
59
|
flags[key] = next;
|
|
59
60
|
i += 1;
|
|
60
61
|
}
|
|
61
|
-
return { command, positionals, flags };
|
|
62
|
-
}
|
|
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
|
+
}
|
|
63
123
|
|
|
64
124
|
function normalizeServer(input) {
|
|
65
125
|
const raw = String(input || '').trim();
|
|
@@ -113,15 +173,24 @@ async function httpJson(url, options = {}) {
|
|
|
113
173
|
return data;
|
|
114
174
|
}
|
|
115
175
|
|
|
116
|
-
async function login(flags) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
+
}
|
|
125
194
|
|
|
126
195
|
const data = await httpJson(`${server}/api/auth/login`, {
|
|
127
196
|
method: 'POST',
|
|
@@ -320,8 +389,25 @@ function toWebSocketUrl(serverUrl, token) {
|
|
|
320
389
|
return url.toString();
|
|
321
390
|
}
|
|
322
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
|
+
|
|
323
406
|
async function shareFile(positionals, flags) {
|
|
324
|
-
|
|
407
|
+
let target = positionals[0];
|
|
408
|
+
if (!target && isInteractive()) {
|
|
409
|
+
target = await askText('Fichier a partager');
|
|
410
|
+
}
|
|
325
411
|
if (!target) {
|
|
326
412
|
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
327
413
|
}
|
|
@@ -354,7 +440,7 @@ async function shareFile(positionals, flags) {
|
|
|
354
440
|
const fileName = path.basename(absPath);
|
|
355
441
|
if (localMode) {
|
|
356
442
|
const mimeType = guessMime(absPath);
|
|
357
|
-
const relayFileId =
|
|
443
|
+
const relayFileId = stableRelayFileId(absPath);
|
|
358
444
|
const wsUrl = toWebSocketUrl(server, token);
|
|
359
445
|
const relaySocket = new WebSocket(wsUrl);
|
|
360
446
|
|
|
@@ -404,6 +490,8 @@ async function shareFile(positionals, flags) {
|
|
|
404
490
|
});
|
|
405
491
|
await helloPromise;
|
|
406
492
|
|
|
493
|
+
relaySocket.send(JSON.stringify({ type: 'register_file', relayFileId }));
|
|
494
|
+
|
|
407
495
|
relaySocket.on('message', async (event) => {
|
|
408
496
|
let msg = null;
|
|
409
497
|
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
@@ -444,7 +532,6 @@ async function shareFile(positionals, flags) {
|
|
|
444
532
|
fileName,
|
|
445
533
|
fileSize: st.size,
|
|
446
534
|
mimeType,
|
|
447
|
-
relayClientId,
|
|
448
535
|
relayFileId
|
|
449
536
|
};
|
|
450
537
|
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
@@ -464,6 +551,7 @@ async function shareFile(positionals, flags) {
|
|
|
464
551
|
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
465
552
|
if (share.token) console.log(`Token: ${share.token}`);
|
|
466
553
|
console.log(`Relay client id: ${relayClientId}`);
|
|
554
|
+
console.log(`Relay file id: ${relayFileId}`);
|
|
467
555
|
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
468
556
|
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
469
557
|
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
@@ -783,11 +871,14 @@ async function mirrorRealtime(server, token, rootDir, state, intervalMs) {
|
|
|
783
871
|
process.off('SIGTERM', onStop);
|
|
784
872
|
}
|
|
785
873
|
|
|
786
|
-
async function syncFolder(positionals, flags) {
|
|
787
|
-
|
|
788
|
-
if (!target) {
|
|
789
|
-
|
|
790
|
-
}
|
|
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
|
+
}
|
|
791
882
|
|
|
792
883
|
const rootDir = path.resolve(process.cwd(), target);
|
|
793
884
|
if (!(await pathExists(rootDir))) {
|