sftp-push-sync 1.0.10 → 1.0.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.
- package/README.md +10 -2
- package/bin/sftp-push-sync.mjs +166 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,6 +66,11 @@ Create a `sync.config.json` in the root folder of your project:
|
|
|
66
66
|
"textExtensions": [
|
|
67
67
|
".html", ".xml", ".txt", ".json", ".js", ".css", ".md", ".svg"
|
|
68
68
|
],
|
|
69
|
+
"progress": {
|
|
70
|
+
"scanChunk": 10,
|
|
71
|
+
"analyzeChunk": 1
|
|
72
|
+
},
|
|
73
|
+
"logLevel": "normal", // or: "verbose", "laconic"
|
|
69
74
|
"uploadList": [],
|
|
70
75
|
"downloadList": [
|
|
71
76
|
"download-counter.json"
|
|
@@ -84,7 +89,6 @@ A list of files that are excluded from the sync comparison and can be downloaded
|
|
|
84
89
|
- Relative to remoteRoot "download-counter.json"
|
|
85
90
|
- or e.g. "logs/download-counter.json"
|
|
86
91
|
|
|
87
|
-
|
|
88
92
|
```bash
|
|
89
93
|
# normal synchronisation
|
|
90
94
|
sftp-push-sync staging
|
|
@@ -97,6 +101,10 @@ sftp-push-sync prod --download-list --dry-run # view first
|
|
|
97
101
|
sftp-push-sync prod --download-list # then do
|
|
98
102
|
```
|
|
99
103
|
|
|
104
|
+
### Logging Progress
|
|
105
|
+
|
|
106
|
+
For >100k files, use ANALYZE_CHUNKS = 10 or 50, otherwise the TTY output itself is a relevant factor.
|
|
107
|
+
|
|
100
108
|
## NPM Scripts
|
|
101
109
|
|
|
102
110
|
- Can be conveniently started via the scripts in `package.json`:
|
|
@@ -146,4 +154,4 @@ The first run always takes a while, especially with lots of images – so be pat
|
|
|
146
154
|
## Links
|
|
147
155
|
|
|
148
156
|
- <https://www.npmjs.com/package/sftp-push-sync>
|
|
149
|
-
- <https://github.com/cnichte/sftp-push-sync>
|
|
157
|
+
- <https://github.com/cnichte/sftp-push-sync>
|
package/bin/sftp-push-sync.mjs
CHANGED
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
* - For example, log files or other special files.
|
|
26
26
|
* - These files can be downloaded or uploaded separately.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
28
|
* The file sftp-push-sync.mjs is pure JavaScript (ESM), not TypeScript.
|
|
30
29
|
* Node.js can execute it directly as long as "type": "module" is specified in package.json
|
|
31
30
|
* or the file has the extension .mjs.
|
|
@@ -41,6 +40,11 @@ import { createHash } from "crypto";
|
|
|
41
40
|
import { Writable } from "stream";
|
|
42
41
|
import pc from "picocolors";
|
|
43
42
|
|
|
43
|
+
// get Versionsnummer
|
|
44
|
+
import { createRequire } from "module";
|
|
45
|
+
const require = createRequire(import.meta.url);
|
|
46
|
+
const pkg = require("../package.json");
|
|
47
|
+
|
|
44
48
|
// Colors for the State (works on dark + light background)
|
|
45
49
|
const ADD = pc.green("+"); // Added
|
|
46
50
|
const CHA = pc.yellow("~"); // Changed
|
|
@@ -54,10 +58,14 @@ const EXC = pc.redBright("-"); // Excluded
|
|
|
54
58
|
const args = process.argv.slice(2);
|
|
55
59
|
const TARGET = args[0];
|
|
56
60
|
const DRY_RUN = args.includes("--dry-run");
|
|
57
|
-
const VERBOSE = args.includes("--verbose") || args.includes("-v");
|
|
58
61
|
const RUN_UPLOAD_LIST = args.includes("--upload-list");
|
|
59
62
|
const RUN_DOWNLOAD_LIST = args.includes("--download-list");
|
|
60
63
|
|
|
64
|
+
// logLevel override via CLI (optional)
|
|
65
|
+
let cliLogLevel = null;
|
|
66
|
+
if (args.includes("--verbose")) cliLogLevel = "verbose";
|
|
67
|
+
if (args.includes("--laconic")) cliLogLevel = "laconic";
|
|
68
|
+
|
|
61
69
|
if (!TARGET) {
|
|
62
70
|
console.error(pc.red("❌ Please specify a connection profile:"));
|
|
63
71
|
console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
|
|
@@ -79,18 +87,18 @@ let CONFIG_RAW;
|
|
|
79
87
|
try {
|
|
80
88
|
CONFIG_RAW = JSON.parse(await fsp.readFile(CONFIG_PATH, "utf8"));
|
|
81
89
|
} catch (err) {
|
|
82
|
-
|
|
90
|
+
console.error(pc.red("❌ Error reading sync.config.json:"), err.message);
|
|
83
91
|
process.exit(1);
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
|
|
87
|
-
|
|
95
|
+
console.error(pc.red("❌ sync.config.json must have a 'connections' field."));
|
|
88
96
|
process.exit(1);
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
|
|
92
100
|
if (!TARGET_CONFIG) {
|
|
93
|
-
|
|
101
|
+
console.error(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
|
|
94
102
|
process.exit(1);
|
|
95
103
|
}
|
|
96
104
|
|
|
@@ -104,8 +112,30 @@ const CONNECTION = {
|
|
|
104
112
|
workers: TARGET_CONFIG.worker ?? 2,
|
|
105
113
|
};
|
|
106
114
|
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// LogLevel + Progress aus Config
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
// logLevel: "verbose", "normal", "laconic"
|
|
120
|
+
let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
|
|
121
|
+
|
|
122
|
+
// CLI-Flags überschreiben Config
|
|
123
|
+
if (cliLogLevel) {
|
|
124
|
+
LOG_LEVEL = cliLogLevel;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const IS_VERBOSE = LOG_LEVEL === "verbose";
|
|
128
|
+
const IS_LACONIC = LOG_LEVEL === "laconic";
|
|
129
|
+
|
|
130
|
+
const PROGRESS = CONFIG_RAW.progress ?? {};
|
|
131
|
+
const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
|
|
132
|
+
const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
|
|
133
|
+
// Für >100k Files eher 10–50, bei Debug/Fehlersuche 1.
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
107
136
|
// Shared config from JSON
|
|
108
|
-
//
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
109
139
|
const INCLUDE = CONFIG_RAW.include ?? [];
|
|
110
140
|
const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
|
|
111
141
|
|
|
@@ -114,8 +144,7 @@ function normalizeList(list) {
|
|
|
114
144
|
if (!Array.isArray(list)) return [];
|
|
115
145
|
return list.flatMap((item) =>
|
|
116
146
|
typeof item === "string"
|
|
117
|
-
?
|
|
118
|
-
item
|
|
147
|
+
? item
|
|
119
148
|
.split(",")
|
|
120
149
|
.map((s) => s.trim())
|
|
121
150
|
.filter(Boolean)
|
|
@@ -145,6 +174,7 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
|
|
|
145
174
|
".md",
|
|
146
175
|
".svg",
|
|
147
176
|
];
|
|
177
|
+
|
|
148
178
|
// Cache file name per connection
|
|
149
179
|
const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
|
|
150
180
|
const CACHE_PATH = path.resolve(syncCacheName);
|
|
@@ -167,7 +197,10 @@ try {
|
|
|
167
197
|
CACHE.remote = raw.remote ?? {};
|
|
168
198
|
}
|
|
169
199
|
} catch (err) {
|
|
170
|
-
|
|
200
|
+
console.warn(
|
|
201
|
+
pc.yellow("⚠️ Could not load cache, starting without:"),
|
|
202
|
+
err.message
|
|
203
|
+
);
|
|
171
204
|
}
|
|
172
205
|
|
|
173
206
|
function cacheKey(relPath) {
|
|
@@ -218,7 +251,7 @@ function log(...msg) {
|
|
|
218
251
|
}
|
|
219
252
|
|
|
220
253
|
function vlog(...msg) {
|
|
221
|
-
if (!
|
|
254
|
+
if (!IS_VERBOSE) return;
|
|
222
255
|
clearProgressLine();
|
|
223
256
|
console.log(...msg);
|
|
224
257
|
}
|
|
@@ -257,27 +290,67 @@ function isTextFile(relPath) {
|
|
|
257
290
|
return TEXT_EXT.includes(ext);
|
|
258
291
|
}
|
|
259
292
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
function shortenPathForProgress(rel) {
|
|
294
|
+
if (!rel) return "";
|
|
295
|
+
// Nur Dateinamen?
|
|
296
|
+
// return path.basename(rel);
|
|
264
297
|
|
|
298
|
+
// Letzte 2 Segmente des Pfades
|
|
299
|
+
const parts = rel.split("/");
|
|
300
|
+
if (parts.length === 1) {
|
|
301
|
+
return rel; // nur Dateiname
|
|
302
|
+
}
|
|
303
|
+
if (parts.length === 2) {
|
|
304
|
+
return rel; // schon kurz genug
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const last = parts[parts.length - 1];
|
|
308
|
+
const prev = parts[parts.length - 2];
|
|
309
|
+
|
|
310
|
+
// z.B. …/images/foo.jpg
|
|
311
|
+
return `…/${prev}/${last}`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Two-line progress bar
|
|
315
|
+
function updateProgress2(prefix, current, total, rel = "") {
|
|
265
316
|
if (!process.stdout.isTTY) {
|
|
266
|
-
// Fallback
|
|
267
|
-
|
|
317
|
+
// Fallback für Pipes / Logs
|
|
318
|
+
if (total && total > 0) {
|
|
319
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
320
|
+
console.log(`${prefix}${current}/${total} Dateien (${percent}%) – ${rel}`);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(`${prefix}${current} Dateien – ${rel}`);
|
|
323
|
+
}
|
|
268
324
|
return;
|
|
269
325
|
}
|
|
270
326
|
|
|
271
327
|
const width = process.stdout.columns || 80;
|
|
272
|
-
const line = msg.padEnd(width - 1);
|
|
273
328
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
329
|
+
let line1;
|
|
330
|
+
if (total && total > 0) {
|
|
331
|
+
const percent = ((current / total) * 100).toFixed(1);
|
|
332
|
+
line1 = `${prefix}${current}/${total} Dateien (${percent}%)`;
|
|
333
|
+
} else {
|
|
334
|
+
// „unknown total“ / Scanner-Modus
|
|
335
|
+
line1 = `${prefix}${current} Dateien`;
|
|
280
336
|
}
|
|
337
|
+
|
|
338
|
+
// Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
|
|
339
|
+
const short = rel ? shortenPathForProgress(rel) : "";
|
|
340
|
+
|
|
341
|
+
let line2 = short;
|
|
342
|
+
|
|
343
|
+
if (line1.length > width) line1 = line1.slice(0, width - 1);
|
|
344
|
+
if (line2.length > width) line2 = line2.slice(0, width - 1);
|
|
345
|
+
|
|
346
|
+
// zwei Zeilen überschreiben
|
|
347
|
+
process.stdout.write("\r" + line1.padEnd(width) + "\n");
|
|
348
|
+
process.stdout.write(line2.padEnd(width));
|
|
349
|
+
|
|
350
|
+
// Cursor wieder nach oben (auf die Fortschrittszeile)
|
|
351
|
+
process.stdout.write("\x1b[1A");
|
|
352
|
+
|
|
353
|
+
progressActive = true;
|
|
281
354
|
}
|
|
282
355
|
|
|
283
356
|
// Simple worker pool for parallel tasks
|
|
@@ -302,7 +375,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
302
375
|
}
|
|
303
376
|
done += 1;
|
|
304
377
|
if (done % 10 === 0 || done === total) {
|
|
305
|
-
|
|
378
|
+
updateProgress2(` ${label}: `, done, total);
|
|
306
379
|
}
|
|
307
380
|
}
|
|
308
381
|
}
|
|
@@ -321,6 +394,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
|
|
|
321
394
|
|
|
322
395
|
async function walkLocal(root) {
|
|
323
396
|
const result = new Map();
|
|
397
|
+
let scanned = 0;
|
|
324
398
|
|
|
325
399
|
async function recurse(current) {
|
|
326
400
|
const entries = await fsp.readdir(current, { withFileTypes: true });
|
|
@@ -330,7 +404,9 @@ async function walkLocal(root) {
|
|
|
330
404
|
await recurse(full);
|
|
331
405
|
} else if (entry.isFile()) {
|
|
332
406
|
const rel = toPosix(path.relative(root, full));
|
|
407
|
+
|
|
333
408
|
if (!isIncluded(rel)) continue;
|
|
409
|
+
|
|
334
410
|
const stat = await fsp.stat(full);
|
|
335
411
|
result.set(rel, {
|
|
336
412
|
rel,
|
|
@@ -339,11 +415,26 @@ async function walkLocal(root) {
|
|
|
339
415
|
mtimeMs: stat.mtimeMs,
|
|
340
416
|
isText: isTextFile(rel),
|
|
341
417
|
});
|
|
418
|
+
|
|
419
|
+
scanned += 1;
|
|
420
|
+
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
421
|
+
if (scanned === 1 || scanned % chunk === 0) {
|
|
422
|
+
// total unbekannt → total = 0 → kein automatisches \n
|
|
423
|
+
updateProgress2(" Scan local: ", scanned, 0, rel);
|
|
424
|
+
}
|
|
342
425
|
}
|
|
343
426
|
}
|
|
344
427
|
}
|
|
345
428
|
|
|
346
429
|
await recurse(root);
|
|
430
|
+
|
|
431
|
+
if (scanned > 0) {
|
|
432
|
+
// letzte Zeile + sauberer Abschluss
|
|
433
|
+
updateProgress2(" Scan local: ", scanned, 0, "fertig");
|
|
434
|
+
process.stdout.write("\n");
|
|
435
|
+
progressActive = false;
|
|
436
|
+
}
|
|
437
|
+
|
|
347
438
|
return result;
|
|
348
439
|
}
|
|
349
440
|
|
|
@@ -353,6 +444,7 @@ async function walkLocal(root) {
|
|
|
353
444
|
|
|
354
445
|
async function walkRemote(sftp, remoteRoot) {
|
|
355
446
|
const result = new Map();
|
|
447
|
+
let scanned = 0;
|
|
356
448
|
|
|
357
449
|
async function recurse(remoteDir, prefix) {
|
|
358
450
|
const items = await sftp.list(remoteDir);
|
|
@@ -375,11 +467,24 @@ async function walkRemote(sftp, remoteRoot) {
|
|
|
375
467
|
size: Number(item.size),
|
|
376
468
|
modifyTime: item.modifyTime ?? 0,
|
|
377
469
|
});
|
|
470
|
+
|
|
471
|
+
scanned += 1;
|
|
472
|
+
const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
|
|
473
|
+
if (scanned === 1 || scanned % chunk === 0) {
|
|
474
|
+
updateProgress2(" Scan remote: ", scanned, 0, rel);
|
|
475
|
+
}
|
|
378
476
|
}
|
|
379
477
|
}
|
|
380
478
|
}
|
|
381
479
|
|
|
382
|
-
await recurse(remoteRoot
|
|
480
|
+
await recurse(remoteRoot);
|
|
481
|
+
|
|
482
|
+
if (scanned > 0) {
|
|
483
|
+
updateProgress2(" Scan remote: ", scanned, 0, "fertig");
|
|
484
|
+
process.stdout.write("\n");
|
|
485
|
+
progressActive = false;
|
|
486
|
+
}
|
|
487
|
+
|
|
383
488
|
return result;
|
|
384
489
|
}
|
|
385
490
|
|
|
@@ -464,7 +569,11 @@ async function main() {
|
|
|
464
569
|
const start = Date.now();
|
|
465
570
|
|
|
466
571
|
log("\n\n==================================================================");
|
|
467
|
-
log(
|
|
572
|
+
log(
|
|
573
|
+
pc.bold(
|
|
574
|
+
`🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
|
|
575
|
+
)
|
|
576
|
+
);
|
|
468
577
|
log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
|
|
469
578
|
log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
|
|
470
579
|
log(` Local: ${pc.green(CONNECTION.localRoot)}`);
|
|
@@ -532,25 +641,34 @@ async function main() {
|
|
|
532
641
|
// Analysis: just decide, don't upload/delete anything yet
|
|
533
642
|
for (const rel of localKeys) {
|
|
534
643
|
checkedCount += 1;
|
|
535
|
-
|
|
536
|
-
|
|
644
|
+
|
|
645
|
+
const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
|
|
646
|
+
if (
|
|
647
|
+
checkedCount === 1 || // sofortige erste Ausgabe
|
|
648
|
+
checkedCount % chunk === 0 ||
|
|
649
|
+
checkedCount === totalToCheck
|
|
650
|
+
) {
|
|
651
|
+
updateProgress2(" Analyse: ", checkedCount, totalToCheck, rel);
|
|
537
652
|
}
|
|
538
653
|
|
|
539
654
|
const l = local.get(rel);
|
|
540
655
|
const r = remote.get(rel);
|
|
541
|
-
|
|
542
656
|
const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
|
|
543
657
|
|
|
544
658
|
if (!r) {
|
|
545
659
|
toAdd.push({ rel, local: l, remotePath });
|
|
546
|
-
|
|
660
|
+
if (!IS_LACONIC) {
|
|
661
|
+
log(`${ADD} ${pc.green("New:")} ${rel}`);
|
|
662
|
+
}
|
|
547
663
|
continue;
|
|
548
664
|
}
|
|
549
665
|
|
|
550
666
|
// 1. size comparison
|
|
551
667
|
if (l.size !== r.size) {
|
|
552
668
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
553
|
-
|
|
669
|
+
if (!IS_LACONIC) {
|
|
670
|
+
log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
|
|
671
|
+
}
|
|
554
672
|
continue;
|
|
555
673
|
}
|
|
556
674
|
|
|
@@ -572,14 +690,16 @@ async function main() {
|
|
|
572
690
|
continue;
|
|
573
691
|
}
|
|
574
692
|
|
|
575
|
-
if (
|
|
693
|
+
if (IS_VERBOSE) {
|
|
576
694
|
const diff = diffWords(remoteStr, localStr);
|
|
577
695
|
const blocks = diff.filter((d) => d.added || d.removed).length;
|
|
578
696
|
vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
|
|
579
697
|
}
|
|
580
698
|
|
|
581
699
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
582
|
-
|
|
700
|
+
if (!IS_LACONIC) {
|
|
701
|
+
log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
|
|
702
|
+
}
|
|
583
703
|
} else {
|
|
584
704
|
// Binary: Hash comparison with cache
|
|
585
705
|
const localMeta = l;
|
|
@@ -595,25 +715,27 @@ async function main() {
|
|
|
595
715
|
continue;
|
|
596
716
|
}
|
|
597
717
|
|
|
598
|
-
if (
|
|
718
|
+
if (IS_VERBOSE) {
|
|
599
719
|
vlog(` ${CHA} Hash different (binary): ${rel}`);
|
|
600
720
|
vlog(` local: ${localHash}`);
|
|
601
721
|
vlog(` remote: ${remoteHash}`);
|
|
602
722
|
}
|
|
603
723
|
|
|
604
724
|
toUpdate.push({ rel, local: l, remote: r, remotePath });
|
|
605
|
-
|
|
725
|
+
if (!IS_LACONIC) {
|
|
726
|
+
log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
|
|
727
|
+
}
|
|
606
728
|
}
|
|
607
729
|
}
|
|
608
730
|
|
|
609
|
-
log(
|
|
610
|
-
"\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
|
|
611
|
-
);
|
|
731
|
+
log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
|
|
612
732
|
for (const rel of remoteKeys) {
|
|
613
733
|
if (!localKeys.has(rel)) {
|
|
614
734
|
const r = remote.get(rel);
|
|
615
735
|
toDelete.push({ rel, remotePath: r.remotePath });
|
|
616
|
-
|
|
736
|
+
if (!IS_LACONIC) {
|
|
737
|
+
log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
|
|
738
|
+
}
|
|
617
739
|
}
|
|
618
740
|
}
|
|
619
741
|
|
|
@@ -758,7 +880,9 @@ async function main() {
|
|
|
758
880
|
log(` ${CHA} Changed: ${toUpdate.length}`);
|
|
759
881
|
log(` ${DEL} Deleted: ${toDelete.length}`);
|
|
760
882
|
if (AUTO_EXCLUDED.size > 0) {
|
|
761
|
-
log(
|
|
883
|
+
log(
|
|
884
|
+
` ${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
|
|
885
|
+
);
|
|
762
886
|
}
|
|
763
887
|
if (toAdd.length || toUpdate.length || toDelete.length) {
|
|
764
888
|
log("\n📄 Changes:");
|
|
@@ -776,9 +900,7 @@ async function main() {
|
|
|
776
900
|
}
|
|
777
901
|
|
|
778
902
|
log("\n" + pc.bold(pc.green("✅ Sync complete.")));
|
|
779
|
-
log(
|
|
780
|
-
"==================================================================\n\n"
|
|
781
|
-
);
|
|
903
|
+
log("==================================================================\n\n");
|
|
782
904
|
} catch (err) {
|
|
783
905
|
elog(pc.red("❌ Synchronisation error:"), err);
|
|
784
906
|
process.exitCode = 1;
|
|
@@ -796,4 +918,4 @@ async function main() {
|
|
|
796
918
|
}
|
|
797
919
|
}
|
|
798
920
|
|
|
799
|
-
main();
|
|
921
|
+
main();
|