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 +20 -16
- package/dist/commandLine.js +3 -3
- package/dist/htmlReporter.d.ts +1 -1
- package/dist/htmlReporter.js +37 -4
- package/dist/reporters/htmlStyles.d.ts +2 -1
- package/dist/reporters/htmlStyles.js +340 -1
- package/dist/reporters/htmlTemplate.d.ts +13 -1
- package/dist/reporters/htmlTemplate.js +36 -3
- package/dist/reporters/treeRenderer.d.ts +4 -1
- package/dist/reporters/treeRenderer.js +11 -5
- package/dist/utils/fileFilter.js +8 -8
- package/package.json +2 -2
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
|
|
701
|
-
| -------- | ------ |
|
|
702
|
-
| (none) | Simple | `-f prod`
|
|
703
|
-
|
|
|
704
|
-
|
|
|
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
|
|
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
|
|
715
|
+
hed -c config.yaml -f values+prod --list-files
|
|
712
716
|
|
|
713
|
-
# Escape literal
|
|
714
|
-
hed -c config.yaml -f "foo
|
|
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
|
|
723
|
+
- Cannot mix `+` and `,` in a single filter (throws error)
|
|
720
724
|
- Case-insensitive matching
|
|
721
|
-
- Empty terms are ignored (`a
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/commandLine.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
package/dist/htmlReporter.d.ts
CHANGED
|
@@ -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;
|
package/dist/htmlReporter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
<
|
|
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">◀</button>
|
|
61
86
|
</div>
|
|
62
87
|
<div class="sidebar-content">
|
|
63
|
-
|
|
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">▶</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">◀</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">▲</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
|
|
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
|
|
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">▼</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
|
package/dist/utils/fileFilter.js
CHANGED
|
@@ -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 (
|
|
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 === '
|
|
18
|
+
if (char === ',' && !isEscaped)
|
|
19
19
|
hasUnescapedOr = true;
|
|
20
|
-
if (char === '
|
|
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
|
|
28
|
-
String.raw `Escape literal characters with backslash:
|
|
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] === '
|
|
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(/\\([
|
|
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.
|
|
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.
|
|
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",
|