dead-links 0.3.0 → 0.4.0

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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
4
+ import { Command, Option } from "commander";
5
5
  import { readFileSync } from "fs";
6
- import { writeFile as writeFile2 } from "fs/promises";
6
+ import { writeFile } from "fs/promises";
7
7
  import ora from "ora";
8
8
 
9
9
  // src/core/vault-loader.ts
@@ -356,72 +356,6 @@ function countConnectedComponents(graph) {
356
356
  return components;
357
357
  }
358
358
 
359
- // src/utils/suggestions.ts
360
- function levenshteinDistance(a, b) {
361
- const matrix = Array(b.length + 1).fill(null).map(() => Array(a.length + 1).fill(0));
362
- for (let i = 0; i <= a.length; i++) matrix[0][i] = i;
363
- for (let j = 0; j <= b.length; j++) matrix[j][0] = j;
364
- for (let j = 1; j <= b.length; j++) {
365
- for (let i = 1; i <= a.length; i++) {
366
- if (a[i - 1] === b[j - 1]) {
367
- matrix[j][i] = matrix[j - 1][i - 1];
368
- } else {
369
- matrix[j][i] = Math.min(
370
- matrix[j][i - 1] + 1,
371
- matrix[j - 1][i] + 1,
372
- matrix[j - 1][i - 1] + 1
373
- );
374
- }
375
- }
376
- }
377
- return matrix[b.length][a.length];
378
- }
379
- function calculateSimilarity(a, b) {
380
- const distance = levenshteinDistance(
381
- a.toLowerCase(),
382
- b.toLowerCase()
383
- );
384
- const maxLen = Math.max(a.length, b.length);
385
- return Math.max(0, 1 - distance / maxLen);
386
- }
387
- function generateSuggestions(brokenTargets, allNotes, threshold = 0.6) {
388
- const availableNotes = allNotes.map((n) => ({
389
- name: n.relativePath.replace(/\.(md|canvas)$/, ""),
390
- basename: n.relativePath.split("/").pop()?.replace(/\.(md|canvas)$/, "") || "",
391
- aliases: n.aliases
392
- }));
393
- const suggestions = [];
394
- for (const broken of brokenTargets) {
395
- const brokenBasename = broken.split("/").pop() || broken;
396
- const candidates = [];
397
- for (const note of availableNotes) {
398
- const basenameScore = calculateSimilarity(brokenBasename, note.basename);
399
- const fullScore = calculateSimilarity(broken, note.name);
400
- for (const alias of note.aliases) {
401
- const aliasScore = calculateSimilarity(broken, alias);
402
- if (aliasScore > basenameScore && aliasScore > fullScore) {
403
- candidates.push({ suggestion: note.name, similarity: aliasScore });
404
- }
405
- }
406
- if (basenameScore >= fullScore && basenameScore > threshold) {
407
- candidates.push({ suggestion: note.name, similarity: basenameScore });
408
- } else if (fullScore > threshold) {
409
- candidates.push({ suggestion: note.name, similarity: fullScore });
410
- }
411
- }
412
- if (candidates.length > 0) {
413
- const best = candidates.sort((a, b) => b.similarity - a.similarity)[0];
414
- suggestions.push({
415
- broken,
416
- suggested: best.suggestion,
417
- similarity: best.similarity,
418
- source: broken
419
- });
420
- }
421
- }
422
- return suggestions.sort((a, b) => b.similarity - a.similarity);
423
- }
424
-
425
359
  // src/utils/engine.ts
426
360
  var DeadLinksEngine = class {
427
361
  async analyze(vaultPath, config = {}) {
@@ -434,12 +368,6 @@ var DeadLinksEngine = class {
434
368
  })
435
369
  );
436
370
  const report = analyze(notes, vaultPath, config.checkAttachments, config.ignoreFolders);
437
- if (config.suggestions || config.fix) {
438
- const brokenTargets = Array.from(
439
- new Set(report.brokenLinks.map((b) => b.link.target))
440
- );
441
- report.suggestions = generateSuggestions(brokenTargets, notes, 0.65);
442
- }
443
371
  report.duration = Date.now() - startTime;
444
372
  return report;
445
373
  }
@@ -476,9 +404,7 @@ var ConfigLoader = class {
476
404
  ignore: cliOptions.ignore || fileConfig.ignore,
477
405
  checkAttachments: cliOptions.attachments ?? fileConfig.checkAttachments ?? false,
478
406
  ignoreFolders: cliOptions.ignoreFolders || fileConfig.ignoreFolders,
479
- suggestions: cliOptions.suggestions ?? fileConfig.suggestions ?? false,
480
- debug: cliOptions.debug ?? fileConfig.debug ?? false,
481
- fix: cliOptions.fix ?? fileConfig.fix ?? false
407
+ debug: cliOptions.debug ?? fileConfig.debug ?? false
482
408
  };
483
409
  }
484
410
  };
@@ -502,9 +428,6 @@ var TerminalReporter = class {
502
428
  output += this.printStats(result);
503
429
  output += this.printBrokenLinks(result.brokenLinks);
504
430
  output += this.printOrphanNotes(result.orphanNotes);
505
- if (result.suggestions && result.suggestions.length > 0) {
506
- output += this.printSuggestions(result.suggestions);
507
- }
508
431
  output += this.printDuration(result.duration);
509
432
  return output;
510
433
  }
@@ -604,22 +527,6 @@ var TerminalReporter = class {
604
527
  return this.chalk.dim(` Completed in ${ms}ms
605
528
  `);
606
529
  }
607
- printSuggestions(suggestions) {
608
- let out = this.chalk.bold.cyan(`
609
- \u{1F4A1} Suggestions (${suggestions.length})
610
-
611
- `);
612
- for (const suggestion of suggestions.slice(0, 5)) {
613
- const confidence = Math.round(suggestion.similarity * 100);
614
- const bar2 = confidence >= 80 ? this.chalk.green("\u2588\u2588\u2588\u2588\u2588") : confidence >= 60 ? this.chalk.yellow("\u2588\u2588\u2588\u2588\u2588") : this.chalk.gray("\u2588\u2588\u2588\u2588\u2588");
615
- out += ` ${this.chalk.gray(suggestion.broken)} \u2192 ${this.chalk.cyan(suggestion.suggested)}
616
- `;
617
- out += ` ${bar2} ${confidence}%
618
-
619
- `;
620
- }
621
- return out;
622
- }
623
530
  };
624
531
 
625
532
  // src/reporters/json-reporter.ts
