@iksdev/shard-cli 0.1.45 → 0.1.46

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 (2) hide show
  1. package/bin/shard.js +396 -86
  2. package/package.json +1 -1
package/bin/shard.js CHANGED
@@ -88,32 +88,32 @@ function printHelp() {
88
88
  ║ Shard ║
89
89
  ╚══════════════════════════════════════════════════╝
90
90
 
91
- Commandes disponibles:
92
- shard login Se connecter au serveur
93
- shard whoami Afficher l'utilisateur connecte
94
- shard account Afficher username, email et plan
95
- shard sync <dossier> Synchroniser un dossier local
96
- shard share <fichier> Partager un fichier via relay
97
- shard logout Se deconnecter
91
+ Commandes disponibles:
92
+ shard login Se connecter au serveur
93
+ shard whoami Afficher l'utilisateur connecte
94
+ shard account Afficher username, email et plan
95
+ shard sync <dossier> Synchroniser un dossier local
96
+ shard share <fichier> Partager un fichier via relay
97
+ shard logout Se deconnecter
98
98
  shard config show Afficher la configuration
99
99
  shard config set-server <url> Changer de serveur
100
100
 
101
101
  Mode interactif:
102
102
  Lance une commande sans arguments et la CLI te guidera etape par etape.
103
103
 
104
- Options avancees:
105
- login --username <n> --password <pass> [--server <url>]
106
- whoami [--server <url>]
107
- account [--server <url>]
108
- sync <dossier> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
109
- share <fichier> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]
104
+ Options avancees:
105
+ login --username <n> --password <pass> [--server <url>]
106
+ whoami [--server <url>]
107
+ account [--server <url>]
108
+ sync <dossier> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
109
+ share <fichier> [--server <url>] [--limits <n>] [--temps <jours>] [--upload]
110
110
 
111
111
  Exemples:
112
- shard login
113
- shard account
114
- shard sync ./MonDossier
115
- shard sync ./MonDossier --once
116
- shard share ./MonFichier.mp4
112
+ shard login
113
+ shard account
114
+ shard sync ./MonDossier
115
+ shard sync ./MonDossier --once
116
+ shard share ./MonFichier.mp4
117
117
  shard share ./MonFichier.mp4 --upload
118
118
 
119
119
  Serveur par defaut: https://shard-0ow4.onrender.com
@@ -300,7 +300,7 @@ async function login(flags) {
300
300
  }
301
301
  }
302
302
 
