sftp-push-sync 1.0.12 → 1.0.14

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 CHANGED
@@ -26,10 +26,16 @@ The file `sftp-push-sync.mjs` is pure JavaScript (ESM), not TypeScript. Node.js
26
26
  ## Install
27
27
 
28
28
  ```bash
29
- npm i sftp-push-sync
29
+ npm i -D sftp-push-sync
30
+ # or
31
+ npm install --save-dev sftp-push-sync
32
+ # or
33
+ yarn add --dev sftp-push-sync
34
+ # or
35
+ pnpm add -D sftp-push-sync
30
36
  ```
31
37
 
32
- ## Config file
38
+ ## Setup
33
39
 
34
40
  Create a `sync.config.json` in the root folder of your project:
35
41
 
@@ -58,23 +64,24 @@ Create a `sync.config.json` in the root folder of your project:
58
64
  }
59
65
  },
60
66
  "include": [],
61
- "exclude": [
62
- "**/.DS_Store",
63
- "**/.git/**",
64
- "**/node_modules/**"
65
- ],
67
+ "exclude": ["**/.DS_Store", "**/.git/**", "**/node_modules/**"],
66
68
  "textExtensions": [
67
- ".html", ".xml", ".txt", ".json", ".js", ".css", ".md", ".svg"
69
+ ".html",
70
+ ".xml",
71
+ ".txt",
72
+ ".json",
73
+ ".js",
74
+ ".css",
75
+ ".md",
76
+ ".svg"
68
77
  ],
69
- "progress": {
78
+ "progress": {
70
79
  "scanChunk": 10,
71
80
  "analyzeChunk": 1
72
81
  },
73
- "logLevel": "normal", // or: "verbose", "laconic"
82
+ "logLevel": "normal",
74
83
  "uploadList": [],
75
- "downloadList": [
76
- "download-counter.json"
77
- ]
84
+ "downloadList": ["download-counter.json"]
78
85
  }
79
86
  ```
80
87
 
@@ -103,7 +110,13 @@ sftp-push-sync prod --download-list # then do
103
110
 
104
111
  ### Logging Progress
105
112
 
106
- For >100k files, use ANALYZE_CHUNKS = 10 or 50, otherwise the TTY output itself is a relevant factor.
113
+ Logging can also be configured.
114
+
115
+ - `logLevel` - normal, verbose, laconic.
116
+ - `scanChunk` - After how many elements should a log output be generated during scanning?
117
+ - `analyzeChunk` - After how many elements should a log output be generated during analysis?
118
+
119
+ For >100k files, use analyzeChunk = 10 or 50, otherwise the TTY output itself is a relevant factor.
107
120
 
108
121
  ## NPM Scripts
109
122
 
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  ** sftp-push-sync.mjs - SFTP Syncronisations Tool
4
4
  *
5
- * @author Carsten Nichte, 2025 / https://carsten-nichte.de
5
+ * @author Carsten Nichte, 2025 / https://carsten-nichte.de/
6
6
  *
7
7
  * SFTP push sync with dry run
8
8
  * 1. Upload new files
@@ -51,6 +51,11 @@ const CHA = pc.yellow("~"); // Changed
51
51
  const DEL = pc.red("-"); // Deleted
52
52
  const EXC = pc.redBright("-"); // Excluded
53
53
 
54
+ const hr1 = () => "─".repeat(65); // horizontal line -
55
+ const hr2 = () => "=".repeat(65); // horizontal line =
56
+ const tab_a = () => " ".repeat(3); // indentation for formatting the terminal output.
57
+ const tab_b = () => " ".repeat(6);
58
+
54
59
  // ---------------------------------------------------------------------------
55
60
  // CLI arguments
56
61
  // ---------------------------------------------------------------------------
@@ -68,7 +73,7 @@ if (args.includes("--laconic")) cliLogLevel = "laconic";
68
73
 
69
74
  if (!TARGET) {
70
75
  console.error(pc.red("❌ Please specify a connection profile:"));
71
- console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
76
+ console.error(pc.yellow(`${tab_a()}sftp-push-sync staging --dry-run`));
72
77
  process.exit(1);
73
78
  }
74
79
 
@@ -98,7 +103,9 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
98
103
 
99
104
  const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
100
105
  if (!TARGET_CONFIG) {
101
- console.error(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
106
+ console.error(
107
+ pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`)
108
+ );
102
109
  process.exit(1);
