@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/bin/shard.js +119 -27
  3. 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 username = flags.username;
117
- const password = flags.password;
118
- if (!username || !password) {
119
- throw new Error('Usage: shard login --username <name> --password <pass> [--server <url>]');
120
- }
121
-
122
- const config = await readConfig();
123
- const server = getServer(flags, config);
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
- const target = positionals[0];
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 = crypto.randomBytes(12).toString('base64url');
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.addEventListener('open', () => {
465
+ relaySocket.on('open', () => {
379
466
  clearTimeout(timer);
380
467
  resolve();
381
468
  });
382
- relaySocket.addEventListener('error', (event) => {
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.addEventListener('message', (event) => {
480
+ relaySocket.on('message', (event) => {
394
481
  let msg = null;
395
- try { msg = JSON.parse(String(event.data || '')); } catch (_) { return; }
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.addEventListener('message', async (event) => {
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.data || '')); } catch (_) { return; }
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.addEventListener('close', () => resolve());
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
- const target = positionals[0];
787
- if (!target) {
788
- throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
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.10",
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
  },