helm-env-delta 1.7.1 → 1.8.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
@@ -376,24 +376,48 @@ skipPath:
376
376
 
377
377
  #### Filter Expressions (Skip by Name)
378
378
 
379
- Skip specific array items by property value using filter syntax:
379
+ Skip specific array items by property value using CSS-style filter operators:
380
+
381
+ | Operator | Name | Example | Matches |
382
+ | -------- | ---------- | ---------------- | ------------------------- |
383
+ | `=` | equals | `[name=DEBUG]` | Exact match |
384
+ | `^=` | startsWith | `[name^=DB_]` | `DB_HOST`, `DB_PORT` |
385
+ | `$=` | endsWith | `[name$=_KEY]` | `API_KEY`, `SECRET_KEY` |
386
+ | `*=` | contains | `[name*=SECRET]` | `MY_SECRET_KEY`, `SECRET` |
380
387
 
381
388
  ```yaml
382
389
  skipPath:
383
390
  '**/*.yaml':
391
+ # Equals (=) - exact match
384
392
  - 'env[name=SECRET_KEY]' # Skip item where name=SECRET_KEY
385
- - 'env[name=INTERNAL_TOKEN].value' # Skip nested field in matching item
386
393
  - 'containers[name=sidecar]' # Skip entire sidecar container
387
- - 'spec.containers[name=app].env[name=DEBUG]' # Nested filters
394
+
395
+ # StartsWith (^=) - prefix match
396
+ - 'env[name^=DB_]' # Skip DB_HOST, DB_PORT, DB_USER
397
+ - 'containers[name^=init-]' # Skip init-db, init-cache
398
+
399
+ # EndsWith ($=) - suffix match
400
+ - 'env[name$=_SECRET]' # Skip API_SECRET, DB_SECRET
401
+ - 'volumes[name$=-data]' # Skip app-data, cache-data
402
+
403
+ # Contains (*=) - substring match
404
+ - 'env[name*=PASSWORD]' # Skip DB_PASSWORD, PASSWORD_HASH
405
+ - 'containers[image*=nginx]' # Skip any nginx image
406
+
407
+ # Nested paths with mixed operators
408
+ - 'spec.containers[name^=sidecar-].env[name$=_KEY]'
388
409
  ```
389
410
 
390
411
  **Syntax:**
391
412
 
392
413
  - `array[prop=value]` - Match items where property equals value
414
+ - `array[prop^=prefix]` - Match items where property starts with prefix
415
+ - `array[prop$=suffix]` - Match items where property ends with suffix
416
+ - `array[prop*=substring]` - Match items where property contains substring
393
417
  - `array[prop="value with spaces"]` - Quoted values for special characters
394
418
  - Combine with wildcards: `containers[name=app].env[*].value`
395
419
 
396
- **Use cases:** Namespaces, replicas, resource limits, secrets, URLs, environment-specific array items.
420
+ **Use cases:** Namespaces, replicas, resource limits, secrets, URLs, environment-specific array items, batch filtering by naming conventions.
397
421
 
398
422
  ---
399
423
 
@@ -642,7 +666,8 @@ hed --config <file> [options] # Short alias
642
666
  | `--diff-json` | Output JSON to stdout (pipe to jq) |
643
667
  | `--list-files` | List source/destination files without processing |
644
668
  | `--show-config` | Display resolved config after inheritance |
645
- | `--skip-format` | Skip YAML formatting |
669
+ | `--format-only` | Format destination files without syncing |
670
+ | `--skip-format` | Skip YAML formatting during sync |
646
671
  | `--no-color` | Disable colored output (CI/accessibility) |
647
672
  | `--verbose` | Show detailed debug info |
648
673
  | `--quiet` | Suppress output except errors |
@@ -679,6 +704,12 @@ hed --config config.yaml
679
704
 
680
705
  # Force override stop rules
681
706
  hed --config config.yaml --force
