helm-env-delta 1.7.0 → 1.7.2

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
@@ -374,7 +374,26 @@ skipPath:
374
374
  - 'resources.limits'
375
375
  ```
376
376
 
377
- **Use cases:** Namespaces, replicas, resource limits, secrets, URLs.
377
+ #### Filter Expressions (Skip by Name)
378
+
379
+ Skip specific array items by property value using filter syntax:
380
+
381
+ ```yaml
382
+ skipPath:
383
+ '**/*.yaml':
384
+ - 'env[name=SECRET_KEY]' # Skip item where name=SECRET_KEY
385
+ - 'env[name=INTERNAL_TOKEN].value' # Skip nested field in matching item
386
+ - 'containers[name=sidecar]' # Skip entire sidecar container
387
+ - 'spec.containers[name=app].env[name=DEBUG]' # Nested filters
388
+ ```
389
+
390
+ **Syntax:**
391
+
392
+ - `array[prop=value]` - Match items where property equals value
393
+ - `array[prop="value with spaces"]` - Quoted values for special characters
394
+ - Combine with wildcards: `containers[name=app].env[*].value`
395
+
396
+ **Use cases:** Namespaces, replicas, resource limits, secrets, URLs, environment-specific array items.
378
397
 
379
398
  ---
380
399
 
@@ -623,7 +642,8 @@ hed --config <file> [options] # Short alias
623
642
  | `--diff-json` | Output JSON to stdout (pipe to jq) |
624
643
  | `--list-files` | List source/destination files without processing |
625
644
  | `--show-config` | Display resolved config after inheritance |
626
- | `--skip-format` | Skip YAML formatting |
645
+ | `--format-only` | Format destination files without syncing |
646
+ | `--skip-format` | Skip YAML formatting during sync |
627
647
  | `--no-color` | Disable colored output (CI/accessibility) |
628
648
  | `--verbose` | Show detailed debug info |
629
649
  | `--quiet` | Suppress output except errors |
@@ -660,6 +680,12 @@ hed --config config.yaml
660
680
 
661
681
  # Force override stop rules
662
682
  hed --config config.yaml --force
683
+
684
+ # Format destination files only (no sync)
685
+ hed --config config.yaml --format-only
686
+
687
+ # Preview format changes
688
+ hed --config config.yaml --format-only --dry-run
663
689
  ```
664
690
 
665
691
  ---
@@ -730,7 +756,7 @@ git push origin main
730
756
 
731
757
  ✅ **Flexibility** - Per-file patterns. Config inheritance. Regex transforms.
732
758
 
733
- ✅ **Reliability** - 847 tests, 84% coverage. Battle-tested.
759
+ ✅ **Reliability** - 920 tests, 84% coverage. Battle-tested.
734
760
 
735
761
  ---
736
762
 
@@ -840,6 +866,7 @@ helm-env-delta --config config.yaml --validate
840
866
  2. **skipPath patterns** - Two-level validation:
841
867
  - Glob pattern must match at least one file
842
868
  - JSONPath must exist in at least one matched file
869
+ - Filter expressions `[prop=value]` validated against actual array items
843
870
  3. **stopRules patterns** - Two-level validation:
844
871
  - Glob pattern must match at least one file
845
872
  - JSONPath (if specified) must exist in at least one matched file
@@ -930,6 +957,8 @@ spec:
930
957
  # ✅ Correct
931
958
  - 'spec.replicas' # No prefix
932
959
  - 'env[*].name' # Array wildcard
