helm-env-delta 1.11.1 → 1.13.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
@@ -52,11 +52,11 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
52
52
 
53
53
  🛡️ **Safety Rules** - Block major version upgrades, scaling violations, and forbidden patterns. Load validation rules from external files. Scan globally or target specific fields.
54
54
 
55
- 🎨 **Format Enforcement** - Standardize YAML across all environments: key ordering, indentation, quoting, array sorting.
55
+ 🎨 **Format Enforcement** - Standardize YAML across all environments: key ordering, alphabetical key sorting, indentation, quoting, array sorting.
56
56
 
57
57
  📦 **Config Inheritance** - Reuse base configurations with environment-specific overrides.
58
58
 
59
- 📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include collapsible diff stats dashboard, synchronized side-by-side scrolling, copy diff buttons, file search, and collapse/expand controls. Empty categories are automatically hidden.
59
+ 📊 **Multiple Reports** - Console, HTML (visual, self-contained), and JSON (CI/CD) output formats. HTML reports include collapsible diff stats dashboard, stop rule violations table (dry-run only), synchronized side-by-side scrolling, copy diff buttons, file search, and collapse/expand controls. Empty categories are automatically hidden.
60
60
 
61
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.
62
62
 
@@ -119,7 +119,7 @@ hed -c config.yaml
119
119
  hed -c config.yaml -H
120
120
  ```
121
121
 
122
- Self-contained HTML report — works offline, no CDN required. Includes collapsible diff stats dashboard, synchronized side-by-side scrolling, copy buttons, sidebar search, and collapse/expand controls. Empty categories are automatically hidden.
122
+ Self-contained HTML report — works offline, no CDN required. Includes collapsible diff stats dashboard, stop rule violations table (shown in dry-run mode), synchronized side-by-side scrolling, copy buttons, sidebar search, and collapse/expand controls. Empty categories are automatically hidden.
123
123
 
124
124
  ### 5️⃣ Get Smart Suggestions (Optional)
125
125
 
@@ -208,6 +208,9 @@ outputFormat:
208
208
  - 'kind'
209
209
  - 'metadata'
210
210
  - 'spec'
211
+ keySort:
212
+ '**/*.yaml':
213
+ - path: 'spec.template.metadata.labels'
211
214
  arraySort:
212
215
  '**/*.yaml':
213
216
  - path: 'env'
@@ -410,10 +413,13 @@ exclude: # Optional: Exclude patterns
410
413
 
411
414
  prune: false # Optional: Delete dest files not in source
412
415
  confirmationDelay: 3000 # Optional: Delay in ms before sync (default: 3000, 0 to disable)
416
+ requiredVersion: '1.10.0' # Optional: Minimum tool version required to process this config
413
417
  ```
414
418
 
415
419
  **Note:** Source and destination paths cannot resolve to the same folder.
416
420
 
421
+ **`requiredVersion`:** When set, the CLI checks that the installed version of helm-env-delta meets this minimum. If the installed version is older, the CLI exits immediately with a clear upgrade message. This prevents running configs that depend on newer features with an outdated tool version. Supports `"1.2.3"` or `"v1.2.3"` format.
422
+
417
423
  ---
418
424
 
419
425
  ### 🔒 Path Filtering (skipPath)
@@ -664,6 +670,8 @@ stopRules:
664
670
 
665
671
  **Override:** Use `--force` to bypass stop rules when needed.
666
672
 
673
+ **Visibility:** Stop rule violations appear in console output, JSON reports, and HTML reports (dry-run mode only, as a collapsible table in the header area).
674
+
667
675
  ---
668
676
 
669
677
  ### 🎨 Output Formatting
@@ -675,13 +683,18 @@ outputFormat:
675
683
  indent: 2 # Indentation size
676
684
  keySeparator: true # Blank line between top-level keys (or second-level keys when single top-level key)
677
685
 
678
- keyOrders: # Custom key ordering
686
+ keyOrders: # Custom key ordering (pin specific keys to top)
679
687
  'apps/*.yaml':
680
688
  - 'apiVersion'
681
689
  - 'kind'
682
690
  - 'metadata'
683
691
  - 'spec'
684
692
 
693
+ keySort: # Sort all keys alphabetically at path
694
+ '**/*.yaml':
695
+ - path: 'spec.template.metadata.labels'
696
+ - path: 'env.vars'
697
+
685
698
  arraySort: # Sort arrays
686
699
  'services/**/values.yaml':
687
700
  - path: 'env'
@@ -767,7 +780,7 @@ stopRules: # Add production safety rules
767
780
 
768
781
  **Merging:**
769
782
 
770
- - Primitives (`source`, `destination`, `prune`, `confirmationDelay`): Child overrides parent
783
+ - Primitives (`source`, `destination`, `prune`, `confirmationDelay`, `requiredVersion`): Child overrides parent
771
784
  - Arrays (`include`, `exclude`): Concatenated (parent + child)
772
785
  - Per-file Records (`skipPath`, `transforms`, `stopRules`, `fixedValues`): Keys merged, arrays concatenated
773
786
  - `outputFormat`: Shallow merged (child fields override parent)
@@ -77,6 +77,9 @@ declare const arraySortRuleSchema: z.ZodObject<{
77
77
  desc: "desc";
78
78
  }>>;
79
79
  }, z.core.$strip>;