707
+
708
+ # Format destination files only (no sync)
709
+ hed --config config.yaml --format-only
710
+
711
+ # Preview format changes
712
+ hed --config config.yaml --format-only --dry-run
682
713
  ```
683
714
 
684
715
  ---
@@ -749,7 +780,7 @@ git push origin main
749
780
 
750
781
  ✅ **Flexibility** - Per-file patterns. Config inheritance. Regex transforms.
751
782
 
752
- ✅ **Reliability** - 917 tests, 84% coverage. Battle-tested.
783
+ ✅ **Reliability** - 920 tests, 84% coverage. Battle-tested.
753
784
 
754
785
  ---
755
786
 
@@ -950,7 +981,10 @@ spec:
950
981
  # ✅ Correct
951
982
  - 'spec.replicas' # No prefix
952
983
  - 'env[*].name' # Array wildcard
953
- - 'env[name=DEBUG]' # Filter by property value
984
+ - 'env[name=DEBUG]' # Filter by exact value
985
+ - 'env[name^=DB_]' # Filter by prefix (startsWith)
986
+ - 'env[name$=_KEY]' # Filter by suffix (endsWith)
987
+ - 'env[name*=SECRET]' # Filter by substring (contains)
954
988
  - 'containers[name=app].env[name=SECRET]' # Nested filters
955
989
  ```
956
990
 
@@ -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'],
@@ -161,7 +161,6 @@ declare const baseConfigSchema: z.ZodObject<{
161
161
  }, z.core.$strict>], "type">>>>;
162
162
  }, z.core.$strip>;
163
163
  declare const finalConfigSchema: z.ZodObject<{
164
- source: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
165
164
  transforms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
166
165
  content: z.ZodOptional<z.ZodArray<z.ZodObject<{
167
166
  find: z.ZodString;
@@ -174,6 +173,7 @@ declare const finalConfigSchema: z.ZodObject<{
174
173
  contentFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
175
174
  filenameFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
176
175
  }, z.core.$strip>>>;
176
+ source: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
177
177
  destination: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
178
178
  skipPath: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
179
179
  stopRules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
package/dist/fileDiff.js CHANGED
@@ -52,7 +52,7 @@ const deleteJsonPathRecursive = (object, parts, index) => {
52
52
  const item = object[index_];
53
53
  if (item && typeof item === 'object') {
54
54
  const itemValue = item[filter.property];
55
- if (String(itemValue) === filter.value)
55
+ if ((0, jsonPath_1.matchesFilter)(itemValue, filter))
56
56
  object.splice(index_, 1);
57
57
  }
58
58
  }
@@ -60,7 +60,7 @@ const deleteJsonPathRecursive = (object, parts, index) => {
60
60
  for (const item of object)
61
61
  if (item && typeof item === 'object') {
62
62
  const itemValue = item[filter.property];
63
- if (String(itemValue) === filter.value)
63
+ if ((0, jsonPath_1.matchesFilter)(itemValue, filter))
64
64
  deleteJsonPathRecursive(item, parts, index + 1);
65
65
  }
66
66
  return;
@@ -4,6 +4,7 @@ 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>;
9
10
  export interface FileLoaderResult {
@@ -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}`);
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)();
@@ -102,13 +105,15 @@ const main = async () => {
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
111
  const sourceFiles = sourceResult.fileMap;
108
112
  const destinationResult = await (0, fileLoader_1.loadFiles)({
109
113
  baseDirectory: config.destination,
110
114
  include: config.include,
111
- exclude: config.exclude
115
+ exclude: config.exclude,
116
+ skipExclude: true
112
117
  }, logger);
113
118
  const destinationFiles = destinationResult.fileMap;
114
119
  logger.progress(`Loaded ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'success');
@@ -171,6 +176,46 @@ const main = async () => {
171
176
  console.log(` ${chalk_1.default.dim(file)}`);
172
177
  return;
173
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
+ }
174
219
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
175
220
  const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger, originalPaths);
176
221
  if (logger.shouldShow('debug'))
@@ -114,7 +114,7 @@ const pathCouldMatch = (object, pathParts) => {
114
114
  if (!item || typeof item !== 'object')
115
115
  return false;
116
116
  const itemValue = item[filter.property];
117
- return String(itemValue) === filter.value;
117
+ return (0, jsonPath_1.matchesFilter)(itemValue, filter);
118
118
  });
119
119
  if (!matched)