@@ -686,194 +593,225 @@ var CsvReporter = class {
686
593
  // src/reporters/html-reporter.ts
687
594
  var HtmlReporter = class {
688
595
  report(result) {
596
+ const summaryHtml = this.renderSummary(result);
597
+ const statsHtml = this.renderStats(result);
689
598
  const brokenLinksHtml = this.renderBrokenLinks(result.brokenLinks);
690
599
  const orphanNotesHtml = this.renderOrphanNotes(result.orphanNotes);
691
600
  const graphHtml = this.renderGraph(result.graph);
692
- const statsHtml = this.renderStats(result.stats, result.duration);
693
601
  return `<!DOCTYPE html>
694
602
  <html lang="en">
695
603
  <head>
696
604
  <meta charset="UTF-8">
697
605
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
698
- <title>Dead Links Report</title>
606
+ <meta name="theme-color" content="#ECEBE8">
607
+ <title>Vault report | Dead Links</title>
699
608
  <style>
700
- * { margin: 0; padding: 0; box-sizing: border-box; }
609
+ :root {
610
+ color-scheme: light;
611
+ --canvas: #ECEBE8;
612
+ --surface: #FFFFFF;
613
+ --surface-raised: #F7F5FC;
614
+ --ink: #0A0A0A;
615
+ --body: #2A2620;
616
+ --muted: #5F5D57;
617
+ --accent: #7C5CFF;
618
+ --accent-ink: #4C3897;
619
+ --accent-wash: #F4F0FF;
620
+ --accent-border: #DCD3FF;
621
+ --border: #D9D6CD;
622
+ --border-soft: #EBE9E3;
623
+ --space-1: 4px;
624
+ --space-2: 8px;
625
+ --space-3: 12px;
626
+ --space-4: 16px;
627
+ --space-5: 20px;
628
+ --space-6: 24px;
629
+ --space-7: 32px;
630
+ --space-section: 40px;
631
+ --font-ui: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
632
+ --font-display: 'Instrument Serif', Georgia, serif;
633
+ --font-mono: 'JetBrains Mono', 'Courier New', monospace;
634
+ }
635
+
636
+ *, *::before, *::after { box-sizing: border-box; }
637
+ html { background: var(--canvas); scroll-behavior: smooth; }
701
638
  body {
702
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
703
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
704
- min-height: 100vh;
705
- padding: 20px;
639
+ margin: 0;
640
+ color: var(--body);
641
+ background: var(--canvas);
642
+ font-family: var(--font-ui);
643
+ font-size: 15px;
644
+ line-height: 1.6;
645
+ -webkit-font-smoothing: antialiased;
646
+ text-rendering: optimizeLegibility;
706
647
  }
707
- .container {
708
- max-width: 1400px;
709
- margin: 0 auto;
710
- background: white;
711
- border-radius: 12px;
712
- box-shadow: 0 20px 60px rgba(0,0,0,0.3);
713
- overflow: hidden;
648
+ ::selection { color: var(--surface); background: var(--accent); }
649
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
650
+ .page { width: min(100% - 48px, 1200px); margin: 0 auto; }
651
+ .masthead {
652
+ position: relative;
653
+ padding: 48px 0 32px;
654
+ border-bottom: 1px solid var(--border);
655
+ background-image:
656
+ radial-gradient(rgba(124, 92, 255, .13) 1px, transparent 1.5px),
657
+ radial-gradient(rgba(124, 92, 255, .06) 1px, transparent 1.5px);
658
+ background-position: 0 0, 8px 8px;
659
+ background-size: 16px 16px;
714
660
  }
715
- header {
716
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
717
- color: white;
718
- padding: 40px;
719
- text-align: center;
661
+ .masthead-inner { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; }
662
+ .masthead h1 {
663
+ margin: 0;
664
+ color: var(--ink);
665
+ font-family: var(--font-display);
666
+ font-size: 44px;
667
+ font-weight: 400;
668
+ line-height: 1.04;
669
+ letter-spacing: -.03em;
720
670
  }
721
- header h1 { font-size: 2.5em; margin-bottom: 10px; }
722
- header p { font-size: 1.1em; opacity: 0.9; }
723
- .content { padding: 40px; }
724
- .section {
725
- margin-bottom: 40px;
726
- border-left: 4px solid #667eea;
727
- padding-left: 20px;
671
+ .masthead p { max-width: 62ch; margin: 12px 0 0; color: var(--muted); }
672
+ main { padding: 32px 0 48px; }
673
+ .overview { margin-bottom: var(--space-section); }
674
+ .overview-intro { display: flex; align-items: baseline; justify-content: space-between; gap: 20px; margin-bottom: 16px; }
675
+ .overview-intro h2, .section-heading h2 {
676
+ margin: 0;
677
+ color: var(--ink);
678
+ font-size: 18.4px;
679
+ font-weight: 600;
680
+ line-height: 1.2;
681
+ letter-spacing: -.02em;
728
682
  }
729
- .section h2 {
730
- font-size: 1.8em;
731
- margin-bottom: 20px;
732
- color: #333;
683
+ .overview-intro p { margin: 0; color: var(--muted); font-size: 12px; }
684
+ .summary {
685
+ margin: 0 0 20px;
686
+ padding: 16px 20px;
687
+ border: 1px solid var(--accent-border);
688
+ background: var(--accent-wash);
689
+ color: var(--ink);
690
+ font-weight: 500;
733
691
  }
734
- .stats-grid {
692
+ .metrics {
735
693
  display: grid;
736
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
737
- gap: 20px;
738
- margin-bottom: 40px;
694
+ grid-template-columns: repeat(4, minmax(0, 1fr));
695
+ gap: 1px;
696
+ border: 1px solid var(--border);
697
+ background: var(--border);
739
698
  }
740
- .stat-card {
741
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
742
- color: white;
743
- padding: 20px;
744
- border-radius: 8px;
745
- text-align: center;
746
- }
747
- .stat-card .value { font-size: 2.5em; font-weight: bold; }
748
- .stat-card .label { font-size: 0.9em; opacity: 0.9; margin-top: 10px; }
749
- table {
699
+ .metric { min-height: 110px; padding: 18px 20px; background: var(--surface); }
700
+ .metric-value { display: block; color: var(--ink); font-size: 28px; font-weight: 600; line-height: 1.15; font-variant-numeric: tabular-nums; letter-spacing: -.03em; }
701
+ .metric-label { display: block; margin-top: 8px; color: var(--muted); font-size: 12px; }
702
+ .metric:nth-child(3) .metric-value, .metric:nth-child(4) .metric-value { color: var(--accent-ink); }
703
+ .section { margin-top: var(--space-section); }
704
+ .section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 16px; }
705
+ .section-count { color: var(--accent-ink); background: var(--accent-wash); padding: 3px 8px; font-family: var(--font-mono); font-size: 11px; font-variant-numeric: tabular-nums; }
706
+ .data-surface { border: 1px solid var(--border); background: var(--surface); }
707
+ .filter-row { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--border-soft); }
708
+ .filter-row label { flex: 0 0 auto; color: var(--muted); font-size: 12px; }
709
+ .filter-row input {
750
710
  width: 100%;
751
- border-collapse: collapse;
752
- margin-top: 20px;
711
+ min-width: 0;
712
+ min-height: 42px;
713
+ padding: 9px 12px;
714
+ border: 1px solid var(--border);
715
+ border-radius: 0;
716
+ color: var(--ink);
717
+ background: var(--surface);
718
+ font: inherit;
719
+ }
720
+ .filter-row input::placeholder { color: var(--muted); opacity: 1; }
721
+ .filter-row input:focus-visible { border-color: var(--accent); outline-offset: 2px; }
722
+ .table-scroll { overflow-x: auto; }
723
+ .bounded-table-scroll {
724
+ max-height: min(55vh, 520px);
725
+ overflow: auto;
726
+ overscroll-behavior: contain;
727
+ scrollbar-color: var(--accent) var(--surface-raised);
728
+ scrollbar-width: thin;
753
729
  }
730
+ .bounded-table-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
731
+ .bounded-table-scroll thead th { position: sticky; top: 0; z-index: 1; }
732
+ table { width: 100%; border-collapse: collapse; text-align: left; }
754
733
  th {
755
- background: #f5f5f5;
756
- padding: 12px;
757
- text-align: left;
758
- border-bottom: 2px solid #667eea;
734
+ padding: 11px 16px;
735
+ color: var(--muted);
736
+ background: var(--surface-raised);
737
+ border-bottom: 1px solid var(--border);
738
+ font-size: 11px;
759
739
  font-weight: 600;
740
+ letter-spacing: .04em;
741
+ text-transform: uppercase;
742
+ white-space: nowrap;
760
743
  }
761
- td {
762
- padding: 12px;
763
- border-bottom: 1px solid #eee;
764
- }
765
- tr:hover { background: #f9f9f9; }
766
- .badge {
767
- display: inline-block;
768
- padding: 4px 8px;
769
- border-radius: 4px;
770
- font-size: 0.85em;
771
- font-weight: 500;
772
- }
773
- .badge-wiki { background: #dbeafe; color: #1e40af; }
774
- .badge-embed { background: #fed7aa; color: #92400e; }
775
- .badge-markdown { background: #d1fae5; color: #065f46; }
776
- .error { color: #dc2626; }
777
- .success { color: #16a34a; }
778
- .tabs {
779
- display: flex;
780
- gap: 0;
781
- border-bottom: 2px solid #eee;
782
- margin-bottom: 20px;
783
- }
784
- .tab-button {
785
- padding: 12px 20px;
786
- border: none;
787
- background: none;
788
- cursor: pointer;
789
- font-size: 1em;
790
- border-bottom: 3px solid transparent;
791
- color: #666;
792
- transition: all 0.3s;
793
- }
794
- .tab-button.active {
795
- color: #667eea;
796
- border-bottom-color: #667eea;
744
+ td { padding: 13px 16px; border-bottom: 1px solid var(--border-soft); color: var(--body); vertical-align: top; }
745
+ tbody tr:last-child td { border-bottom: 0; }
746
+ tbody tr:hover { background: var(--surface-raised); }
747
+ td:first-child { color: var(--ink); font-weight: 500; }
748
+ .type-label { color: var(--muted); font-family: var(--font-mono); font-size: 11px; }
749
+ .numeric { font-variant-numeric: tabular-nums; white-space: nowrap; }
750
+ .empty-state { margin: 0; padding: 22px 16px; color: var(--muted); }
751
+ .empty-state strong { color: var(--ink); font-weight: 500; }
752
+ .connections-table td:not(:first-child), .connections-table th:not(:first-child) { text-align: right; }
753
+ footer { padding: 18px 0 28px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; }
754
+ footer time { font-variant-numeric: tabular-nums; }
755
+
756
+ @media (max-width: 768px) {
757
+ .page { width: min(100% - 32px, 600px); }
758
+ .masthead { padding-top: 32px; }
759
+ .masthead-inner { align-items: flex-start; }
760
+ .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
761
+ .section-heading { align-items: flex-start; }
762
+ th, td { padding-right: 12px; padding-left: 12px; }
797
763
  }
798
- .tab-content { display: none; }
799
- .tab-content.active { display: block; }
800
- .filter-box {
801
- margin: 20px 0;
802
- padding: 15px;
803
- background: #f9f9f9;
804
- border-radius: 8px;
764
+ @media (max-width: 420px) {
765
+ .page { width: min(100% - 24px, 600px); }
766
+ .masthead { padding: 28px 0 24px; }
767
+ .masthead h1 { font-size: 40px; }
768
+ main { padding-top: 24px; }
769
+ .overview-intro { display: block; }
770
+ .overview-intro p { margin-top: 5px; }
771
+ .metric { min-height: 96px; padding: 14px 12px; }
772
+ .metric-value { font-size: 18.4px; }
773
+ .filter-row { align-items: stretch; flex-direction: column; gap: 6px; }
805
774
  }
806
- .filter-box input {
807
- width: 100%;
808
- padding: 8px;
809
- border: 1px solid #ddd;
810
- border-radius: 4px;
811
- font-size: 1em;
812
- }
813
- footer {
814
- background: #f5f5f5;
815
- padding: 20px;
816
- text-align: center;
817
- color: #666;
818
- font-size: 0.9em;
775
+ @media (prefers-reduced-motion: reduce) {
776
+ html { scroll-behavior: auto; }
777
+ *, *::before, *::after { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
819
778
  }
820
779
  </style>
821
780
  </head>
822
781
  <body>
823
- <div class="container">
824
- <header>
825
- <h1>\u{1F50D} Dead Links Report</h1>
826
- <p>Obsidian Vault Analysis</p>
827
- </header>
828
-
829
- <div class="content">
830
- ${statsHtml}
831
- ${brokenLinksHtml}
832
- ${orphanNotesHtml}
833
- ${graphHtml}
782
+ <header class="masthead">
783
+ <div class="page masthead-inner">
784
+ <div>
785
+ <h1>Dead Links Report</h1>
786
+ <p>Obsidian Vault Analysis</p>
787
+ </div>
834
788
  </div>
835
-
836
- <footer>
837
- Generated on ${(/* @__PURE__ */ new Date()).toLocaleString()}
838
- </footer>
839
- </div>
840
-
789
+ </header>
790
+ <main class="page">
791
+ ${summaryHtml}
792
+ ${statsHtml}
793
+ ${brokenLinksHtml}
794
+ ${orphanNotesHtml}
795
+ ${graphHtml}
796
+ </main>
797
+ <footer>
798
+ <div class="page">Generated <time>${this.escapeHtml((/* @__PURE__ */ new Date()).toLocaleString())}</time> \xB7 Completed in ${result.duration}ms</div>
799
+ </footer>
841
800
  <script>
842
- function setupTabs() {
843
- const buttons = document.querySelectorAll('.tab-button');
844
- const contents = document.querySelectorAll('.tab-content');
845
-
846
- buttons.forEach((button, index) => {
847
- button.addEventListener('click', () => {
848
- buttons.forEach(b => b.classList.remove('active'));
849
- contents.forEach(c => c.classList.remove('active'));
850
- button.classList.add('active');
851
- contents[index].classList.add('active');
852
- });
853
- });
854
-
855
- // Activate first tab
856
- if (buttons.length > 0) buttons[0].classList.add('active');
857
- if (contents.length > 0) contents[0].classList.add('active');
858
- }
859
-
860
801
  function setupFilter(tableId, inputId) {
861
802
  const input = document.getElementById(inputId);
862
803
  const table = document.getElementById(tableId);
863
804
  if (!input || !table) return;
864
805
 
865
- input.addEventListener('keyup', (e) => {
866
- const filter = e.target.value.toLowerCase();
867
- const rows = table.querySelectorAll('tbody tr');
868
- rows.forEach(row => {
869
- const text = row.textContent.toLowerCase();
870
- row.style.display = text.includes(filter) ? '' : 'none';
806
+ input.addEventListener('input', (event) => {
807
+ const filter = event.currentTarget.value.toLocaleLowerCase();
808
+ table.querySelectorAll('tbody tr').forEach((row) => {
809
+ row.hidden = !row.textContent.toLocaleLowerCase().includes(filter);
871
810
  });
872
811
  });
873
812
  }
874
813
 
875
814
  document.addEventListener('DOMContentLoaded', () => {
876
- setupTabs();
877
815
  setupFilter('broken-table', 'broken-filter');
878
816
  setupFilter('orphan-table', 'orphan-filter');
879
817
  });
@@ -881,147 +819,124 @@ var HtmlReporter = class {
881
819
  </body>
882
820
  </html>`;
883
821
  }
884
- renderStats(stats, duration) {
885
- const healthColor = stats.brokenCount === 0 && stats.orphanCount === 0 ? "#16a34a" : "#dc2626";
822
+ renderSummary(result) {
823
+ const brokenCount = result.stats.brokenCount;
824
+ const orphanCount = result.stats.orphanCount;
825
+ const message = brokenCount === 0 && orphanCount === 0 ? "No broken links or orphan notes found." : `${brokenCount} broken link${brokenCount === 1 ? "" : "s"} \xB7 ${orphanCount} orphan note${orphanCount === 1 ? "" : "s"}`;
886
826
  return `
887
- <div class="section">
888
- <h2>\u{1F4CA} Overview</h2>
889
- <div class="stats-grid">
890
- <div class="stat-card">
891
- <div class="value">${stats.totalNotes}</div>
892
- <div class="label">Total Notes</div>
893
- </div>
894
- <div class="stat-card">
895
- <div class="value">${stats.totalLinks}</div>
896
- <div class="label">Total Links</div>
897
- </div>
898
- <div class="stat-card" style="background: linear-gradient(135deg, #dc2626 0%, #991b1b 100%);">
899
- <div class="value">${stats.brokenCount}</div>
900
- <div class="label">Broken Links</div>
901
- </div>
902
- <div class="stat-card" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);">
903
- <div class="value">${stats.orphanCount}</div>
904
- <div class="label">Orphan Notes</div>
905
- </div>
906
- <div class="stat-card" style="background: linear-gradient(135deg, ${healthColor} 0%, ${healthColor === "#16a34a" ? "#15803d" : "#991b1b"} 100%);">
907
- <div class="value">${stats.connectedComponents}</div>
908
- <div class="label">Connected Components</div>
909
- </div>
910
- <div class="stat-card" style="background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%);">
911
- <div class="value">${duration}ms</div>
912
- <div class="label">Scan Duration</div>
913
- </div>
827
+ <section class="overview" aria-labelledby="overview-title">
828
+ <div class="overview-intro">
829
+ <h2 id="overview-title">At a glance</h2>
830
+ <p>Scan completed in ${result.duration}ms</p>
914
831
  </div>
915
- </div>
832
+ <p class="summary">${this.escapeHtml(message)}</p>
833
+ </section>
834
+ `;
835
+ }
836
+ renderStats(result) {
837
+ const { stats } = result;
838
+ const averageLinks = stats.totalNotes === 0 ? 0 : stats.totalLinks / stats.totalNotes;
839
+ const healthScore = stats.totalLinks === 0 ? 100 : 100 - stats.brokenCount / stats.totalLinks * 100;
840
+ const metrics = [
841
+ ["Total Notes", String(stats.totalNotes)],
842
+ ["Links", String(stats.totalLinks)],
843
+ ["Broken links", String(stats.brokenCount)],
844
+ ["Orphan notes", String(stats.orphanCount)],
845
+ ["Avg. links per note", averageLinks.toFixed(2)],
846
+ ["Vault health", `${healthScore.toFixed(1)}%`],
847
+ ["Connected components", String(stats.connectedComponents)]
848
+ ];
849
+ return `
850
+ <section class="overview" aria-label="Vault statistics">
851
+ <div class="metrics">
852
+ ${metrics.map(([label, value]) => `
853
+ <div class="metric">
854
+ <span class="metric-value">${this.escapeHtml(value)}</span>
855
+ <span class="metric-label">${this.escapeHtml(label)}</span>
856
+ </div>`).join("")}
857
+ </div>
858
+ </section>
916
859
  `;
917
860
  }
918
861
  renderBrokenLinks(brokenLinks) {
919
- if (brokenLinks.length === 0) {
920
- return `
921
- <div class="section">
922
- <h2>\u2705 Broken Links</h2>
923
- <p class="success">No broken links found!</p>
924
- </div>
925
- `;
926
- }
927
- const rows = brokenLinks.map(
928
- (link) => `
862
+ const rows = brokenLinks.map((link) => `
929
863
  <tr>
930
864
  <td>${this.escapeHtml(link.source)}</td>
931
865
  <td>${this.escapeHtml(link.link.target)}</td>
932
- <td><span class="badge badge-${link.link.type}">${link.link.type}</span></td>
933
- <td>${link.link.line}</td>
866
+ <td><span class="type-label">${this.escapeHtml(link.link.type)}</span></td>
867
+ <td class="numeric">${link.link.line}</td>
934
868
  <td>${this.escapeHtml(link.reason)}</td>
935
869
  </tr>
936
- `
937
- ).join("");
870
+ `).join("");
938
871
  return `
939
- <div class="section">
940
- <h2>\u{1F534} Broken Links (${brokenLinks.length})</h2>
941
- <div class="filter-box">
942
- <input type="text" id="broken-filter" placeholder="Filter by source or target..." />
872
+ <section class="section" aria-labelledby="broken-title">
873
+ <div class="section-heading">
874
+ <h2 id="broken-title">Broken links</h2>
875
+ <span class="section-count">${brokenLinks.length} found</span>
943
876
  </div>
944
- <table id="broken-table">
945
- <thead>
946
- <tr>
947
- <th>Source</th>
948
- <th>Target</th>
949
- <th>Type</th>
950
- <th>Line</th>
951
- <th>Reason</th>
952
- </tr>
953
- </thead>
954
- <tbody>
955
- ${rows}
956
- </tbody>
957
- </table>
958
- </div>
877
+ <div class="data-surface">
878
+ ${brokenLinks.length === 0 ? '<p class="empty-state">No broken links found.</p>' : `<div class="filter-row">
879
+ <label for="broken-filter">Filter broken links</label>
880
+ <input type="search" id="broken-filter" aria-label="Filter broken links" placeholder="Search source, target, type, or reason">
881
+ </div>
882
+ <div class="table-scroll bounded-table-scroll" tabindex="0" role="region" aria-label="Scrollable broken links">
883
+ <table id="broken-table">
884
+ <thead><tr><th>Source</th><th>Target</th><th>Type</th><th>Line</th><th>Reason</th></tr></thead>
885
+ <tbody>${rows}</tbody>
886
+ </table>
887
+ </div>`}
888
+ </div>
889
+ </section>
959
890
  `;
960
891
  }
961
892
  renderOrphanNotes(orphanNotes) {
962
- if (orphanNotes.length === 0) {
963
- return `
964
- <div class="section">
965
- <h2>\u2705 Orphan Notes</h2>
966
- <p class="success">No orphan notes found!</p>
967
- </div>
968
- `;
969
- }
970
- const rows = orphanNotes.map(
971
- (note) => `
972
- <tr>
973
- <td>${this.escapeHtml(note.relativePath)}</td>
974
- </tr>
975
- `
976
- ).join("");
893
+ const rows = orphanNotes.map((note) => `
894
+ <tr><td>${this.escapeHtml(note.relativePath)}</td></tr>
895
+ `).join("");
977
896
  return `
978
- <div class="section">
979
- <h2>\u{1F7E1} Orphan Notes (${orphanNotes.length})</h2>
980
- <div class="filter-box">
981
- <input type="text" id="orphan-filter" placeholder="Filter by path..." />
897
+ <section class="section" aria-labelledby="orphans-title">
898
+ <div class="section-heading">
899
+ <h2 id="orphans-title">Orphan notes</h2>
900
+ <span class="section-count">${orphanNotes.length} found</span>
982
901
  </div>
983
- <table id="orphan-table">
984
- <thead>
985
- <tr>
986
- <th>Path</th>
987
- </tr>
988
- </thead>
989
- <tbody>
990
- ${rows}
991
- </tbody>
992
- </table>
993
- </div>
902
+ <div class="data-surface">
903
+ ${orphanNotes.length === 0 ? '<p class="empty-state">No orphan notes found!</p>' : `<div class="filter-row">
904
+ <label for="orphan-filter">Filter orphan notes</label>
905
+ <input type="search" id="orphan-filter" aria-label="Filter orphan notes" placeholder="Search note paths">
906
+ </div>
907
+ <div class="table-scroll bounded-table-scroll" tabindex="0" role="region" aria-label="Scrollable orphan notes">
908
+ <table id="orphan-table">
909
+ <thead><tr><th>Note path</th></tr></thead>
910
+ <tbody>${rows}</tbody>
911
+ </table>
912
+ </div>`}
913
+ </div>
914
+ </section>
994
915
  `;
995
916
  }
996
917
  renderGraph(graph) {
997
918
  const mostConnected = [...graph].sort((a, b) => b.incoming.length + b.outgoing.length - (a.incoming.length + a.outgoing.length)).slice(0, 5);
998
- const rows = mostConnected.map(
999
- (node) => `
919
+ const rows = mostConnected.map((node) => `
1000
920
  <tr>
1001
921
  <td>${this.escapeHtml(node.note.relativePath)}</td>
1002
- <td>${node.incoming.length}</td>
1003
- <td>${node.outgoing.length}</td>
1004
- <td>${node.incoming.length + node.outgoing.length}</td>
922
+ <td class="numeric">${node.incoming.length}</td>
923
+ <td class="numeric">${node.outgoing.length}</td>
924
+ <td class="numeric">${node.incoming.length + node.outgoing.length}</td>
1005
925
  </tr>
1006
- `
1007
- ).join("");
926
+ `).join("");
1008
927
  return `
1009
- <div class="section">
1010
- <h2>\u{1F310} Most Connected Notes</h2>
1011
- <table>
1012
- <thead>
1013
- <tr>
1014
- <th>Note</th>
1015
- <th>Incoming</th>
1016
- <th>Outgoing</th>
1017
- <th>Total</th>
1018
- </tr>
1019
- </thead>
1020
- <tbody>
1021
- ${rows}
1022
- </tbody>
1023
- </table>
1024
- </div>
928
+ <section class="section" aria-labelledby="connections-title">
929
+ <div class="section-heading">
930
+ <h2 id="connections-title">Most connected notes</h2>
931
+ <span class="section-count">Top ${mostConnected.length}</span>
932
+ </div>
933
+ <div class="data-surface table-scroll">
934
+ ${mostConnected.length === 0 ? '<p class="empty-state">No connections to show.</p>' : `<table class="connections-table">
935
+ <thead><tr><th>Note</th><th>Incoming</th><th>Outgoing</th><th>Total</th></tr></thead>
936
+ <tbody>${rows}</tbody>
937
+ </table>`}
938
+ </div>
939
+ </section>
1025
940
  `;
1026
941
  }
1027
942
  escapeHtml(text) {
@@ -1029,126 +944,6 @@ var HtmlReporter = class {
1029
944
  }
1030
945
  };
1031
946
 
1032
- // src/reporters/mermaid-reporter.ts
1033
- var MermaidReporter = class {
1034
- report(result, maxNodes = 50) {
1035
- let output = "graph LR\n";
1036
- const nodesToShow = result.graph.slice(0, maxNodes);
1037
- const nodeMap = new Map(nodesToShow.map((n) => [n.note.path, n.note.relativePath]));
1038
- for (const node of nodesToShow) {
1039
- const id = this.sanitizeId(node.note.relativePath);
1040
- const label = this.escapeLabel(node.note.relativePath);
1041
- if (result.orphanNotes.some((o) => o.path === node.note.path)) {
1042
- output += ` ${id}["<b>\u{1F534} ${label}</b>"]:::orphan
1043
- `;
1044
- } else if (node.incoming.length === 0) {
1045
- output += ` ${id}["\u{1F4C4} ${label}"]:::source
1046
- `;
1047
- } else if (node.outgoing.length === 0) {
1048
- output += ` ${id}["\u{1F4CC} ${label}"]:::sink
1049
- `;
1050
- } else {
1051
- output += ` ${id}["${label}"]
1052
- `;
1053
- }
1054
- }
1055
- const addedEdges = /* @__PURE__ */ new Set();
1056
- for (const node of nodesToShow) {
1057
- const sourceId = this.sanitizeId(node.note.relativePath);
1058
- for (const targetPath of node.outgoing) {
1059
- const targetName = nodeMap.get(targetPath);
1060
- if (!targetName) continue;
1061
- const targetId = this.sanitizeId(targetName);
1062
- const edgeKey = `${sourceId}-${targetId}`;
1063
- if (!addedEdges.has(edgeKey)) {
1064
- output += ` ${sourceId} --> ${targetId}
1065
- `;
1066
- addedEdges.add(edgeKey);
1067
- }
1068
- }
1069
- }
1070
- output += "\n classDef orphan fill:#ff6b6b,stroke:#c92a2a,color:#fff\n";
1071
- output += " classDef source fill:#4dabf7,stroke:#1971c2,color:#fff\n";
1072
- output += " classDef sink fill:#51cf66,stroke:#2f9e44,color:#fff\n";
1073
- if (result.graph.length > maxNodes) {
1074
- output += `
1075
- note["Graph limited to ${maxNodes} of ${result.graph.length} nodes for readability"]
1076
- `;
1077
- }
1078
- return output;
1079
- }
1080
- sanitizeId(name) {
1081
- return name.replace(/\.(md|canvas)$/, "").replace(/[^a-zA-Z0-9_]/g, "_").substring(0, 30);
1082
- }
1083
- escapeLabel(name) {
1084
- return name.replace(/"/g, '\\"').substring(0, 50);
1085
- }
1086
- };
1087
-
1088
- // src/reporters/graphviz-reporter.ts
1089
- var GraphVizReporter = class {
1090
- report(result, maxNodes = 100) {
1091
- let output = "digraph VaultGraph {\n";
1092
- output += " rankdir=LR;\n";
1093
- output += " node [shape=box, style=rounded];\n";
1094
- output += " edge [arrowsize=0.5];\n\n";
1095
- const nodesToShow = result.graph.slice(0, maxNodes);
1096
- const nodeMap = new Map(nodesToShow.map((n) => [n.note.path, n.note.relativePath]));
1097
- for (const node of nodesToShow) {
1098
- const id = this.sanitizeId(node.note.relativePath);
1099
- const label = this.escapeLabel(node.note.relativePath);
1100
- let style = "filled";
1101
- let color = "#ffffff";
1102
- let fontColor = "#000000";
1103
- let shape = "box";
1104
- if (result.orphanNotes.some((o) => o.path === node.note.path)) {
1105
- color = "#ff6b6b";
1106
- fontColor = "#ffffff";
1107
- shape = "box";
1108
- } else if (node.incoming.length === 0) {
1109
- color = "#4dabf7";
1110
- fontColor = "#ffffff";
1111
- } else if (node.outgoing.length === 0) {
1112
- color = "#51cf66";
1113
- fontColor = "#ffffff";
1114
- }
1115
- output += ` "${id}" [label="${label}", fillcolor="${color}", fontcolor="${fontColor}", shape=${shape}];
1116
- `;
1117
- }
1118
- output += "\n";
1119
- const addedEdges = /* @__PURE__ */ new Set();
1120
- for (const node of nodesToShow) {
1121
- const sourceId = this.sanitizeId(node.note.relativePath);
1122
- for (const targetPath of node.outgoing) {
1123
- const targetName = nodeMap.get(targetPath);
1124
- if (!targetName) continue;
1125
- const targetId = this.sanitizeId(targetName);
1126
- const edgeKey = `${sourceId}->${targetId}`;
1127
- if (!addedEdges.has(edgeKey)) {
1128
- output += ` "${sourceId}" -> "${targetId}";
1129
- `;
1130
- addedEdges.add(edgeKey);
1131
- }
1132
- }
1133
- }
1134
- output += "\n // Legend\n";
1135
- output += " subgraph legend {\n";
1136
- output += " rank=sink;\n";
1137
- output += ' orphan [label="\u{1F534} Orphan", fillcolor="#ff6b6b", fontcolor="white"];\n';
1138
- output += ' source [label="\u{1F4C4} Source (no incoming)", fillcolor="#4dabf7", fontcolor="white"];\n';
1139
- output += ' sink [label="\u{1F4CC} Sink (no outgoing)", fillcolor="#51cf66", fontcolor="white"];\n';
1140
- output += " }\n";
1141
- output += "}\n";
1142
- return output;
1143
- }
1144
- sanitizeId(name) {
1145
- return name.replace(/\.(md|canvas)$/, "").replace(/[^a-zA-Z0-9_]/g, "_");
1146
- }
1147
- escapeLabel(label) {
1148
- return label.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1149
- }
1150
- };
1151
-
1152
947
  // src/utils/markdown-report.ts
1153
948
  function pill(target, isMultiple) {
1154
949
  return isMultiple ? `[${target}]` : `[${target}]`;
@@ -1171,6 +966,10 @@ function generateMarkdownReport(report) {
1171
966
  lines.push(`|--------|-------|`);
1172
967
  lines.push(`| Notes | ${report.stats.totalNotes} |`);
1173
968
  lines.push(`| Links | ${report.stats.totalLinks} |`);
969
+ const averageLinks = report.stats.totalNotes === 0 ? 0 : report.stats.totalLinks / report.stats.totalNotes;
970
+ const healthScore = report.stats.totalLinks === 0 ? 100 : 100 - report.stats.brokenCount / report.stats.totalLinks * 100;
971
+ lines.push(`| Avg links per note | ${averageLinks.toFixed(2)} |`);
972
+ lines.push(`| Health score | ${healthScore.toFixed(1)}% |`);
1174
973
  lines.push(`| Broken | ${report.stats.brokenCount} |`);
1175
974
  lines.push(`| Orphans | ${report.stats.orphanCount} |`);
1176
975
  lines.push(`| Components | ${report.stats.connectedComponents} |`);
@@ -1214,104 +1013,22 @@ function generateMarkdownReport(report) {
1214
1013
  }
1215
1014
  }
1216
1015
  lines.push("");
1217
- return lines.join("\n");
1218
- }
1219
-
1220
- // src/utils/auto-fixer.ts
1221
- import { readFile as readFile3, writeFile } from "fs/promises";
1222
- import { resolve as resolve4 } from "path";
1223
- function escapeRegExp(string) {
1224
- return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1225
- }
1226
- async function autoFixBrokenLinks(fixMap, filePath) {
1227
- const results = [];
1228
- try {
1229
- const content = await readFile3(filePath, "utf-8");
1230
- let newContent = content;
1231
- for (const [oldTarget, newTarget] of fixMap.entries()) {
1232
- const wikiRegex = new RegExp(
1233
- `(!?\\[\\[${escapeRegExp(oldTarget)}(?:\\|[^\\]]*)?\\]\\])`,
1234
- "g"
1235
- );
1236
- const wikiMatches = content.match(wikiRegex);
1237
- if (wikiMatches) {
1238
- for (const match of wikiMatches) {
1239
- const isEmbed = match.startsWith("!");
1240
- const hasAlias = match.includes("|");
1241
- let replacement;
1242
- if (hasAlias) {
1243
- const parts = match.split("|");
1244
- replacement = `${isEmbed ? "!" : ""}[[${newTarget}|${parts[1]}`;
1245
- } else {
1246
- replacement = `${isEmbed ? "!" : ""}[[${newTarget}]]`;
1247
- }
1248
- newContent = newContent.replace(match, replacement);
1249
- results.push({
1250
- file: filePath,
1251
- originalLink: match,
1252
- newLink: replacement,
1253
- success: true
1254
- });
1255
- }
1256
- }
1257
- const markdownRegex = new RegExp(
1258
- `(\\[[^\\]]*\\]\\(${escapeRegExp(oldTarget)}(?:#[^)]*)?\\))`,
1259
- "g"
1260
- );
1261
- const markdownMatches = content.match(markdownRegex);
1262
- if (markdownMatches) {
1263
- for (const match of markdownMatches) {
1264
- const linkPart = match.match(/\(([^)]+)\)/)?.[1] || "";
1265
- const heading = linkPart.includes("#") ? linkPart.split("#")[1] : "";
1266
- const newLink = heading ? `${newTarget}#${heading}` : newTarget;
1267
- const textPart = match.match(/\[([^\]]*)\]/)?.[0] || "[]";
1268
- const replacement = `${textPart}(${newLink})`;
1269
- newContent = newContent.replace(match, replacement);
1270
- results.push({
1271
- file: filePath,
1272
- originalLink: match,
1273
- newLink: replacement,
1274
- success: true
1275
- });
1276
- }
1277
- }
1278
- }
1279
- if (newContent !== content) {
1280
- await writeFile(filePath, newContent, "utf-8");
1281
- }
1282
- return results;
1283
- } catch (error) {
1284
- return [
1285
- {
1286
- file: filePath,
1287
- originalLink: "",
1288
- newLink: "",
1289
- success: false,
1290
- error: error instanceof Error ? error.message : "Unknown error"
1291
- }
1292
- ];
1293
- }
1294
- }
1295
- async function fixBrokenLinksBatch(brokenLinks, suggestionMap, vaultPath) {
1296
- const fileFixMap = /* @__PURE__ */ new Map();
1297
- for (const broken of brokenLinks) {
1298
- const suggestion = suggestionMap.get(broken.link.target);
1299
- if (!suggestion) continue;
1300
- if (!fileFixMap.has(broken.source)) {
1301
- fileFixMap.set(broken.source, /* @__PURE__ */ new Map());
1016
+ lines.push("## Connections");
1017
+ lines.push("");
1018
+ lines.push("| Note | Incoming | Outgoing | Total |");
1019
+ lines.push("|------|----------|----------|-------|");
1020
+ const mostConnected = [...report.graph].sort((a, b) => b.incoming.length + b.outgoing.length - (a.incoming.length + a.outgoing.length)).slice(0, 10);
1021
+ if (mostConnected.length === 0) {
1022
+ lines.push("| No connected notes | 0 | 0 | 0 |");
1023
+ } else {
1024
+ for (const node of mostConnected) {
1025
+ const incoming = node.incoming.length;
1026
+ const outgoing = node.outgoing.length;
1027
+ lines.push(`| ${node.note.relativePath} | ${incoming} | ${outgoing} | ${incoming + outgoing} |`);
1302
1028
  }
1303
- fileFixMap.get(broken.source).set(broken.link.target, suggestion);
1304
1029
  }
1305
- const allResults = /* @__PURE__ */ new Map();
1306
- const promises = Array.from(fileFixMap.entries()).map(
1307
- async ([source, fixMap]) => {
1308
- const filePath = resolve4(vaultPath, source);
1309
- const results = await autoFixBrokenLinks(fixMap, filePath);
1310
- allResults.set(source, results);
1311
- }
1312
- );
1313
- await Promise.all(promises);
1314
- return allResults;
1030
+ lines.push("");
1031
+ return lines.join("\n");
1315
1032
  }
1316
1033
 
1317
1034
  // src/index.ts
@@ -1327,40 +1044,13 @@ async function loadConfigAndAnalyze(vaultPath, cliOptions) {
1327
1044
  const engine = new DeadLinksEngine();
1328
1045
  return engine.analyze(vaultPath, config);
1329
1046
  }
1330
- function printBrokenVerbose(brokenLinks) {
1331
- const grouped = /* @__PURE__ */ new Map();
1332
- for (const b of brokenLinks) {
1333
- const existing = grouped.get(b.source) || [];
1334
- existing.push(b);
1335
- grouped.set(b.source, existing);
1336
- }
1337
- for (const [source, links] of grouped) {
1338
- for (const link of links) {
1339
- console.log(` ${source} \u2192 ${link.link.target} (line ${link.link.line})`);
1340
- }
1341
- }
1342
- }
1343
- function printOrphansVerbose(orphanNotes) {
1344
- for (const note of orphanNotes) {
1345
- console.log(` ${note.relativePath}`);
1346
- }
1347
- }
1348
- program.command("scan").description("Scan the vault for all issues").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).option("--format <type>", "Output format: text, json, markdown, csv, html, mermaid, dot", "text").option("--output <file>", "Output file path").option("--ignore <patterns...>", "Ignore patterns (glob)").option("--attachments", "Also check embedded attachments (images, PDFs, etc.)").option("--ignore-folders <folders...>", "Exclude folders from orphan report").option("--quiet", "Only show issues, no stats or summary").option("--no-color", "Disable colored output").option("--suggestions", "Show smart suggestions for broken links").option("--debug", "Enable debug mode with verbose output").option("--fix", "Automatically fix broken links (requires suggestions)").action(async (options) => {
1047
+ program.command("scan").description("Scan the vault for all issues").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).addOption(
1048
+ new Option("--format <type>", "Output format: text, json, markdown, csv, html").choices(["text", "json", "markdown", "csv", "html"]).default("text")
1049
+ ).option("--output <file>", "Output file path").option("--ignore <patterns...>", "Ignore patterns (glob)").option("--attachments", "Also check embedded attachments (images, PDFs, etc.)").option("--ignore-folders <folders...>", "Exclude folders from orphan report").option("--quiet", "Only show issues, no stats or summary").option("--no-color", "Disable colored output").option("--debug", "Enable debug mode with verbose output").action(async (options) => {
1349
1050
  const spinner = ora("Scanning vault...").start();
1350
1051
  try {
1351
1052
  const report = await loadConfigAndAnalyze(options.vault, options);
1352
1053
  spinner.succeed(`Scanned ${report.stats.totalNotes} notes in ${report.duration}ms`);
1353
- let fixSummary = "";
1354
- if (options.fix) {
1355
- const suggestionMap = new Map(
1356
- (report.suggestions || []).map((suggestion) => [suggestion.broken, suggestion.suggested])
1357
- );
1358
- const fixResults = await fixBrokenLinksBatch(report.brokenLinks, suggestionMap, options.vault);
1359
- const changed = [...fixResults.values()].flat().filter((result) => result.success).length;
1360
- const failed = [...fixResults.values()].flat().filter((result) => !result.success).length;
1361
- const fixSummary2 = `Auto-fix: ${changed} link(s) changed${failed > 0 ? `, ${failed} failed` : ""}.`;
1362
- console.error(fixSummary2);
1363
- }
1364
1054
  if (options.debug) {
1365
1055
  console.log(`[DEBUG] Vault: ${options.vault}`);
1366
1056
  console.log(`[DEBUG] Format: ${options.format}`);
@@ -1379,12 +1069,6 @@ program.command("scan").description("Scan the vault for all issues").option("--v
1379
1069
  } else if (options.format === "html") {
1380
1070
  const reporter = new HtmlReporter();
1381
1071
  output = reporter.report(report);
1382
- } else if (options.format === "mermaid") {
1383
- const reporter = new MermaidReporter();
1384
- output = reporter.report(report);
1385
- } else if (options.format === "dot") {
1386
- const reporter = new GraphVizReporter();
1387
- output = reporter.report(report);
1388
1072
  } else {
1389
1073
  const reporter = new TerminalReporter(
1390
1074
  options.ignoreFolders || [],
@@ -1394,10 +1078,8 @@ program.command("scan").description("Scan the vault for all issues").option("--v
1394
1078
  output = reporter.report(report);
1395
1079
  }
1396
1080
  if (options.output) {
1397
- await writeFile2(options.output, output);
1081
+ await writeFile(options.output, output);
1398
1082
  console.log(`Report written to ${options.output}`);
1399
- } else if (options.format === "text") {
1400
- console.log(output);
1401
1083
  } else {
1402
1084
  console.log(output);
1403
1085
  }
@@ -1408,119 +1090,13 @@ program.command("scan").description("Scan the vault for all issues").option("--v
1408
1090
  process.exit(1);
1409
1091
  }
1410
1092
  });
1411
- program.command("broken").description("Show only broken links").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).option("--ignore <patterns...>", "Ignore patterns (glob)").option("--attachments", "Also check embedded attachments (images, PDFs, etc.)").option("--verbose", "Show line numbers and full details").option("--suggestions", "Show smart suggestions for broken links").option("--debug", "Enable debug mode with verbose output").action(async (options) => {
1412
- const spinner = ora("Scanning vault...").start();
1413
- try {
1414
- const report = await loadConfigAndAnalyze(options.vault, options);
1415
- spinner.succeed(`Scanned ${report.stats.totalNotes} notes in ${report.duration}ms`);
1416
- if (report.brokenLinks.length === 0) {
1417
- console.log("No broken links found.");
1418
- } else {
1419
- console.log(`
1420
- Broken links (${report.brokenLinks.length})
1421
- `);
1422
- if (options.verbose) {
1423
- printBrokenVerbose(report.brokenLinks);
1424
- } else {
1425
- const grouped = /* @__PURE__ */ new Map();
1426
- for (const b of report.brokenLinks) {
1427
- const existing = grouped.get(b.source) || [];
1428
- existing.push(b);
1429
- grouped.set(b.source, existing);
1430
- }
1431
- for (const [source, links] of grouped) {
1432
- const targets = links.map((l) => l.link.target).join(", ");
1433
- console.log(` ${source} \u2192 ${targets}`);
1434
- }
1435
- }
1436
- console.log();
1437
- if (report.suggestions && report.suggestions.length > 0) {
1438
- console.log("\u{1F4A1} Suggestions:");
1439
- for (const suggestion of report.suggestions.slice(0, 5)) {
1440
- const confidence = Math.round(suggestion.similarity * 100);
1441
- console.log(` ${suggestion.broken} \u2192 ${suggestion.suggested} (${confidence}%)`);
1442
- }
1443
- console.log();
1444
- }
1445
- }
1446
- process.exit(report.stats.brokenCount > 0 ? 1 : 0);
1447
- } catch (error) {
1448
- spinner.fail("Scan failed");
1449
- console.error(error instanceof Error ? error.message : error);
1450
- process.exit(1);
1451
- }
1452
- });
1453
- program.command("orphans").description("Show only orphan notes").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).option("--ignore <patterns...>", "Ignore patterns (glob)").option("--ignore-folders <folders...>", "Exclude folders from report").option("--verbose", "Show full list of orphan notes").action(async (options) => {
1454
- const spinner = ora("Scanning vault...").start();
1455
- try {
1456
- const report = await loadConfigAndAnalyze(options.vault, options);
1457
- spinner.succeed(`Scanned ${report.stats.totalNotes} notes in ${report.duration}ms`);
1458
- if (report.orphanNotes.length === 0) {
1459
- console.log("No orphan notes found.");
1460
- } else {
1461
- console.log(`
1462
- Orphan notes (${report.orphanNotes.length})
1463
- `);
1464
- if (options.verbose) {
1465
- printOrphansVerbose(report.orphanNotes);
1466
- } else {
1467
- const ignoreFolders = options.ignoreFolders || [];
1468
- const byFolder = /* @__PURE__ */ new Map();
1469
- for (const note of report.orphanNotes) {
1470
- const folder = note.relativePath.includes("/") ? note.relativePath.split("/").slice(0, -1).join("/") + "/" : "./";
1471
- if (ignoreFolders.some((f) => folder.startsWith(f + "/") || folder === f + "/")) {
1472
- continue;
1473
- }
1474
- byFolder.set(folder, (byFolder.get(folder) || 0) + 1);
1475
- }
1476
- for (const [folder, count] of [...byFolder.entries()].sort((a, b) => b[1] - a[1])) {
1477
- console.log(` ${folder} ${count}`);
1478
- }
1479
- }
1480
- console.log();
1481
- }
1482
- process.exit(report.stats.orphanCount > 0 ? 1 : 0);
1483
- } catch (error) {
1484
- spinner.fail("Scan failed");
1485
- console.error(error instanceof Error ? error.message : error);
1486
- process.exit(1);
1487
- }
1488
- });
1489
- program.command("graph").description("Show connection graph").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).option("--format <type>", "Output format: text, json", "text").option("--ignore <patterns...>", "Ignore patterns (glob)").action(async (options) => {
1490
- const spinner = ora("Scanning vault...").start();
1491
- try {
1492
- const report = await loadConfigAndAnalyze(options.vault, options);
1493
- spinner.succeed(`Scanned ${report.stats.totalNotes} notes in ${report.duration}ms`);
1494
- if (options.format === "json") {
1495
- console.log(JSON.stringify(report.graph, null, 2));
1496
- } else {
1497
- console.log(
1498
- `Graph: ${report.stats.totalNotes} notes, ${report.stats.totalLinks} links (${report.duration}ms)
1499
- `
1500
- );
1501
- const topConnected = [...report.graph].sort((a, b) => b.incoming.length - a.incoming.length).slice(0, 10).filter((n) => n.incoming.length > 0);
1502
- if (topConnected.length > 0) {
1503
- console.log("Most linked-to notes:");
1504
- for (const node of topConnected) {
1505
- console.log(
1506
- ` ${node.note.relativePath} (${node.incoming.length} incoming, ${node.outgoing.length} outgoing)`
1507
- );
1508
- }
1509
- }
1510
- }
1511
- } catch (error) {
1512
- spinner.fail("Scan failed");
1513
- console.error(error instanceof Error ? error.message : error);
1514
- process.exit(1);
1515
- }
1516
- });
1517
1093
  program.command("report").description("Generate a markdown report").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).option("--output <file>", "Output file path", "dead-links-report.md").option("--ignore <patterns...>", "Ignore patterns (glob)").action(async (options) => {
1518
1094
  const spinner = ora("Scanning vault...").start();
1519
1095
  try {
1520
1096
  const report = await loadConfigAndAnalyze(options.vault, options);
1521
1097
  spinner.succeed(`Scanned ${report.stats.totalNotes} notes in ${report.duration}ms`);
1522
1098
  const markdown = generateMarkdownReport(report);
1523
- await writeFile2(options.output, markdown);
1099
+ await writeFile(options.output, markdown);
1524
1100
  console.log(`Report written to ${options.output}`);
1525
1101
  } catch (error) {
1526
1102
  spinner.fail("Scan failed");
@@ -1528,40 +1104,5 @@ program.command("report").description("Generate a markdown report").option("--va
1528
1104
  process.exit(1);
1529
1105
  }
1530
1106
  });
1531
- program.command("stats").description("Show detailed vault statistics").option("--vault <path>", "Path to the Obsidian vault", process.cwd()).option("--ignore <patterns...>", "Ignore patterns (glob)").option("--debug", "Enable debug mode").action(async (options) => {
1532
- const spinner = ora("Analyzing vault...").start();
1533
- try {
1534
- const report = await loadConfigAndAnalyze(options.vault, options);
1535
- spinner.succeed(`Analysis complete in ${report.duration}ms`);
1536
- console.log("\n\u{1F4CA} Vault Statistics\n");
1537
- console.log(` Total Notes: ${report.stats.totalNotes}`);
1538
- console.log(` Total Links: ${report.stats.totalLinks}`);
1539
- const averageLinks = report.stats.totalNotes === 0 ? 0 : report.stats.totalLinks / report.stats.totalNotes;
1540
- const healthScore = report.stats.totalLinks === 0 ? 100 : 100 - report.stats.brokenCount / report.stats.totalLinks * 100;
1541
- console.log(` Avg Links per Note: ${averageLinks.toFixed(2)}`);
1542
- console.log(` Broken Links: ${report.stats.brokenCount}`);
1543
- console.log(` Orphan Notes: ${report.stats.orphanCount}`);
1544
- console.log(` Connected Components: ${report.stats.connectedComponents}`);
1545
- console.log(` Health Score: ${healthScore.toFixed(1)}%`);
1546
- const mostConnected = [...report.graph].sort((a, b) => b.incoming.length + b.outgoing.length - (a.incoming.length + a.outgoing.length)).slice(0, 5);
1547
- console.log("\n\u{1F310} Most Connected Notes:");
1548
- for (const node of mostConnected) {
1549
- const total = node.incoming.length + node.outgoing.length;
1550
- console.log(` ${node.note.relativePath} (${total} connections: ${node.incoming.length} in, ${node.outgoing.length} out)`);
1551
- }
1552
- const lonelyNotes = report.graph.filter((n) => n.incoming.length === 0 && n.outgoing.length === 0).slice(0, 5);
1553
- if (lonelyNotes.length > 0) {
1554
- console.log("\n\u{1F534} Orphan Notes:");
1555
- for (const node of lonelyNotes) {
1556
- console.log(` ${node.note.relativePath}`);
1557
- }
1558
- }
1559
- console.log();
1560
- } catch (error) {
1561
- spinner.fail("Analysis failed");
1562
- console.error(error instanceof Error ? error.message : error);
1563
- process.exit(1);
1564
- }
1565
- });
1566
1107
  program.parse();
1567
1108
  //# sourceMappingURL=index.js.map