80
+ declare const keySortRuleSchema: z.ZodObject<{
81
+ path: z.ZodString;
82
+ }, z.core.$strip>;
80
83
  declare const fixedValueRuleSchema: z.ZodObject<{
81
84
  path: z.ZodString;
82
85
  value: z.ZodUnknown;
@@ -99,6 +102,7 @@ declare const transformRulesSchema: z.ZodObject<{
99
102
  }, z.core.$strip>;
100
103
  declare const baseConfigSchema: z.ZodObject<{
101
104
  extends: z.ZodOptional<z.ZodString>;
105
+ requiredVersion: z.ZodOptional<z.ZodString>;
102
106
  source: z.ZodOptional<z.ZodString>;
103
107
  destination: z.ZodOptional<z.ZodString>;
104
108
  include: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -111,6 +115,9 @@ declare const baseConfigSchema: z.ZodObject<{
111
115
  keySeparator: z.ZodOptional<z.ZodBoolean>;
112
116
  quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
113
117
  keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
118
+ keySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
119
+ path: z.ZodString;
120
+ }, z.core.$strip>>>>;
114
121
  arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
115
122
  path: z.ZodString;
116
123
  sortBy: z.ZodString;
@@ -182,6 +189,7 @@ declare const finalConfigSchema: z.ZodObject<{
182
189
  contentFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
183
190
  filenameFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
184
191
  }, z.core.$strip>>>;
192
+ requiredVersion: z.ZodOptional<z.ZodString>;
185
193
  source: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
186
194
  destination: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
187
195
  skipPath: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
@@ -230,6 +238,9 @@ declare const finalConfigSchema: z.ZodObject<{
230
238
  keySeparator: z.ZodDefault<z.ZodBoolean>;
231
239
  quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
232
240
  keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
241
+ keySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
242
+ path: z.ZodString;
243
+ }, z.core.$strip>>>>;
233
244
  arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
234
245
  path: z.ZodString;
235
246
  sortBy: z.ZodString;
@@ -253,6 +264,7 @@ declare const formatOnlyConfigSchema: z.ZodObject<{
253
264
  contentFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
254
265
  filenameFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
255
266
  }, z.core.$strip>>>;
267
+ requiredVersion: z.ZodOptional<z.ZodString>;
256
268
  source: z.ZodOptional<z.ZodString>;
257
269
  destination: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
258
270
  skipPath: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
@@ -301,6 +313,9 @@ declare const formatOnlyConfigSchema: z.ZodObject<{
301
313
  keySeparator: z.ZodDefault<z.ZodBoolean>;
302
314
  quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
303
315
  keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
316
+ keySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
317
+ path: z.ZodString;
318
+ }, z.core.$strip>>>>;
304
319
  arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
305
320
  path: z.ZodString;
306
321
  sortBy: z.ZodString;
@@ -324,6 +339,7 @@ export type RegexFileRule = z.infer<typeof regexFileRuleSchema>;
324
339
  export type RegexFileKeyRule = z.infer<typeof regexFileKeyRuleSchema>;
325
340
  export type VersionFormatRule = z.infer<typeof versionFormatRuleSchema>;
326
341
  export type ArraySortRule = z.infer<typeof arraySortRuleSchema>;
342
+ export type KeySortRule = z.infer<typeof keySortRuleSchema>;
327
343
  export type TransformRule = z.infer<typeof transformRuleSchema>;
328
344
  export type TransformRules = z.infer<typeof transformRulesSchema>;
329
345
  export type TransformConfig = Record<string, TransformRules>;
@@ -79,6 +79,7 @@ const arraySortRuleSchema = zod_1.z.object({
79
79
  sortBy: zod_1.z.string().min(1),
80
80
  order: zod_1.z.enum(['asc', 'desc']).default('asc')
81
81
  });
82
+ const keySortRuleSchema = zod_1.z.object({ path: zod_1.z.string().min(1) });
82
83
  const fixedValueRuleSchema = zod_1.z.object({
83
84
  path: zod_1.z.string().min(1).describe('JSONPath to the value to set'),
84
85
  value: zod_1.z.unknown().describe('The constant value to set (any type: string, number, boolean, null, object, array)')
@@ -115,6 +116,13 @@ const transformRulesSchema = zod_1.z
115
116
  });
116
117
  const baseConfigSchema = zod_1.z.object({
117
118
  extends: zod_1.z.string().min(1).optional(),
119
+ requiredVersion: zod_1.z
120
+ .string()
121
+ .min(1)
122
+ .regex(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/, {
123
+ message: 'Must be a valid semver version (e.g., "1.2.3" or "v1.2.3")'
124
+ })
125
+ .optional(),
118
126
  source: zod_1.z.string().min(1).optional(),
119
127
  destination: zod_1.z.string().min(1).optional(),
120
128
  include: zod_1.z.array(zod_1.z.string().min(1)).optional(),
@@ -128,6 +136,7 @@ const baseConfigSchema = zod_1.z.object({
128
136
  keySeparator: zod_1.z.boolean().optional(),
129
137
  quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
130
138
  keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
139
+ keySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(keySortRuleSchema)).optional(),
131
140
  arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
132
141
  })
133
142
  .optional(),
@@ -149,6 +158,7 @@ const finalConfigSchema = baseConfigSchema
149
158
  keySeparator: zod_1.z.boolean().default(false),
150
159
  quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
151
160
  keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
161
+ keySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(keySortRuleSchema)).optional(),
152
162
  arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
153
163
  })
154
164
  .optional()
@@ -176,6 +186,7 @@ const formatOnlyConfigSchema = baseConfigSchema
176
186
  keySeparator: zod_1.z.boolean().default(false),
177
187
  quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
178
188
  keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
189
+ keySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(keySortRuleSchema)).optional(),
179
190
  arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
180
191
  })
181
192
  .optional()
@@ -1,6 +1,25 @@
1
1
  import { type FinalConfig, type FormatOnlyConfig } from './configFile';
