helm-env-delta 1.10.2 → 1.11.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/README.md CHANGED
@@ -12,7 +12,9 @@
12
12
 
13
13
  HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows while protecting your production-specific settings and preventing dangerous changes.
14
14
 
15
- ---
15
+ <p align="center">
16
+ <img src="helm-env-delta.png" alt="helm-env-delta" />
17
+ </p>
16
18
 
17
19
  ## 👤 Who Is This For?
18
20
 
@@ -54,7 +56,7 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
54
56
 
55
57
  📦 **Config Inheritance** - Reuse base configurations with environment-specific overrides.
56
58
 
57
- 📊 **Multiple Reports** - Console, HTML (visual), and JSON (CI/CD) output formats.
59
+ 📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include diff stats dashboard, per-file line change badges, copy diff buttons, file search, and collapse/expand controls.
58
60
 
59
61
  🔍 **Discovery Tools** - Preview files (`-l`), inspect config (`--show-config`), filter by filename/content (`-f`), filter by change type (`-m`), validate with comprehensive warnings including unused pattern detection.
60
62
 
@@ -117,6 +119,8 @@ hed -c config.yaml
117
119
  hed -c config.yaml -H
118
120
  ```
119
121
 
122
+ Self-contained HTML report — works offline, no CDN required. Includes diff stats, line change badges, copy buttons, sidebar search, and collapse/expand controls.
123
+
120
124
  ### 5️⃣ Get Smart Suggestions (Optional)
121
125
 
122
126
  ```bash
@@ -697,28 +701,28 @@ outputFormat:
697
701
 
698
702
  The `-f/--filter` flag supports logical operators for complex filtering:
699
703
 
700
- | Operator | Name | Example | Matches |
701
- | -------- | ------ | -------------------- | ----------------------------------------------- |
702
- | (none) | Simple | `-f prod` | Files where filename or content contains "prod" |
703
- | `\|` | OR | `-f "prod\|staging"` | Files matching "prod" OR "staging" |
704
- | `&` | AND | `-f "values&prod"` | Files matching "values" AND "prod" |
704
+ | Operator | Name | Example | Matches |
705
+ | -------- | ------ | ----------------- | ----------------------------------------------- |
706
+ | (none) | Simple | `-f prod` | Files where filename or content contains "prod" |
707
+ | `,` | OR | `-f prod,staging` | Files matching "prod" OR "staging" |
708
+ | `+` | AND | `-f values+prod` | Files matching "values" AND "prod" |
705
709
 
706
710
  ```bash
707
711
  # OR: match ANY term (filename or content)
708
- hed -c config.yaml -f "prod|staging" --list-files
712
+ hed -c config.yaml -f prod,staging --list-files
709
713
 
710
714
  # AND: match ALL terms (can be split between filename and content)
711
- hed -c config.yaml -f "values&prod" --list-files
715
+ hed -c config.yaml -f values+prod --list-files
712
716
 
713
- # Escape literal | or & with backslash
714
- hed -c config.yaml -f "foo\|bar" --list-files
717
+ # Escape literal , or + with backslash
718
+ hed -c config.yaml -f "foo\,bar" --list-files
715
719
  ```
716
720
 
717
721
  **Constraints:**
718
722
 
719
- - Cannot mix `&` and `|` in a single filter (throws error)
723
+ - Cannot mix `+` and `,` in a single filter (throws error)
720
724
  - Case-insensitive matching
721
- - Empty terms are ignored (`a||b` becomes `a|b`)
725
+ - Empty terms are ignored (`a,,b` becomes `a,b`)
722
726
 
723
727
  ---
724
728
 
@@ -797,7 +801,7 @@ hed --config <file> [options] # Short alias
797
801
  | `--show-config` | | Display resolved config after inheritance |
798
802
  | `--format-only` | | Format destination files only (source not required) |
799
803
  | `--skip-format` | `-S` | Skip YAML formatting during sync |
800
- | `--filter <string>` | `-f` | Filter files by filename/content (supports `\|` OR, `&` AND) |
804
+ | `--filter <string>` | `-f` | Filter files by filename/content (supports `,` OR, `+` AND) |
801
805
  | `--mode <type>` | `-m` | Filter by change type: new, modified, deleted, all (default: all) |
802
806
  | `--no-color` | | Disable colored output (CI/accessibility) |
803
807
  | `--verbose` | | Show detailed debug info |
@@ -840,10 +844,10 @@ hed -c config.yaml --force
840
844
  hed -c config.yaml -f prod -d
841
845
 
842
846
  # Filter with OR: match files containing 'prod' OR 'staging'
843
- hed -c config.yaml -f "prod|staging" -l
847
+ hed -c config.yaml -f prod,staging -l
844
848
 
845
849
  # Filter with AND: match files containing BOTH 'values' AND 'prod'
846
- hed -c config.yaml -f "values&prod" -d
850
+ hed -c config.yaml -f values+prod -d
847
851
 
848
852
  # Sync only new files
849
853
  hed -c config.yaml -m new
