sftp-push-sync 1.0.11 → 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,18 +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
  ],
78
+ "progress": {
79
+ "scanChunk": 10,
80
+ "analyzeChunk": 1
81
+ },
82
+ "logLevel": "normal",
69
83
  "uploadList": [],
70
- "downloadList": [
71
- "download-counter.json"
72
- ]
84
+ "downloadList": ["download-counter.json"]
73
85
  }
74
86
  ```
75
87
 
@@ -84,7 +96,6 @@ A list of files that are excluded from the sync comparison and can be downloaded
84
96
  - Relative to remoteRoot "download-counter.json"
85
97
  - or e.g. "logs/download-counter.json"
86
98
 
87
-
88
99
  ```bash
89
100
  # normal synchronisation
90
101
  sftp-push-sync staging
@@ -97,6 +108,16 @@ sftp-push-sync prod --download-list --dry-run # view first
97
108
  sftp-push-sync prod --download-list # then do
98
109
  ```
99
110
 
111
+ ### Logging Progress
112
+
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.
120
+
100
121
  ## NPM Scripts
101
122
 
102
123
  - Can be conveniently started via the scripts in `package.json`:
@@ -146,4 +167,4 @@ The first run always takes a while, especially with lots of images – so be pat
146
167
  ## Links
147
168
 
148
169
  - <https://www.npmjs.com/package/sftp-push-sync>
149
- - <https://github.com/cnichte/sftp-push-sync>
170
+ - <https://github.com/cnichte/sftp-push-sync>
@@ -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
@@ -40,12 +40,21 @@ import { createHash } from "crypto";
40
40
  import { Writable } from "stream";
41
41
  import pc from "picocolors";
42
42
 
43
+ // get Versionsnummer
44
+ import { createRequire } from "module";
45
+ const require = createRequire(import.meta.url);
46
+ const pkg = require("../package.json");
47
+
43
48
  // Colors for the State (works on dark + light background)
44
49
  const ADD = pc.green("+"); // Added
45
50
  const CHA = pc.yellow("~"); // Changed
46
51
  const DEL = pc.red("-"); // Deleted
47
52
  const EXC = pc.redBright("-"); // Excluded
48
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
+
49
58
  // ---------------------------------------------------------------------------
50
59
  // CLI arguments
51
60
  // ---------------------------------------------------------------------------
@@ -53,10 +62,14 @@ const EXC = pc.redBright("-"); // Excluded
53
62
  const args = process.argv.slice(2);
54
63
  const TARGET = args[0];
55
64
  const DRY_RUN = args.includes("--dry-run");
56
- const VERBOSE = args.includes("--verbose") || args.includes("-v");
57
65
  const RUN_UPLOAD_LIST = args.includes("--upload-list");
58
66
  const RUN_DOWNLOAD_LIST = args.includes("--download-list");
59
67
 
68
+ // logLevel override via CLI (optional)
69
+ let cliLogLevel = null;
70
+ if (args.includes("--verbose")) cliLogLevel = "verbose";
71
+ if (args.includes("--laconic")) cliLogLevel = "laconic";
72
+
60
73
  if (!TARGET) {
61
74
  console.error(pc.red("❌ Please specify a connection profile:"));
62
75
  console.error(pc.yellow(" sftp-push-sync staging --dry-run"));
@@ -78,18 +91,20 @@ let CONFIG_RAW;
78
91
  try {
79
92
  CONFIG_RAW = JSON.parse(await fsp.readFile(CONFIG_PATH, "utf8"));
80
93
  } catch (err) {
81
- elog(pc.red("❌ Error reading sync.config.json:"), err.message);
94
+ console.error(pc.red("❌ Error reading sync.config.json:"), err.message);
82
95
  process.exit(1);
83
96
  }
84
97
 
85
98
  if (!CONFIG_RAW.connections || typeof CONFIG_RAW.connections !== "object") {
86
- elog(pc.red("❌ sync.config.json must have a 'connections' field."));
99
+ console.error(pc.red("❌ sync.config.json must have a 'connections' field."));
87
100
  process.exit(1);
88
101
  }
89
102
 
90
103
  const TARGET_CONFIG = CONFIG_RAW.connections[TARGET];
91
104
  if (!TARGET_CONFIG) {
92
- elog(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
+ );
93
108
  process.exit(1);
94
109
  }
95
110
 
@@ -103,18 +118,39 @@ const CONNECTION = {
103
118
  workers: TARGET_CONFIG.worker ?? 2,
104
119
  };
105
120
 
121
+ // ---------------------------------------------------------------------------
122
+ // LogLevel + Progress aus Config
123
+ // ---------------------------------------------------------------------------
124
+
125
+ // logLevel: "verbose", "normal", "laconic"
126
+ let LOG_LEVEL = (CONFIG_RAW.logLevel ?? "normal").toLowerCase();
127
+
128
+ // Override config with CLI flags
129
+ if (cliLogLevel) {
130
+ LOG_LEVEL = cliLogLevel;
131
+ }
132
+
133
+ const IS_VERBOSE = LOG_LEVEL === "verbose";
134
+ const IS_LACONIC = LOG_LEVEL === "laconic";
135
+
136
+ const PROGRESS = CONFIG_RAW.progress ?? {};
137
+ const SCAN_CHUNK = PROGRESS.scanChunk ?? (IS_VERBOSE ? 1 : 100);
138
+ const ANALYZE_CHUNK = PROGRESS.analyzeChunk ?? (IS_VERBOSE ? 1 : 10);
139
+ // For >100k files, rather 10–50, for debugging/troubleshooting 1.
140
+
141
+ // ---------------------------------------------------------------------------
106
142
  // Shared config from JSON
107
- // Shared config from JSON
143
+ // ---------------------------------------------------------------------------
144
+
108
145
  const INCLUDE = CONFIG_RAW.include ?? [];
109
146
  const BASE_EXCLUDE = CONFIG_RAW.exclude ?? [];
110
147
 
111
- // Spezial: Listen für gezielte Uploads / Downloads
148
+ // Special: Lists for targeted uploads/downloads
112
149
  function normalizeList(list) {
113
150
  if (!Array.isArray(list)) return [];
114
151
  return list.flatMap((item) =>
115
152
  typeof item === "string"
116
- ? // erlaubt: ["a.json, b.json"] -> ["a.json", "b.json"]
117
- item
153
+ ? item
118
154
  .split(",")
119
155
  .map((s) => s.trim())
120
156
  .filter(Boolean)
@@ -128,7 +164,7 @@ const DOWNLOAD_LIST = normalizeList(CONFIG_RAW.downloadList ?? []);
128
164
  // Effektive Exclude-Liste: explizites exclude + Upload/Download-Listen
129
165
  const EXCLUDE = [...BASE_EXCLUDE, ...UPLOAD_LIST, ...DOWNLOAD_LIST];
130
166
 
131
- // Liste ALLER Dateien, die wegen uploadList/downloadList ausgeschlossen wurden
167
+ // List of ALL files that were excluded due to uploadList/downloadList
132
168
  const AUTO_EXCLUDED = new Set();
133
169
 
134
170
  const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
@@ -144,6 +180,7 @@ const TEXT_EXT = CONFIG_RAW.textExtensions ?? [
144
180
  ".md",
145
181
  ".svg",
146
182
  ];
183
+
147
184
  // Cache file name per connection
148
185
  const syncCacheName = TARGET_CONFIG.syncCache || `.sync-cache.${TARGET}.json`;
149
186
  const CACHE_PATH = path.resolve(syncCacheName);
@@ -166,7 +203,10 @@ try {
166
203
  CACHE.remote = raw.remote ?? {};
167
204
  }
168
205
  } catch (err) {
169
- wlog(pc.yellow("⚠️ Could not load cache, starting without:"), err.message);
206
+ console.warn(
207
+ pc.yellow("⚠️ Could not load cache, starting without:"),
208
+ err.message
209
+ );
170
210
  }
171
211
 
172
212
  function cacheKey(relPath) {
@@ -217,7 +257,7 @@ function log(...msg) {
217
257
  }
218
258
 
219
259
  function vlog(...msg) {
220
- if (!VERBOSE) return;
260
+ if (!IS_VERBOSE) return;
221
261
  clearProgressLine();
222
262
  console.log(...msg);
223
263
  }
@@ -256,27 +296,63 @@ function isTextFile(relPath) {
256
296
  return TEXT_EXT.includes(ext);
257
297
  }
258
298
 
259
- // Single-line progress bar
260
- function updateProgress(prefix, current, total) {
261
- const percent = total > 0 ? ((current / total) * 100).toFixed(1) : "0.0";
262
- const msg = `${prefix}${current}/${total} Dateien (${percent}%)`;
299
+ function shortenPathForProgress(rel) {
300
+ if (!rel) return "";
301
+ const parts = rel.split("/");
302
+ if (parts.length === 1) {
303
+ return rel; // nur Dateiname
304
+ }
305
+ if (parts.length === 2) {
306
+ return rel; // schon kurz genug
307
+ }
308
+
309
+ const last = parts[parts.length - 1];
310
+ const prev = parts[parts.length - 2];
311
+
312
+ // z.B. …/images/foo.jpg
313
+ return `…/${prev}/${last}`;
314
+ }
263
315
 
316
+ // Two-line progress bar
317
+ function updateProgress2(prefix, current, total, rel = "") {
264
318
  if (!process.stdout.isTTY) {
265
- // Fallback: simply log
266
- console.log(" " + msg);
319
+ // Fallback für Pipes / Logs
320
+ if (total && total > 0) {
321
+ const percent = ((current / total) * 100).toFixed(1);
322
+ console.log(
323
+ `${tab_a()}${prefix}${current}/${total} Files (${percent}%) – ${rel}`
324
+ );
325
+ } else {
326
+ console.log(`${tab_a()}${prefix}${current} Files – ${rel}`);
327
+ }
267
328
  return;
268
329
  }
269
330
 
270
331
  const width = process.stdout.columns || 80;
271
- const line = msg.padEnd(width - 1);
272
-
273
- progressActive = true;
274
- process.stdout.write("\r" + line);
275
332
 
276
- if (current === total) {
277
- process.stdout.write("\n");
278
- progressActive = false;
333
+ let line1;
334
+ if (total && total > 0) {
335
+ const percent = ((current / total) * 100).toFixed(1);
336
+ line1 = `${tab_a()}${prefix}${current}/${total} Files (${percent}%)`;
337
+ } else {
338
+ // „unknown total“ / Scanner-Modus
339
+ line1 = `${tab_a()}${prefix}${current} Files`;
279
340
  }
341
+
342
+ const short = rel ? shortenPathForProgress(rel) : "";
343
+ let line2 = short;
344
+
345
+ if (line1.length > width) line1 = line1.slice(0, width - 1);
346
+ if (line2.length > width) line2 = line2.slice(0, width - 1);
347
+
348
+ // zwei Zeilen überschreiben
349
+ process.stdout.write("\r" + line1.padEnd(width) + "\n");
350
+ process.stdout.write(line2.padEnd(width));
351
+
352
+ // Cursor wieder nach oben (auf die Fortschrittszeile)
353
+ process.stdout.write("\x1b[1A");
354
+
355
+ progressActive = true;
280
356
  }
281
357
 
282
358
  // Simple worker pool for parallel tasks
@@ -297,11 +373,11 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
297
373
  try {
298
374
  await handler(item);
299
375
  } catch (err) {
300
- elog(pc.red(` ⚠️ Error in ${label}:`), err.message || err);
376
+ elog(pc.red(`${tab_a()}⚠️ Error in ${label}:`), err.message || err);
301
377
  }
302
378
  done += 1;
303
379
  if (done % 10 === 0 || done === total) {
304
- updateProgress(` ${label}: `, done, total);
380
+ updateProgress2(`${tab_a()}${label}: `, done, total);
305
381
  }
306
382
  }
307
383
  }
@@ -320,6 +396,7 @@ async function runTasks(items, workerCount, handler, label = "Tasks") {
320
396
 
321
397
  async function walkLocal(root) {
322
398
  const result = new Map();
399
+ let scanned = 0;
323
400
 
324
401
  async function recurse(current) {
325
402
  const entries = await fsp.readdir(current, { withFileTypes: true });
@@ -329,7 +406,9 @@ async function walkLocal(root) {
329
406
  await recurse(full);
330
407
  } else if (entry.isFile()) {
331
408
  const rel = toPosix(path.relative(root, full));
409
+
332
410
  if (!isIncluded(rel)) continue;
411
+
333
412
  const stat = await fsp.stat(full);
334
413
  result.set(rel, {
335
414
  rel,
@@ -338,11 +417,26 @@ async function walkLocal(root) {
338
417
  mtimeMs: stat.mtimeMs,
339
418
  isText: isTextFile(rel),
340
419
  });
420
+
421
+ scanned += 1;
422
+ const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
423
+ if (scanned === 1 || scanned % chunk === 0) {
424
+ // totally unknown → totally = 0 → no automatic \n
425
+ updateProgress2(" Scan local: ", scanned, 0, rel);
426
+ }
341
427
  }
342
428
  }
343
429
  }
344
430
 
345
431
  await recurse(root);
432
+
433
+ if (scanned > 0) {
434
+ // last line + neat finish
435
+ updateProgress2(" Scan local: ", scanned, 0, "fertig");
436
+ process.stdout.write("\n");
437
+ progressActive = false;
438
+ }
439
+
346
440
  return result;
347
441
  }
348
442
 
@@ -352,6 +446,7 @@ async function walkLocal(root) {
352
446
 
353
447
  async function walkRemote(sftp, remoteRoot) {
354
448
  const result = new Map();
449
+ let scanned = 0;
355
450
 
356
451
  async function recurse(remoteDir, prefix) {
357
452
  const items = await sftp.list(remoteDir);
@@ -374,11 +469,24 @@ async function walkRemote(sftp, remoteRoot) {
374
469
  size: Number(item.size),
375
470
  modifyTime: item.modifyTime ?? 0,
376
471
  });
472
+
473
+ scanned += 1;
474
+ const chunk = IS_VERBOSE ? 1 : SCAN_CHUNK;
475
+ if (scanned === 1 || scanned % chunk === 0) {
476
+ updateProgress2(" Scan remote: ", scanned, 0, rel);
477
+ }
377
478
  }
378
479
  }
379
480
  }
380
481
 
381
- await recurse(remoteRoot, "");
482
+ await recurse(remoteRoot);
483
+
484
+ if (scanned > 0) {
485
+ updateProgress2(" Scan remote: ", scanned, 0, "fertig");
486
+ process.stdout.write("\n");
487
+ progressActive = false;
488
+ }
489
+
382
490
  return result;
383
491
  }
384
492
 
@@ -455,6 +563,45 @@ async function getRemoteHash(rel, meta, sftp) {
455
563
  return hash;
456
564
  }
457
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
+
458
605
  // ---------------------------------------------------------------------------
459
606
  // MAIN
460
607
  // ---------------------------------------------------------------------------
@@ -462,39 +609,46 @@ async function getRemoteHash(rel, meta, sftp) {
462
609
  async function main() {
463
610
  const start = Date.now();
464
611
 
465
- log("\n\n==================================================================");
466
- log(pc.bold("🔐 SFTP Push-Synchronisation: sftp-push-sync"));
467
- log(` Connection: ${pc.cyan(TARGET)} (Worker: ${CONNECTION.workers})`);
468
- log(` Host: ${pc.green(CONNECTION.host)}:${pc.green(CONNECTION.port)}`);
469
- log(` Local: ${pc.green(CONNECTION.localRoot)}`);
470
- log(` Remote: ${pc.green(CONNECTION.remoteRoot)}`);
612
+ log(`\n\n${hr2()}`);
613
+ log(
614
+ pc.bold(
615
+ `🔐 SFTP Push-Synchronisation: sftp-push-sync v${pkg.version} [logLevel=${LOG_LEVEL}]`
616
+ )
617
+ );
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)}`);
471
623
  if (DRY_RUN) log(pc.yellow(" Mode: DRY-RUN (no changes)"));