960
+ - 'env[name=DEBUG]' # Filter by property value
961
+ - 'containers[name=app].env[name=SECRET]' # Nested filters
933
962
  ```
934
963
 
935
964
  ---
@@ -6,6 +6,7 @@ export type SyncCommand = {
6
6
  diffHtml: boolean;
7
7
  diffJson: boolean;
8
8
  skipFormat: boolean;
9
+ formatOnly: boolean;
9
10
  validate: boolean;
10
11
  verbose: boolean;
11
12
  quiet: boolean;
@@ -19,6 +19,7 @@ const parseCommandLine = (argv) => {
19
19
  .option('--diff-html', 'Generate and open HTML diff report in browser', false)
20
20
  .option('--diff-json', 'Output diff as JSON to stdout', false)
21
21
  .option('--skip-format', 'Skip YAML formatting (outputFormat section)', false)
22
+ .option('--format-only', 'Format YAML files in destination without syncing', false)
22
23
  .option('--validate', 'Validate configuration file and exit', false)
23
24
  .option('--list-files', 'List files that would be synced without processing diffs', false)
24
25
  .option('--show-config', 'Display resolved configuration after inheritance and exit', false)
@@ -53,6 +54,10 @@ Documentation: https://github.com/balazscsaba2006/helm-env-delta
53
54
  console.error('Error: --verbose and --quiet flags are mutually exclusive');
54
55
  process.exit(1);
55
56
  }
57
+ if (options['formatOnly'] && options['skipFormat']) {
58
+ console.error('Error: --format-only and --skip-format flags are mutually exclusive');
59
+ process.exit(1);
60
+ }
56
61
  const threshold = Number.parseFloat(options['suggestThreshold']);
57
62
  if (Number.isNaN(threshold) || threshold < 0 || threshold > 1) {
58
63
  console.error('Error: --suggest-threshold must be a number between 0 and 1');
@@ -66,6 +71,7 @@ Documentation: https://github.com/balazscsaba2006/helm-env-delta
66
71
  diffHtml: options['diffHtml'],
67
72
  diffJson: options['diffJson'],
68
73
  skipFormat: options['skipFormat'],
74
+ formatOnly: options['formatOnly'],
69
75
  validate: options['validate'],
70
76
  listFiles: options['listFiles'],
71
77
  showConfig: options['showConfig'],
@@ -8,6 +8,7 @@ export interface FileDiffResult {
8
8
  }
9
9
  export interface ChangedFile {
10
10
  path: string;
11
+ originalPath?: string;
11
12
  sourceContent: string;
12
13
  destinationContent: string;
13
14
  processedSourceContent: unknown;
@@ -46,5 +47,5 @@ export declare class FileDiffError extends FileDiffErrorClass {
46
47
  }
47
48
  export declare const isFileDiffError: (error: unknown) => error is FileDiffError;
48
49
  export declare const getSkipPathsForFile: (filePath: string, skipPath?: Record<string, string[]>) => string[];
49
- export declare const computeFileDiff: (sourceFiles: FileMap, destinationFiles: FileMap, config: Config, logger?: import("./logger").Logger) => FileDiffResult;
50
+ export declare const computeFileDiff: (sourceFiles: FileMap, destinationFiles: FileMap, config: Config, logger?: import("./logger").Logger, originalPaths?: Map<string, string>) => FileDiffResult;
50
51
  export {};
package/dist/fileDiff.js CHANGED
@@ -41,6 +41,30 @@ const deleteJsonPathRecursive = (object, parts, index) => {
41
41
  const currentPart = parts[index];
42
42
  if (!currentPart)
43
43
  return;
44
+ if ((0, jsonPath_1.isFilterSegment)(currentPart)) {
45
+ if (!Array.isArray(object))
46
+ return;
47
+ const filter = (0, jsonPath_1.parseFilterSegment)(currentPart);
48
+ if (!filter)
49
+ return;
50
+ if (index === parts.length - 1)
51
+ for (let index_ = object.length - 1; index_ >= 0; index_--) {
52
+ const item = object[index_];
53
+ if (item && typeof item === 'object') {
54
+ const itemValue = item[filter.property];
55
+ if (String(itemValue) === filter.value)
56
+ object.splice(index_, 1);
57
+ }
58
+ }
59
+ else
60
+ for (const item of object)
61
+ if (item && typeof item === 'object') {
62
+ const itemValue = item[filter.property];
63
+ if (String(itemValue) === filter.value)
64
+ deleteJsonPathRecursive(item, parts, index + 1);
65
+ }
66
+ return;
67
+ }
44
68
  if (index === parts.length - 1) {
45
69
  if (currentPart === '*' && Array.isArray(object))
46
70
  object.length = 0;
@@ -142,13 +166,14 @@ const processYamlFile = (options) => {
142
166
  parsedDest: destinationParsed
143
167
  };
144
168
  };
145
- const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms) => {
169
+ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms, originalPaths) => {
146
170
  const changedFiles = [];
147
171
  const unchangedFiles = [];
148
172
  for (const [path, sourceContent] of sourceFiles.entries()) {
149
173
  if (!destinationFiles.has(path))
150
174
  continue;
151
175
  const destinationContent = destinationFiles.get(path);
176
+ const originalPath = originalPaths?.get(path);
152
177
  const isYaml = (0, fileType_1.isYamlFile)(path);
153
178
  if (isYaml) {
154
179
  const changed = processYamlFile({
@@ -158,8 +183,11 @@ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms
158
183
  skipPath,
159
184
  transforms
160
185
  });
161
- if (changed)
186
+ if (changed) {
187
+ if (originalPath)
188
+ changed.originalPath = originalPath;
162
189
  changedFiles.push(changed);
190
+ }
163
191
  else
164
192
  unchangedFiles.push(path);
165
193
  }
@@ -168,6 +196,7 @@ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms
168
196
  else
169
197
  changedFiles.push({
170
198
  path,
199
+ originalPath,
171
200
  sourceContent,
172
201
  destinationContent: destinationContent,
173
202
  processedSourceContent: sourceContent,
@@ -179,7 +208,7 @@ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms
179
208
  }
180
209
  return { changedFiles, unchangedFiles };
181
210
  };
182
- const computeFileDiff = (sourceFiles, destinationFiles, config, logger) => {
211
+ const computeFileDiff = (sourceFiles, destinationFiles, config, logger, originalPaths) => {
183
212
  if (logger?.shouldShow('debug')) {
184
213
  logger.debug('Computing file differences:');
185
214
  logger.debug(` Source files: ${sourceFiles.size}`);
@@ -193,7 +222,7 @@ const computeFileDiff = (sourceFiles, destinationFiles, config, logger) => {
193
222
  }
194
223
  const addedFiles = detectAddedFiles(sourceFiles, destinationFiles);
195
224
  const deletedFiles = config.prune ? detectDeletedFiles(sourceFiles, destinationFiles) : [];
196
- const { changedFiles, unchangedFiles } = processChangedFiles(sourceFiles, destinationFiles, config.skipPath, config.transforms);
225
+ const { changedFiles, unchangedFiles } = processChangedFiles(sourceFiles, destinationFiles, config.skipPath, config.transforms, originalPaths);
197
226
  return { addedFiles, deletedFiles, changedFiles, unchangedFiles };
198
227
  };
199
228
  exports.computeFileDiff = computeFileDiff;
@@ -4,8 +4,13 @@ export interface FileLoaderOptions {
4
4
  include: string[];
5
5
  exclude: string[];
6
6
  transforms?: TransformConfig;
7
+ skipExclude?: boolean;
7
8
  }
8
9
  export type FileMap = Map<string, string>;
10
+ export interface FileLoaderResult {
11
+ fileMap: FileMap;
12
+ originalPaths: Map<string, string>;
13
+ }
9
14
  declare const FileLoaderErrorClass: {
10
15
  new (message: string, options?: import("./utils/errors").ErrorOptions): {
11
16
  [key: string]: unknown;
@@ -24,5 +29,5 @@ declare const FileLoaderErrorClass: {
24
29
  export declare class FileLoaderError extends FileLoaderErrorClass {
25
30
  }
26
31
  export declare const isFileLoaderError: (error: unknown) => error is FileLoaderError;
27
- export declare const loadFiles: (options: FileLoaderOptions, logger?: import("./logger").Logger) => Promise<FileMap>;
32
+ export declare const loadFiles: (options: FileLoaderOptions, logger?: import("./logger").Logger) => Promise<FileLoaderResult>;
28
33
  export {};
@@ -67,10 +67,12 @@ const validateAndResolveBaseDirectory = async (baseDirectory) => {
67
67
  throw accessError;
68
68
  }
69
69
  };
70
- const findMatchingFiles = async (baseDirectory, includePatterns, excludePatterns, transforms) => {
70
+ const findMatchingFiles = async (baseDirectory, includePatterns, excludePatterns, transforms, skipExclude) => {
71
71
  try {
72
72
  if (!transforms) {
73
- const allPatterns = [...includePatterns, ...excludePatterns.map((pattern) => `!${pattern}`)];
73
+ const allPatterns = skipExclude
74
+ ? [...includePatterns]
75
+ : [...includePatterns, ...excludePatterns.map((pattern) => `!${pattern}`)];
74
76
  const matchedFiles = await (0, tinyglobby_1.glob)(allPatterns, {
75
77
  cwd: baseDirectory,
76
78
  absolute: true,
@@ -94,9 +96,11 @@ const findMatchingFiles = async (baseDirectory, includePatterns, excludePatterns
94
96
  const included = includePatterns.some((pattern) => patternMatcher_1.globalMatcher.match(transformedPath, pattern));
95
97
  if (!included)
96
98
  continue;
97
- const excluded = excludePatterns.some((pattern) => patternMatcher_1.globalMatcher.match(transformedPath, pattern));
98
- if (excluded)
99
- continue;
99
+ if (!skipExclude) {
100
+ const excluded = excludePatterns.some((pattern) => patternMatcher_1.globalMatcher.match(transformedPath, pattern));
101
+ if (excluded)
102
+ continue;
103
+ }
100
104
  filtered.push(absolutePath);
101
105
  }
102
106
  return filtered;
@@ -153,7 +157,7 @@ const loadFiles = async (options, logger) => {
153
157
  const absoluteBaseDirectory = await validateAndResolveBaseDirectory(options.baseDirectory);
154
158
  const includePatterns = options.include ?? ['**/*'];
155
159
  const excludePatterns = options.exclude ?? [];
156
- const files = await findMatchingFiles(absoluteBaseDirectory, includePatterns, excludePatterns, options.transforms);
160
+ const files = await findMatchingFiles(absoluteBaseDirectory, includePatterns, excludePatterns, options.transforms, options.skipExclude);
157
161
  if (logger?.shouldShow('debug')) {
158
162
  logger.debug('Glob matching:');
159
163
  logger.debug(` Directory: ${absoluteBaseDirectory}`);
@@ -162,20 +166,20 @@ const loadFiles = async (options, logger) => {
162
166
  logger.debug(` Matched: ${files.length} file(s)`);
163
167
  }
164
168
  const fileMap = await readFilesIntoMap(absoluteBaseDirectory, files);
165
- const transformedMap = options.transforms ? (0, filenameTransformer_1.transformFilenameMap)(fileMap, options.transforms) : fileMap;
169
+ const transformResult = (0, filenameTransformer_1.transformFilenameMap)(fileMap, options.transforms);
166
170
  if (options.transforms && logger?.shouldShow('debug')) {
167
- logger.debug(`Filename transforms applied: ${fileMap.size} → ${transformedMap.size} files`);
171
+ logger.debug(`Filename transforms applied: ${fileMap.size} → ${transformResult.fileMap.size} files`);
168
172
  let exampleCount = 0;
169
- for (const [transformed, content] of transformedMap.entries()) {
170
- const original = [...fileMap.entries()].find(([_key, c]) => c === content)?.[0];
171
- if (original && original !== transformed) {
172
- logger.debug(` ${original} ${transformed}`);
173
- exampleCount++;
174
- if (exampleCount >= 3)
175
- break;
176
- }
173
+ for (const [transformedPath, originalPath] of transformResult.originalPaths.entries()) {
174
+ logger.debug(` ${originalPath} ${transformedPath}`);
175
+ exampleCount++;
176
+ if (exampleCount >= 3)
177
+ break;
177
178
  }
178
179
  }
179
- return sortMapByKeys(transformedMap);
180
+ return {
181
+ fileMap: sortMapByKeys(transformResult.fileMap),
182
+ originalPaths: transformResult.originalPaths
183
+ };
180
184
  };
181
185
  exports.loadFiles = loadFiles;
@@ -72,8 +72,14 @@ const generateArrayDiffHtml = (change) => {
72
72
  html += '</div>';
73
73
  return html;
74
74
  };
75
+ const generateFileSummary = (file) => {
76
+ if (!file.originalPath)
77
+ return file.path;
78
+ return `<span class="filename-transform">${file.originalPath} → ${file.path}</span>`;
79
+ };
75
80
  const generateChangedFileSection = (file) => {
76
81
  const isYaml = (0, fileType_1.isYamlFile)(file.path);
82
+ const summary = generateFileSummary(file);
77
83
  if (!isYaml) {
78
84
  const destinationContent = (0, serialization_1.serializeForDiff)(file.processedDestContent, false);
79
85
  const sourceContent = (0, serialization_1.serializeForDiff)(file.processedSourceContent, false);
@@ -81,7 +87,7 @@ const generateChangedFileSection = (file) => {
81
87
  const diffHtml = generateDiffHtml(unifiedDiff);
82
88
  return `