2
+ declare const ConfigLoaderErrorClass: {
3
+ new (message: string, options?: import("./utils").ErrorOptions): {
4
+ [key: string]: unknown;
5
+ readonly code?: string;
6
+ readonly path?: string;
7
+ readonly cause?: Error;
8
+ readonly hints?: string[];
9
+ name: string;
10
+ message: string;
11
+ stack?: string;
12
+ };
13
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
14
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
15
+ stackTraceLimit: number;
16
+ };
17
+ export declare class ConfigLoaderError extends ConfigLoaderErrorClass {
18
+ }
19
+ export declare const isConfigLoaderError: (error: unknown) => error is ConfigLoaderError;
2
20
  export type Config = FinalConfig;
3
21
  export type LoadConfigOptions = {
4
22
  formatOnly?: boolean;
5
23
  };
6
24
  export declare const loadConfigFile: (configPath: string, quiet?: boolean, logger?: import("./logger").Logger, options?: LoadConfigOptions) => FinalConfig | FormatOnlyConfig;
25
+ export {};
@@ -3,11 +3,36 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.loadConfigFile = void 0;
6
+ exports.loadConfigFile = exports.isConfigLoaderError = exports.ConfigLoaderError = void 0;
7
7
  const node_path_1 = __importDefault(require("node:path"));
8
+ const package_json_1 = __importDefault(require("../package.json"));
8
9
  const configFile_1 = require("./configFile");
9
10
  const configMerger_1 = require("./configMerger");
10
11
  const utils_1 = require("./utils");
12
+ const ConfigLoaderErrorClass = (0, utils_1.createErrorClass)('Config Loader Error', {
13
+ VERSION_REQUIREMENT: 'Installed version does not meet the required version'
14
+ }, (message, options) => {
15
+ let fullMessage = `Config Loader Error: ${message}`;
16
+ if (options['requiredVersion'])
17
+ fullMessage += `\n Required version: ${options['requiredVersion']}`;
18
+ if (options['currentVersion'])
19
+ fullMessage += `\n Current version: ${options['currentVersion']}`;
20
+ fullMessage += '\n\n Hint: Run "npm install" to update helm-env-delta to the required version.';
21
+ return fullMessage;
22
+ });
23
+ class ConfigLoaderError extends ConfigLoaderErrorClass {
24
+ }
25
+ exports.ConfigLoaderError = ConfigLoaderError;
26
+ exports.isConfigLoaderError = (0, utils_1.createErrorTypeGuard)(ConfigLoaderError);
27
+ const checkRequiredVersion = (requiredVersion) => {
28
+ const currentVersion = package_json_1.default.version;
29
+ if ((0, utils_1.isNewerVersion)(currentVersion, requiredVersion))
30
+ throw new ConfigLoaderError(`This config requires helm-env-delta v${requiredVersion} or newer`, {
31
+ code: 'VERSION_REQUIREMENT',
32
+ requiredVersion,
33
+ currentVersion
34
+ });
35
+ };
11
36
  const expandTransformFiles = (config, configDirectory) => {
12
37
  if (!config.transforms)
13
38
  return config;
@@ -34,6 +59,8 @@ const loadConfigFile = (configPath, quiet = false, logger, options = {}) => {
34
59
  const configDirectory = node_path_1.default.dirname(node_path_1.default.resolve(configPath));
35
60
  const mergedConfig = (0, configMerger_1.resolveConfigWithExtends)(configPath, new Set(), 0, logger);
36
61
  const expandedConfig = expandTransformFiles(mergedConfig, configDirectory);
62
+ if (expandedConfig.requiredVersion)
63
+ checkRequiredVersion(expandedConfig.requiredVersion);
37
64
  const config = options.formatOnly
38
65
  ? (0, configFile_1.parseFormatOnlyConfig)(expandedConfig, configPath)
39
66
  : (0, configFile_1.parseFinalConfig)(expandedConfig, configPath);
@@ -88,6 +88,10 @@ const mergeConfigs = (parent, child) => {
88
88
  merged.confirmationDelay = child.confirmationDelay;
89
89
  else if (parent.confirmationDelay !== undefined)
90
90
  merged.confirmationDelay = parent.confirmationDelay;
91
+ if (child.requiredVersion !== undefined)
92
+ merged.requiredVersion = child.requiredVersion;
93
+ else if (parent.requiredVersion !== undefined)
94
+ merged.requiredVersion = parent.requiredVersion;
91
95
  const parentInclude = parent.include ?? [];
92
96
  const childInclude = child.include ?? [];
93
97
  if (parentInclude.length > 0 || childInclude.length > 0)
@@ -1,5 +1,6 @@
1
1
  import { Config } from './configFile';
2
2
  import { FileDiffResult } from './fileDiff';
3
+ import type { ValidationResult } from './stopRulesValidator';
3
4
  export type { DiffStats, ReportMetadata } from './reporters/htmlTemplate';
4
5
  declare const HtmlReporterErrorClass: {
5
6
  new (message: string, options?: import("./utils/errors").ErrorOptions): {
@@ -19,4 +20,4 @@ declare const HtmlReporterErrorClass: {
19
20
  export declare class HtmlReporterError extends HtmlReporterErrorClass {
20
21
  }
21
22
  export declare const isHtmlReporterError: (error: unknown) => error is HtmlReporterError;
22
- export declare const generateHtmlReport: (diffResult: FileDiffResult, formattedFiles: string[], config: Config, dryRun: boolean, logger?: import("./logger").Logger) => Promise<void>;
23
+ export declare const generateHtmlReport: (diffResult: FileDiffResult, formattedFiles: string[], config: Config, dryRun: boolean, logger?: import("./logger").Logger, validationResult?: ValidationResult) => Promise<void>;
@@ -118,7 +118,7 @@ const writeHtmlFile = async (htmlContent, outputPath) => {
118
118
  });
119
119
  }
120
120
  };
121
- const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, logger) => {
121
+ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, logger, validationResult) => {
122
122
  const reportPath = generateTemporaryFilePath();
123
123
  const metadata = {
124
124
  timestamp: new Date().toISOString(),
@@ -151,7 +151,17 @@ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, lo
151
151
  }
152
152
  statsArray.sort((a, b) => b.added + b.removed - (a.added + a.removed));
153
153
  const diffStats = { totalAdded, totalRemoved, fileStats: statsArray };
154
- const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds, addedSections, addedFileIds, diffStats);
154
+ const stopRuleViolations = validationResult && validationResult.violations.length > 0
155
+ ? validationResult.violations.map((violation) => ({
156
+ file: violation.file,
157
+ rule: { type: violation.rule.type, path: violation.rule.path },
158
+ path: violation.path,
159
+ oldValue: violation.oldValue,
160
+ updatedValue: violation.updatedValue,
161
+ message: violation.message
162
+ }))
163
+ : undefined;
164
+ const htmlContent = (0, htmlTemplate_1.generateHtmlTemplate)(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds, addedSections, addedFileIds, diffStats, stopRuleViolations);
155
165
  await writeHtmlFile(htmlContent, reportPath);
156
166
  logger?.log(`✓ HTML report generated: ${reportPath}, opening in browser...`);
157
167
  try {
package/dist/index.js CHANGED
@@ -326,7 +326,7 @@ const main = async () => {
326
326
  }
327
327
  const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, syncConfig, command.dryRun, command.skipFormat, logger);
328
328
  if (command.diffHtml && !command.quiet)
329
- await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, syncConfig, command.dryRun, logger);
329
+ await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, syncConfig, command.dryRun, logger, command.dryRun ? validationResult : undefined);
330
330
  if (command.diffJson)
