@iksdev/shard-cli 0.1.2 → 0.1.5
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/bin/shard.js +372 -3
- package/package.json +1 -1
package/bin/shard.js
CHANGED
|
@@ -4,6 +4,8 @@ const fs = require('fs');
|
|
|
4
4
|
const fsp = require('fs/promises');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const { Readable } = require('stream');
|
|
8
|
+
const { pipeline } = require('stream/promises');
|
|
7
9
|
|
|
8
10
|
const CONFIG_DIR = path.join(os.homedir(), '.shard-cli');
|
|
9
11
|
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -17,7 +19,8 @@ function printHelp() {
|
|
|
17
19
|
Usage:
|
|
18
20
|
shard login --username <name> --password <pass> [--server <url>]
|
|
19
21
|
shard whoami [--server <url>]
|
|
20
|
-
shard sync <folder> [--server <url>] [--dry-run] [--force]
|
|
22
|
+
shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]
|
|
23
|
+
shard share <file> [--server <url>] [--limits <n>] [--temps <jours>]
|
|
21
24
|
shard logout
|
|
22
25
|
shard config show
|
|
23
26
|
shard config set-server <url>
|
|
@@ -25,7 +28,9 @@ Usage:
|
|
|
25
28
|
Examples:
|
|
26
29
|
shard login --server http://localhost:3000 --username admin --password secret
|
|
27
30
|
shard sync ./MonDossier
|
|
31
|
+
shard sync ./MonDossier --once
|
|
28
32
|
shard sync ./MonDossier --dry-run
|
|
33
|
+
shard share ./MonFichier.mp4 --limits 0 --temps 0
|
|
29
34
|
`);
|
|
30
35
|
}
|
|
31
36
|
|
|
@@ -285,10 +290,357 @@ async function uploadOneFile(server, token, file) {
|
|
|
285
290
|
});
|
|
286
291
|
}
|
|
287
292
|
|
|
293
|
+
async function findRemoteFileByNameAndSize(server, token, fileName, fileSize) {
|
|
294
|
+
const data = await httpJson(`${server}/api/files?limit=100&offset=0&sort=created_at&order=desc&search=${encodeURIComponent(fileName)}`, {
|
|
295
|
+
method: 'GET',
|
|
296
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
297
|
+
});
|
|
298
|
+
const rows = Array.isArray(data.files) ? data.files : [];
|
|
299
|
+
return rows.find((row) => row.original_name === fileName && Number(row.file_size || 0) === Number(fileSize || 0)) || null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseOptionalPositiveInt(raw, flagName) {
|
|
303
|
+
if (raw === undefined || raw === null) return undefined;
|
|
304
|
+
const n = parseInt(String(raw), 10);
|
|
305
|
+
if (Number.isNaN(n) || n < 0) {
|
|
306
|
+
throw new Error(`${flagName} doit etre un entier >= 0`);
|
|
307
|
+
}
|
|
308
|
+
return n;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function shareFile(positionals, flags) {
|
|
312
|
+
const target = positionals[0];
|
|
313
|
+
if (!target) {
|
|
314
|
+
throw new Error('Usage: shard share <file> [--server <url>] [--limits <n>] [--temps <jours>]');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const absPath = path.resolve(process.cwd(), target);
|
|
318
|
+
if (!(await pathExists(absPath))) {
|
|
319
|
+
throw new Error(`Fichier introuvable: ${absPath}`);
|
|
320
|
+
}
|
|
321
|
+
const st = await fsp.stat(absPath);
|
|
322
|
+
if (!st.isFile()) {
|
|
323
|
+
throw new Error(`Ce n'est pas un fichier: ${absPath}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const limits = parseOptionalPositiveInt(flags.limits, '--limits');
|
|
327
|
+
const temps = parseOptionalPositiveInt(flags.temps, '--temps');
|
|
328
|
+
|
|
329
|
+
const config = await readConfig();
|
|
330
|
+
const server = getServer(flags, config);
|
|
331
|
+
const token = getToken(config);
|
|
332
|
+
if (!token) {
|
|
333
|
+
throw new Error('Non connecte. Lance: shard login --username ... --password ...');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await httpJson(`${server}/api/auth/verify`, {
|
|
337
|
+
method: 'POST',
|
|
338
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const fileName = path.basename(absPath);
|
|
342
|
+
let remote = await findRemoteFileByNameAndSize(server, token, fileName, st.size);
|
|
343
|
+
let fileId = remote?.id || null;
|
|
344
|
+
|
|
345
|
+
if (!fileId) {
|
|
346
|
+
console.log(`Upload necessaire: ${fileName} (${formatBytes(st.size)})`);
|
|
347
|
+
const uploaded = await uploadOneFile(server, token, {
|
|
348
|
+
absPath,
|
|
349
|
+
relPath: fileName,
|
|
350
|
+
size: st.size,
|
|
351
|
+
mtimeMs: Math.round(st.mtimeMs)
|
|
352
|
+
});
|
|
353
|
+
fileId = uploaded?.file?.id;
|
|
354
|
+
if (!fileId) {
|
|
355
|
+
throw new Error('Upload reussi mais ID fichier manquant');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const payload = { fileId };
|
|
360
|
+
if (limits !== undefined && limits > 0) payload.maxDownloads = limits;
|
|
361
|
+
if (temps !== undefined && temps > 0) payload.expiresInDays = temps;
|
|
362
|
+
|
|
363
|
+
const created = await httpJson(`${server}/api/share/create`, {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: {
|
|
366
|
+
Authorization: `Bearer ${token}`,
|
|
367
|
+
'Content-Type': 'application/json'
|
|
368
|
+
},
|
|
369
|
+
body: JSON.stringify(payload)
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const share = created?.share || {};
|
|
373
|
+
console.log(`Partage cree pour: ${fileName}`);
|
|
374
|
+
if (share.url) console.log(`URL: ${share.url}`);
|
|
375
|
+
if (share.token) console.log(`Token: ${share.token}`);
|
|
376
|
+
console.log(`Limite downloads: ${limits && limits > 0 ? limits : 'illimitee'}`);
|
|
377
|
+
console.log(`Expiration: ${temps && temps > 0 ? `${temps} jour(s)` : 'aucune'}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function fileListToMap(files) {
|
|
381
|
+
const map = new Map();
|
|
382
|
+
for (const file of files) {
|
|
383
|
+
map.set(file.relPath, file);
|
|
384
|
+
}
|
|
385
|
+
return map;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function makeFingerprint(file) {
|
|
389
|
+
return `${file.size}:${file.mtimeMs}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function normalizeRemoteRelPath(name) {
|
|
393
|
+
return String(name || '').replace(/\\/g, '/');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function fetchAllRemoteFiles(server, token) {
|
|
397
|
+
const out = [];
|
|
398
|
+
const limit = 100;
|
|
399
|
+
let offset = 0;
|
|
400
|
+
|
|
401
|
+
while (true) {
|
|
402
|
+
const data = await httpJson(`${server}/api/files?limit=${limit}&offset=${offset}&sort=created_at&order=asc`, {
|
|
403
|
+
method: 'GET',
|
|
404
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
405
|
+
});
|
|
406
|
+
const rows = Array.isArray(data.files) ? data.files : [];
|
|
407
|
+
for (const row of rows) {
|
|
408
|
+
out.push({
|
|
409
|
+
id: row.id,
|
|
410
|
+
relPath: normalizeRemoteRelPath(row.original_name),
|
|
411
|
+
size: Number(row.file_size || 0)
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
if (!data.pagination?.hasMore) break;
|
|
415
|
+
offset += limit;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return out;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function downloadRemoteFile(server, token, remoteFile, destPath) {
|
|
422
|
+
const res = await fetch(`${server}/api/files/${remoteFile.id}/download`, {
|
|
423
|
+
method: 'GET',
|
|
424
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!res.ok) {
|
|
428
|
+
let detail = `HTTP ${res.status}`;
|
|
429
|
+
try {
|
|
430
|
+
const data = await res.json();
|
|
431
|
+
detail = data.message || data.error || detail;
|
|
432
|
+
} catch {
|
|
433
|
+
// ignore JSON parse failures
|
|
434
|
+
}
|
|
435
|
+
throw new Error(detail);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!res.body) {
|
|
439
|
+
throw new Error('Reponse de telechargement vide');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
await fsp.mkdir(path.dirname(destPath), { recursive: true });
|
|
443
|
+
const tempPath = `${destPath}.part`;
|
|
444
|
+
const writable = fs.createWriteStream(tempPath);
|
|
445
|
+
await pipeline(Readable.fromWeb(res.body), writable);
|
|
446
|
+
await fsp.rename(tempPath, destPath);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async function pullMissingRemoteFiles(server, token, rootDir, state, localFiles, remoteFiles, dryRun) {
|
|
450
|
+
const localMap = fileListToMap(localFiles);
|
|
451
|
+
const missingRemote = remoteFiles.filter((rf) => !localMap.has(rf.relPath));
|
|
452
|
+
|
|
453
|
+
if (missingRemote.length === 0) return { downloaded: 0, errors: 0 };
|
|
454
|
+
|
|
455
|
+
console.log(`A telecharger: ${missingRemote.length}`);
|
|
456
|
+
if (dryRun) {
|
|
457
|
+
for (const rf of missingRemote.slice(0, 50)) console.log(`- ${rf.relPath} (remote)`); // eslint-disable-line no-console
|
|
458
|
+
if (missingRemote.length > 50) console.log(`... +${missingRemote.length - 50} autres`);
|
|
459
|
+
return { downloaded: 0, errors: 0 };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
let downloaded = 0;
|
|
463
|
+
let errors = 0;
|
|
464
|
+
for (let i = 0; i < missingRemote.length; i += 1) {
|
|
465
|
+
const rf = missingRemote[i];
|
|
466
|
+
const label = `[DOWN ${i + 1}/${missingRemote.length}]`;
|
|
467
|
+
try {
|
|
468
|
+
const absPath = path.join(rootDir, ...rf.relPath.split('/'));
|
|
469
|
+
console.log(`${label} GET ${rf.relPath} (${formatBytes(rf.size)})`);
|
|
470
|
+
await downloadRemoteFile(server, token, rf, absPath);
|
|
471
|
+
const st = await fsp.stat(absPath);
|
|
472
|
+
state.files[rf.relPath] = {
|
|
473
|
+
size: st.size,
|
|
474
|
+
mtimeMs: Math.round(st.mtimeMs),
|
|
475
|
+
uploadedAt: new Date().toISOString(),
|
|
476
|
+
remoteId: rf.id
|
|
477
|
+
};
|
|
478
|
+
downloaded += 1;
|
|
479
|
+
console.log(`${label} OK ${rf.relPath}`);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
errors += 1;
|
|
482
|
+
console.error(`${label} FAIL ${rf.relPath} -> ${error.message}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return { downloaded, errors };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function seedStateFromRemote(state, localFiles, remoteFiles) {
|
|
490
|
+
const remoteByPath = new Map(remoteFiles.map((rf) => [rf.relPath, rf]));
|
|
491
|
+
for (const lf of localFiles) {
|
|
492
|
+
if (state.files[lf.relPath]) continue;
|
|
493
|
+
const remote = remoteByPath.get(lf.relPath);
|
|
494
|
+
if (!remote) continue;
|
|
495
|
+
if (Number(remote.size) !== Number(lf.size)) continue;
|
|
496
|
+
state.files[lf.relPath] = {
|
|
497
|
+
size: lf.size,
|
|
498
|
+
mtimeMs: lf.mtimeMs,
|
|
499
|
+
uploadedAt: new Date().toISOString(),
|
|
500
|
+
remoteId: remote.id
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function deleteRemoteFile(server, token, remoteId) {
|
|
506
|
+
return httpJson(`${server}/api/files/${remoteId}`, {
|
|
507
|
+
method: 'DELETE',
|
|
508
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function renameRemoteFile(server, token, remoteId, newName) {
|
|
513
|
+
return httpJson(`${server}/api/files/${remoteId}/rename`, {
|
|
514
|
+
method: 'PATCH',
|
|
515
|
+
headers: {
|
|
516
|
+
Authorization: `Bearer ${token}`,
|
|
517
|
+
'Content-Type': 'application/json'
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({ newName })
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function sleep(ms) {
|
|
524
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function mirrorRealtime(server, token, rootDir, state, intervalMs) {
|
|
528
|
+
console.log(`Mode temps reel actif (${intervalMs} ms). Ctrl+C pour quitter.`);
|
|
529
|
+
|
|
530
|
+
let stopped = false;
|
|
531
|
+
const onStop = () => { stopped = true; };
|
|
532
|
+
process.on('SIGINT', onStop);
|
|
533
|
+
process.on('SIGTERM', onStop);
|
|
534
|
+
|
|
535
|
+
let previousFiles = await listFilesRecursive(rootDir);
|
|
536
|
+
let previousMap = fileListToMap(previousFiles);
|
|
537
|
+
|
|
538
|
+
while (!stopped) {
|
|
539
|
+
await sleep(intervalMs);
|
|
540
|
+
if (stopped) break;
|
|
541
|
+
|
|
542
|
+
let currentFiles;
|
|
543
|
+
try {
|
|
544
|
+
currentFiles = await listFilesRecursive(rootDir);
|
|
545
|
+
} catch (error) {
|
|
546
|
+
console.error(`Watcher error: ${error.message}`);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const currentMap = fileListToMap(currentFiles);
|
|
551
|
+
const prevKeys = new Set(previousMap.keys());
|
|
552
|
+
const currKeys = new Set(currentMap.keys());
|
|
553
|
+
const removed = [...prevKeys].filter((k) => !currKeys.has(k)).map((k) => previousMap.get(k));
|
|
554
|
+
const added = [...currKeys].filter((k) => !prevKeys.has(k)).map((k) => currentMap.get(k));
|
|
555
|
+
const changed = [...currKeys]
|
|
556
|
+
.filter((k) => prevKeys.has(k))
|
|
557
|
+
.map((k) => ({ prev: previousMap.get(k), cur: currentMap.get(k) }))
|
|
558
|
+
.filter((p) => p.prev.size !== p.cur.size || p.prev.mtimeMs !== p.cur.mtimeMs)
|
|
559
|
+
.map((p) => p.cur);
|
|
560
|
+
|
|
561
|
+
// Detecter les renames simples (meme taille + mtime) pour appeler PATCH rename.
|
|
562
|
+
const addedByFinger = new Map();
|
|
563
|
+
for (const a of added) {
|
|
564
|
+
const key = makeFingerprint(a);
|
|
565
|
+
if (!addedByFinger.has(key)) addedByFinger.set(key, []);
|
|
566
|
+
addedByFinger.get(key).push(a);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const renamedPairs = [];
|
|
570
|
+
const trulyRemoved = [];
|
|
571
|
+
for (const r of removed) {
|
|
572
|
+
const matches = addedByFinger.get(makeFingerprint(r)) || [];
|
|
573
|
+
const remoteId = state.files[r.relPath]?.remoteId || null;
|
|
574
|
+
if (remoteId && matches.length > 0) {
|
|
575
|
+
const to = matches.shift();
|
|
576
|
+
renamedPairs.push({ from: r, to, remoteId });
|
|
577
|
+
} else {
|
|
578
|
+
trulyRemoved.push(r);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const renamedTargets = new Set(renamedPairs.map((p) => p.to.relPath));
|
|
582
|
+
const trulyAdded = added.filter((a) => !renamedTargets.has(a.relPath));
|
|
583
|
+
|
|
584
|
+
for (const pair of renamedPairs) {
|
|
585
|
+
try {
|
|
586
|
+
await renameRemoteFile(server, token, pair.remoteId, pair.to.relPath);
|
|
587
|
+
delete state.files[pair.from.relPath];
|
|
588
|
+
state.files[pair.to.relPath] = {
|
|
589
|
+
size: pair.to.size,
|
|
590
|
+
mtimeMs: pair.to.mtimeMs,
|
|
591
|
+
uploadedAt: new Date().toISOString(),
|
|
592
|
+
remoteId: pair.remoteId
|
|
593
|
+
};
|
|
594
|
+
console.log(`[WATCH] RENAME ${pair.from.relPath} -> ${pair.to.relPath}`);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error(`[WATCH] RENAME FAIL ${pair.from.relPath} -> ${pair.to.relPath}: ${error.message}`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
for (const file of trulyRemoved) {
|
|
601
|
+
const remoteId = state.files[file.relPath]?.remoteId || null;
|
|
602
|
+
if (!remoteId) {
|
|
603
|
+
delete state.files[file.relPath];
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
await deleteRemoteFile(server, token, remoteId);
|
|
608
|
+
delete state.files[file.relPath];
|
|
609
|
+
console.log(`[WATCH] DELETE ${file.relPath}`);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
console.error(`[WATCH] DELETE FAIL ${file.relPath}: ${error.message}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const uploads = [...trulyAdded, ...changed];
|
|
616
|
+
for (const file of uploads) {
|
|
617
|
+
try {
|
|
618
|
+
const result = await uploadOneFile(server, token, file);
|
|
619
|
+
state.files[file.relPath] = {
|
|
620
|
+
size: file.size,
|
|
621
|
+
mtimeMs: file.mtimeMs,
|
|
622
|
+
uploadedAt: new Date().toISOString(),
|
|
623
|
+
remoteId: result?.file?.id || state.files[file.relPath]?.remoteId || null
|
|
624
|
+
};
|
|
625
|
+
console.log(`[WATCH] UPLOAD ${file.relPath} (${formatBytes(file.size)})`);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
console.error(`[WATCH] UPLOAD FAIL ${file.relPath}: ${error.message}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
await writeState(rootDir, state);
|
|
632
|
+
previousFiles = currentFiles;
|
|
633
|
+
previousMap = currentMap;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
process.off('SIGINT', onStop);
|
|
637
|
+
process.off('SIGTERM', onStop);
|
|
638
|
+
}
|
|
639
|
+
|
|
288
640
|
async function syncFolder(positionals, flags) {
|
|
289
641
|
const target = positionals[0];
|
|
290
642
|
if (!target) {
|
|
291
|
-
throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force]');
|
|
643
|
+
throw new Error('Usage: shard sync <folder> [--server <url>] [--dry-run] [--force] [--once] [--interval-ms <n>]');
|
|
292
644
|
}
|
|
293
645
|
|
|
294
646
|
const rootDir = path.resolve(process.cwd(), target);
|
|
@@ -312,10 +664,18 @@ async function syncFolder(positionals, flags) {
|
|
|
312
664
|
headers: { Authorization: `Bearer ${token}` }
|
|
313
665
|
});
|
|
314
666
|
|
|
315
|
-
const files = await listFilesRecursive(rootDir);
|
|
316
667
|
const state = await readState(rootDir);
|
|
317
668
|
const force = Boolean(flags.force);
|
|
318
669
|
const dryRun = Boolean(flags['dry-run']);
|
|
670
|
+
const once = Boolean(flags.once);
|
|
671
|
+
const intervalMs = Math.max(parseInt(flags['interval-ms'] || '2000', 10) || 2000, 500);
|
|
672
|
+
|
|
673
|
+
const remoteFiles = await fetchAllRemoteFiles(server, token);
|
|
674
|
+
const initialLocalFiles = await listFilesRecursive(rootDir);
|
|
675
|
+
await pullMissingRemoteFiles(server, token, rootDir, state, initialLocalFiles, remoteFiles, dryRun);
|
|
676
|
+
|
|
677
|
+
const files = await listFilesRecursive(rootDir);
|
|
678
|
+
seedStateFromRemote(state, files, remoteFiles);
|
|
319
679
|
|
|
320
680
|
const changed = files.filter((f) => {
|
|
321
681
|
if (force) return true;
|
|
@@ -328,6 +688,7 @@ async function syncFolder(positionals, flags) {
|
|
|
328
688
|
console.log(`Server: ${server}`);
|
|
329
689
|
console.log(`Dossier: ${rootDir}`);
|
|
330
690
|
console.log(`Total: ${files.length} fichier(s)`);
|
|
691
|
+
console.log(`Distants: ${remoteFiles.length}`);
|
|
331
692
|
console.log(`A uploader: ${changed.length}`);
|
|
332
693
|
console.log(`Inchanges: ${unchanged}`);
|
|
333
694
|
|
|
@@ -370,6 +731,9 @@ async function syncFolder(positionals, flags) {
|
|
|
370
731
|
|
|
371
732
|
console.log(`Termine. Uploades: ${success}, erreurs: ${failed}`);
|
|
372
733
|
if (failed > 0) process.exitCode = 2;
|
|
734
|
+
if (!once && !dryRun) {
|
|
735
|
+
await mirrorRealtime(server, token, rootDir, state, intervalMs);
|
|
736
|
+
}
|
|
373
737
|
}
|
|
374
738
|
|
|
375
739
|
async function main() {
|
|
@@ -400,6 +764,11 @@ async function main() {
|
|
|
400
764
|
return;
|
|
401
765
|
}
|
|
402
766
|
|
|
767
|
+
if (command === 'share') {
|
|
768
|
+
await shareFile(positionals, flags);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
403
772
|
if (command === 'config') {
|
|
404
773
|
const sub = positionals[0];
|
|
405
774
|
if (sub === 'show') {
|