83
89
  <details class="file-section" open>
84
- <summary>${file.path}</summary>
90
+ <summary>${summary}</summary>
85
91
  <div class="diff-container">
86
92
  ${diffHtml}
87
93
  </div>
@@ -96,7 +102,7 @@ const generateChangedFileSection = (file) => {
96
102
  const diffHtml = generateDiffHtml(unifiedDiff);
97
103
  return `
98
104
  <details class="file-section" open>
99
- <summary>${file.path}</summary>
105
+ <summary>${summary}</summary>
100
106
  <div class="diff-container">
101
107
  ${diffHtml}
102
108
  </div>
@@ -109,18 +115,18 @@ const generateChangedFileSection = (file) => {
109
115
  const diffHtml = generateDiffHtml(unifiedDiff);
110
116
  let arrayDiffsHtml = '';
111
117
  if (arrayInfo.hasChanges) {
112
- arrayDiffsHtml = '<div class="array-details"><h3>Array-specific details:</h3>';
118
+ arrayDiffsHtml = '<details class="array-details"><summary>Array-specific details:</summary>';
113
119
  for (const change of arrayInfo.changes) {
114
120
  const pathString = change.path.join('.');
115
121
  arrayDiffsHtml += `<div class="array-section"><h4>${pathString}:</h4>`;
116
122
  arrayDiffsHtml += generateArrayDiffHtml(change);
117
123
  arrayDiffsHtml += '</div>';
118
124
  }
119
- arrayDiffsHtml += '</div>';
125
+ arrayDiffsHtml += '</details>';
120
126
  }
121
127
  return `
122
128
  <details class="file-section" open>
123
- <summary>${file.path}</summary>
129
+ <summary>${summary}</summary>
124
130
  <div class="diff-container">
125
131
  ${diffHtml}
126
132
  ${arrayDiffsHtml}
package/dist/index.js CHANGED
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const node_fs_1 = require("node:fs");
40
+ const promises_1 = require("node:fs/promises");
40
41
  const node_os_1 = require("node:os");
41
42
  const node_path_1 = __importDefault(require("node:path"));
42
43
  const chalk_1 = __importDefault(require("chalk"));
@@ -60,7 +61,9 @@ const stopRulesValidator_1 = require("./stopRulesValidator");
60
61
  const suggestionEngine_1 = require("./suggestionEngine");
61
62
  const collisionDetector_1 = require("./utils/collisionDetector");
62
63
  const filenameTransformer_1 = require("./utils/filenameTransformer");
64
+ const fileType_1 = require("./utils/fileType");
63
65
  const versionChecker_1 = require("./utils/versionChecker");
66
+ const yamlFormatter_1 = require("./yamlFormatter");
64
67
  const ZodError_1 = require("./ZodError");
65
68
  const main = async () => {
66
69
  const command = (0, commandLine_1.parseCommandLine)();
@@ -98,17 +101,21 @@ const main = async () => {
98
101
  console.warn(chalk_1.default.yellow(` • ${warning}`));
99
102
  }
100
103
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files for validation...', 'loading'));
101
- const sourceFiles = await (0, fileLoader_1.loadFiles)({
104
+ const sourceResult = await (0, fileLoader_1.loadFiles)({
102
105
  baseDirectory: config.source,
103
106
  include: config.include,
104
107
  exclude: config.exclude,
105
- transforms: config.transforms
108
+ transforms: config.transforms,
109
+ skipExclude: true
106
110
  }, logger);
107
- const destinationFiles = await (0, fileLoader_1.loadFiles)({
111
+ const sourceFiles = sourceResult.fileMap;
112
+ const destinationResult = await (0, fileLoader_1.loadFiles)({
108
113
  baseDirectory: config.destination,
109
114
  include: config.include,
110
- exclude: config.exclude
115
+ exclude: config.exclude,
116
+ skipExclude: true
111
117
  }, logger);
118
+ const destinationFiles = destinationResult.fileMap;
112
119
  logger.progress(`Loaded ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'success');
113
120
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating pattern usage...', 'info'));
114
121
  const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(config, sourceFiles, destinationFiles);
@@ -136,23 +143,26 @@ const main = async () => {
136
143
  logger.debug(` Prune enabled: ${config.prune}`);
137
144
  }
138
145
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
139
- const sourceFiles = await (0, fileLoader_1.loadFiles)({
146
+ const sourceResult = await (0, fileLoader_1.loadFiles)({
140
147
  baseDirectory: config.source,
141
148
  include: config.include,
142
149
  exclude: config.exclude,
143
150
  transforms: config.transforms
144
151
  }, logger);
152
+ const sourceFiles = sourceResult.fileMap;
153
+ const originalPaths = sourceResult.originalPaths;
145
154
  logger.progress(`Loaded ${sourceFiles.size} source file(s)`, 'success');
146
155
  const collisions = (0, collisionDetector_1.detectCollisions)(sourceFiles, config.transforms);
147
156
  if (collisions.length > 0)
148
157
  (0, collisionDetector_1.validateNoCollisions)(collisions);
149
158
  if (logger.shouldShow('debug'))
150
159
  logger.debug('Filename collision check: passed');
151
- const destinationFiles = await (0, fileLoader_1.loadFiles)({
160
+ const destinationResult = await (0, fileLoader_1.loadFiles)({
152
161
  baseDirectory: config.destination,
153
162
  include: config.include,
154
163
  exclude: config.exclude
155
164
  }, logger);
165
+ const destinationFiles = destinationResult.fileMap;
156
166
  logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
157
167
  if (command.listFiles) {
158
168
  const sourceFilesList = [...sourceFiles.keys()].toSorted();
@@ -166,8 +176,48 @@ const main = async () => {
166
176
  console.log(` ${chalk_1.default.dim(file)}`);
167
177
  return;
168
178
  }
179
+ if (command.formatOnly) {
180
+ if (!config.outputFormat) {
181
+ logger.log(chalk_1.default.yellow('\n⚠️ No outputFormat configured. Nothing to format.'));
182
+ return;
183
+ }
184
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Formatting files...', 'info'));
185
+ let formattedCount = 0;
186
+ const errors = [];
187
+ for (const [relativePath, content] of destinationFiles) {
188
+ if (!(0, fileType_1.isYamlFile)(relativePath))
189
+ continue;
190
+ try {
191
+ const formatted = (0, yamlFormatter_1.formatYaml)(content, relativePath, config.outputFormat);
192
+ if (formatted !== content) {
193
+ const absolutePath = node_path_1.default.join(config.destination, relativePath);
194
+ if (command.dryRun)
195
+ logger.fileOp('format', relativePath, true);
196
+ else {
197
+ await (0, promises_1.writeFile)(absolutePath, formatted, 'utf8');
198
+ logger.fileOp('format', relativePath, false);
199
+ }
200
+ formattedCount++;
201
+ }
202
+ }
203
+ catch (error) {
204
+ errors.push({ path: relativePath, error: error });
205
+ }
206
+ }
207
+ if (command.dryRun)
208
+ logger.log(`\n[DRY RUN] Would format ${formattedCount} file(s)`);
209
+ else
210
+ logger.log(`\n✓ Formatted ${formattedCount} file(s)`);
211
+ if (errors.length > 0) {
212
+ logger.error(`\n❌ Encountered ${errors.length} error(s):`, 'critical');
213
+ for (const { path: errorPath, error } of errors)
214
+ logger.error(` ${errorPath}: ${error.message}`, 'critical');
215
+ process.exit(1);
216
+ }
217
+ return;
218
+ }
169
219
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
170
- const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger);
220
+ const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger, originalPaths);
171
221
  if (logger.shouldShow('debug'))
172
222
  logger.debug('Diff pipeline: parse → transforms → skipPath → normalize → compare');
173
223
  if (command.diff && !command.quiet)
@@ -99,6 +99,44 @@ const validateStopRulePatterns = (config, sourceFiles, destinationFiles) => {
99
99
  return warnings;
100
100
  };
101
101
  const hasPathField = (rule) => 'path' in rule && typeof rule.path === 'string';
102
+ const pathCouldMatch = (object, pathParts) => {
103
+ let current = object;
104
+ for (const part of pathParts) {
105
+ if (!current || typeof current !== 'object')
106
+ return false;
107
+ if ((0, jsonPath_1.isFilterSegment)(part)) {
108
+ if (!Array.isArray(current))
109
+ return false;
110
+ const filter = (0, jsonPath_1.parseFilterSegment)(part);
111
+ if (!filter)
112
+ return false;
113
+ const matched = current.find((item) => {
114
+ if (!item || typeof item !== 'object')
115
+ return false;
116
+ const itemValue = item[filter.property];
117
+ return String(itemValue) === filter.value;
118
+ });
119
+ if (!matched)
120
+ return false;
121
+ current = matched;
122
+ continue;
123
+ }
124
+ if (Array.isArray(current)) {
125
+ if (part === '*') {
126
+ if (pathParts.indexOf(part) === pathParts.length - 1)
127
+ return true;
128
+ return current.some((item) => item !== undefined);
129
+ }
130
+ const index = Number(part);
131
+ if (Number.isNaN(index))
132
+ return false;
133
+ current = current[index];
134
+ }
135
+ else
136
+ current = current[part];
137
+ }
138
+ return current !== undefined;
139
+ };
102
140
  const validateJsonPathInFiles = (jsonPath, filePaths, sourceFiles, destinationFiles) => {
103
141
  const pathParts = (0, jsonPath_1.parseJsonPath)(jsonPath);
104
142
  for (const filePath of filePaths) {
@@ -107,8 +145,7 @@ const validateJsonPathInFiles = (jsonPath, filePaths, sourceFiles, destinationFi
107
145
  continue;
108
146
  try {
109
147
  const parsed = yaml_1.default.parse(content);
110
- const value = (0, jsonPath_1.getValueAtPath)(parsed, pathParts);
111
- if (value !== undefined)
148
+ if (pathCouldMatch(parsed, pathParts))
112
149
  return true;
113
150
  }
114
151
  catch {
@@ -1,2 +1,2 @@
1
- export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .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 .array-details {\n margin: 20px 0;\n padding: 15px;\n background: #f6f8fa;\n border-radius: 6px;\n border-left: 3px solid #0969da;\n }\n\n .array-details h3 {\n margin: 0 0 15px 0;\n color: #0969da;\n font-size: 16px;\n }\n\n .array-section {\n margin: 15px 0;\n padding: 10px;\n background: white;\n border-radius: 4px;\n border: 1px solid #d0d7de;\n }\n\n .array-section h4 {\n margin: 0 0 10px 0;\n color: #24292e;\n font-size: 14px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n }\n\n .array-unchanged {\n padding: 8px;\n color: #586069;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .added-items, .removed-items {\n margin: 10px 0;\n }\n\n .added-items h4 {\n color: #1a7f37;\n margin: 0 0 8px 0;\n font-size: 13px;\n }\n\n .removed-items h4 {\n color: #cf222e;\n margin: 0 0 8px 0;\n font-size: 13px;\n }\n\n .added-items ul, .removed-items ul {\n list-style: none;\n padding: 0;\n margin: 0;\n }\n\n .added-items li {\n padding: 6px 10px;\n background: #dafbe1;\n border-left: 3px solid #1a7f37;\n margin: 4px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .removed-items li {\n padding: 6px 10px;\n background: #ffebe9;\n border-left: 3px solid #cf222e;\n margin: 4px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .added-items pre, .removed-items pre {\n margin: 0;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .unchanged-count {\n padding: 8px 10px;\n color: #586069;\n font-size: 12px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n background: #f6f8fa;\n border-radius: 4px;\n margin: 10px 0;\n }\n";
1
+ export declare const HTML_STYLES = "\n /* Custom styles */\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\n margin: 0;\n padding: 20px;\n background: #f6f8fa;\n }\n\n header {\n background: white;\n padding: 20px;\n border-radius: 6px;\n margin-bottom: 20px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n h1 {\n margin: 0 0 10px 0;\n color: #24292e;\n }\n\n .metadata {\n display: flex;\n gap: 20px;\n margin: 10px 0;\n color: #586069;\n font-size: 14px;\n }\n\n .dry-run-badge {\n display: inline-block;\n padding: 4px 8px;\n background: #cfe2ff;\n color: #084298;\n border-radius: 4px;\n font-weight: bold;\n font-size: 12px;\n }\n\n .summary {\n display: flex;\n gap: 12px;\n margin: 15px 0;\n }\n\n .stat {\n padding: 8px 16px;\n border-radius: 6px;\n font-weight: 600;\n font-size: 14px;\n }\n\n .stat.added { background: #d4edda; color: #155724; }\n .stat.changed { background: #fff3cd; color: #856404; }\n .stat.deleted { background: #f8d7da; color: #721c24; }\n .stat.formatted { background: #d1ecf1; color: #0c5460; }\n .stat.unchanged { background: #e2e3e5; color: #383d41; }\n\n .tabs {\n display: flex;\n background: white;\n border-radius: 6px 6px 0 0;\n border-bottom: 1px solid #d0d7de;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab {\n padding: 12px 24px;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 14px;\n color: #586069;\n transition: all 0.2s;\n }\n\n .tab:hover {\n color: #24292e;\n }\n\n .tab.active {\n border-bottom: 2px solid #0969da;\n color: #0969da;\n font-weight: 600;\n }\n\n main {\n background: white;\n padding: 20px;\n border-radius: 0 0 6px 6px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.12);\n }\n\n .tab-content {\n display: none;\n }\n\n .tab-content.active {\n display: block;\n }\n\n .file-section {\n margin: 12px 0;\n border: 1px solid #d0d7de;\n border-radius: 6px;\n }\n\n .file-section summary {\n padding: 12px 16px;\n background: #f6f8fa;\n cursor: pointer;\n font-weight: 600;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #24292e;\n }\n\n .file-section summary:hover {\n background: #eaeef2;\n }\n\n .filename-transform {\n color: #0969da;\n }\n\n .diff-container {\n padding: 0;\n }\n\n /* Hide diff2html file header with rename badge */\n .d2h-file-header {\n display: none;\n }\n\n .file-list {\n margin: 20px 0;\n }\n\n .file-list ul {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n }\n\n .file-list li {\n padding: 8px 16px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 13px;\n color: #586069;\n border-bottom: 1px solid #f6f8fa;\n }\n\n .file-list li:hover {\n background: #f6f8fa;\n }\n\n .array-details {\n margin: 20px 0;\n background: #f6f8fa;\n border-radius: 6px;\n border-left: 3px solid #0969da;\n }\n\n .array-details summary {\n padding: 12px 15px;\n cursor: pointer;\n color: #0969da;\n font-size: 13px;\n font-weight: 600;\n }\n\n .array-details summary:hover {\n background: #eaeef2;\n }\n\n .array-details > .array-section {\n margin: 0 15px 15px 15px;\n }\n\n .array-section {\n margin: 15px 0;\n padding: 10px;\n background: white;\n border-radius: 4px;\n border: 1px solid #d0d7de;\n }\n\n .array-section h4 {\n margin: 0 0 10px 0;\n color: #24292e;\n font-size: 14px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n }\n\n .array-unchanged {\n padding: 8px;\n color: #586069;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .added-items, .removed-items {\n margin: 10px 0;\n }\n\n .added-items h4 {\n color: #1a7f37;\n margin: 0 0 8px 0;\n font-size: 13px;\n }\n\n .removed-items h4 {\n color: #cf222e;\n margin: 0 0 8px 0;\n font-size: 13px;\n }\n\n .added-items ul, .removed-items ul {\n list-style: none;\n padding: 0;\n margin: 0;\n }\n\n .added-items li {\n padding: 6px 10px;\n background: #dafbe1;\n border-left: 3px solid #1a7f37;\n margin: 4px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .removed-items li {\n padding: 6px 10px;\n background: #ffebe9;\n border-left: 3px solid #cf222e;\n margin: 4px 0;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n font-size: 12px;\n }\n\n .added-items pre, .removed-items pre {\n margin: 0;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n\n .unchanged-count {\n padding: 8px 10px;\n color: #586069;\n font-size: 12px;\n font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace;\n background: #f6f8fa;\n border-radius: 4px;\n margin: 10px 0;\n }\n";
2
2
  export declare const TAB_SCRIPT = "\n // Tab switching\n document.querySelectorAll('.tab').forEach(tab => {\n tab.addEventListener('click', () => {\n const tabName = tab.getAttribute('data-tab');\n\n // Update tabs\n document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));\n tab.classList.add('active');\n\n // Update content\n document.querySelectorAll('.tab-content').forEach(content => {\n content.classList.remove('active');\n });\n document.getElementById(tabName).classList.add('active');\n });\n });\n";
@@ -123,6 +123,10 @@ exports.HTML_STYLES = `
123
123
  background: #eaeef2;
124
124
  }
125
125
 
126
+ .filename-transform {
127
+ color: #0969da;
128
+ }
129
+
126
130
  .diff-container {
127
131
  padding: 0;
128
132
  }
@@ -156,16 +160,25 @@ exports.HTML_STYLES = `
156
160
 
157
161
  .array-details {
158
162
  margin: 20px 0;
159
- padding: 15px;
160
163
  background: #f6f8fa;
161
164
  border-radius: 6px;
162
165
  border-left: 3px solid #0969da;
163
166
  }
164
167
 
165
- .array-details h3 {
166
- margin: 0 0 15px 0;
168
+ .array-details summary {
169
+ padding: 12px 15px;
170
+ cursor: pointer;
167
171
  color: #0969da;
168
- font-size: 16px;
172
+ font-size: 13px;
173
+ font-weight: 600;
174
+ }
175
+
176
+ .array-details summary:hover {
177
+ background: #eaeef2;
178
+ }
179
+
180
+ .array-details > .array-section {
181
+ margin: 0 15px 15px 15px;
169
182
  }
170
183
 
171
184
  .array-section {
@@ -26,4 +26,8 @@ export declare const isFilenameTransformerError: (error: unknown) => error is {
26
26
  };
27
27
  export declare const getFilenameTransformsForFile: (filePath: string, transforms?: TransformConfig) => TransformRule[];
28
28
  export declare const transformFilename: (filePath: string, transforms?: TransformConfig) => string;
29
- export declare const transformFilenameMap: (fileMap: Map<string, string>, transforms?: TransformConfig) => Map<string, string>;
29
+ export interface TransformMapResult {
30
+ fileMap: Map<string, string>;
31
+ originalPaths: Map<string, string>;
32
+ }
33
+ export declare const transformFilenameMap: (fileMap: Map<string, string>, transforms?: TransformConfig) => TransformMapResult;
@@ -92,13 +92,16 @@ const transformFilename = (filePath, transforms) => {
92
92
  };
93
93
  exports.transformFilename = transformFilename;
94
94
  const transformFilenameMap = (fileMap, transforms) => {
95
+ const originalPaths = new Map();
95
96
  if (!transforms)
96
- return fileMap;
97
+ return { fileMap, originalPaths };
97
98
  const transformed = new Map();
98
99
  for (const [originalPath, content] of fileMap.entries()) {
99
100
  const transformedPath = (0, exports.transformFilename)(originalPath, transforms);
100
101
  transformed.set(transformedPath, content);
102
+ if (transformedPath !== originalPath)
103
+ originalPaths.set(transformedPath, originalPath);
101
104
  }
102
- return transformed;
105
+ return { fileMap: transformed, originalPaths };
103
106
  };
104
107
  exports.transformFilenameMap = transformFilenameMap;
@@ -2,7 +2,7 @@ export type { ErrorOptions } from './errors';
2
2
  export { createErrorClass, createErrorTypeGuard } from './errors';
3
3
  export { deepEqual } from './deepEqual';
4
4
  export { normalizeForComparison, serializeForDiff } from './serialization';
5
- export { clearJsonPathCache, getValueAtPath, parseJsonPath } from './jsonPath';
5
+ export { clearJsonPathCache, getValueAtPath, isFilterSegment, parseFilterSegment, parseJsonPath } from './jsonPath';
6
6
  export { isYamlFile } from './fileType';
7
7
  export { globalMatcher, PatternMatcher } from './patternMatcher';
8
8
  export { generateUnifiedDiff } from './diffGenerator';
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SEMANTIC_KEYWORDS = exports.PROBLEMATIC_REGEX_CHARS = 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.detectArrayChanges = exports.generateUnifiedDiff = exports.PatternMatcher = exports.globalMatcher = exports.isYamlFile = exports.parseJsonPath = exports.getValueAtPath = exports.clearJsonPathCache = exports.serializeForDiff = exports.normalizeForComparison = exports.deepEqual = exports.createErrorTypeGuard = exports.createErrorClass = void 0;
4
- exports.UUID_PATTERN = exports.SEMVER_PATTERN = exports.SEMANTIC_PATTERNS = void 0;
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.detectArrayChanges = exports.generateUnifiedDiff = exports.PatternMatcher = exports.globalMatcher = exports.isYamlFile = exports.parseJsonPath = exports.parseFilterSegment = exports.isFilterSegment = exports.getValueAtPath = exports.clearJsonPathCache = exports.serializeForDiff = exports.normalizeForComparison = exports.deepEqual = exports.createErrorTypeGuard = exports.createErrorClass = void 0;
4
+ exports.UUID_PATTERN = exports.SEMVER_PATTERN = exports.SEMANTIC_PATTERNS = exports.SEMANTIC_KEYWORDS = exports.PROBLEMATIC_REGEX_CHARS = 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; } });
@@ -13,6 +13,8 @@ Object.defineProperty(exports, "serializeForDiff", { enumerable: true, get: func
13
13
  var jsonPath_1 = require("./jsonPath");
14
14
  Object.defineProperty(exports, "clearJsonPathCache", { enumerable: true, get: function () { return jsonPath_1.clearJsonPathCache; } });
15
15
  Object.defineProperty(exports, "getValueAtPath", { enumerable: true, get: function () { return jsonPath_1.getValueAtPath; } });
16
+ Object.defineProperty(exports, "isFilterSegment", { enumerable: true, get: function () { return jsonPath_1.isFilterSegment; } });
17
+ Object.defineProperty(exports, "parseFilterSegment", { enumerable: true, get: function () { return jsonPath_1.parseFilterSegment; } });
16
18
  Object.defineProperty(exports, "parseJsonPath", { enumerable: true, get: function () { return jsonPath_1.parseJsonPath; } });
17
19
  var fileType_1 = require("./fileType");
18
20
  Object.defineProperty(exports, "isYamlFile", { enumerable: true, get: function () { return fileType_1.isYamlFile; } });
@@ -1,3 +1,8 @@
1
+ export declare const isFilterSegment: (segment: string) => boolean;
2
+ export declare const parseFilterSegment: (segment: string) => {
3
+ property: string;
4
+ value: string;
5
+ } | undefined;
1
6
  export declare const parseJsonPath: (path: string) => string[];
2
7
  export declare const clearJsonPathCache: () => void;
3
8
  export declare const getValueAtPath: (object: unknown, path: string[]) => unknown;
@@ -1,15 +1,70 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getValueAtPath = exports.clearJsonPathCache = exports.parseJsonPath = void 0;
3
+ exports.getValueAtPath = exports.clearJsonPathCache = exports.parseJsonPath = exports.parseFilterSegment = exports.isFilterSegment = void 0;
4
4
  const pathCache = new Map();
5
+ const FILTER_PREFIX = 'filter:';
6
+ const FILTER_REGEX = /^\[([A-Z_a-z]\w*)=("([^"]*)"|([^\]]*))]/;
7
+ const isFilterSegment = (segment) => segment.startsWith(FILTER_PREFIX);
8
+ exports.isFilterSegment = isFilterSegment;
9
+ const parseFilterSegment = (segment) => {
10
+ if (!(0, exports.isFilterSegment)(segment))
11
+ return undefined;
12
+ const content = segment.slice(FILTER_PREFIX.length);
13
+ const equalIndex = content.indexOf('=');
14
+ if (equalIndex === -1)
15
+ return undefined;
16
+ return {
17
+ property: content.slice(0, equalIndex),
18
+ value: content.slice(equalIndex + 1)
19
+ };
20
+ };
21
+ exports.parseFilterSegment = parseFilterSegment;
5
22
  const parseJsonPath = (path) => {
6
23
  const cached = pathCache.get(path);
7
24
  if (cached !== undefined)
8
25
  return cached;
9
- const parsed = path
10
- .replaceAll(/\[(\*|\d+)]/g, '.$1')
11
- .split('.')
12
- .filter((part) => part.length > 0);
26
+ const result = [];
27
+ let index = 0;
28
+ let currentSegment = '';
29
+ while (index < path.length) {
30
+ const char = path[index];
31
+ if (char === '.') {
32
+ if (currentSegment) {
33
+ result.push(currentSegment);
34
+ currentSegment = '';
35
+ }
36
+ index++;
37
+ }
38
+ else if (char === '[') {
39
+ if (currentSegment) {
40
+ result.push(currentSegment);
41
+ currentSegment = '';
42
+ }
43
+ const remaining = path.slice(index);
44
+ const filterMatch = FILTER_REGEX.exec(remaining);
45
+ if (filterMatch) {
46
+ const property = filterMatch[1];
47
+ const value = filterMatch[3] === undefined ? filterMatch[4] : filterMatch[3];
48
+ result.push(`${FILTER_PREFIX}${property}=${value}`);
49
+ index += filterMatch[0].length;
50
+ continue;
51
+ }
52
+ const indexMatch = /^\[(\*|\d+)]/.exec(remaining);
53
+ if (indexMatch?.[1]) {
54
+ result.push(indexMatch[1]);
55
+ index += indexMatch[0].length;
56
+ continue;
57
+ }
58
+ index++;
59
+ }
60
+ else {
61
+ currentSegment += char;
62
+ index++;
63
+ }
64
+ }
65
+ if (currentSegment)
66
+ result.push(currentSegment);
67
+ const parsed = result.filter((part) => part.length > 0);
13
68
  pathCache.set(path, parsed);
14
69
  return parsed;
15
70
  };
@@ -23,6 +78,21 @@ const getValueAtPath = (object, path) => {
23
78
  for (const part of path) {
24
79
  if (!current || typeof current !== 'object')
25
80
  return undefined;
81
+ if ((0, exports.isFilterSegment)(part)) {
82
+ if (!Array.isArray(current))
83
+ return undefined;
84
+ const filter = (0, exports.parseFilterSegment)(part);
85
+ if (!filter)
86
+ return undefined;
87
+ const matched = current.find((item) => {
88
+ if (!item || typeof item !== 'object')
89
+ return false;
90
+ const itemValue = item[filter.property];
91
+ return String(itemValue) === filter.value;
92
+ });
93
+ current = matched;
94
+ continue;
95
+ }
26
96
  if (Array.isArray(current)) {
27
97
  if (part === '*')
28
98
  return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.7.0",
3
+ "version": "1.7.2",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -62,24 +62,24 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@types/hogan.js": "^3.0.5",
65
- "@types/node": "^25.0.3",
65
+ "@types/node": "^25.0.10",
66
66
  "@types/picomatch": "^4.0.2",
67
- "@typescript-eslint/eslint-plugin": "^8.52.0",
68
- "@typescript-eslint/parser": "^8.52.0",
69
- "@vitest/coverage-v8": "^4.0.16",
67
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
68
+ "@typescript-eslint/parser": "^8.53.1",
69
+ "@vitest/coverage-v8": "^4.0.17",
70
70
  "eslint": "^9.39.2",
71
71
  "eslint-config-prettier": "^10.1.8",
72
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
73
73
  "eslint-plugin-unicorn": "^62.0.0",
74
- "prettier": "^3.7.4",
74
+ "prettier": "^3.8.1",
75
75
  "tsx": "^4.21.0",
76
76
  "typescript": "^5.9.3",
77
- "vitest": "^4.0.16"
77
+ "vitest": "^4.0.17"
78
78
  },
79
79
  "dependencies": {
80
80
  "chalk": "^5.6.2",
81
81
  "commander": "^14.0.2",
82
- "diff": "^8.0.2",
82
+ "diff": "^8.0.3",
83
83
  "diff2html": "3.4.52",
84
84
  "open": "^11.0.0",
85
85
  "picomatch": "^4.0.3",