@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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/bin/shard.js +111 -20
  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,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 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);
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
- const target = positionals[0];
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 = crypto.randomBytes(12).toString('base64url');
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
- 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
- }
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))) {
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.12",
4
4
  "description": "CLI pour synchroniser un dossier local avec Shard",
5
5
  "bin": {
6
6
  "shard": "bin/shard.js"