103
110
  }
104
111
 
@@ -119,7 +126,7 @@ const CONNECTION = {
119
126
  // logLevel: "verbose", "normal", "laconic"
120
127
  let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
121
128
 
122
- // CLI-Flags überschreiben Config
129
+ // Override config with CLI flags
123
130
  if (cliLogLevel) {
124
131
  LOG_LEVEL = cliLogLevel;
125
132
  }
@@ -130,7 +137,7 @@ const IS_LACONIC = LOG_LEVEL === "laconic";
130
137
  const PROGRESS = CONFIG_RAW.progress ?? {};
131
138
  const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
132
139
  const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
133
- // Für >100k Files eher 10–50, bei Debug/Fehlersuche 1.
140
+ // For >100k files, rather 10–50, for debugging/troubleshooting 1.
134
141
 
135
142
  // ---------------------------------------------------------------------------
136
143
  // Shared config from JSON
@@ -139,7 +146,7 @@ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
139
146
  const INCLUDE = CONFIG_RAW.include ?? [];
140
147
  const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
141
148
 
142
- // Spezial: Listen für gezielte Uploads / Downloads
149
+ // Special: Lists for targeted uploads/downloads
143
150
  function normalizeList(list) {
144
151
  if (!Array.isArray(list)) return [];
145
152
  return list.flatMap((item) =>
@@ -158,7 +165,7 @@ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
158
165
  // Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
159
166
  const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
160
167
 
161
- // Liste ALLER Dateien, die wegen uploadList/downloadList ausgeschlossen wurden
168
+ // List of ALL files that were excluded due to uploadList/downloadList
162
169
  const AUTO_EXCLUDED = new Set();
163
170
 
164
171
  const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
@@ -198,7 +205,7 @@ try {
198
205
  }
199
206
  } catch (err) {
200
207
  console.warn(
201
- pc.yellow("⚠️ Could not load cache, starting without:"),
208
+ pc.yellow("⚠️ Could not load cache, starting without:"),
202
209
  err.message
203
210
  );
204
211
  }
@@ -236,8 +243,11 @@ let progressActive = false;
236
243
  function clearProgressLine() {
237
244
  if (!process.stdout.isTTY || !progressActive) return;
238
245
  const width = process.stdout.columns || 80;
239
- const blank = " ".repeat(width - 1);
240
- process.stdout.write("\r" + blank + "\r");
246
+ const blank = " ".repeat(width);
247
+
248
+ // Beide Progress-Zeilen leeren
249
+ process.stdout.write("\r" + blank + "\n" + blank + "\r");
250
+
241
251
  progressActive = false;
242
252
  }
243
253
 
@@ -292,10 +302,6 @@ function isTextFile(relPath) {
292
302
 
293
303
  function shortenPathForProgress(rel) {
294
304
  if (!rel) return "";
295
- // Nur Dateinamen?
296
- // return path.basename(rel);
297
-
298
- // Letzte 2 Segmente des Pfades
299
305
  const parts = rel.split("/");
300
306
  if (parts.length === 1) {
301
307
  return rel; // nur Dateiname
@@ -317,9 +323,11 @@ function updateProgress2(prefix, current, total, rel = "") {
317
323
  // Fallback für Pipes / Logs
318
324
  if (total && total > 0) {
319
325
  const percent = ((current / total) * 100).toFixed(1);
320
- console.log(`${prefix}${current}/${total} Dateien (${percent}%) – ${rel}`);
326
+ console.log(
327
+ `${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${rel}`
328
+ );
321
329
  } else {
322
- console.log(`${prefix}${current} Dateien – ${rel}`);
330
+ console.log(`${tab_a()}${prefix}${current} Files – ${rel}`);
323
331
  }
324
332
  return;
325
333
  }
@@ -329,15 +337,13 @@ function updateProgress2(prefix, current, total, rel = "") {
329
337
  let line1;
330
338
  if (total && total > 0) {
331
339
  const percent = ((current / total) * 100).toFixed(1);
332
- line1 = `${prefix}${current}/${total} Dateien (${percent}%)`;
340
+ line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
333
341
  } else {
334
342
  // „unknown total“ / Scanner-Modus
335
- line1 = `${prefix}${current} Dateien`;
343
+ line1 = `${tab_a()}${prefix}${current} Files`;
336
344
  }
337
345
 
338
- // Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
339
346
  const short = rel ? shortenPathForProgress(rel) : "";
340
-
341
347
  let line2 = short;
342
348
 
343
349
  if (line1.length > width) line1 = line1.slice(0, width - 1);
@@ -371,11 +377,11 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
371
377
  try {
372
378
  await handler(item);
373
379
  } catch (err) {
374
- elog(pc.red(` ⚠️ Error in ${label}:`), err.message || err);
380
+ elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
375
381
  }
376
382
  done += 1;
377
383
  if (done % 10 === 0 || done === total) {
378
- updateProgress2(` ${label}: `, done, total);
384
+ updateProgress2(`${tab_a()}${label}: `, done, total);
379
385
  }
380
386
  }
381
387
  }
@@ -419,8 +425,8 @@ async function walkLocal(root) {
419
425
  scanned += 1;
420
426
  const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
421
427
  if (scanned === 1 || scanned % chunk === 0) {
422
- // total unbekannt → total = 0 → kein automatisches \n
423
- updateProgress2(" Scan local: ", scanned, 0, rel);
428
+ // totally unknown → total = 0 → no automatic \n
429
+ updateProgress2(`${tab_a()}Scan local: `, scanned, 0, rel);
424
430
  }
425
431
  }
426
432
  }
@@ -429,8 +435,8 @@ async function walkLocal(root) {
429
435
  await recurse(root);
430
436
 
431
437
  if (scanned > 0) {
432
- // letzte Zeile + sauberer Abschluss
433
- updateProgress2(" Scan local: ", scanned, 0, "fertig");
438
+ // last line + neat finish
439
+ updateProgress2(`${tab_a()}Scan local: `, scanned, 0, "fertig");
434
440
  process.stdout.write("\n");
435
441
  progressActive = false;
436
442
  }
@@ -471,7 +477,7 @@ async function walkRemote(sftp, remoteRoot) {
471
477
  scanned += 1;
472
478
  const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
473
479
  if (scanned === 1 || scanned % chunk === 0) {
474
- updateProgress2(" Scan remote: ", scanned, 0, rel);
480
+ updateProgress2(`${tab_a()}Scan remote: `, scanned, 0, rel);
475
481
  }
476
482
  }
477
483
  }
@@ -480,7 +486,7 @@ async function walkRemote(sftp, remoteRoot) {
480
486
  await recurse(remoteRoot);
481
487
 
482
488
  if (scanned > 0) {
483
- updateProgress2(" Scan remote: ", scanned, 0, "fertig");
489
+ updateProgress2(`${tab_a()}Scan remote: `, scanned, 0, "fertig");
484
490
  process.stdout.write("\n");
485
491
  progressActive = false;
486
492
  }
@@ -561,6 +567,45 @@ async function getRemoteHash(rel, meta, sftp) {
561
567
  return hash;
562
568
  }
563
569
 
570
+ // ---------------------------------------------------------------------------
571
+ // SFTP error explanation (for clearer messages)
572
+ // ---------------------------------------------------------------------------
573
+
574
+ function describeSftpError(err) {
575
+ if (!err) return "";
576
+
577
+ const code = err.code || err.errno || "";
578
+ const msg = (err.message || "").toLowerCase();
579
+
580
+ // Netzwerk / DNS
581
+ if (code === "ENOTFOUND") {
582
+ return "Host not found (ENOTFOUND) – Check hostname or DNS entry.";
583
+ }
584
+ if (code === "EHOSTUNREACH") {
585
+ return "Host not reachable (EHOSTUNREACH) – Check network/firewall.";
586
+ }
587
+ if (code === "ECONNREFUSED") {
588
+ return "Connection refused (ECONNREFUSED) – Check the port or SSH service.";
589
+ }
590
+ if (code === "ECONNRESET") {
591
+ return "Connection was reset by the server (ECONNRESET).";
592
+ }
593
+ if (code === "ETIMEDOUT") {
594
+ return "Connection timeout (ETIMEDOUT) – Server is not responding or is blocked.";
595
+ }
596
+
597
+ // Auth / Authorisations
598
+ if (msg.includes("all configured authentication methods failed")) {
599
+ return "Authentication failed – check your username/password or SSH keys.";
600
+ }
601
+ if (msg.includes("permission denied")) {
602
+ return "Access denied – check permissions on the server.";
603
+ }
604
+
605
+ // Fallback
606
+ return "";
607
+ }
608
+
564
609
  // ---------------------------------------------------------------------------
565
610
  // MAIN
566
611
  // ---------------------------------------------------------------------------
@@ -568,43 +613,46 @@ async function getRemoteHash(rel, meta, sftp) {
568
613
  async function main() {
569
614
  const start = Date.now();
570
615
 
571
- log("\n\n==================================================================");
616
+ log(`\n\n${hr2()}`);
572
617
  log(
573
618
  pc.bold(
574
619
  `🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
575
620
  )
576
621
  );
577
- log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
578
- log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
579
- log(` Local: ${pc.green(CONNECTION.localRoot)}`);
580
- log(` Remote: ${pc.green(CONNECTION.remoteRoot)}`);
581
- if (DRY_RUN) log(pc.yellow(" Mode: DRY-RUN (no changes)"));
622
+ log(`${tab_a()}Connection: ${pc.cyan(TARGET)}`);
623
+ log(`${tab_a()}Worker: ${CONNECTION.workers}`);
624
+ log(`${tab_a()}Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
625
+ log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
626
+ log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
627
+ if (DRY_RUN) log(pc.yellow(`${tab_a()}Mode: DRY-RUN (no changes)`));
582
628
  if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
583
629
  log(
584
630
  pc.blue(
585
- ` Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
631
+ `${tab_a()}Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
586
632
  RUN_DOWNLOAD_LIST ? "downloadList" : ""
587
633
  }`
588
634
  )
589
635
  );
590
636
  }
591
- log("-----------------------------------------------------------------\n");
637
+ log(`${hr1()}\n`);
592
638
 
593
639
  const sftp = new SftpClient();
640
+ let connected = false;
594
641
 
595
642
  const toAdd = [];
596
643
  const toUpdate = [];
597
644
  const toDelete = [];
598
645
 
599
646
  try {
647
+ log(pc.cyan("🔌 Connecting to SFTP server …"));
600
648
  await sftp.connect({
601
649
  host: CONNECTION.host,
602
650
  port: CONNECTION.port,
603
651
  username: CONNECTION.user,
604
652
  password: CONNECTION.password,
605
653
  });
606
-
607
- vlog(pc.dim(" Connection established."));
654
+ connected = true;
655
+ log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
608
656
 
609
657
  if (!fs.existsSync(CONNECTION.localRoot)) {
610
658
  console.error(
@@ -616,20 +664,20 @@ async function main() {
616
664
 
617
665
  log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
618
666
  const local = await walkLocal(CONNECTION.localRoot);
619
- log(` → ${local.size} local files`);
667
+ log(`${tab_a()}→ ${local.size} local files`);
620
668
 
621
669
  if (AUTO_EXCLUDED.size > 0) {
622
670
  log("");
623
671
  log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
624
672
  [...AUTO_EXCLUDED].sort().forEach((file) => {
625
- log(pc.dim(` - ${file}`));
673
+ log(pc.dim(`${tab_a()} - ${file}`));
626
674
  });
627
675
  log("");
628
676
  }
629
677
 
630
678
  log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
631
679
  const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
632
- log(` → ${remote.size} remote files\n`);
680
+ log(`${tab_a()}→ ${remote.size} remote files\n`);
633
681
 
634
682
  const localKeys = new Set(local.keys());
635
683
  const remoteKeys = new Set(remote.keys());
@@ -644,7 +692,7 @@ async function main() {
644
692
 
645
693
  const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
646
694
  if (
647
- checkedCount === 1 || // sofortige erste Ausgabe
695
+ checkedCount === 1 || // immediate first issue
648
696
  checkedCount % chunk === 0 ||
649
697
  checkedCount === totalToCheck
650
698
  ) {
@@ -686,19 +734,19 @@ async function main() {
686
734
  ).toString("utf8");
687
735
 
688
736
  if (localStr === remoteStr) {
689
- vlog(` ${pc.dim("✓ Unchanged (Text):")} ${rel}`);
737
+ vlog(`${tab_a()}${pc.dim("✓ Unchanged (Text):")} ${rel}`);
690
738
  continue;
691
739
  }
692
740
 
693
741
  if (IS_VERBOSE) {
694
742
  const diff = diffWords(remoteStr, localStr);
695
743
  const blocks = diff.filter((d) => d.added || d.removed).length;
696
- vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
744
+ vlog(`${tab_a()}${CHA} Text difference (${blocks} blocks) in ${rel}`);
697
745
  }
698
746
 
699
747
  toUpdate.push({ rel, local: l, remote: r, remotePath });
700
748
  if (!IS_LACONIC) {
701
- log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
749
+ log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
702
750
  }
703
751
  } else {
704
752
  // Binary: Hash comparison with cache
@@ -711,14 +759,14 @@ async function main() {
711
759
  ]);
712
760
 
713
761
  if (localHash === remoteHash) {
714
- vlog(` ${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
762
+ vlog(`${tab_a()}${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
715
763
  continue;
716
764
  }
717
765
 
718
766
  if (IS_VERBOSE) {
719
- vlog(` ${CHA} Hash different (binary): ${rel}`);
720
- vlog(` local: ${localHash}`);
721
- vlog(` remote: ${remoteHash}`);
767
+ vlog(`${tab_a()}${CHA} Hash different (binary): ${rel}`);
768
+ vlog(`${tab_b()}local: ${localHash}`);
769
+ vlog(`${tab_b()}remote: ${remoteHash}`);
722
770
  }
723
771
 
724
772
  toUpdate.push({ rel, local: l, remote: r, remotePath });
@@ -728,17 +776,29 @@ async function main() {
728
776
  }
729
777
  }
730
778
 
731
- log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
779
+ // Wenn Phase 3 nichts gefunden hat, explizit sagen
780
+ if (toAdd.length === 0 && toUpdate.length === 0) {
781
+ log(`${tab_a()}No differences found. Everything is up to date.`);
782
+ }
783
+
784
+ log(
785
+ "\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
786
+ );
732
787
  for (const rel of remoteKeys) {
733
788
  if (!localKeys.has(rel)) {
734
789
  const r = remote.get(rel);
735
790
  toDelete.push({ rel, remotePath: r.remotePath });
736
791
  if (!IS_LACONIC) {
737
- log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
792
+ log(`${tab_a()}${DEL} ${pc.red("Remove:")} ${rel}`);
738
793
  }
739
794
  }
740
795
  }
741
796
 
797
+ // Auch für Phase 4 eine „nix zu tun“-Meldung
798
+ if (toDelete.length === 0) {
799
+ log(`${tab_a()}No orphaned remote files found.`);
800
+ }
801
+
742
802
  // -------------------------------------------------------------------
743
803
  // Phase 5: Execute changes (parallel, worker-based)
744
804
  // -------------------------------------------------------------------
@@ -796,7 +856,11 @@ async function main() {
796
856
  "Deletes"
797
857
  );
798
858
  } else {
799
- log(pc.yellow("\n💡 DRY-RUN: No files transferred or deleted."));
859
+ log(
860
+ pc.yellow(
861
+ "\n💡 DRY-RUN: Connection tested, no files transferred or deleted."
862
+ )
863
+ );
800
864
  }
801
865
 
802
866
  // -------------------------------------------------------------------
@@ -806,7 +870,7 @@ async function main() {
806
870
  if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
807
871
  log(
808
872
  "\n" +
809
- pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
873
+ pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
810
874
  );
811
875
 
812
876
  const tasks = UPLOAD_LIST.map((rel) => ({
@@ -817,7 +881,7 @@ async function main() {
817
881
 
818
882
  if (DRY_RUN) {
819
883
  for (const t of tasks) {
820
- log(` ${ADD} would upload (uploadList): ${t.rel}`);
884
+ log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
821
885
  }
822
886
  } else {
823
887
  await runTasks(
@@ -831,7 +895,7 @@ async function main() {
831
895
  // ignore
832
896
  }
833
897
  await sftp.put(localPath, remotePath);
834
- log(` ${ADD} uploadList: ${rel}`);
898
+ log(`${tab_a()}${ADD} uploadList: ${rel}`);
835
899
  },
836
900
  "Upload-List"
837
901
  );
@@ -841,7 +905,7 @@ async function main() {
841
905
  if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
842
906
  log(
843
907
  "\n" +
844
- pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
908
+ pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
845
909
  );
846
910
 
847
911
  const tasks = DOWNLOAD_LIST.map((rel) => ({
@@ -852,7 +916,7 @@ async function main() {
852
916
 
853
917
  if (DRY_RUN) {
854
918
  for (const t of tasks) {
855
- log(` ${ADD} would download (downloadList): ${t.rel}`);
919
+ log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
856
920
  }
857
921
  } else {
858
922
  await runTasks(
@@ -861,7 +925,7 @@ async function main() {
861
925
  async ({ remotePath, localPath, rel }) => {
862
926
  await fsp.mkdir(path.dirname(localPath), { recursive: true });
863
927
  await sftp.fastGet(remotePath, localPath);
864
- log(` ${ADD} downloadList: ${rel}`);
928
+ log(`${tab_a()}${ADD} downloadList: ${rel}`);
865
929
  },
866
930
  "Download-List"
867
931
  );
@@ -875,34 +939,41 @@ async function main() {
875
939
 
876
940
  // Summary
877
941
  log("\n" + pc.bold(pc.cyan("📊 Summary:")));
878
- log(` Duration: ${pc.green(duration + " s")}`);
879
- log(` ${ADD} Added : ${toAdd.length}`);
880
- log(` ${CHA} Changed: ${toUpdate.length}`);
881
- log(` ${DEL} Deleted: ${toDelete.length}`);
942
+ log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
943
+ log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
944
+ log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
945
+ log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
882
946
  if (AUTO_EXCLUDED.size > 0) {
883
947
  log(
884
- ` ${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
948
+ `${tab_a()}${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
885
949
  );
886
950
  }
887
951
  if (toAdd.length || toUpdate.length || toDelete.length) {
888
952
  log("\n📄 Changes:");
889
953
  [...toAdd.map((t) => t.rel)]
890
954
  .sort()
891
- .forEach((f) => console.log(` ${ADD} ${f}`));
955
+ .forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
892
956
  [...toUpdate.map((t) => t.rel)]
893
957
  .sort()
894
- .forEach((f) => console.log(` ${CHA} ${f}`));
958
+ .forEach((f) => console.log(`${tab_a()}${CHA} ${f}`));
895
959
  [...toDelete.map((t) => t.rel)]
896
960
  .sort()
897
- .forEach((f) => console.log(` ${DEL} ${f}`));
961
+ .forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
898
962
  } else {
899
963
  log("\nNo changes.");
900
964
  }
901
965
 
902
966
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
903
- log("==================================================================\n\n");
904
967
  } catch (err) {
905
- elog(pc.red("❌ Synchronisation error:"), err);
968
+ const hint = describeSftpError(err);
969
+ elog(pc.red("❌ Synchronisation error:"), err.message || err);
970
+ if (hint) {
971
+ wlog(pc.yellow(`${tab_a()}Mögliche Ursache:`), hint);
972
+ }
973
+ if (IS_VERBOSE) {
974
+ // Vollständiges Error-Objekt nur in verbose anzeigen
975
+ console.error(err);
976
+ }
906
977
  process.exitCode = 1;
907
978
  try {
908
979
  await saveCache(true);
@@ -911,11 +982,18 @@ async function main() {
911
982
  }
912
983
  } finally {
913
984
  try {
914
- await sftp.end();
915
- } catch {
916
- // ignore
985
+ if (connected) {
986
+ await sftp.end();
987
+ log(pc.green(`${tab_a()}✔ Connection closed.`));
988
+ }
989
+ } catch (e) {
990
+ wlog(
991
+ pc.yellow("⚠️ Could not close SFTP connection cleanly:"),
992
+ e.message || e
993
+ );
917
994
  }
918
995
  }
996
+ log(`${hr2()}\n\n`);
919
997
  }
920
998
 
921
999
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "1.0.12",
3
+ "version": "1.0.14",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {