sftp-push-sync 1.0.12 → 1.0.13

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,10 @@ 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 output.
57
+
54
58
  // ---------------------------------------------------------------------------
55
59
  // CLI arguments
56
60
  // ---------------------------------------------------------------------------
@@ -98,7 +102,9 @@ if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
98
102
 
99
103
  const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
100
104
  if (!TARGET_CONFIG) {
101
- console.error(pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`));
105
+ console.error(
106
+ pc.red(`❌ Connection '${TARGET}' not found in sync.config.json.`)
107
+ );
102
108
  process.exit(1);
103
109
  }
104
110
 
@@ -119,7 +125,7 @@ const CONNECTION = {
119
125
  // logLevel: "verbose", "normal", "laconic"
120
126
  let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
121
127
 
122
- // CLI-Flags überschreiben Config
128
+ // Override config with CLI flags
123
129
  if (cliLogLevel) {
124
130
  LOG_LEVEL = cliLogLevel;
125
131
  }
@@ -130,7 +136,7 @@ const IS_LACONIC = LOG_LEVEL === "laconic";
130
136
  const PROGRESS = CONFIG_RAW.progress ?? {};
131
137
  const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
132
138
  const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
133
- // Für >100k Files eher 10–50, bei Debug/Fehlersuche 1.
139
+ // For >100k files, rather 10–50, for debugging/troubleshooting 1.
134
140
 
135
141
  // ---------------------------------------------------------------------------
136
142
  // Shared config from JSON
@@ -139,7 +145,7 @@ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
139
145
  const INCLUDE = CONFIG_RAW.include ?? [];
140
146
  const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
141
147
 
142
- // Spezial: Listen für gezielte Uploads / Downloads
148
+ // Special: Lists for targeted uploads/downloads
143
149
  function normalizeList(list) {
144
150
  if (!Array.isArray(list)) return [];
145
151
  return list.flatMap((item) =>
@@ -158,7 +164,7 @@ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
158
164
  // Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
159
165
  const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
160
166
 
161
- // Liste ALLER Dateien, die wegen uploadList/downloadList ausgeschlossen wurden
167
+ // List of ALL files that were excluded due to uploadList/downloadList
162
168
  const AUTO_EXCLUDED = new Set();
163
169
 
164
170
  const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
@@ -198,7 +204,7 @@ try {
198
204
  }
199
205
  } catch (err) {
200
206
  console.warn(
201
- pc.yellow("⚠️ Could not load cache, starting without:"),
207
+ pc.yellow("⚠️ Could not load cache, starting without:"),
202
208
  err.message
203
209
  );
204
210
  }
@@ -292,10 +298,6 @@ function isTextFile(relPath) {
292
298
 
293
299
  function shortenPathForProgress(rel) {
294
300
  if (!rel) return "";
295
- // Nur Dateinamen?
296
- // return path.basename(rel);
297
-
298
- // Letzte 2 Segmente des Pfades
299
301
  const parts = rel.split("/");
300
302
  if (parts.length === 1) {
301
303
  return rel; // nur Dateiname
@@ -317,9 +319,11 @@ function updateProgress2(prefix, current, total, rel = "") {
317
319
  // Fallback für Pipes / Logs
318
320
  if (total && total > 0) {
319
321
  const percent = ((current / total) * 100).toFixed(1);
320
- console.log(`${prefix}${current}/${total} Dateien (${percent}%) – ${rel}`);
322
+ console.log(
323
+ `${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${rel}`
324
+ );
321
325
  } else {
322
- console.log(`${prefix}${current} Dateien – ${rel}`);
326
+ console.log(`${tab_a()}${prefix}${current} Files – ${rel}`);
323
327
  }
324
328
  return;
325
329
  }
@@ -329,15 +333,13 @@ function updateProgress2(prefix, current, total, rel = "") {
329
333
  let line1;
330
334
  if (total && total > 0) {
331
335
  const percent = ((current / total) * 100).toFixed(1);
332
- line1 = `${prefix}${current}/${total} Dateien (${percent}%)`;
336
+ line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
333
337
  } else {
334
338
  // „unknown total“ / Scanner-Modus
335
- line1 = `${prefix}${current} Dateien`;
339
+ line1 = `${tab_a()}${prefix}${current} Files`;
336
340
  }
337
341
 
338
- // Pfad einkürzen falls nötig (deine bestehende Funktion verwenden)
339
342
  const short = rel ? shortenPathForProgress(rel) : "";
340
-
341
343
  let line2 = short;
342
344
 
343
345
  if (line1.length > width) line1 = line1.slice(0, width - 1);
@@ -371,11 +373,11 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
371
373
  try {
372
374
  await handler(item);
373
375
  } catch (err) {
374
- elog(pc.red(` ⚠️ Error in ${label}:`), err.message || err);
376
+ elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
375
377
  }
376
378
  done += 1;
377
379
  if (done % 10 === 0 || done === total) {
378
- updateProgress2(` ${label}: `, done, total);
380
+ updateProgress2(`${tab_a()}${label}: `, done, total);
379
381
  }
380
382
  }
381
383
  }
@@ -419,7 +421,7 @@ async function walkLocal(root) {
419
421
  scanned += 1;
420
422
  const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
421
423
  if (scanned === 1 || scanned % chunk === 0) {
422
- // total unbekannttotal = 0 → kein automatisches \n
424
+ // totally unknowntotally = 0 → no automatic \n
423
425
  updateProgress2(" Scan local: ", scanned, 0, rel);
424
426
  }
425
427
  }
@@ -429,7 +431,7 @@ async function walkLocal(root) {
429
431
  await recurse(root);
430
432
 
431
433
  if (scanned > 0) {
432
- // letzte Zeile + sauberer Abschluss
434
+ // last line + neat finish
433
435
  updateProgress2(" Scan local: ", scanned, 0, "fertig");
434
436
  process.stdout.write("\n");
435
437
  progressActive = false;
@@ -561,6 +563,45 @@ async function getRemoteHash(rel, meta, sftp) {
561
563
  return hash;
562
564
  }
563
565
 
566
+ // ---------------------------------------------------------------------------
567
+ // SFTP error explanation (for clearer messages)
568
+ // ---------------------------------------------------------------------------
569
+
570
+ function describeSftpError(err) {
571
+ if (!err) return "";
572
+
573
+ const code = err.code || err.errno || "";
574
+ const msg = (err.message || "").toLowerCase();
575
+
576
+ // Netzwerk / DNS
577
+ if (code === "ENOTFOUND") {
578
+ return "Host not found (ENOTFOUND) – Check hostname or DNS entry.";
579
+ }
580
+ if (code === "EHOSTUNREACH") {
581
+ return "Host not reachable (EHOSTUNREACH) – Check network/firewall.";
582
+ }
583
+ if (code === "ECONNREFUSED") {
584
+ return "Connection refused (ECONNREFUSED) – Check the port or SSH service.";
585
+ }
586
+ if (code === "ECONNRESET") {
587
+ return "Connection was reset by the server (ECONNRESET).";
588
+ }
589
+ if (code === "ETIMEDOUT") {
590
+ return "Connection timeout (ETIMEDOUT) – Server is not responding or is blocked.";
591
+ }
592
+
593
+ // Auth / Authorisations
594
+ if (msg.includes("all configured authentication methods failed")) {
595
+ return "Authentication failed – check your username/password or SSH keys.";
596
+ }
597
+ if (msg.includes("permission denied")) {
598
+ return "Access denied – check permissions on the server.";
599
+ }
600
+
601
+ // Fallback
602
+ return "";
603
+ }
604
+
564
605
  // ---------------------------------------------------------------------------
565
606
  // MAIN
566
607
  // ---------------------------------------------------------------------------
@@ -568,43 +609,46 @@ async function getRemoteHash(rel, meta, sftp) {
568
609
  async function main() {
569
610
  const start = Date.now();
570
611
 
571
- log("\n\n==================================================================");
612
+ log(`\n\n${hr2()}`);
572
613
  log(
573
614
  pc.bold(
574
615
  `🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
575
616
  )
576
617
  );
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)}`);
618
+ log(`${tab_a()}Connection: ${pc.cyan(TARGET)}`);
619
+ log(`Worker: ${CONNECTION.workers}`);
620
+ log(`${tab_a()}Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
621
+ log(`${tab_a()}Local: ${pc.green(CONNECTION.localRoot)}`);
622
+ log(`${tab_a()}Remote: ${pc.green(CONNECTION.remoteRoot)}`);
581
623
  if (DRY_RUN) log(pc.yellow(" Mode: DRY-RUN (no changes)"));
582
624
  if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
583
625
  log(
584
626
  pc.blue(
585
- ` Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
627
+ `${tab_a()}Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
586
628
  RUN_DOWNLOAD_LIST ? "downloadList" : ""
587
629
  }`
588
630
  )
589
631
  );
590
632
  }
591
- log("-----------------------------------------------------------------\n");
633
+ log(`${hr1()}\n`);
592
634
 
593
635
  const sftp = new SftpClient();
636
+ let connected = false;
594
637
 
595
638
  const toAdd = [];
596
639
  const toUpdate = [];
597
640
  const toDelete = [];
598
641
 
599
642
  try {
643
+ log(pc.cyan("🔌 Connecting to SFTP server …"));
600
644
  await sftp.connect({
601
645
  host: CONNECTION.host,
602
646
  port: CONNECTION.port,
603
647
  username: CONNECTION.user,
604
648
  password: CONNECTION.password,
605
649
  });
606
-
607
- vlog(pc.dim(" Connection established."));
650
+ connected = true;
651
+ log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
608
652
 
609
653
  if (!fs.existsSync(CONNECTION.localRoot)) {
610
654
  console.error(
@@ -616,20 +660,20 @@ async function main() {
616
660
 
617
661
  log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
618
662
  const local = await walkLocal(CONNECTION.localRoot);
619
- log(` → ${local.size} local files`);
663
+ log(`${tab_a()}→ ${local.size} local files`);
620
664
 
621
665
  if (AUTO_EXCLUDED.size > 0) {
622
666
  log("");
623
667
  log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
624
668
  [...AUTO_EXCLUDED].sort().forEach((file) => {
625
- log(pc.dim(` - ${file}`));
669
+ log(pc.dim(`${tab_a()} - ${file}`));
626
670
  });
627
671
  log("");
628
672
  }
629
673
 
630
674
  log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
631
675
  const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
632
- log(` → ${remote.size} remote files\n`);
676
+ log(`${tab_a()}→ ${remote.size} remote files\n`);
633
677
 
634
678
  const localKeys = new Set(local.keys());
635
679
  const remoteKeys = new Set(remote.keys());
@@ -644,7 +688,7 @@ async function main() {
644
688
 
645
689
  const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
646
690
  if (
647
- checkedCount === 1 || // sofortige erste Ausgabe
691
+ checkedCount === 1 || // immediate first issue
648
692
  checkedCount % chunk === 0 ||
649
693
  checkedCount === totalToCheck
650
694
  ) {
@@ -686,19 +730,19 @@ async function main() {
686
730
  ).toString("utf8");
687
731
 
688
732
  if (localStr === remoteStr) {
689
- vlog(` ${pc.dim("✓ Unchanged (Text):")} ${rel}`);
733
+ vlog(`${tab_a()}${pc.dim("✓ Unchanged (Text):")} ${rel}`);
690
734
  continue;
691
735
  }
692
736
 
693
737
  if (IS_VERBOSE) {
694
738
  const diff = diffWords(remoteStr, localStr);
695
739
  const blocks = diff.filter((d) => d.added || d.removed).length;
696
- vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
740
+ vlog(`${tab_a()}${CHA} Text difference (${blocks} blocks) in ${rel}`);
697
741
  }
698
742
 
699
743
  toUpdate.push({ rel, local: l, remote: r, remotePath });
700
744
  if (!IS_LACONIC) {
701
- log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
745
+ log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
702
746
  }
703
747
  } else {
704
748
  // Binary: Hash comparison with cache
@@ -711,14 +755,14 @@ async function main() {
711
755
  ]);
712
756
 
713
757
  if (localHash === remoteHash) {
714
- vlog(` ${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
758
+ vlog(`${tab_a()}${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
715
759
  continue;
716
760
  }
717
761
 
718
762
  if (IS_VERBOSE) {
719
- vlog(` ${CHA} Hash different (binary): ${rel}`);
720
- vlog(` local: ${localHash}`);
721
- vlog(` remote: ${remoteHash}`);
763
+ vlog(`${tab_a()}${CHA} Hash different (binary): ${rel}`);
764
+ vlog(`${tab_a()} local: ${localHash}`);
765
+ vlog(`${tab_a()} remote: ${remoteHash}`);
722
766
  }
723
767
 
724
768
  toUpdate.push({ rel, local: l, remote: r, remotePath });
@@ -728,13 +772,15 @@ async function main() {
728
772
  }
729
773
  }
730
774
 
731
- log("\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …")));
775
+ log(
776
+ "\n" + pc.bold(pc.cyan("🧹 Phase 4: Removing orphaned remote files …"))
777
+ );
732
778
  for (const rel of remoteKeys) {
733
779
  if (!localKeys.has(rel)) {
734
780
  const r = remote.get(rel);
735
781
  toDelete.push({ rel, remotePath: r.remotePath });
736
782
  if (!IS_LACONIC) {
737
- log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
783
+ log(`${tab_a()}${DEL} ${pc.red("Remove:")} ${rel}`);
738
784
  }
739
785
  }
740
786
  }
@@ -796,7 +842,11 @@ async function main() {
796
842
  "Deletes"
797
843
  );
798
844
  } else {
799
- log(pc.yellow("\n💡 DRY-RUN: No files transferred or deleted."));
845
+ log(
846
+ pc.yellow(
847
+ "\n💡 DRY-RUN: Connection tested, no files transferred or deleted."
848
+ )
849
+ );
800
850
  }
801
851
 
802
852
  // -------------------------------------------------------------------
@@ -806,7 +856,7 @@ async function main() {
806
856
  if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
807
857
  log(
808
858
  "\n" +
809
- pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
859
+ pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
810
860
  );
811
861
 
812
862
  const tasks = UPLOAD_LIST.map((rel) => ({
@@ -817,7 +867,7 @@ async function main() {
817
867
 
818
868
  if (DRY_RUN) {
819
869
  for (const t of tasks) {
820
- log(` ${ADD} would upload (uploadList): ${t.rel}`);
870
+ log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
821
871
  }
822
872
  } else {
823
873
  await runTasks(
@@ -831,7 +881,7 @@ async function main() {
831
881
  // ignore
832
882
  }
833
883
  await sftp.put(localPath, remotePath);
834
- log(` ${ADD} uploadList: ${rel}`);
884
+ log(`${tab_a()}${ADD} uploadList: ${rel}`);
835
885
  },
836
886
  "Upload-List"
837
887
  );
@@ -841,7 +891,7 @@ async function main() {
841
891
  if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
842
892
  log(
843
893
  "\n" +
844
- pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
894
+ pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
845
895
  );
846
896
 
847
897
  const tasks = DOWNLOAD_LIST.map((rel) => ({
@@ -852,7 +902,7 @@ async function main() {
852
902
 
853
903
  if (DRY_RUN) {
854
904
  for (const t of tasks) {
855
- log(` ${ADD} would download (downloadList): ${t.rel}`);
905
+ log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
856
906
  }
857
907
  } else {
858
908
  await runTasks(
@@ -861,7 +911,7 @@ async function main() {
861
911
  async ({ remotePath, localPath, rel }) => {
862
912
  await fsp.mkdir(path.dirname(localPath), { recursive: true });
863
913
  await sftp.fastGet(remotePath, localPath);
864
- log(` ${ADD} downloadList: ${rel}`);
914
+ log(`${tab_a()}${ADD} downloadList: ${rel}`);
865
915
  },
866
916
  "Download-List"
867
917
  );
@@ -875,34 +925,41 @@ async function main() {
875
925
 
876
926
  // Summary
877
927
  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}`);
928
+ log(`${tab_a()}Duration: ${pc.green(duration + " s")}`);
929
+ log(`${tab_a()}${ADD} Added : ${toAdd.length}`);
930
+ log(`${tab_a()}${CHA} Changed: ${toUpdate.length}`);
931
+ log(`${tab_a()}${DEL} Deleted: ${toDelete.length}`);
882
932
  if (AUTO_EXCLUDED.size > 0) {
883
933
  log(
884
- ` ${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
934
+ `${tab_a()}${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
885
935
  );
886
936
  }
887
937
  if (toAdd.length || toUpdate.length || toDelete.length) {
888
938
  log("\n📄 Changes:");
889
939
  [...toAdd.map((t) => t.rel)]
890
940
  .sort()
891
- .forEach((f) => console.log(` ${ADD} ${f}`));
941
+ .forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
892
942
  [...toUpdate.map((t) => t.rel)]
893
943
  .sort()
894
- .forEach((f) => console.log(` ${CHA} ${f}`));
944
+ .forEach((f) => console.log(`${tab_a()}${CHA} ${f}`));
895
945
  [...toDelete.map((t) => t.rel)]
896
946
  .sort()
897
- .forEach((f) => console.log(` ${DEL} ${f}`));
947
+ .forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
898
948
  } else {
899
949
  log("\nNo changes.");
900
950
  }
901
951
 
902
952
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
903
- log("==================================================================\n\n");
904
953
  } catch (err) {
905
- elog(pc.red("❌ Synchronisation error:"), err);
954
+ const hint = describeSftpError(err);
955
+ elog(pc.red("❌ Synchronisation error:"), err.message || err);
956
+ if (hint) {
957
+ wlog(pc.yellow(`${tab_a()}Mögliche Ursache:`), hint);
958
+ }
959
+ if (IS_VERBOSE) {
960
+ // Vollständiges Error-Objekt nur in verbose anzeigen
961
+ console.error(err);
962
+ }
906
963
  process.exitCode = 1;
907
964
  try {
908
965
  await saveCache(true);
@@ -911,11 +968,18 @@ async function main() {
911
968
  }
912
969
  } finally {
913
970
  try {
914
- await sftp.end();
915
- } catch {
916
- // ignore
971
+ if (connected) {
972
+ await sftp.end();
973
+ log(pc.green(`${tab_a()}✔ Connection closed.`));
974
+ }
975
+ } catch (e) {
976
+ wlog(
977
+ pc.yellow("⚠️ Could not close SFTP connection cleanly:"),
978
+ e.message || e
979
+ );
917
980
  }
918
981
  }
982
+ log(`${hr2()}\n\n`);
919
983
  }
920
984
 
921
- main();
985
+ 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.13",
4
4
  "description": "SFTP sync tool for Hugo projects (local to remote, with hash cache)",
5
5
  "type": "module",
6
6
  "bin": {