120
120
  return false;
@@ -2,7 +2,8 @@ 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, isFilterSegment, parseFilterSegment, parseJsonPath } from './jsonPath';
5
+ export type { FilterOperator } from './jsonPath';
6
+ export { clearJsonPathCache, getValueAtPath, isFilterSegment, matchesFilter, parseFilterSegment, parseJsonPath } from './jsonPath';
6
7
  export { isYamlFile } from './fileType';
7
8
  export { globalMatcher, PatternMatcher } from './patternMatcher';
8
9
  export { generateUnifiedDiff } from './diffGenerator';
@@ -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.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;
3
+ 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.matchesFilter = 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 = exports.NUMERIC_MIN_MULTIPLIER = 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; } });
@@ -14,6 +14,7 @@ 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
16
  Object.defineProperty(exports, "isFilterSegment", { enumerable: true, get: function () { return jsonPath_1.isFilterSegment; } });
17
+ Object.defineProperty(exports, "matchesFilter", { enumerable: true, get: function () { return jsonPath_1.matchesFilter; } });
17
18
  Object.defineProperty(exports, "parseFilterSegment", { enumerable: true, get: function () { return jsonPath_1.parseFilterSegment; } });
18
19
  Object.defineProperty(exports, "parseJsonPath", { enumerable: true, get: function () { return jsonPath_1.parseJsonPath; } });
19
20
  var fileType_1 = require("./fileType");
@@ -1,8 +1,15 @@
1
+ export type FilterOperator = 'eq' | 'startsWith' | 'endsWith' | 'contains';
1
2
  export declare const isFilterSegment: (segment: string) => boolean;
2
3
  export declare const parseFilterSegment: (segment: string) => {
3
4
  property: string;
4
5
  value: string;
6
+ operator: FilterOperator;
5
7
  } | undefined;
8
+ export declare const matchesFilter: (itemValue: unknown, filter: {
9
+ property: string;
10
+ value: string;
11
+ operator: FilterOperator;
12
+ }) => boolean;
6
13
  export declare const parseJsonPath: (path: string) => string[];
7
14
  export declare const clearJsonPathCache: () => void;
8
15
  export declare const getValueAtPath: (object: unknown, path: string[]) => unknown;
@@ -1,24 +1,50 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getValueAtPath = exports.clearJsonPathCache = exports.parseJsonPath = exports.parseFilterSegment = exports.isFilterSegment = void 0;
3
+ exports.getValueAtPath = exports.clearJsonPathCache = exports.parseJsonPath = exports.matchesFilter = exports.parseFilterSegment = exports.isFilterSegment = void 0;
4
4
  const pathCache = new Map();
5
5
  const FILTER_PREFIX = 'filter:';
6
- const FILTER_REGEX = /^\[([A-Z_a-z]\w*)=("([^"]*)"|([^\]]*))]/;
6
+ const OPERATOR_MAP = {
7
+ '=': 'eq',
8
+ '^=': 'startsWith',
9
+ '$=': 'endsWith',
10
+ '*=': 'contains'
11
+ };
12
+ const FILTER_REGEX = /^\[([A-Z_a-z]\w*)(\^=|\$=|\*=|=)("([^"]*)"|([^\]]*))]/;
7
13
  const isFilterSegment = (segment) => segment.startsWith(FILTER_PREFIX);
8
14
  exports.isFilterSegment = isFilterSegment;
9
15
  const parseFilterSegment = (segment) => {
10
16
  if (!(0, exports.isFilterSegment)(segment))
11
17
  return undefined;
12
18
  const content = segment.slice(FILTER_PREFIX.length);
13
- const equalIndex = content.indexOf('=');
14
- if (equalIndex === -1)
19
+ const firstColon = content.indexOf(':');
20
+ if (firstColon === -1)
21
+ return undefined;
22
+ const property = content.slice(0, firstColon);
23
+ const rest = content.slice(firstColon + 1);
24
+ const secondColon = rest.indexOf(':');
25
+ if (secondColon === -1)
26
+ return undefined;
27
+ const operator = rest.slice(0, secondColon);
28
+ const value = rest.slice(secondColon + 1);
29
+ if (!['eq', 'startsWith', 'endsWith', 'contains'].includes(operator))
15
30
  return undefined;
16
- return {
17
- property: content.slice(0, equalIndex),
18
- value: content.slice(equalIndex + 1)
19
- };
31
+ return { property, value, operator };
20
32
  };