472
624
  if (RUN_UPLOAD_LIST || RUN_DOWNLOAD_LIST) {
473
625
  log(
474
626
  pc.blue(
475
- ` Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
627
+ `${tab_a()}Extra: ${RUN_UPLOAD_LIST ? "uploadList " : ""}${
476
628
  RUN_DOWNLOAD_LIST ? "downloadList" : ""
477
629
  }`
478
630
  )
479
631
  );
480
632
  }
481
- log("-----------------------------------------------------------------\n");
633
+ log(`${hr1()}\n`);
482
634
 
483
635
  const sftp = new SftpClient();
636
+ let connected = false;
484
637
 
485
638
  const toAdd = [];
486
639
  const toUpdate = [];
487
640
  const toDelete = [];
488
641
 
489
642
  try {
643
+ log(pc.cyan("🔌 Connecting to SFTP server …"));
490
644
  await sftp.connect({
491
645
  host: CONNECTION.host,
492
646
  port: CONNECTION.port,
493
647
  username: CONNECTION.user,
494
648
  password: CONNECTION.password,
495
649
  });
496
-
497
- vlog(pc.dim(" Connection established."));
650
+ connected = true;
651
+ log(pc.green(`${tab_a()}✔ Connected to SFTP.`));
498
652
 
499
653
  if (!fs.existsSync(CONNECTION.localRoot)) {
500
654
  console.error(
@@ -506,20 +660,20 @@ async function main() {
506
660
 
507
661
  log(pc.bold(pc.cyan("📥 Phase 1: Scan local files …")));
508
662
  const local = await walkLocal(CONNECTION.localRoot);
509
- log(` → ${local.size} local files`);
663
+ log(`${tab_a()}→ ${local.size} local files`);
510
664
 
511
665
  if (AUTO_EXCLUDED.size > 0) {
512
666
  log("");
513
667
  log(pc.dim(" Auto-excluded (uploadList/downloadList):"));
514
668
  [...AUTO_EXCLUDED].sort().forEach((file) => {
515
- log(pc.dim(` - ${file}`));
669
+ log(pc.dim(`${tab_a()} - ${file}`));
516
670
  });
517
671
  log("");
518
672
  }
519
673
 
520
674
  log(pc.bold(pc.cyan("📤 Phase 2: Scan remote files …")));
521
675
  const remote = await walkRemote(sftp, CONNECTION.remoteRoot);
522
- log(` → ${remote.size} remote files\n`);
676
+ log(`${tab_a()}→ ${remote.size} remote files\n`);
523
677
 
524
678
  const localKeys = new Set(local.keys());
525
679
  const remoteKeys = new Set(remote.keys());
@@ -531,28 +685,34 @@ async function main() {
531
685
  // Analysis: just decide, don't upload/delete anything yet
532
686
  for (const rel of localKeys) {
533
687
  checkedCount += 1;
688
+
689
+ const chunk = IS_VERBOSE ? 1 : ANALYZE_CHUNK;
534
690
  if (
535
- checkedCount === 1 || // sofortige erste Ausgabe
536
- checkedCount % 100 === 0 || // aktualisieren alle 100
537
- checkedCount === totalToCheck // letzte Ausgabe immer
691
+ checkedCount === 1 || // immediate first issue
692
+ checkedCount % chunk === 0 ||
693
+ checkedCount === totalToCheck
538
694
  ) {
539
- updateProgress(" Analyse: ", checkedCount, totalToCheck);
695
+ updateProgress2(" Analyse: ", checkedCount, totalToCheck, rel);
540
696
  }
697
+
541
698
  const l = local.get(rel);
542
699
  const r = remote.get(rel);
543
-
544
700
  const remotePath = path.posix.join(CONNECTION.remoteRoot, rel);
545
701
 
546
702
  if (!r) {
547
703
  toAdd.push({ rel, local: l, remotePath });
548
- log(`${ADD} ${pc.green("New:")} ${rel}`);
704
+ if (!IS_LACONIC) {
705
+ log(`${ADD} ${pc.green("New:")} ${rel}`);
706
+ }
549
707
  continue;
550
708
  }
551
709
 
552
710
  // 1. size comparison
553
711
  if (l.size !== r.size) {
554
712
  toUpdate.push({ rel, local: l, remote: r, remotePath });
555
- log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
713
+ if (!IS_LACONIC) {
714
+ log(`${CHA} ${pc.yellow("Size changed:")} ${rel}`);
715
+ }
556
716
  continue;
557
717
  }
558
718
 
@@ -570,18 +730,20 @@ async function main() {
570
730
  ).toString("utf8");
571
731
 
572
732
  if (localStr === remoteStr) {
573
- vlog(` ${pc.dim("✓ Unchanged (Text):")} ${rel}`);
733
+ vlog(`${tab_a()}${pc.dim("✓ Unchanged (Text):")} ${rel}`);
574
734
  continue;
575
735
  }
576
736
 
577
- if (VERBOSE) {
737
+ if (IS_VERBOSE) {
578
738
  const diff = diffWords(remoteStr, localStr);
579
739
  const blocks = diff.filter((d) => d.added || d.removed).length;
580
- vlog(` ${CHA} Text difference (${blocks} blocks) in ${rel}`);
740
+ vlog(`${tab_a()}${CHA} Text difference (${blocks} blocks) in ${rel}`);
581
741
  }
582
742
 
583
743
  toUpdate.push({ rel, local: l, remote: r, remotePath });
584
- log(`${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
744
+ if (!IS_LACONIC) {
745
+ log(`${tab_a()}${CHA} ${pc.yellow("Content changed (Text):")} ${rel}`);
746
+ }
585
747
  } else {
586
748
  // Binary: Hash comparison with cache
587
749
  const localMeta = l;
@@ -593,18 +755,20 @@ async function main() {
593
755
  ]);
594
756
 
595
757
  if (localHash === remoteHash) {
596
- vlog(` ${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
758
+ vlog(`${tab_a()}${pc.dim("✓ Unchanged (binary, hash):")} ${rel}`);
597
759
  continue;
598
760
  }
599
761
 
600
- if (VERBOSE) {
601
- vlog(` ${CHA} Hash different (binary): ${rel}`);
602
- vlog(` local: ${localHash}`);
603
- vlog(` remote: ${remoteHash}`);
762
+ if (IS_VERBOSE) {
763
+ vlog(`${tab_a()}${CHA} Hash different (binary): ${rel}`);
764
+ vlog(`${tab_a()} local: ${localHash}`);
765
+ vlog(`${tab_a()} remote: ${remoteHash}`);
604
766
  }
605
767
 
606
768
  toUpdate.push({ rel, local: l, remote: r, remotePath });
607
- log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
769
+ if (!IS_LACONIC) {
770
+ log(`${CHA} ${pc.yellow("Content changed (Binary):")} ${rel}`);
771
+ }
608
772
  }
609
773
  }
610
774
 
@@ -615,7 +779,9 @@ async function main() {
615
779
  if (!localKeys.has(rel)) {
616
780
  const r = remote.get(rel);
617
781
  toDelete.push({ rel, remotePath: r.remotePath });
618
- log(` ${DEL} ${pc.red("Remove:")} ${rel}`);
782
+ if (!IS_LACONIC) {
783
+ log(`${tab_a()}${DEL} ${pc.red("Remove:")} ${rel}`);
784
+ }
619
785
  }
620
786
  }
621
787
 
@@ -676,7 +842,11 @@ async function main() {
676
842
  "Deletes"
677
843
  );
678
844
  } else {
679
- 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
+ );
680
850
  }
681
851
 
682
852
  // -------------------------------------------------------------------
@@ -686,7 +856,7 @@ async function main() {
686
856
  if (RUN_UPLOAD_LIST && UPLOAD_LIST.length > 0) {
687
857
  log(
688
858
  "\n" +
689
- pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
859
+ pc.bold(pc.cyan("⬆️ Extra Phase: Upload-List (explicit files) …"))
690
860
  );
691
861
 
692
862
  const tasks = UPLOAD_LIST.map((rel) => ({
@@ -697,7 +867,7 @@ async function main() {
697
867
 
698
868
  if (DRY_RUN) {
699
869
  for (const t of tasks) {
700
- log(` ${ADD} would upload (uploadList): ${t.rel}`);
870
+ log(`${tab_a()}${ADD} would upload (uploadList): ${t.rel}`);
701
871
  }
702
872
  } else {
703
873
  await runTasks(
@@ -711,7 +881,7 @@ async function main() {
711
881
  // ignore
712
882
  }
713
883
  await sftp.put(localPath, remotePath);
714
- log(` ${ADD} uploadList: ${rel}`);
884
+ log(`${tab_a()}${ADD} uploadList: ${rel}`);
715
885
  },
716
886
  "Upload-List"
717
887
  );
@@ -721,7 +891,7 @@ async function main() {
721
891
  if (RUN_DOWNLOAD_LIST && DOWNLOAD_LIST.length > 0) {
722
892
  log(
723
893
  "\n" +
724
- pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
894
+ pc.bold(pc.cyan("⬇️ Extra Phase: Download-List (explicit files) …"))
725
895
  );
726
896
 
727
897
  const tasks = DOWNLOAD_LIST.map((rel) => ({
@@ -732,7 +902,7 @@ async function main() {
732
902
 
733
903
  if (DRY_RUN) {
734
904
  for (const t of tasks) {
735
- log(` ${ADD} would download (downloadList): ${t.rel}`);
905
+ log(`${tab_a()}${ADD} would download (downloadList): ${t.rel}`);
736
906
  }
737
907
  } else {
738
908
  await runTasks(
@@ -741,7 +911,7 @@ async function main() {
741
911
  async ({ remotePath, localPath, rel }) => {
742
912
  await fsp.mkdir(path.dirname(localPath), { recursive: true });
743
913
  await sftp.fastGet(remotePath, localPath);
744
- log(` ${ADD} downloadList: ${rel}`);
914
+ log(`${tab_a()}${ADD} downloadList: ${rel}`);
745
915
  },
746
916
  "Download-List"
747
917
  );
@@ -755,36 +925,41 @@ async function main() {
755
925
 
756
926
  // Summary
757
927
  log("\n" + pc.bold(pc.cyan("📊 Summary:")));
758
- log(` Duration: ${pc.green(duration + " s")}`);
759
- log(` ${ADD} Added : ${toAdd.length}`);
760
- log(` ${CHA} Changed: ${toUpdate.length}`);
761
- 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}`);
762
932
  if (AUTO_EXCLUDED.size > 0) {
763
933
  log(
764
- ` ${EXC} Excluded via uploadList | downloadList): ${AUTO_EXCLUDED.size}`
934
+ `${tab_a()}${EXC} Excluded via uploadList | downloadList: ${AUTO_EXCLUDED.size}`
765
935
  );
766
936
  }
767
937
  if (toAdd.length || toUpdate.length || toDelete.length) {
768
938
  log("\n📄 Changes:");
769
939
  [...toAdd.map((t) => t.rel)]
770
940
  .sort()
771
- .forEach((f) => console.log(` ${ADD} ${f}`));
941
+ .forEach((f) => console.log(`${tab_a()}${ADD} ${f}`));
772
942
  [...toUpdate.map((t) => t.rel)]
773
943
  .sort()
774
- .forEach((f) => console.log(` ${CHA} ${f}`));
944
+ .forEach((f) => console.log(`${tab_a()}${CHA} ${f}`));
775
945
  [...toDelete.map((t) => t.rel)]
776
946
  .sort()
777
- .forEach((f) => console.log(` ${DEL} ${f}`));
947
+ .forEach((f) => console.log(`${tab_a()}${DEL} ${f}`));
778
948
  } else {
779
949
  log("\nNo changes.");
780
950
  }
781
951
 
782
952
  log("\n" + pc.bold(pc.green("✅ Sync complete.")));
783
- log(
784
- "==================================================================\n\n"
785
- );
786
953
  } catch (err) {
787
- 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
+ }
788
963
  process.exitCode = 1;
789
964
  try {
790
965
  await saveCache(true);
@@ -793,11 +968,18 @@ async function main() {
793
968
  }
794
969
  } finally {
795
970
  try {
796
- await sftp.end();
797
- } catch {
798
- // 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
+ );
799
980
  }
800
981
  }
982
+ log(`${hr2()}\n\n`);
801
983
  }
802
984
 
803
985
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sftp-push-sync",
3
- "version": "1.0.11",
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": {