331
331
  (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, syncConfig, command.dryRun, package_json_1.default.version);
332
332
  };
@@ -337,6 +337,8 @@ const main = async () => {
337
337
  catch (error) {
338
338
  if ((0, configMerger_1.isConfigMergerError)(error))
339
339
  console.error(error.message);
340
+ else if ((0, configLoader_1.isConfigLoaderError)(error))
341
+ console.error(error.message);
340
342
  else if ((0, ZodError_1.isZodValidationError)(error))
341
343
  console.error(error.message);
342
344
  else if ((0, fileLoader_1.isFileLoaderError)(error))
@@ -1,3 +1,3 @@
1
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 /* 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 /* Stats toggle button */\n .stats-toggle-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 .stats-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\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
+ 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 /* 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 /* Stats toggle button */\n .stats-toggle-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 .stats-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\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\n /* Stop rules violations badge */\n .stat.violations { background: #f8d7da; color: #721c24; }\n\n /* Violations section */\n .violations-section {\n margin: 15px 0 0;\n padding: 12px 0 0;\n border-top: 1px solid #e1e4e8;\n }\n\n .violations-toggle-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 .violations-toggle-btn:hover {\n background: #f6f8fa;\n color: #24292e;\n }\n\n .violations-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n margin-top: 10px;\n }\n\n .violations-table th {\n background: #f6f8fa;\n text-align: left;\n padding: 8px 12px;\n border-bottom: 2px solid #d0d7de;\n font-weight: 600;\n color: #24292e;\n }\n\n .violations-table td {\n padding: 8px 12px;\n border-bottom: 1px solid #e1e4e8;\n color: #24292e;\n vertical-align: top;\n }\n\n .violations-table tr:hover td {\n background: #f6f8fa;\n }\n\n .violation-rule-badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 10px;\n font-size: 11px;\n font-weight: 600;\n background: #fff3cd;\n color: #856404;\n white-space: nowrap;\n }\n\n .violation-value {\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n background: #f6f8fa;\n padding: 2px 6px;\n border-radius: 3px;\n border: 1px solid #e1e4e8;\n }\n";
3
3
  export declare const TAB_SCRIPT: string;
@@ -617,6 +617,79 @@ exports.HTML_STYLES = `
617
617
  white-space: nowrap;
618
618
  flex-shrink: 0;
619
619
  }
620
+
621
+ /* Stop rules violations badge */
622
+ .stat.violations { background: #f8d7da; color: #721c24; }
623
+
624
+ /* Violations section */
625
+ .violations-section {
626
+ margin: 15px 0 0;
627
+ padding: 12px 0 0;
628
+ border-top: 1px solid #e1e4e8;
629
+ }
630
+
631
+ .violations-toggle-btn {
632
+ padding: 4px 12px;
633
+ border: 1px solid #d0d7de;
634
+ border-radius: 4px;
635
+ background: none;
636
+ cursor: pointer;
637
+ font-size: 12px;
638
+ color: #586069;
639
+ transition: all 0.2s;
640
+ }
641
+
642
+ .violations-toggle-btn:hover {
643
+ background: #f6f8fa;
644
+ color: #24292e;
645
+ }
646
+
647
+ .violations-table {
648
+ width: 100%;
649
+ border-collapse: collapse;
650
+ font-size: 13px;
651
+ margin-top: 10px;
652
+ }
653
+
654
+ .violations-table th {
655
+ background: #f6f8fa;
656
+ text-align: left;
657
+ padding: 8px 12px;
658
+ border-bottom: 2px solid #d0d7de;
659
+ font-weight: 600;
660
+ color: #24292e;
661
+ }
662
+
663
+ .violations-table td {
664
+ padding: 8px 12px;
665
+ border-bottom: 1px solid #e1e4e8;
666
+ color: #24292e;
667
+ vertical-align: top;
668
+ }
669
+
670
+ .violations-table tr:hover td {
671
+ background: #f6f8fa;
672
+ }
673
+
674
+ .violation-rule-badge {
675
+ display: inline-block;
676
+ padding: 2px 8px;
677
+ border-radius: 10px;
678
+ font-size: 11px;
679
+ font-weight: 600;
680
+ background: #fff3cd;
681
+ color: #856404;
682
+ white-space: nowrap;
683
+ }
684
+
685
+ .violation-value {
686
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
687
+ font-size: 12px;
688
+ background: #f6f8fa;
689
+ padding: 2px 6px;
690
+ border-radius: 3px;
691
+ border: 1px solid #e1e4e8;
692
+ }
620
693
  `;
621
694
  exports.TAB_SCRIPT = String.raw `
622
695
  // Tab switching
@@ -915,6 +988,18 @@ exports.TAB_SCRIPT = String.raw `
915
988
  });
916
989
  }