21
33
  exports.parseFilterSegment = parseFilterSegment;
34
+ const matchesFilter = (itemValue, filter) => {
35
+ const stringValue = String(itemValue);
36
+ switch (filter.operator) {
37
+ case 'eq':
38
+ return stringValue === filter.value;
39
+ case 'startsWith':
40
+ return stringValue.startsWith(filter.value);
41
+ case 'endsWith':
42
+ return stringValue.endsWith(filter.value);
43
+ case 'contains':
44
+ return stringValue.includes(filter.value);
45
+ }
46
+ };
47
+ exports.matchesFilter = matchesFilter;
22
48
  const parseJsonPath = (path) => {
23
49
  const cached = pathCache.get(path);
24
50
  if (cached !== undefined)
@@ -44,8 +70,10 @@ const parseJsonPath = (path) => {
44
70
  const filterMatch = FILTER_REGEX.exec(remaining);
45
71
  if (filterMatch) {
46
72
  const property = filterMatch[1];
47
- const value = filterMatch[3] === undefined ? filterMatch[4] : filterMatch[3];
48
- result.push(`${FILTER_PREFIX}${property}=${value}`);
73
+ const operatorString = filterMatch[2];
74
+ const value = filterMatch[4] === undefined ? filterMatch[5] : filterMatch[4];
75
+ const operator = OPERATOR_MAP[operatorString] ?? 'eq';
76
+ result.push(`${FILTER_PREFIX}${property}:${operator}:${value}`);
49
77
  index += filterMatch[0].length;
50
78
  continue;
51
79
  }
@@ -88,7 +116,7 @@ const getValueAtPath = (object, path) => {
88
116
  if (!item || typeof item !== 'object')
89
117
  return false;
90
118
  const itemValue = item[filter.property];
91
- return String(itemValue) === filter.value;
119
+ return (0, exports.matchesFilter)(itemValue, filter);
92
120
  });
93
121
  current = matched;
94
122
  continue;
package/package.json CHANGED
@@ -1,12 +1,18 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
7
- "exports": "./dist/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
8
13
  "main": "./dist/index.js",
9
14
  "types": "./dist/index.d.ts",
15
+ "sideEffects": false,
10
16
  "engines": {
11
17
  "node": ">=22",
12
18
  "npm": ">=9"
@@ -62,19 +68,19 @@
62
68
  },
63
69
  "devDependencies": {
64
70
  "@types/hogan.js": "^3.0.5",
65
- "@types/node": "^25.0.9",
71
+ "@types/node": "^25.0.10",
66
72
  "@types/picomatch": "^4.0.2",
67
- "@typescript-eslint/eslint-plugin": "^8.53.0",
68
- "@typescript-eslint/parser": "^8.53.0",
69
- "@vitest/coverage-v8": "^4.0.17",
73
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
74
+ "@typescript-eslint/parser": "^8.53.1",
75
+ "@vitest/coverage-v8": "^4.0.18",
70
76
  "eslint": "^9.39.2",
71
77
  "eslint-config-prettier": "^10.1.8",
72
78
  "eslint-plugin-simple-import-sort": "^12.1.1",
73
79
  "eslint-plugin-unicorn": "^62.0.0",
74
- "prettier": "^3.8.0",
80
+ "prettier": "^3.8.1",
75
81
  "tsx": "^4.21.0",
76
82
  "typescript": "^5.9.3",
77
- "vitest": "^4.0.17"
83
+ "vitest": "^4.0.18"
78
84
  },
79
85
  "dependencies": {
80
86
  "chalk": "^5.6.2",
@@ -85,6 +91,6 @@
85
91
  "picomatch": "^4.0.3",
86
92
  "tinyglobby": "^0.2.15",
87
93
  "yaml": "^2.8.2",
88
- "zod": "^4.3.5"
94
+ "zod": "^4.3.6"
89
95
  }
90
96
  }