303
- async function whoami(flags) {
303
+ async function whoami(flags) {
304
304
  const config = await readConfig();
305
305
  const server = getServer(flags, config);
306
306
  const token = getToken(config);
@@ -316,28 +316,28 @@ async function whoami(flags) {
316
316
  const user = data.user || {};
317
317
  console.log(`Server: ${server}`);
318
318
  console.log(`User: ${user.username || user.userId || 'inconnu'}`);
319
- if (user.email) console.log(`Email: ${user.email}`);
320
- }
321
-
322
- async function account(flags) {
323
- const config = await readConfig();
324
- const server = getServer(flags, config);
325
- const token = getToken(config);
326
- if (!token) {
327
- throw new Error('Non connecte. Lance: shard login --username ... --password ...');
328
- }
329
-
330
- const data = await httpJson(`${server}/api/auth/profile`, {
331
- method: 'GET',
332
- headers: { Authorization: `Bearer ${token}` }
333
- });
334
-
335
- const user = data.user || {};
336
- const plan = String(user.billing_plan || 'free').toLowerCase();
337
- console.log(`Username: ${user.username || 'inconnu'}`);
338
- console.log(`Email: ${user.email || 'inconnu'}`);
339
- console.log(`Plan: ${plan}`);
340
- }
319
+ if (user.email) console.log(`Email: ${user.email}`);
320
+ }
321
+
322
+ async function account(flags) {
323
+ const config = await readConfig();
324
+ const server = getServer(flags, config);
325
+ const token = getToken(config);
326
+ if (!token) {
327
+ throw new Error('Non connecte. Lance: shard login --username ... --password ...');
328
+ }
329
+
330
+ const data = await httpJson(`${server}/api/auth/profile`, {
331
+ method: 'GET',
332
+ headers: { Authorization: `Bearer ${token}` }
333
+ });
334
+
335
+ const user = data.user || {};
336
+ const plan = String(user.billing_plan || 'free').toLowerCase();
337
+ console.log(`Username: ${user.username || 'inconnu'}`);
338
+ console.log(`Email: ${user.email || 'inconnu'}`);
339
+ console.log(`Plan: ${plan}`);
340
+ }
341
341
 
342
342
  async function logout() {
343
343
  const config = await readConfig();
@@ -1144,7 +1144,299 @@ async function syncFolder(positionals, flags) {
1144
1144
  }
1145
1145
  }
1146
1146
 
1147
- async function main() {
1147
+ // ─── Couleurs ANSI ───────────────────────────────────────────────────────────
1148
+
1149
+ const C = {
1150
+ reset: '\x1b[0m',
1151
+ bold: '\x1b[1m',
1152
+ dim: '\x1b[2m',
1153
+ cyan: '\x1b[36m',
1154
+ blue: '\x1b[34m',
1155
+ green: '\x1b[32m',
1156
+ yellow: '\x1b[33m',
1157
+ red: '\x1b[31m',
1158
+ white: '\x1b[97m',
1159
+ gray: '\x1b[90m',
1160
+ bgBlue: '\x1b[44m',
1161
+ bCyan: '\x1b[1m\x1b[36m',
1162
+ bWhite: '\x1b[1m\x1b[97m',
1163
+ bGreen: '\x1b[1m\x1b[32m',
1164
+ bRed: '\x1b[1m\x1b[31m',
1165
+ bYellow: '\x1b[1m\x1b[33m',
1166
+ };
1167
+
1168
+ function c(color, text) { return `${color}${text}${C.reset}`; }
1169
+
1170
+ // ─── Panel interactif ────────────────────────────────────────────────────────
1171
+
1172
+ const W = 64;
1173
+
1174
+ function clearScreen() {
1175
+ process.stdout.write('\x1Bc');
1176
+ }
1177
+
1178
+ function line(char = '─', color = C.gray) {
1179
+ return c(color, char.repeat(W));
1180
+ }
1181
+
1182
+ function boxLine(content = '', color = C.cyan) {
1183
+ const visible = content.replace(/\x1b\[[0-9;]*m/g, '');
1184
+ const pad = W - 2 - visible.length;
1185
+ return `${c(color, '│')} ${content}${' '.repeat(Math.max(0, pad))}${c(color, '│')}`;
1186
+ }
1187
+
1188
+ function centered(text, totalWidth = W - 2) {
1189
+ const visible = text.replace(/\x1b\[[0-9;]*m/g, '');
1190
+ const left = Math.floor((totalWidth - visible.length) / 2);
1191
+ const right = totalWidth - visible.length - left;
1192
+ return ' '.repeat(left) + text + ' '.repeat(right);
1193
+ }
1194
+
1195
+ function printPanel(username, plan, statusMsg) {
1196
+ const now = new Date();
1197
+ const timeStr = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
1198
+
1199
+ console.log('');
1200
+ console.log(`${c(C.cyan, '╔' + '═'.repeat(W - 2) + '╗')}`);
1201
+ console.log(`${c(C.cyan, '║')}${centered(c(C.bCyan, '◈ S H A R D ◈'), W - 2)}${c(C.cyan, '║')}`);
1202
+ console.log(`${c(C.cyan, '╠' + '═'.repeat(W - 2) + '╣')}`);
1203
+
1204
+ // Infos utilisateur
1205
+ if (username) {
1206
+ const userInfo = `${c(C.gray, 'connecté ')}${c(C.bWhite, username)}${plan ? c(C.gray, ' · ') + c(C.bYellow, plan.toUpperCase()) : ''}`;
1207
+ console.log(boxLine(centered(userInfo, W - 2), C.cyan));
1208
+ }
1209
+ console.log(`${c(C.cyan, '╠' + '═'.repeat(W - 2) + '╣')}`);
1210
+
1211
+ // Menu
1212
+ const entries = [
1213
+ { key: '1', icon: '⬆', label: 'share', desc: 'Partager un fichier via relay' },
1214
+ { key: '2', icon: '↻', label: 'sync', desc: 'Synchroniser un dossier local' },
1215
+ { key: '3', icon: '◉', label: 'account', desc: 'Infos du compte' },
1216
+ { key: '4', icon: '?', label: 'whoami', desc: 'Utilisateur connecté' },
1217
+ { key: '5', icon: '⚙', label: 'config', desc: 'Configuration serveur' },
1218
+ { key: '6', icon: '✕', label: 'logout', desc: 'Se déconnecter et quitter' },
1219
+ ];
1220
+
1221
+ console.log(boxLine('', C.cyan));
1222
+ for (const e of entries) {
1223
+ const keyPart = ` ${c(C.bgBlue + C.bWhite, ` ${e.key} `)}`;
1224
+ const iconPart = ` ${c(C.cyan, e.icon)}`;
1225
+ const lblPart = ` ${c(C.bWhite, e.label.padEnd(10))}`;
1226
+ const dscPart = c(C.gray, e.desc);
1227
+ console.log(boxLine(`${keyPart}${iconPart}${lblPart}${dscPart}`, C.cyan));
1228
+ }
1229
+ console.log(boxLine('', C.cyan));
1230
+
1231
+ console.log(`${c(C.cyan, '╠' + '═'.repeat(W - 2) + '╣')}`);
1232
+
1233
+ // Status / message
1234
+ if (statusMsg) {
1235
+ console.log(boxLine(centered(statusMsg, W - 2), C.cyan));
1236
+ } else {
1237
+ console.log(boxLine(centered(c(C.gray, `${timeStr} · tape un numéro ou le nom de la commande`), W - 2), C.cyan));
1238
+ }
1239
+
1240
+ console.log(`${c(C.cyan, '╚' + '═'.repeat(W - 2) + '╝')}`);
1241
+ console.log('');
1242
+ }
1243
+
1244
+ function printSubHeader(title, icon = '◈') {
1245
+ console.log('');
1246
+ console.log(line('─', C.cyan));
1247
+ console.log(` ${c(C.cyan, icon)} ${c(C.bWhite, title)}`);
1248
+ console.log(line('─', C.gray));
1249
+ console.log('');
1250
+ }
1251
+
1252
+ function printSuccess(msg) { console.log(`\n ${c(C.bGreen, '✔')} ${c(C.green, msg)}`); }
1253
+ function printError(msg) { console.log(`\n ${c(C.bRed, '✘')} ${c(C.red, msg)}`); }
1254
+ function printInfo(msg) { console.log(` ${c(C.cyan, '·')} ${c(C.white, msg)}`); }
1255
+
1256
+ async function askPanelInput(prompt) {
1257
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1258
+ const answer = await new Promise((resolve) => rl.question(prompt, resolve));
1259
+ rl.close();
1260
+ return answer.trim();
1261
+ }
1262
+
1263
+ async function pressEnter() {
1264
+ await askPanelInput(`\n ${c(C.gray, 'Appuie sur Entrée pour revenir au menu…')}`);
1265
+ }
1266
+
1267
+ async function panelShare() {
1268
+ clearScreen();
1269
+ printSubHeader('Partager un fichier', '⬆');
1270
+
1271
+ const target = await askText(` ${c(C.cyan, '›')} Chemin du fichier`);
1272
+ if (!target) return;
1273
+
1274
+ const limits = await askText(` ${c(C.cyan, '›')} Limite de téléchargements ${c(C.gray, '(0 = illimité)')}`, '0');
1275
+ const temps = await askText(` ${c(C.cyan, '›')} Expiration en jours ${c(C.gray, '(0 = aucune)')}`, '0');
1276
+ const modeRaw = await askText(` ${c(C.cyan, '›')} Mode upload serveur ? ${c(C.gray, '[o/n]')}`, 'n');
1277
+
1278
+ const flags = {};
1279
+ if (parseInt(limits, 10) > 0) flags.limits = limits;
1280
+ if (parseInt(temps, 10) > 0) flags.temps = temps;
1281
+ if (modeRaw.toLowerCase() === 'o') flags.upload = true;
1282
+
1283
+ console.log('');
1284
+ console.log(line('─', C.gray));
1285
+ try {
1286
+ await shareFile([target], flags);
1287
+ printSuccess('Partage créé avec succès.');
1288
+ } catch (err) {
1289
+ if (err?.data?.code === 'PLAN_FILE_LIMIT_EXCEEDED') {
1290
+ printPlanLimitBox(err);
1291
+ } else {
1292
+ printError(err.message);
1293
+ }
1294
+ }
1295
+ await pressEnter();
1296
+ }
1297
+
1298
+ async function panelSync() {
1299
+ clearScreen();
1300
+ printSubHeader('Synchroniser un dossier', '↻');
1301
+
1302
+ const target = await askText(` ${c(C.cyan, '›')} Chemin du dossier`);
1303
+ if (!target) return;
1304
+
1305
+ const onceRaw = await askText(` ${c(C.cyan, '›')} Sync unique (pas de surveillance) ? ${c(C.gray, '[o/n]')}`, 'n');
1306
+ const dryRaw = await askText(` ${c(C.cyan, '›')} Simulation (dry-run) ? ${c(C.gray, '[o/n]')}`, 'n');
1307
+ const forceRaw = await askText(` ${c(C.cyan, '›')} Forcer le re-upload de tout ? ${c(C.gray, '[o/n]')}`, 'n');
1308
+
1309
+ const flags = {};
1310
+ if (onceRaw.toLowerCase() === 'o') flags.once = true;
1311
+ if (dryRaw.toLowerCase() === 'o') flags['dry-run'] = true;
1312
+ if (forceRaw.toLowerCase() === 'o') flags.force = true;
1313
+
1314
+ console.log('');
1315
+ console.log(line('─', C.gray));
1316
+ try {
1317
+ await syncFolder([target], flags);
1318
+ if (flags.once || flags['dry-run']) {
1319
+ printSuccess('Synchronisation terminée.');
1320
+ await pressEnter();
1321
+ }
1322
+ // mode watch : syncFolder bloque, on revient au menu quand il se termine
1323
+ } catch (err) {
1324
+ printError(err.message);
1325
+ await pressEnter();
1326
+ }
1327
+ }
1328
+
1329
+ async function panelAccount() {
1330
+ clearScreen();
1331
+ printSubHeader('Informations du compte', '◉');
1332
+ try {
1333
+ await account({});
1334
+ } catch (err) {
1335
+ printError(err.message);
1336
+ }
1337
+ await pressEnter();
1338
+ }
1339
+
1340
+ async function panelWhoami() {
1341
+ clearScreen();
1342
+ printSubHeader('Utilisateur connecté', '?');
1343
+ try {
1344
+ await whoami({});
1345
+ } catch (err) {
1346
+ printError(err.message);
1347
+ }
1348
+ await pressEnter();
1349
+ }
1350
+
1351
+ async function panelConfig() {
1352
+ clearScreen();
1353
+ printSubHeader('Configuration', '⚙');
1354
+
1355
+ printInfo(`${c(C.gray, '[a]')} Afficher la configuration actuelle`);
1356
+ printInfo(`${c(C.gray, '[b]')} Changer de serveur`);
1357
+ console.log('');
1358
+
1359
+ const choice = await askPanelInput(` ${c(C.cyan, '›')} Choix ${c(C.gray, '[a/b]')} : `);
1360
+
1361
+ if (choice.toLowerCase() === 'a') {
1362
+ console.log('');
1363
+ console.log(line('─', C.gray));
1364
+ try {
1365
+ await showConfig();
1366
+ } catch (err) {
1367
+ printError(err.message);
1368
+ }
1369
+ } else if (choice.toLowerCase() === 'b') {
1370
+ const url = await askText(` ${c(C.cyan, '›')} Nouvelle URL du serveur`);
1371
+ if (url) {
1372
+ try {
1373
+ await setServer([url]);
1374
+ printSuccess(`Serveur mis à jour : ${url}`);
1375
+ } catch (err) {
1376
+ printError(err.message);
1377
+ }
1378
+ }
1379
+ } else {
1380
+ printError('Choix invalide.');
1381
+ }
1382
+ await pressEnter();
1383
+ }
1384
+
1385
+ async function runPanel(username, plan) {
1386
+ // Logout automatique à la fermeture du process
1387
+ process.on('exit', () => {
1388
+ try {
1389
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
1390
+ const parsed = JSON.parse(raw);
1391
+ parsed.token = '';
1392
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(parsed, null, 2), 'utf8');
1393
+ } catch (_) {}
1394
+ });
1395
+
1396
+ const handleStop = async (sig) => {
1397
+ process.stdout.write(`\n\n ${c(C.gray, `Fermeture (${sig})…`)}\n`);
1398
+ try {
1399
+ const cfg = await readConfig();
1400
+ await writeConfig({ ...cfg, token: '' });
1401
+ process.stdout.write(` ${c(C.green, '✔')} Session fermée. À bientôt !\n\n`);
1402
+ } catch (_) {}
1403
+ process.exit(0);
1404
+ };
1405
+ process.on('SIGINT', handleStop);
1406
+ process.on('SIGTERM', handleStop);
1407
+
1408
+ let statusMsg = null;
1409
+
1410
+ while (true) {
1411
+ clearScreen();
1412
+ printPanel(username, plan, statusMsg);
1413
+ statusMsg = null;
1414
+
1415
+ const answer = (await askPanelInput(` ${c(C.bCyan, '›')} `)).toLowerCase();
1416
+
1417
+ if (answer === '1' || answer === 'share') { await panelShare(); }
1418
+ else if (answer === '2' || answer === 'sync') { await panelSync(); }
1419
+ else if (answer === '3' || answer === 'account') { await panelAccount(); }
1420
+ else if (answer === '4' || answer === 'whoami') { await panelWhoami(); }
1421
+ else if (answer === '5' || answer === 'config') { await panelConfig(); }
1422
+ else if (answer === '6' || answer === 'logout') {
1423
+ clearScreen();
1424
+ process.removeListener('SIGINT', handleStop);
1425
+ process.removeListener('SIGTERM', handleStop);
1426
+ await logout();
1427
+ console.log(`\n ${c(C.bGreen, '✔')} Déconnecté avec succès. À bientôt !\n`);
1428
+ process.exit(0);
1429
+ } else if (answer === '') {
1430
+ // reboucle silencieusement
1431
+ } else {
1432
+ statusMsg = c(C.bRed, `✘ Option inconnue : "${answer}" · choisis 1–6`);
1433
+ }
1434
+ }
1435
+ }
1436
+
1437
+ // ─── main ─────────────────────────────────────────────────────────────────────
1438
+
1439
+ async function main() {
1148
1440
  const { command, positionals, flags } = parseArgs(process.argv.slice(2));
1149
1441
 
1150
1442
  // Vérifie la mise à jour à chaque commande (sauf --help)
@@ -1171,19 +1463,37 @@ async function main() {
1171
1463
 
1172
1464
  if (command === 'login') {
1173
1465
  await login(flags);
1466
+ // Après un login réussi en mode interactif, on lance le panel
1467
+ if (isInteractive()) {
1468
+ const config = await readConfig();
1469
+ const server = getServer(flags, config);
1470
+ const token = getToken(config);
1471
+ let username = String(flags.username || '').trim();
1472
+ // Récupère le username depuis le serveur si possible
1473
+ if (!username && token) {
1474
+ try {
1475
+ const data = await httpJson(`${server}/api/auth/verify`, {
1476
+ method: 'POST',
1477
+ headers: { Authorization: `Bearer ${token}` }
1478
+ });
1479
+ username = data.user?.username || data.user?.userId || '';
1480
+ } catch (_) {}
1481
+ }
1482
+ await runPanel(username);
1483
+ }
1484
+ return;
1485
+ }
1486
+
1487
+ if (command === 'whoami') {
1488
+ await whoami(flags);
1489
+ return;
1490
+ }
1491
+
1492
+ if (command === 'account') {
1493
+ await account(flags);
1174
1494
  return;
1175
1495
  }
1176
1496
 
1177
- if (command === 'whoami') {
1178
- await whoami(flags);
1179
- return;
1180
- }
1181
-
1182
- if (command === 'account') {
1183
- await account(flags);
1184
- return;
1185
- }
1186
-
1187
1497
  if (command === 'logout') {
1188
1498
  await logout();
1189
1499
  return;
@@ -1212,37 +1522,37 @@ async function main() {
1212
1522
  throw new Error('Usage: shard config show | shard config set-server <url>');
1213
1523
  }
1214
1524
 
1215
- throw new Error(`Commande inconnue: ${command}`);
1216
- }
1217
-
1218
- function printPlanLimitBox(error) {
1219
- const data = error?.data || {};
1220
- const plan = String(data.plan || '').toUpperCase() || 'FREE';
1221
- const maxFileSize = Number(data.maxFileSize || 0);
1222
- const fileSize = Number(data.fileSize || 0);
1223
- const maxLabel = maxFileSize > 0 ? formatBytes(maxFileSize) : '?';
1224
- const fileLabel = fileSize > 0 ? formatBytes(fileSize) : '?';
1225
- const lines = [
1226
- '',
1227
- '╔════════════════════════════════════════════════════╗',
1228
- `║ ⚠ Limite du plan ${plan}`.padEnd(52) + ' ║',
1229
- '╠════════════════════════════════════════════════════╣',
1230
- `║ Taille fichier : ${fileLabel}`.padEnd(52) + ' ║',
1231
- `║ Limite max : ${maxLabel} / fichier`.padEnd(52) + ' ║',
1232
- '║ ║',
1233
- '║ Passe a une offre superieure pour continuer. ║',
1234
- '╚════════════════════════════════════════════════════╝',
1235
- ''
1236
- ];
1237
- console.error(lines.join('\n'));
1238
- }
1239
-
1240
- main().catch((error) => {
1241
- if (error?.data?.code === 'PLAN_FILE_LIMIT_EXCEEDED') {
1242
- printPlanLimitBox(error);
1243
- process.exitCode = 1;
1244
- return;
1245
- }
1246
- console.error(`Erreur: ${error.message}`);
1247
- process.exitCode = 1;
1248
- });
1525
+ throw new Error(`Commande inconnue: ${command}`);
1526
+ }
1527
+
1528
+ function printPlanLimitBox(error) {
1529
+ const data = error?.data || {};
1530
+ const plan = String(data.plan || '').toUpperCase() || 'FREE';
1531
+ const maxFileSize = Number(data.maxFileSize || 0);
1532
+ const fileSize = Number(data.fileSize || 0);
1533
+ const maxLabel = maxFileSize > 0 ? formatBytes(maxFileSize) : '?';
1534
+ const fileLabel = fileSize > 0 ? formatBytes(fileSize) : '?';
1535
+ const lines = [
1536
+ '',
1537
+ '╔════════════════════════════════════════════════════╗',
1538
+ `║ ⚠ Limite du plan ${plan}`.padEnd(52) + ' ║',
1539
+ '╠════════════════════════════════════════════════════╣',
1540
+ `║ Taille fichier : ${fileLabel}`.padEnd(52) + ' ║',
1541
+ `║ Limite max : ${maxLabel} / fichier`.padEnd(52) + ' ║',
1542
+ '║ ║',
1543
+ '║ Passe a une offre superieure pour continuer. ║',
1544
+ '╚════════════════════════════════════════════════════╝',
1545
+ ''
1546
+ ];
1547
+ console.error(lines.join('\n'));
1548
+ }
1549
+
1550
+ main().catch((error) => {
1551
+ if (error?.data?.code === 'PLAN_FILE_LIMIT_EXCEEDED') {
1552
+ printPlanLimitBox(error);
1553
+ process.exitCode = 1;
1554
+ return;
1555
+ }
1556
+ console.error(`Erreur: ${error.message}`);
1557
+ process.exitCode = 1;
1558
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iksdev/shard-cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.46",
4
4
  "description": "CLI pour synchroniser un dossier local avec Shard",
5
5
  "bin": {
6
6
  "shard": "bin/shard.js"