917
990
 
991
+ // Violations section toggle
992
+ const violationsToggleBtn = document.getElementById('violations-toggle-btn');
993
+ const violationsContent = document.getElementById('violations-content');
994
+ if (violationsToggleBtn && violationsContent) {
995
+ violationsToggleBtn.addEventListener('click', () => {
996
+ const isHidden = violationsContent.style.display === 'none';
997
+ violationsContent.style.display = isHidden ? 'block' : 'none';
998
+ const count = violationsToggleBtn.textContent.match(/\d+/)?.[0] || '0';
999
+ violationsToggleBtn.textContent = isHidden ? 'Hide Violations (' + count + ')' : 'Show Violations (' + count + ')';
1000
+ });
1001
+ }
1002
+
918
1003
  // Synchronized horizontal scrolling for side-by-side diff panels
919
1004
  document.querySelectorAll('.d2h-files-diff').forEach(container => {
920
1005
  const panels = container.querySelectorAll('.d2h-file-side-diff');
@@ -1,4 +1,15 @@
1
1
  import { FileDiffResult } from '../fileDiff';
2
+ export interface HtmlStopRuleViolation {
3
+ file: string;
4
+ rule: {
5
+ type: string;
6
+ path?: string;
7
+ };
8
+ path: string;
9
+ oldValue: unknown;
10
+ updatedValue: unknown;
11
+ message: string;
12
+ }
2
13
  export interface ReportMetadata {
3
14
  timestamp: string;
4
15
  source: string;
@@ -14,4 +25,4 @@ export interface DiffStats {
14
25
  removed: number;
15
26
  }>;
16
27
  }
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>, diffStats?: DiffStats) => string;
28
+ export declare const generateHtmlTemplate: (diffResult: FileDiffResult, formattedFiles: string[], trulyUnchangedFiles: string[], metadata: ReportMetadata, changedSections: string[], changedFileIds?: Map<string, string>, addedSections?: string[], addedFileIds?: Map<string, string>, diffStats?: DiffStats, stopRuleViolations?: HtmlStopRuleViolation[]) => string;
@@ -4,6 +4,46 @@ 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 formatViolationValue = (value) => {
8
+ if (value === undefined || value === null)
9
+ return '<span class="violation-value">-</span>';
10
+ return `<span class="violation-value">${(0, treeRenderer_1.escapeHtml)(String(value))}</span>`;
11
+ };
12
+ const renderStopRulesSection = (violations) => {
13
+ if (violations.length === 0)
14
+ return '';
15
+ const rows = violations
16
+ .map((v) => `<tr>
17
+ <td>${(0, treeRenderer_1.escapeHtml)(v.file)}</td>
18
+ <td><span class="violation-rule-badge">${(0, treeRenderer_1.escapeHtml)(v.rule.type)}</span></td>
19
+ <td>${(0, treeRenderer_1.escapeHtml)(v.path)}</td>
20
+ <td>${formatViolationValue(v.oldValue)}</td>
21
+ <td>${formatViolationValue(v.updatedValue)}</td>
22
+ <td>${(0, treeRenderer_1.escapeHtml)(v.message)}</td>
23
+ </tr>`)
24
+ .join('');
25
+ return `
26
+ <div class="violations-section">
27
+ <button class="violations-toggle-btn" id="violations-toggle-btn">Show Violations (${violations.length})</button>
28
+ <div id="violations-content" style="display: none">
29
+ <table class="violations-table">
30
+ <thead>
31
+ <tr>
32
+ <th>File</th>
33
+ <th>Rule</th>
34
+ <th>Path</th>
35
+ <th>Old Value</th>
36
+ <th>New Value</th>
37
+ <th>Message</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ ${rows}
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ </div>`;
46
+ };
7
47
  const renderStatsDashboard = (diffStats) => {
8
48
  const total = diffStats.totalAdded + diffStats.totalRemoved;
9
49
  if (total === 0)
@@ -31,7 +71,7 @@ const renderStatsDashboard = (diffStats) => {
31
71
  </div>
32
72
  </div>`;
33
73
  };
34
- const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds = new Map(), addedSections = [], addedFileIds = new Map(), diffStats) => {
74
+ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections, changedFileIds = new Map(), addedSections = [], addedFileIds = new Map(), diffStats, stopRuleViolations) => {
35
75
  const changedFilePaths = diffResult.changedFiles.map((f) => f.path);
36
76
  const changedTree = (0, treeBuilder_1.buildFileTree)(changedFilePaths);
37
77
  const addedFilePaths = diffResult.addedFiles.map((f) => f.path);
@@ -48,9 +88,11 @@ const generateHtmlTemplate = (diffResult, formattedFiles, trulyUnchangedFiles, m
48
88
  ];
49
89
  const activeCategories = categories.filter((c) => c.count > 0);
50
90
  const firstActiveTab = activeCategories[0]?.id ?? 'changed';
51
- const summaryBadges = activeCategories
52
- .map((c) => `<span class="stat ${c.id}">${c.count} ${c.label}</span>`)
53
- .join('\n ');
91
+ const violationsBadge = stopRuleViolations && stopRuleViolations.length > 0
92
+ ? `<span class="stat violations">${stopRuleViolations.length} Violation${stopRuleViolations.length === 1 ? '' : 's'}</span>`
93
+ : '';
94
+ const summaryBadges = activeCategories.map((c) => `<span class="stat ${c.id}">${c.count} ${c.label}</span>`).join('\n ') +
95
+ (violationsBadge ? `\n ${violationsBadge}` : '');
54
96
  const tabButtons = activeCategories
55
97
  .map((c) => `<button class="tab${c.id === firstActiveTab ? ' active' : ''}" data-tab="${c.id}">${c.label} (${c.count})</button>`)
56
98
  .join('\n ');
@@ -140,6 +182,7 @@ ${htmlStyles_1.HTML_STYLES}
140
182
  ${summaryBadges}
141
183
  </div>
142
184
  ${diffStats ? renderStatsDashboard(diffStats) : ''}
185
+ ${stopRuleViolations && stopRuleViolations.length > 0 ? renderStopRulesSection(stopRuleViolations) : ''}
143
186
  </header>
144
187
 
145
188
  <nav class="tabs">
@@ -7,7 +7,8 @@ export { clearJsonPathCache, getValueAtPath, isFilterSegment, matchesFilter, par
7
7
  export { isYamlFile } from './fileType';
8
8
  export { globalMatcher, PatternMatcher } from './patternMatcher';
9
9
  export { generateUnifiedDiff } from './diffGenerator';
10
- export { checkForUpdates, isVersionCheckerError, VersionCheckerError } from './versionChecker';
10
+ export type { SemverParts } from './versionChecker';
11
+ export { checkForUpdates, isNewerVersion, isVersionCheckerError, parseVersion, VersionCheckerError } from './versionChecker';
11
12
  export { escapeRegex, isYamlFileLoaderError, loadYamlFile, YamlFileLoaderError } from './yamlFileLoader';
12
13
  export { isTransformFileLoaderError, loadTransformFile, loadTransformFiles, TransformFileLoaderError } from './transformFileLoader';
13
14
  export { isRegexPatternFileLoaderError, loadRegexPatternArray, loadRegexPatternsFromKeys, RegexPatternFileLoaderError } from './regexPatternFileLoader';
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NUMERIC_MIN_MULTIPLIER = exports.NUMERIC_MIN_FLOOR = exports.MAX_EXAMPLES_PER_SUGGESTION = exports.ISO_TIMESTAMP_PATTERN = exports.FILTER_THRESHOLDS = exports.CONSTRAINT_FIELD_NAMES = exports.CONFIDENCE_DEFAULTS = exports.ARRAY_KEY_FIELDS = exports.ANTONYM_PAIRS = exports.isYamlSeq = exports.isYamlMap = exports.isYamlCollection = exports.isScalar = exports.extractScalarValue = exports.extractKeyValue = exports.validateVersionString = exports.applyRegexRulesSequentially = exports.validateTargetedRegex = exports.validatePathlessRegex = exports.getAllValuesRecursive = exports.RegexPatternFileLoaderError = exports.loadRegexPatternsFromKeys = exports.loadRegexPatternArray = exports.isRegexPatternFileLoaderError = exports.TransformFileLoaderError = exports.loadTransformFiles = exports.loadTransformFile = exports.isTransformFileLoaderError = exports.YamlFileLoaderError = exports.loadYamlFile = exports.isYamlFileLoaderError = exports.escapeRegex = exports.VersionCheckerError = exports.isVersionCheckerError = exports.checkForUpdates = exports.generateUnifiedDiff = exports.PatternMatcher = exports.globalMatcher = exports.isYamlFile = exports.parseJsonPath = exports.parseFilterSegment = exports.matchesFilter = exports.isFilterSegment = exports.getValueAtPath = exports.clearJsonPathCache = exports.serializeForDiff = exports.normalizeForComparison = exports.deepEqual = exports.createErrorTypeGuard = exports.createErrorClass = void 0;
4
- exports.parseFilterExpression = exports.isFilterParseError = exports.FilterParseError = exports.filterFileMaps = exports.filterFileMap = exports.fileMatchesFilter = exports.isCommentOnlyContent = exports.shouldPreserveItem = exports.itemMatchesAnyFilter = exports.getApplicableArrayFilters = exports.findMatchingTargetItem = exports.setValueAtPath = exports.getFixedValuesForFile = exports.applyFixedValues = exports.UUID_PATTERN = exports.SEMVER_PATTERN = exports.SEMANTIC_PATTERNS = exports.SEMANTIC_KEYWORDS = exports.PROBLEMATIC_REGEX_CHARS = void 0;
3
+ exports.MAX_EXAMPLES_PER_SUGGESTION = exports.ISO_TIMESTAMP_PATTERN = exports.FILTER_THRESHOLDS = exports.CONSTRAINT_FIELD_NAMES = exports.CONFIDENCE_DEFAULTS = exports.ARRAY_KEY_FIELDS = exports.ANTONYM_PAIRS = exports.isYamlSeq = exports.isYamlMap = exports.isYamlCollection = exports.isScalar = exports.extractScalarValue = exports.extractKeyValue = exports.validateVersionString = exports.applyRegexRulesSequentially = exports.validateTargetedRegex = exports.validatePathlessRegex = exports.getAllValuesRecursive = exports.RegexPatternFileLoaderError = exports.loadRegexPatternsFromKeys = exports.loadRegexPatternArray = exports.isRegexPatternFileLoaderError = exports.TransformFileLoaderError = exports.loadTransformFiles = exports.loadTransformFile = exports.isTransformFileLoaderError = exports.YamlFileLoaderError = exports.loadYamlFile = exports.isYamlFileLoaderError = exports.escapeRegex = exports.VersionCheckerError = exports.parseVersion = exports.isVersionCheckerError = exports.isNewerVersion = exports.checkForUpdates = exports.generateUnifiedDiff = exports.PatternMatcher = exports.globalMatcher = exports.isYamlFile = exports.parseJsonPath = exports.parseFilterSegment = exports.matchesFilter = exports.isFilterSegment = exports.getValueAtPath = exports.clearJsonPathCache = exports.serializeForDiff = exports.normalizeForComparison = exports.deepEqual = exports.createErrorTypeGuard = exports.createErrorClass = void 0;
4
+ exports.parseFilterExpression = exports.isFilterParseError = exports.FilterParseError = exports.filterFileMaps = exports.filterFileMap = exports.fileMatchesFilter = exports.isCommentOnlyContent = exports.shouldPreserveItem = exports.itemMatchesAnyFilter = exports.getApplicableArrayFilters = exports.findMatchingTargetItem = exports.setValueAtPath = exports.getFixedValuesForFile = exports.applyFixedValues = exports.UUID_PATTERN = exports.SEMVER_PATTERN = exports.SEMANTIC_PATTERNS = exports.SEMANTIC_KEYWORDS = exports.PROBLEMATIC_REGEX_CHARS = exports.NUMERIC_MIN_MULTIPLIER = exports.NUMERIC_MIN_FLOOR = void 0;
5
5
  var errors_1 = require("./errors");
6
6
  Object.defineProperty(exports, "createErrorClass", { enumerable: true, get: function () { return errors_1.createErrorClass; } });
7
7
  Object.defineProperty(exports, "createErrorTypeGuard", { enumerable: true, get: function () { return errors_1.createErrorTypeGuard; } });
@@ -26,7 +26,9 @@ var diffGenerator_1 = require("./diffGenerator");
26
26
  Object.defineProperty(exports, "generateUnifiedDiff", { enumerable: true, get: function () { return diffGenerator_1.generateUnifiedDiff; } });
27
27
  var versionChecker_1 = require("./versionChecker");
28
28
  Object.defineProperty(exports, "checkForUpdates", { enumerable: true, get: function () { return versionChecker_1.checkForUpdates; } });
29
+ Object.defineProperty(exports, "isNewerVersion", { enumerable: true, get: function () { return versionChecker_1.isNewerVersion; } });
29
30
  Object.defineProperty(exports, "isVersionCheckerError", { enumerable: true, get: function () { return versionChecker_1.isVersionCheckerError; } });
31
+ Object.defineProperty(exports, "parseVersion", { enumerable: true, get: function () { return versionChecker_1.parseVersion; } });
30
32
  Object.defineProperty(exports, "VersionCheckerError", { enumerable: true, get: function () { return versionChecker_1.VersionCheckerError; } });
31
33
  var yamlFileLoader_1 = require("./yamlFileLoader");
32
34
  Object.defineProperty(exports, "escapeRegex", { enumerable: true, get: function () { return yamlFileLoader_1.escapeRegex; } });
@@ -16,5 +16,12 @@ declare const VersionCheckerErrorClass: {
16
16
  export declare class VersionCheckerError extends VersionCheckerErrorClass {
17
17
  }
18
18
  export declare const isVersionCheckerError: (error: unknown) => error is VersionCheckerError;
19
+ export interface SemverParts {
20
+ major: number;
21
+ minor: number;
22
+ patch: number;
23
+ }
24
+ export declare const parseVersion: (version: string) => SemverParts | undefined;
25
+ export declare const isNewerVersion: (current: string, latest: string) => boolean;
19
26
  export declare const checkForUpdates: (currentVersion: string) => Promise<void>;
20
27
  export {};
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.checkForUpdates = exports.isVersionCheckerError = exports.VersionCheckerError = void 0;
6
+ exports.checkForUpdates = exports.isNewerVersion = exports.parseVersion = exports.isVersionCheckerError = exports.VersionCheckerError = void 0;
7
7
  const node_https_1 = __importDefault(require("node:https"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const package_json_1 = __importDefault(require("../../package.json"));
@@ -44,9 +44,10 @@ const parseVersion = (version) => {
44
44
  patch: Number.parseInt(match[3], 10)
45
45
  };
46
46
  };
47
+ exports.parseVersion = parseVersion;
47
48
  const isNewerVersion = (current, latest) => {
48
- const currentParts = parseVersion(current);
49
- const latestParts = parseVersion(latest);
49
+ const currentParts = (0, exports.parseVersion)(current);
50
+ const latestParts = (0, exports.parseVersion)(latest);
50
51
  if (!currentParts || !latestParts)
51
52
  return false;
52
53
  if (latestParts.major > currentParts.major)
@@ -59,6 +60,7 @@ const isNewerVersion = (current, latest) => {
59
60
  return false;
60
61
  return latestParts.patch > currentParts.patch;
61
62
  };
63
+ exports.isNewerVersion = isNewerVersion;
62
64
  const fetchLatestVersion = (packageName, timeout) => {
63
65
  return new Promise((resolve, reject) => {
64
66
  const url = `https://registry.npmjs.org/${packageName}/latest`;
@@ -108,7 +110,7 @@ const checkForUpdates = async (currentVersion) => {
108
110
  return;
109
111
  try {
110
112
  const latestVersion = await fetchLatestVersion('helm-env-delta', 3000);
111
- if (isNewerVersion(currentVersion, latestVersion))
113
+ if ((0, exports.isNewerVersion)(currentVersion, latestVersion))
112
114
  displayUpdateNotification(currentVersion, latestVersion);
113
115
  }
114
116
  catch {
@@ -20,12 +20,16 @@ exports.YamlFormatterError = (0, errors_1.createErrorClass)('YAML Formatter Erro
20
20
  exports.isYamlFormatterError = (0, errors_1.createErrorTypeGuard)(exports.YamlFormatterError);
21
21
  const getFormattingRules = (filePath, outputFormat) => {
22
22
  const keyOrders = [];
23
+ const keySort = [];
23
24
  const arraySort = [];
24
25
  const quoteValues = [];
25
26
  const allPatterns = new Set();
26
27
  if (outputFormat?.keyOrders)
27
28
  for (const pattern of Object.keys(outputFormat.keyOrders))
28
29
  allPatterns.add(pattern);
30
+ if (outputFormat?.keySort)
31
+ for (const pattern of Object.keys(outputFormat.keySort))
32
+ allPatterns.add(pattern);
29
33
  if (outputFormat?.arraySort)
30
34
  for (const pattern of Object.keys(outputFormat.arraySort))
31
35
  allPatterns.add(pattern);
@@ -38,6 +42,9 @@ const getFormattingRules = (filePath, outputFormat) => {
38
42
  const keyOrder = outputFormat?.keyOrders?.[pattern];
39
43
  if (keyOrder)
40
44
  keyOrders.push(keyOrder);
45
+ const keySortRule = outputFormat?.keySort?.[pattern];
46
+ if (keySortRule)
47
+ keySort.push(keySortRule);
41
48
  const arrayRule = outputFormat?.arraySort?.[pattern];
42
49
  if (arrayRule)
43
50
  arraySort.push(arrayRule);
@@ -45,7 +52,7 @@ const getFormattingRules = (filePath, outputFormat) => {
45
52
  if (quoteValue)
46
53
  quoteValues.push(quoteValue);
47
54
  }
48
- return { keyOrders, arraySort, quoteValues };
55
+ return { keyOrders, keySort, arraySort, quoteValues };
49
56
  };
50
57
  const preserveMultilineStrings = (yamlDocument) => {
51
58
  if (!yamlDocument.contents)
@@ -81,6 +88,8 @@ const formatYaml = (content, filePath, outputFormat) => {
81
88
  const rules = getFormattingRules(filePath, outputFormat);
82
89
  if (rules.keyOrders.length > 0)
83
90
  applyKeyOrdering(yamlDocument, rules.keyOrders);
91
+ if (rules.keySort.length > 0)
92
+ applyKeySort(yamlDocument, rules.keySort);
84
93
  if (rules.arraySort.length > 0)
85
94
  applyArraySorting(yamlDocument, rules.arraySort);
86
95
  if (rules.quoteValues.length > 0)
@@ -171,6 +180,43 @@ const applyOrderingToMap = (map, currentPath, orderHierarchy) => {
171
180
  }
172
181
  }
173
182
  };
183
+ const applyKeySort = (yamlDocument, sortRules) => {
184
+ if (sortRules.length === 0)
185
+ return;
186
+ const allRules = sortRules.flat();
187
+ for (const rule of allRules) {
188
+ const pathParts = (0, jsonPath_1.parseJsonPath)(rule.path);
189
+ if (pathParts.length === 0)
190
+ continue;
191
+ if (yamlDocument.contents)
192
+ traverseAndSortKeys(yamlDocument.contents, [], pathParts);
193
+ }
194
+ };
195
+ const traverseAndSortKeys = (node, currentPath, targetPath) => {
196
+ if (!node || typeof node !== 'object')
197
+ return;
198
+ if (matchPath(currentPath, targetPath)) {
199
+ if ((0, yamlTypeGuards_1.isYamlMap)(node))
200
+ sortMapKeysAlphabetically(node);
201
+ return;
202
+ }
203
+ if ((0, yamlTypeGuards_1.isYamlMap)(node))
204
+ for (const item of node.items) {
205
+ const keyValue = (0, yamlTypeGuards_1.extractKeyValue)(item);
206
+ if (keyValue && item.value) {
207
+ const childPath = [...currentPath, keyValue];
208
+ if (isPotentialMatch(childPath, targetPath))
209
+ traverseAndSortKeys(item.value, childPath, targetPath);
210
+ }
211
+ }
212
+ };
213
+ const sortMapKeysAlphabetically = (map) => {
214
+ map.items.sort((a, b) => {
215
+ const aKey = a.key && typeof a.key === 'object' && 'value' in a.key ? String(a.key.value) : '';
216
+ const bKey = b.key && typeof b.key === 'object' && 'value' in b.key ? String(b.key.value) : '';
217
+ return aKey.localeCompare(bKey);
218
+ });
219
+ };
174
220
  const applyValueQuoting = (yamlDocument, quoteLists) => {
175
221
  if (quoteLists.length === 0)
176
222
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.11.1",
3
+ "version": "1.13.0",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -68,15 +68,15 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/hogan.js": "^3.0.5",
71
- "@types/node": "^25.2.1",
71
+ "@types/node": "^25.2.3",
72
72
  "@types/picomatch": "^4.0.2",
73
- "@typescript-eslint/eslint-plugin": "^8.54.0",
74
- "@typescript-eslint/parser": "^8.54.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.55.0",
74
+ "@typescript-eslint/parser": "^8.55.0",
75
75
  "@vitest/coverage-v8": "^4.0.18",
76
76
  "eslint": "^9.39.2",
77
77
  "eslint-config-prettier": "^10.1.8",
78
78
  "eslint-plugin-simple-import-sort": "^12.1.1",
79
- "eslint-plugin-unicorn": "^62.0.0",
79
+ "eslint-plugin-unicorn": "^63.0.0",
80
80
  "prettier": "^3.8.1",
81
81
  "tsx": "^4.21.0",
82
82
  "typescript": "^5.9.3",