@iksdev/shard-cli 0.1.11 → 0.1.13
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 +121 -19
- 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,76 @@ 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
|
+
function stripWrappingQuotes(value) {
|
|
70
|
+
let out = String(value || '').trim();
|
|
71
|
+
if (!out) return out;
|
|
72
|
+
if ((out.startsWith('"') && out.endsWith('"')) || (out.startsWith("'") && out.endsWith("'"))) {
|
|
73
|
+
out = out.slice(1, -1).trim();
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function askText(label, defaultValue = '') {
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout
|
|
82
|
+
});
|
|
83
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
84
|
+
const answer = await new Promise((resolve) => rl.question(`${label}${suffix}: `, resolve));
|
|
85
|
+
rl.close();
|
|
86
|
+
const trimmed = stripWrappingQuotes(answer);
|
|
87
|
+
return trimmed || stripWrappingQuotes(defaultValue);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function askSecret(label) {
|
|
91
|
+
if (!isInteractive()) return '';
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const stdin = process.stdin;
|
|
94
|
+
let value = '';
|
|
95
|
+
process.stdout.write(`${label}: `);
|
|
96
|
+
|
|
97
|
+
const cleanup = () => {
|
|
98
|
+
stdin.off('data', onData);
|
|
99
|
+
try { if (stdin.isTTY) stdin.setRawMode(false); } catch (_) {}
|
|
100
|
+
stdin.pause();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const onData = (buf) => {
|
|
104
|
+
const char = String(buf || '');
|
|
105
|
+
if (char === '\u0003') {
|
|
106
|
+
cleanup();
|
|
107
|
+
reject(new Error('Interrompu'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (char === '\r' || char === '\n') {
|
|
111
|
+
process.stdout.write('\n');
|
|
112
|
+
cleanup();
|
|
113
|
+
resolve(value.trim());
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (char === '\u007f' || char === '\b') {
|
|
117
|
+
if (value.length > 0) {
|
|
118
|
+
value = value.slice(0, -1);
|
|
119
|
+
process.stdout.write('\b \b');
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
value += char;
|
|
124
|
+
process.stdout.write('*');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
try { if (stdin.isTTY) stdin.setRawMode(true); } catch (_) {}
|
|
128
|
+
stdin.resume();
|
|
129
|
+
stdin.on('data', onData);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
63
132
|
|
|
64
133
|
function normalizeServer(input) {
|
|
65
134
|
const raw = String(input || '').trim();
|
|
@@ -113,15 +182,24 @@ async function httpJson(url, options = {}) {
|
|
|
113
182
|
return data;
|
|
114
183
|
}
|
|
115
184
|
|
|
116
|
-
async function login(flags) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
185
|
+
async function login(flags) {
|
|
186
|
+
const config = await readConfig();
|
|
187
|
+
let username = String(flags.username || '').trim();
|
|
188
|
+
let password = String(flags.password || '').trim();
|
|
189
|
+
let server = getServer(flags, config);
|
|
190
|
+
|
|
191
|
+
if (isInteractive()) {
|
|
192
|
+
if (!username) username = await askText('Username');
|
|
193
|
+
if (!password) password = await askSecret('Password');
|
|
194
|
+
if (!flags.server) {
|
|
195
|
+
const typedServer = await askText('Serveur', server);
|
|
196
|
+
server = normalizeServer(typedServer || server);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!username || !password) {
|
|
201
|
+
throw new Error('Usage: shard login --username <name> --password <pass> [--server <url>]');
|
|
202
|
+
}
|
|
125
203
|
|
|
126
204
|
const data = await httpJson(`${server}/api/auth/login`, {
|
|
127
205
|
method: 'POST',
|
|
@@ -320,8 +398,25 @@ function toWebSocketUrl(serverUrl, token) {
|
|
|
320
398
|
return url.toString();
|
|
321
399
|
}
|
|
322
400
|
|
|
401
|
+
function normalizeAbsPathForId(absPath) {
|
|
402
|
+
const raw = String(absPath || '').trim();
|
|
403
|
+
if (process.platform === 'win32') return raw.toLowerCase();
|
|
404
|
+
return raw;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function stableRelayFileId(absPath) {
|
|
408
|
+
return crypto
|
|
409
|
+
.createHash('sha256')
|
|
410
|
+
.update(normalizeAbsPathForId(absPath))
|
|
411
|
+
.digest('base64url')
|
|
412
|
+
.slice(0, 24);
|
|
413
|
+
}
|
|
414
|
+
|
|
323
415
|
async function shareFile(positionals, flags) {
|
|
324
|
-
|
|
416
|
+
let target = stripWrappingQuotes(positionals[0]);
|
|
417
|
+
if (!target && isInteractive()) {
|
|
418
|
+
target = await askText('Fichier a partager');
|
|
419
|
+
}
|
|
325
420
|
if (!target) {
|
|
326
421
|
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]');
|
|
327
422
|
}
|
|
@@ -354,7 +449,7 @@ async function shareFile(positionals, flags) {
|
|
|
354
449
|
const fileName = path.basename(absPath);
|
|
355
450
|
if (localMode) {
|
|
356
451
|
const mimeType = guessMime(absPath);
|
|
357
|
-
const relayFileId =
|
|
452
|
+
const relayFileId = stableRelayFileId(absPath);
|
|
358
453
|
const wsUrl = toWebSocketUrl(server, token);
|
|
359
454
|
const relaySocket = new WebSocket(wsUrl);
|
|
360
455
|
|
|
@@ -404,6 +499,8 @@ async function shareFile(positionals, flags) {
|
|
|
404
499
|
});
|
|
405
500
|
await helloPromise;
|
|
406
501
|
|
|
502
|
+
relaySocket.send(JSON.stringify({ type: 'register_file', relayFileId }));
|
|
503
|
+
|
|
407
504
|
relaySocket.on('message', async (event) => {
|
|
408
505
|
let msg = null;
|
|
409
506
|
try { msg = JSON.parse(String(event || '')); } catch (_) { return; }
|
|
@@ -444,6 +541,7 @@ async function shareFile(positionals, flags) {
|
|
|
444
541
|
fileName,
|
|
445
542
|
fileSize: st.size,
|
|
446
543
|
mimeType,
|
|
544
|
+
// Compat backend ancien + nouveau:
|
|
447
545
|
relayClientId,
|
|
448
546
|
relayFileId
|
|
449
547
|
};
|
|
@@ -464,6 +562,7 @@ async function shareFile(positionals, flags) {
|
|
|
464
562
|
if (share.url) console.log(`URL Shard: ${share.url}`);
|
|
465
563
|
if (share.token) console.log(`Token: ${share.token}`);
|
|
466
564
|
console.log(`Relay client id: ${relayClientId}`);
|
|
565
|
+
console.log(`Relay file id: ${relayFileId}`);
|
|
467
566
|
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
468
567
|
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
469
568
|
console.log('Laisse cette commande ouverte tant que le partage doit fonctionner.');
|
|
@@ -783,11 +882,14 @@ async function mirrorRealtime(server, token, rootDir, state, intervalMs) {
|
|
|
783
882
|
process.off('SIGTERM', onStop);
|
|
784
883
|
}
|
|
785
884
|
|
|
786
|
-
async function syncFolder(positionals, flags) {
|
|
787
|
-
|
|
788
|
-
if (!target) {
|
|
789
|
-
|
|
790
|
-
}
|
|
885
|
+
async function syncFolder(positionals, flags) {
|
|
886
|
+
let target = positionals[0];
|
|
887
|
+
if (!target && isInteractive()) {
|
|
888
|
+
target = await askText('Dossier a synchroniser');
|
|
889
|
+
}
|
|
890
|
+
if (!target) {
|
|
891
|
+
throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
|
|
892
|
+
}
|
|
791
893
|
|
|
792
894
|
const rootDir = path.resolve(process.cwd(), target);
|
|
793
895
|
if (!(await pathExists(rootDir))) {
|