@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/bin/shard.js +121 -19
  3. 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 username = flags.username;
118
- const password = flags.password;
119
- if (!username || !password) {
120
- throw new Error('Usage: shard login --username <name> --password <pass> [--server <url>]');
121
- }
122
-
123
- const config = await readConfig();
124
- const server = getServer(flags, config);
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
- const target = positionals[0];
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 = crypto.randomBytes(12).toString('base64url');
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
- const target = positionals[0];
788
- if (!target) {
789
- throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
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))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iksdev/shard-cli",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "CLI pour synchroniser un dossier local avec Shard",
5
5
  "bin": {
6
6
  "shard": "bin/shard.js"