@@ -26,7 +26,7 @@ const parseCommandLine = (argv) => {
26
26
  .option('--suggest', 'Analyze differences and suggest transforms and stop rules', false)
27
27
  .option('--suggest-threshold <number>', 'Minimum confidence for suggestions (0-1, default: 0.3)', '0.3')
28
28
  .option('--no-color', 'Disable colored output')
29
- .option('-f, --filter <string>', 'Filter files by filename or content (supports | for OR, & for AND)')
29
+ .option('-f, --filter <string>', 'Filter files by filename or content (supports , for OR, + for AND)')
30
30
  .option('-m, --mode <type>', 'Filter by change type: new, modified, deleted, all', 'all')
31
31
  .option('--verbose', 'Show detailed debug information', false)
32
32
  .option('--quiet', 'Suppress all output except critical errors', false)
@@ -51,10 +51,10 @@ Examples:
51
51
  $ helm-env-delta --config config.yaml -f prod --diff
52
52
 
53
53
  # Filter with OR: files matching 'prod' OR 'staging'
54
- $ helm-env-delta --config config.yaml -f "prod|staging" --diff
54
+ $ helm-env-delta --config config.yaml -f prod,staging --diff
55
55
 
56
56
  # Filter with AND: files matching 'values' AND 'prod'
57
- $ helm-env-delta --config config.yaml -f "values&prod" --diff
57
+ $ helm-env-delta --config config.yaml -f values+prod --diff
58
58
 
59
59
  # Sync only new files
60
60
  $ helm-env-delta --config config.yaml --mode new
@@ -1,6 +1,6 @@
1
1
  import { Config } from './configFile';
2
2
  import { FileDiffResult } from './fileDiff';
3
- export type { ReportMetadata } from './reporters/htmlTemplate';
3
+ export type { DiffStats, ReportMetadata } from './reporters/htmlTemplate';
4
4
  declare const HtmlReporterErrorClass: {
5
5
  new (message: string, options?: import("./utils/errors").ErrorOptions): {
6
6
  [key: string]: unknown;
@@ -38,6 +38,17 @@ const DIFF2HTML_OPTIONS = {
38
38
  outputFormat: 'side-by-side'
39
39
  };
40
40
  const generateDiffHtml = (unifiedDiff) => (0, diff2html_1.html)(unifiedDiff, DIFF2HTML_OPTIONS);
41
+ const countDiffLines = (unifiedDiff) => {
42
+ const lines = unifiedDiff.split('\n');
43
+ let added = 0;
44
+ let removed = 0;
45
+ for (const line of lines)
46
+ if (line.startsWith('+') && !line.startsWith('+++'))
47
+ added++;
48
+ else if (line.startsWith('-') && !line.startsWith('---'))
49
+ removed++;
50
+ return { added, removed };
51
+ };
41
52
  const generateFileSummary = (file) => {
42
53
  if (!file.originalPath)
43
54
  return file.path;
@@ -72,14 +83,21 @@ const generateChangedFileSection = (file, fileId) => {
72
83
  const sourceContent = (0, serialization_1.serializeForDiff)(file.processedSourceContent, isYaml);
73
84
  const unifiedDiff = (0, diffGenerator_1.generateUnifiedDiff)(file.path, destinationContent, sourceContent);
74
85
  const diffHtml = generateDiffHtml(unifiedDiff);
75
- return `
86
+ const { added, removed } = countDiffLines(unifiedDiff);
87
+ const escapedDiff = (0, treeRenderer_1.escapeHtml)(unifiedDiff);
88
+ const html = `
76
89
  <details class="file-section" id="${fileId}" data-file-id="${fileId}" open>
77
- <summary>${summary}</summary>
90
+ <summary>${summary}<span class="summary-badges"><span class="line-badge line-added">+${added}</span><span class="line-badge line-removed">-${removed}</span></span></summary>
91
+ <div class="diff-toolbar">
92
+ <button class="copy-diff-btn" data-file-id="${fileId}">Copy Diff</button>
93
+ </div>
94
+ <pre class="unified-diff-source" style="display:none">${escapedDiff}</pre>
78
95
  <div class="diff-container">
79
96
  ${diffHtml}
80
97
  </div>
81
98
  </details>
82
99
  `;
100
+ return { html, added, removed };
83
101
  };
84
102
  const writeHtmlFile = async (htmlContent, outputPath) => {
85
103
  try {
@@ -113,12 +131,27 @@ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, lo
113
131
  const changedFileIds = new Map();
114
132
  for (const [index, file] of diffResult.changedFiles.entries())
115
133
  changedFileIds.set(file.path, `file-${index}`);
116
- const changedSections = diffResult.changedFiles.map((file, index) => generateChangedFileSection(file, `file-${index}`));
134
+ const fileStats = new Map();
135
+ const changedSections = diffResult.changedFiles.map((file, index) => {
136
+ const result = generateChangedFileSection(file, `file-${index}`);
137
+ fileStats.set(file.path, { added: result.added, removed: result.removed });
138
+ return result.html;
139
+ });
117
140
  const addedFileIds = new Map();
118
141
  for (const [index, file] of diffResult.addedFiles.entries())
119
142
  addedFileIds.set(file.path, `added-file-${index}`);
120
143
  const addedSections = diffResult.addedFiles.map((file, index) => generateAddedFileSection(file, `added-file-${index}`));
121
- const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds, addedSections, addedFileIds);
144
+ let totalAdded = 0;
145
+ let totalRemoved = 0;
146
+ const statsArray = [];
147
+ for (const [filePath, stats] of fileStats) {
148
+ totalAdded += stats.added;
149
+ totalRemoved += stats.removed;
150
+ statsArray.push({ path: filePath, added: stats.added, removed: stats.removed });
151
+ }
152
+ statsArray.sort((a, b) => b.added + b.removed - (a.added + a.removed));
153
+ const diffStats = { totalAdded, totalRemoved, fileStats: statsArray };
154
+ const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds, addedSections, addedFileIds, fileStats, diffStats);
122
155
  await writeHtmlFile(htmlContent, reportPath);
123
156
  logger?.log(`✓ HTML report generated: ${reportPath}, opening in browser...`);
124
157
  try {
@@ -1,2 +1,3 @@
1
- export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n /* Treeview styles */\n .tree-root {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n }\n\n .tree-root ul {\n list-style: none;\n padding-left: 20px;\n margin: 0;\n }\n\n .tree-folder,\n .tree-file {\n padding: 4px 8px;\n border-radius: 4px;\n cursor: default;\n }\n\n .tree-folder:hover,\n .tree-file:hover {\n background: #f6f8fa;\n }\n\n .tree-toggle {\n display: inline-block;\n width: 16px;\n cursor: pointer;\n color: #586069;\n font-size: 10px;\n user-select: none;\n }\n\n .tree-folder.collapsed > .tree-toggle {\n transform: rotate(-90deg);\n }\n\n .tree-folder.collapsed > .tree-children {\n display: none;\n }\n\n .tree-folder-name {\n color: #0969da;\n font-weight: 500;\n }\n\n .tree-file-name {\n color: #586069;\n padding-left: 16px;\n }\n\n /* Sidebar styles */\n .sidebar-container {\n display: flex;\n gap: 0;\n }\n\n .sidebar {\n width: 280px;\n min-width: 280px;\n border-right: 1px solid #d0d7de;\n background: #f6f8fa;\n transition: width 0.2s, min-width 0.2s, padding 0.2s, opacity 0.2s;\n }\n\n .sidebar.collapsed {\n width: 0;\n min-width: 0;\n padding: 0;\n overflow: hidden;\n border-right: none;\n }\n\n .sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #fff;\n font-weight: 600;\n font-size: 14px;\n color: #24292e;\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .sidebar-toggle {\n background: none;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n cursor: pointer;\n padding: 4px 8px;\n color: #586069;\n font-size: 12px;\n }\n\n .sidebar-toggle:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .sidebar-content {\n padding: 8px;\n }\n\n .sidebar-tree .tree-file-link {\n color: #586069;\n text-decoration: none;\n padding-left: 16px;\n display: block;\n }\n\n .sidebar-tree .tree-file-link:hover {\n color: #0969da;\n }\n\n .sidebar-tree .tree-file.active .tree-file-link {\n color: #0969da;\n font-weight: 600;\n }\n\n .changed-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n .sidebar-expand-btn {\n display: none;\n position: fixed;\n left: 0;\n top: 50%;\n transform: translateY(-50%);\n background: #f6f8fa;\n border: 1px solid #d0d7de;\n border-left: none;\n border-radius: 0 4px 4px 0;\n padding: 8px 4px;\n cursor: pointer;\n color: #586069;\n z-index: 100;\n }\n\n .sidebar-expand-btn:hover {\n background: #eaeef2;\n color: #24292e;\n }\n\n .sidebar.collapsed ~ .sidebar-expand-btn {\n display: block;\n }\n\n /* Added content area (same as changed-content) */\n .added-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n /* Content container for added files */\n .content-container {\n padding: 16px;\n background: #f6f8fa;\n border-top: 1px solid #d0d7de;\n }\n\n .content-actions {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n }\n\n .copy-btn,\n .download-btn {\n padding: 6px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-btn:hover,\n .download-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n .file-content {\n margin: 0;\n padding: 16px;\n background: white;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n }\n\n .file-content code {\n font-family: inherit;\n }\n";
1
+ export declare const DIFF2HTML_STYLES: string;
2
+ export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .file-section[open] > summary {\n position: sticky;\n top: 0;\n z-index: 10;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n /* Treeview styles */\n .tree-root {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n }\n\n .tree-root ul {\n list-style: none;\n padding-left: 20px;\n margin: 0;\n }\n\n .tree-folder,\n .tree-file {\n padding: 4px 8px;\n border-radius: 4px;\n cursor: default;\n }\n\n .tree-folder:hover,\n .tree-file:hover {\n background: #f6f8fa;\n }\n\n .tree-toggle {\n display: inline-block;\n width: 16px;\n cursor: pointer;\n color: #586069;\n font-size: 10px;\n user-select: none;\n }\n\n .tree-folder.collapsed > .tree-toggle {\n transform: rotate(-90deg);\n }\n\n .tree-folder.collapsed > .tree-children {\n display: none;\n }\n\n .tree-folder-name {\n color: #0969da;\n font-weight: 500;\n }\n\n .tree-file-name {\n color: #586069;\n padding-left: 16px;\n }\n\n /* Sidebar styles */\n .sidebar-container {\n display: flex;\n gap: 0;\n }\n\n .sidebar {\n width: 280px;\n min-width: 280px;\n border-right: 1px solid #d0d7de;\n background: #f6f8fa;\n transition: width 0.2s, min-width 0.2s, padding 0.2s, opacity 0.2s;\n }\n\n .sidebar.collapsed {\n width: 0;\n min-width: 0;\n padding: 0;\n overflow: hidden;\n border-right: none;\n }\n\n .sidebar-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #fff;\n font-weight: 600;\n font-size: 14px;\n color: #24292e;\n position: sticky;\n top: 0;\n z-index: 1;\n }\n\n .sidebar-toggle {\n background: none;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n cursor: pointer;\n padding: 4px 8px;\n color: #586069;\n font-size: 12px;\n }\n\n .sidebar-toggle:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .sidebar-content {\n padding: 8px;\n }\n\n .sidebar-tree .tree-file-link {\n color: #586069;\n text-decoration: none;\n padding-left: 16px;\n display: block;\n }\n\n .sidebar-tree .tree-file-link:hover {\n color: #0969da;\n }\n\n .sidebar-tree .tree-file.active .tree-file-link {\n color: #0969da;\n font-weight: 600;\n }\n\n .changed-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n .sidebar-expand-btn {\n display: none;\n position: fixed;\n left: 0;\n top: 50%;\n transform: translateY(-50%);\n background: #f6f8fa;\n border: 1px solid #d0d7de;\n border-left: none;\n border-radius: 0 4px 4px 0;\n padding: 8px 4px;\n cursor: pointer;\n color: #586069;\n z-index: 100;\n }\n\n .sidebar-expand-btn:hover {\n background: #eaeef2;\n color: #24292e;\n }\n\n .sidebar.collapsed ~ .sidebar-expand-btn {\n display: block;\n }\n\n /* Added content area (same as changed-content) */\n .added-content {\n flex: 1;\n min-width: 0;\n padding-left: 20px;\n }\n\n /* Content container for added files */\n .content-container {\n padding: 16px;\n background: #f6f8fa;\n border-top: 1px solid #d0d7de;\n }\n\n .content-actions {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n }\n\n .copy-btn,\n .download-btn {\n padding: 6px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 13px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-btn:hover,\n .download-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n .file-content {\n margin: 0;\n padding: 16px;\n background: white;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n overflow-x: auto;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre;\n }\n\n .file-content code {\n font-family: inherit;\n }\n\n /* Scroll-to-top button */\n .scroll-to-top {\n display: none;\n position: fixed;\n bottom: 30px;\n right: 30px;\n width: 40px;\n height: 40px;\n border: none;\n border-radius: 50%;\n background: #0969da;\n color: white;\n font-size: 18px;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n transition: opacity 0.2s, background 0.2s;\n z-index: 200;\n line-height: 40px;\n text-align: center;\n padding: 0;\n }\n\n .scroll-to-top:hover {\n background: #0550ae;\n }\n\n .scroll-to-top.visible {\n display: block;\n }\n\n /* Content toolbar (collapse/expand buttons) */\n .content-toolbar {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n margin-bottom: 12px;\n }\n\n .collapse-all-btn,\n .expand-all-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n background: none;\n cursor: pointer;\n font-size: 12px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .collapse-all-btn:hover,\n .expand-all-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n /* Line change count badges */\n .summary-badges {\n float: right;\n display: inline-flex;\n gap: 6px;\n margin-left: 12px;\n }\n\n .line-badge {\n display: inline-block;\n padding: 1px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n line-height: 18px;\n }\n\n .line-added {\n background: #d4edda;\n color: #155724;\n }\n\n .line-removed {\n background: #f8d7da;\n color: #721c24;\n }\n\n .sidebar-tree .line-badge {\n font-size: 10px;\n padding: 0 5px;\n line-height: 16px;\n }\n\n /* Diff toolbar */\n .diff-toolbar {\n display: flex;\n justify-content: flex-end;\n padding: 8px 16px;\n border-bottom: 1px solid #d0d7de;\n background: #f6f8fa;\n }\n\n .copy-diff-btn {\n padding: 4px 12px;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n background: white;\n cursor: pointer;\n font-size: 12px;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n color: #24292e;\n transition: all 0.2s;\n }\n\n .copy-diff-btn:hover {\n background: #f3f4f6;\n border-color: #b0b7be;\n }\n\n .copy-diff-btn.copied {\n background: #d4edda;\n border-color: #28a745;\n color: #155724;\n }\n\n /* Sidebar search */\n .sidebar-search {\n width: 100%;\n padding: 6px 8px;\n border: 1px solid #d0d7de;\n border-radius: 4px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n margin-bottom: 8px;\n box-sizing: border-box;\n position: sticky;\n top: 0;\n z-index: 1;\n background: #fff;\n }\n\n .sidebar-search:focus {\n outline: none;\n border-color: #0969da;\n box-shadow: 0 0 0 3px rgba(9,105,218,0.15);\n }\n\n /* Statistics dashboard */\n .stats-dashboard {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .stats-summary {\n display: flex;\n gap: 16px;\n align-items: center;\n margin-bottom: 10px;\n }\n\n .stats-summary .total-added {\n font-weight: 700;\n color: #155724;\n font-size: 16px;\n }\n\n .stats-summary .total-removed {\n font-weight: 700;\n color: #721c24;\n font-size: 16px;\n }\n\n .stats-bar {\n display: flex;\n height: 8px;\n border-radius: 4px;\n overflow: hidden;\n background: #e1e4e8;\n margin-bottom: 10px;\n }\n\n .stats-segment {\n height: 100%;\n min-width: 2px;\n }\n\n .stats-segment.added-segment {\n background: #28a745;\n }\n\n .stats-segment.removed-segment {\n background: #d73a49;\n }\n\n .top-changed-files {\n list-style: none;\n padding: 0;\n margin: 0;\n font-size: 13px;\n }\n\n .top-changed-files li {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 3px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n color: #586069;\n }\n\n .top-changed-files .file-path {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n margin-right: 8px;\n }\n\n .top-changed-files .file-stats {\n white-space: nowrap;\n flex-shrink: 0;\n }\n";
2
3
  export declare const TAB_SCRIPT: string;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.TAB_SCRIPT = exports.HTML_STYLES = void 0;
3
+ exports.TAB_SCRIPT = exports.HTML_STYLES = exports.DIFF2HTML_STYLES = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ exports.DIFF2HTML_STYLES = (0, node_fs_1.readFileSync)(require.resolve('diff2html/bundles/css/diff2html.min.css'), 'utf8');
4
6
  exports.HTML_STYLES = `
5
7
  /* Custom styles */
6
8
  body {
@@ -123,6 +125,14 @@ exports.HTML_STYLES = `
123
125
  background: #eaeef2;
124
126
  }
125
127
 
128
+ .file-section[open] > summary {
129
+ position: sticky;
130
+ top: 0;
131
+ z-index: 10;
132
+ border-bottom: 1px solid #d0d7de;
133
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
134
+ }
135
+
126
136
  .filename-transform {
127
137
  color: #0969da;
128
138
  }
@@ -376,6 +386,226 @@ exports.HTML_STYLES = `
376
386
  .file-content code {
377
387
  font-family: inherit;
378
388
  }
389
+
390
+ /* Scroll-to-top button */
391
+ .scroll-to-top {
392
+ display: none;
393
+ position: fixed;
394
+ bottom: 30px;
395
+ right: 30px;
396
+ width: 40px;
397
+ height: 40px;
398
+ border: none;
399
+ border-radius: 50%;
400
+ background: #0969da;
401
+ color: white;
402
+ font-size: 18px;
403
+ cursor: pointer;
404
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
405
+ transition: opacity 0.2s, background 0.2s;
406
+ z-index: 200;
407
+ line-height: 40px;
408
+ text-align: center;
409
+ padding: 0;
410
+ }
411
+
412
+ .scroll-to-top:hover {
413
+ background: #0550ae;
414
+ }
415
+
416
+ .scroll-to-top.visible {
417
+ display: block;
418
+ }
419
+
420
+ /* Content toolbar (collapse/expand buttons) */
421
+ .content-toolbar {
422
+ display: flex;
423
+ gap: 8px;
424
+ justify-content: flex-end;
425
+ margin-bottom: 12px;
426
+ }
427
+
428
+ .collapse-all-btn,
429
+ .expand-all-btn {
430
+ padding: 4px 12px;
431
+ border: 1px solid #d0d7de;
432
+ border-radius: 4px;
433
+ background: none;
434
+ cursor: pointer;
435
+ font-size: 12px;
436
+ color: #586069;
437
+ transition: all 0.2s;
438
+ }
439
+
440
+ .collapse-all-btn:hover,
441
+ .expand-all-btn:hover {
442
+ background: #f6f8fa;
443
+ color: #24292e;
444
+ }
445
+
446
+ /* Line change count badges */
447
+ .summary-badges {
448
+ float: right;
449
+ display: inline-flex;
450
+ gap: 6px;
451
+ margin-left: 12px;
452
+ }
453
+
454
+ .line-badge {
455
+ display: inline-block;
456
+ padding: 1px 8px;
457
+ border-radius: 10px;
458
+ font-size: 11px;
459
+ font-weight: 600;
460
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
461
+ line-height: 18px;
462
+ }
463
+
464
+ .line-added {
465
+ background: #d4edda;
466
+ color: #155724;
467
+ }
468
+
469
+ .line-removed {
470
+ background: #f8d7da;
471
+ color: #721c24;
472
+ }
473
+
474
+ .sidebar-tree .line-badge {
475
+ font-size: 10px;
476
+ padding: 0 5px;
477
+ line-height: 16px;
478
+ }
479
+
480
+ /* Diff toolbar */
481
+ .diff-toolbar {
482
+ display: flex;
483
+ justify-content: flex-end;
484
+ padding: 8px 16px;
485
+ border-bottom: 1px solid #d0d7de;
486
+ background: #f6f8fa;
487
+ }
488
+
489
+ .copy-diff-btn {
490
+ padding: 4px 12px;
491
+ border: 1px solid #d0d7de;
492
+ border-radius: 6px;
493
+ background: white;
494
+ cursor: pointer;
495
+ font-size: 12px;
496
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
497
+ color: #24292e;
498
+ transition: all 0.2s;
499
+ }
500
+
501
+ .copy-diff-btn:hover {
502
+ background: #f3f4f6;
503
+ border-color: #b0b7be;
504
+ }
505
+
506
+ .copy-diff-btn.copied {
507
+ background: #d4edda;
508
+ border-color: #28a745;
509
+ color: #155724;
510
+ }
511
+
512
+ /* Sidebar search */
513
+ .sidebar-search {
514
+ width: 100%;
515
+ padding: 6px 8px;
516
+ border: 1px solid #d0d7de;
517
+ border-radius: 4px;
518
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
519
+ font-size: 12px;
520
+ margin-bottom: 8px;
521
+ box-sizing: border-box;
522
+ position: sticky;
523
+ top: 0;
524
+ z-index: 1;
525
+ background: #fff;
526
+ }
527
+
528
+ .sidebar-search:focus {
529
+ outline: none;
530
+ border-color: #0969da;
531
+ box-shadow: 0 0 0 3px rgba(9,105,218,0.15);
532
+ }
533
+
534
+ /* Statistics dashboard */
535
+ .stats-dashboard {
536
+ margin: 15px 0 0;
537
+ padding: 12px 0 0;
538
+ border-top: 1px solid #e1e4e8;
539
+ }
540
+
541
+ .stats-summary {
542
+ display: flex;
543
+ gap: 16px;
544
+ align-items: center;
545
+ margin-bottom: 10px;
546
+ }
547
+
548
+ .stats-summary .total-added {
549
+ font-weight: 700;
550
+ color: #155724;
551
+ font-size: 16px;
552
+ }
553
+
554
+ .stats-summary .total-removed {
555
+ font-weight: 700;
556
+ color: #721c24;
557
+ font-size: 16px;
558
+ }
559
+
560
+ .stats-bar {
561
+ display: flex;
562
+ height: 8px;
563
+ border-radius: 4px;
564
+ overflow: hidden;
565
+ background: #e1e4e8;
566
+ margin-bottom: 10px;
567
+ }
568
+
569
+ .stats-segment {
570
+ height: 100%;
571
+ min-width: 2px;
572
+ }
573
+
574
+ .stats-segment.added-segment {
575
+ background: #28a745;
576
+ }
577
+
578
+ .stats-segment.removed-segment {
579
+ background: #d73a49;
580
+ }
581
+
582
+ .top-changed-files {
583
+ list-style: none;
584
+ padding: 0;
585
+ margin: 0;
586
+ font-size: 13px;
587
+ }
588
+
589
+ .top-changed-files li {
590
+ display: flex;
591
+ justify-content: space-between;
592
+ align-items: center;
593
+ padding: 3px 0;
594
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
595
+ color: #586069;
596
+ }
597
+
598
+ .top-changed-files .file-path {
599
+ overflow: hidden;
600
+ text-overflow: ellipsis;
601
+ white-space: nowrap;
602
+ margin-right: 8px;
603
+ }
604
+
605
+ .top-changed-files .file-stats {
606
+ white-space: nowrap;
607
+ flex-shrink: 0;
608
+ }
379
609
  `;
380
610
  exports.TAB_SCRIPT = String.raw `
381
611
  // Tab switching
@@ -553,4 +783,113 @@ exports.TAB_SCRIPT = String.raw `
553
783
  URL.revokeObjectURL(url);
554
784
  });
555
785
  });
786
+
787
+ // Scroll-to-top button
788
+ const scrollToTopBtn = document.querySelector('.scroll-to-top');
789
+ if (scrollToTopBtn) {
790
+ window.addEventListener('scroll', () => {
791
+ scrollToTopBtn.classList.toggle('visible', window.scrollY > 200);
792
+ });
793
+ scrollToTopBtn.addEventListener('click', () => {
794
+ window.scrollTo({ top: 0, behavior: 'smooth' });
795
+ });
796
+ }
797
+
798
+ // Collapse All / Expand All buttons
799
+ document.querySelectorAll('.collapse-all-btn').forEach(btn => {
800
+ btn.addEventListener('click', () => {
801
+ const tabContent = btn.closest('.tab-content');
802
+ if (!tabContent) return;
803
+ tabContent.querySelectorAll('.file-section[open]').forEach(d => { d.open = false; });
804
+ });
805
+ });
806
+ document.querySelectorAll('.expand-all-btn').forEach(btn => {
807
+ btn.addEventListener('click', () => {
808
+ const tabContent = btn.closest('.tab-content');
809
+ if (!tabContent) return;
810
+ tabContent.querySelectorAll('.file-section').forEach(d => { d.open = true; });
811
+ });
812
+ });
813
+
814
+ // Copy Diff button functionality
815
+ document.querySelectorAll('.copy-diff-btn').forEach(btn => {
816
+ btn.addEventListener('click', async () => {
817
+ const fileId = btn.getAttribute('data-file-id');
818
+ const section = document.getElementById(fileId);
819
+ if (!section) return;
820
+
821
+ const diffSource = section.querySelector('.unified-diff-source');
822
+ if (!diffSource) return;
823
+
824
+ const content = diffSource.textContent || '';
825
+
826
+ try {
827
+ if (navigator.clipboard && navigator.clipboard.writeText) {
828
+ await navigator.clipboard.writeText(content);
829
+ } else {
830
+ const textarea = document.createElement('textarea');
831
+ textarea.value = content;
832
+ textarea.style.position = 'fixed';
833
+ textarea.style.opacity = '0';
834
+ document.body.appendChild(textarea);
835
+ textarea.select();
836
+ document.execCommand('copy');
837
+ document.body.removeChild(textarea);
838
+ }
839
+
840
+ const originalText = btn.textContent;
841
+ btn.textContent = '\u2713 Copied!';
842
+ btn.classList.add('copied');
843
+ setTimeout(() => {
844
+ btn.textContent = originalText;
845
+ btn.classList.remove('copied');
846
+ }, 2000);
847
+ } catch (err) {
848
+ console.error('Failed to copy diff:', err);
849
+ btn.textContent = '\u2717 Failed';
850
+ setTimeout(() => {
851
+ btn.textContent = 'Copy Diff';
852
+ }, 2000);
853
+ }
854
+ });
855
+ });
856
+
857
+ // Sidebar search/filter functionality
858
+ document.querySelectorAll('.sidebar-search').forEach(input => {
859
+ input.addEventListener('input', () => {
860
+ const searchText = input.value.toLowerCase().trim();
861
+ const sidebar = input.closest('.sidebar-content');
862
+ if (!sidebar) return;
863
+
864
+ const files = sidebar.querySelectorAll('.tree-file');
865
+ const folders = sidebar.querySelectorAll('.tree-folder');
866
+
867
+ if (!searchText) {
868
+ files.forEach(f => { f.style.display = ''; });
869
+ folders.forEach(f => { f.style.display = ''; f.classList.remove('collapsed'); });
870
+ return;
871
+ }
872
+
873
+ // First hide all files that don't match
874
+ files.forEach(f => {
875
+ const path = (f.getAttribute('data-path') || '').toLowerCase();
876
+ f.style.display = path.includes(searchText) ? '' : 'none';
877
+ });
878
+
879
+ // Then show/hide folders based on whether they have visible children
880
+ // Process from deepest to shallowest
881
+ const folderArray = Array.from(folders).reverse();
882
+ folderArray.forEach(folder => {
883
+ const childrenContainer = folder.querySelector('.tree-children');
884
+ if (!childrenContainer) return;
885
+ const hasVisibleChild = childrenContainer.querySelector('.tree-file:not([style*="display: none"]), .tree-folder:not([style*="display: none"])');
886
+ if (hasVisibleChild) {
887
+ folder.style.display = '';
888
+ folder.classList.remove('collapsed');
889
+ } else {
890
+ folder.style.display = 'none';
891
+ }
892
+ });
893
+ });
894
+ });
556
895
  `;
@@ -5,4 +5,16 @@ export interface ReportMetadata {
5
5
  destination: string;
6
6
  dryRun: boolean;
7
7
  }
8
- export declare const generateHtmlTemplate: (diffResult: FileDiffResult, formattedFiles: string[], trulyUnchangedFiles: string[], metadata: ReportMetadata, changedSections: string[], changedFileIds?: Map<string, string>, addedSections?: string[], addedFileIds?: Map<string, string>) => string;
8
+ export interface DiffStats {
9
+ totalAdded: number;
10
+ totalRemoved: number;
11
+ fileStats: Array<{
12
+ path: string;
13
+ added: number;
14
+ removed: number;
15
+ }>;
16
+ }
17
+ export declare const generateHtmlTemplate: (diffResult: FileDiffResult, formattedFiles: string[], trulyUnchangedFiles: string[], metadata: ReportMetadata, changedSections: string[], changedFileIds?: Map<string, string>, addedSections?: string[], addedFileIds?: Map<string, string>, fileStats?: Map<string, {
18
+ added: number;
19
+ removed: number;
20
+ }>, diffStats?: DiffStats) => string;
@@ -4,7 +4,31 @@ exports.generateHtmlTemplate = void 0;
4
4
  const htmlStyles_1 = require("./htmlStyles");
5
5
  const treeBuilder_1 = require("./treeBuilder");
6
6
  const treeRenderer_1 = require("./treeRenderer");
7
- const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds = new Map(), addedSections = [], addedFileIds = new Map()) => {
7
+ const renderStatsDashboard = (diffStats) => {
8
+ const total = diffStats.totalAdded + diffStats.totalRemoved;
9
+ if (total === 0)
10
+ return '';
11
+ const addedPercent = Math.round((diffStats.totalAdded / total) * 100);
12
+ const removedPercent = 100 - addedPercent;
13
+ const top5 = diffStats.fileStats.slice(0, 5);
14
+ const topFilesHtml = top5
15
+ .map((f) => `<li><span class="file-path">${(0, treeRenderer_1.escapeHtml)(f.path)}</span><span class="file-stats"><span class="line-badge line-added">+${f.added}</span> <span class="line-badge line-removed">-${f.removed}</span></span></li>`)
16
+ .join('');
17
+ return `
18
+ <div class="stats-dashboard">
19
+ <div class="stats-summary">
20
+ <span class="total-added">+${diffStats.totalAdded}</span>
21
+ <span class="total-removed">-${diffStats.totalRemoved}</span>
22
+ <span style="color: #586069; font-size: 13px;">lines across ${diffStats.fileStats.length} file${diffStats.fileStats.length === 1 ? '' : 's'}</span>
23
+ </div>
24
+ <div class="stats-bar">
25
+ <div class="stats-segment added-segment" style="width: ${addedPercent}%"></div>
26
+ <div class="stats-segment removed-segment" style="width: ${removedPercent}%"></div>
27
+ </div>
28
+ ${top5.length > 0 ? `<ul class="top-changed-files">${topFilesHtml}</ul>` : ''}
29
+ </div>`;
30
+ };
31
+ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds = new Map(), addedSections = [], addedFileIds = new Map(), fileStats = new Map(), diffStats) => {
8
32
  const changedFilePaths = diffResult.changedFiles.map((f) => f.path);
9
33
  const changedTree = (0, treeBuilder_1.buildFileTree)(changedFilePaths);
10
34
  const addedFilePaths = diffResult.addedFiles.map((f) => f.path);
@@ -18,7 +42,7 @@ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, m
18
42
  <head>
19
43
  <meta charset="UTF-8">
20
44
  <title>helm-env-delta Report - ${metadata.timestamp}</title>
21
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css">
45
+ <style>${htmlStyles_1.DIFF2HTML_STYLES}</style>
22
46
  <style>
23
47
  ${htmlStyles_1.HTML_STYLES}
24
48
  </style>
@@ -39,6 +63,7 @@ ${htmlStyles_1.HTML_STYLES}
39
63
  <span class="stat formatted">${formattedFiles.length} Formatted</span>
40
64
  <span class="stat unchanged">${trulyUnchangedFiles.length} Unchanged</span>
41
65
  </div>
66
+ ${diffStats ? renderStatsDashboard(diffStats) : ''}
42
67
  </header>
43
68
 
44
69
  <nav class="tabs">
@@ -60,11 +85,16 @@ ${htmlStyles_1.HTML_STYLES}
60
85
  <button class="sidebar-toggle">&#9664;</button>
61
86
  </div>
62
87
  <div class="sidebar-content">
63
- ${(0, treeRenderer_1.renderSidebarTree)(changedTree, changedFileIds)}
88
+ <input type="text" class="sidebar-search" placeholder="Filter files..." />
89
+ ${(0, treeRenderer_1.renderSidebarTree)(changedTree, changedFileIds, fileStats)}
64
90
  </div>
65
91
  </aside>
66
92
  <button class="sidebar-expand-btn">&#9654;</button>
67
93
  <div class="changed-content">
94
+ <div class="content-toolbar">
95
+ <button class="collapse-all-btn">Collapse All</button>
96
+ <button class="expand-all-btn">Expand All</button>
97
+ </div>
68
98
  ${changedSections.join('\n')}
69
99
  </div>
70
100
  </div>
@@ -82,6 +112,7 @@ ${htmlStyles_1.HTML_STYLES}
82
112
  <button class="sidebar-toggle" data-sidebar="added">&#9664;</button>
83
113
  </div>
84
114
  <div class="sidebar-content">
115
+ <input type="text" class="sidebar-search" placeholder="Filter files..." />
85
116
  ${(0, treeRenderer_1.renderSidebarTree)(addedTree, addedFileIds)}
86
117
  </div>
87
118
  </aside>
@@ -125,6 +156,8 @@ ${htmlStyles_1.HTML_STYLES}
125
156
  </section>
126
157
  </main>
127
158
 
159
+ <button class="scroll-to-top" title="Scroll to top">&#9650;</button>
160
+
128
161
  <script>
129
162
  ${htmlStyles_1.TAB_SCRIPT}
130
163
  </script>
@@ -1,4 +1,7 @@
1
1
  import { TreeNode } from './treeBuilder';
2
2
  export declare const renderTreeview: (nodes: TreeNode[]) => string;
3
- export declare const renderSidebarTree: (nodes: TreeNode[], fileIds: Map<string, string>) => string;
3
+ export declare const renderSidebarTree: (nodes: TreeNode[], fileIds: Map<string, string>, fileStats?: Map<string, {
4
+ added: number;
5
+ removed: number;
6
+ }>) => string;
4
7
  export declare const escapeHtml: (text: string) => string;
@@ -22,15 +22,17 @@ const renderTreeNode = (node) => {
22
22
  <span class="tree-file-name">${(0, exports.escapeHtml)(node.name)}</span>
23
23
  </li>`;
24
24
  };
25
- const renderSidebarTree = (nodes, fileIds) => {
25
+ const renderSidebarTree = (nodes, fileIds, fileStats) => {
26
26
  if (nodes.length === 0)
27
27
  return '';
28
- return `<ul class="tree-root sidebar-tree">${nodes.map((node) => renderSidebarNode(node, fileIds)).join('')}</ul>`;
28
+ return `<ul class="tree-root sidebar-tree">${nodes.map((node) => renderSidebarNode(node, fileIds, fileStats)).join('')}</ul>`;
29
29
  };
30
30
  exports.renderSidebarTree = renderSidebarTree;
31
- const renderSidebarNode = (node, fileIds) => {
31
+ const renderSidebarNode = (node, fileIds, fileStats) => {
32
32
  if (node.isFolder) {
33
- const children = node.children ? node.children.map((child) => renderSidebarNode(child, fileIds)).join('') : '';
33
+ const children = node.children
34
+ ? node.children.map((child) => renderSidebarNode(child, fileIds, fileStats)).join('')
35
+ : '';
34
36
  return `
35
37
  <li class="tree-folder" data-path="${(0, exports.escapeHtml)(node.path)}">
36
38
  <span class="tree-toggle">&#9660;</span>
@@ -39,9 +41,13 @@ const renderSidebarNode = (node, fileIds) => {
39
41
  </li>`;
40
42
  }
41
43
  const fileId = fileIds.get(node.path) || '';
44
+ const stats = fileStats?.get(node.path);
45
+ const badges = stats
46
+ ? ` <span class="line-badge line-added">+${stats.added}</span><span class="line-badge line-removed">-${stats.removed}</span>`
47
+ : '';
42
48
  return `
43
49
  <li class="tree-file" data-path="${(0, exports.escapeHtml)(node.path)}" data-file-id="${(0, exports.escapeHtml)(fileId)}">
44
- <a href="#${(0, exports.escapeHtml)(fileId)}" class="tree-file-link">${(0, exports.escapeHtml)(node.name)}</a>
50
+ <a href="#${(0, exports.escapeHtml)(fileId)}" class="tree-file-link">${(0, exports.escapeHtml)(node.name)}</a>${badges}
45
51
  </li>`;
46
52
  };
47
53
  const escapeHtml = (text) => text
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.filterFileMaps = exports.filterFileMap = exports.filterDiffResultByMode = exports.fileMatchesFilter = exports.parseFilterExpression = exports.isFilterParseError = exports.FilterParseError = void 0;
4
4
  const errors_1 = require("./errors");
5
5
  const filterParseErrorCodes = {
6
- MIXED_OPERATORS: 'Cannot combine AND (&) and OR (|) operators in a single filter expression'
6
+ MIXED_OPERATORS: 'Cannot combine AND (+) and OR (,) operators in a single filter expression'
7
7
  };
8
8
  exports.FilterParseError = (0, errors_1.createErrorClass)('FilterParseError', filterParseErrorCodes);
9
9
  exports.isFilterParseError = (0, errors_1.createErrorTypeGuard)(exports.FilterParseError);
@@ -15,17 +15,17 @@ const parseFilterExpression = (filter) => {
15
15
  for (let index = 0; index < filter.length; index++) {
16
16
  const char = filter[index];
17
17
  const isEscaped = index > 0 && filter[index - 1] === '\\';
18
- if (char === '|' && !isEscaped)
18
+ if (char === ',' && !isEscaped)
19
19
  hasUnescapedOr = true;
20
- if (char === '&' && !isEscaped)
20
+ if (char === '+' && !isEscaped)
21
21
  hasUnescapedAnd = true;
22
22
  }
23
23
  if (hasUnescapedOr && hasUnescapedAnd)
24
24
  throw new exports.FilterParseError('Mixed operators detected', {
25
25
  code: 'MIXED_OPERATORS',
26
26
  hints: [
27
- 'Use only | (OR) or only & (AND) in a single filter',
28
- String.raw `Escape literal characters with backslash: \| or \&`
27
+ 'Use only , (OR) or only + (AND) in a single filter',
28
+ String.raw `Escape literal characters with backslash: \, or \+`
29
29
  ]
30
30
  });
31
31
  const operator = hasUnescapedOr ? 'OR' : hasUnescapedAnd ? 'AND' : 'NONE';
@@ -33,7 +33,7 @@ const parseFilterExpression = (filter) => {
33
33
  if (operator === 'NONE')
34
34
  terms = [filter];
35
35
  else {
36
- const splitChar = operator === 'OR' ? '|' : '&';
36
+ const splitChar = operator === 'OR' ? ',' : '+';
37
37
  terms = [];
38
38
  let currentTerm = '';
39
39
  for (let index = 0; index < filter.length; index++) {
@@ -43,7 +43,7 @@ const parseFilterExpression = (filter) => {
43
43
  terms.push(currentTerm);
44
44
  currentTerm = '';
45
45
  }
46
- else if (char === '\\' && index + 1 < filter.length && (filter[index + 1] === '|' || filter[index + 1] === '&'))
46
+ else if (char === '\\' && index + 1 < filter.length && (filter[index + 1] === ',' || filter[index + 1] === '+'))
47
47
  continue;
48
48
  else
49
49
  currentTerm += char;
@@ -52,7 +52,7 @@ const parseFilterExpression = (filter) => {
52
52
  }
53
53
  const processedTerms = terms
54
54
  .map((term) => term
55
- .replaceAll(/\\([&|])/g, '$1')
55
+ .replaceAll(/\\([+,])/g, '$1')
56
56
  .trim()
57
57
  .toLowerCase())
58
58
  .filter((term) => term.length > 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/hogan.js": "^3.0.5",
71
- "@types/node": "^25.2.0",
71
+ "@types/node": "^25.2.1",
72
72
  "@types/picomatch": "^4.0.2",
73
73
  "@typescript-eslint/eslint-plugin": "^8.54.0",
74
74
  "@typescript-eslint/parser": "^8.54.0",