helm-env-delta 1.10.0 → 1.10.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
@@ -693,6 +693,35 @@ outputFormat:
693
693
 
694
694
  ---
695
695
 
696
+ ### 🔍 CLI Filter Operators
697
+
698
+ The `-f/--filter` flag supports logical operators for complex filtering:
699
+
700
+ | Operator | Name | Example | Matches |
701
+ | -------- | ------ | -------------------- | ----------------------------------------------- |
702
+ | (none) | Simple | `-f prod` | Files where filename or content contains "prod" |
703
+ | `\|` | OR | `-f "prod\|staging"` | Files matching "prod" OR "staging" |
704
+ | `&` | AND | `-f "values&prod"` | Files matching "values" AND "prod" |
705
+
706
+ ```bash
707
+ # OR: match ANY term (filename or content)
708
+ hed -c config.yaml -f "prod|staging" --list-files
709
+
710
+ # AND: match ALL terms (can be split between filename and content)
711
+ hed -c config.yaml -f "values&prod" --list-files
712
+
713
+ # Escape literal | or & with backslash
714
+ hed -c config.yaml -f "foo\|bar" --list-files
715
+ ```
716
+
717
+ **Constraints:**
718
+
719
+ - Cannot mix `&` and `|` in a single filter (throws error)
720
+ - Case-insensitive matching
721
+ - Empty terms are ignored (`a||b` becomes `a|b`)
722
+
723
+ ---
724
+
696
725
  ### 🔗 Config Inheritance
697
726
 
698
727
  Reuse base configurations across environment pairs.
@@ -768,7 +797,7 @@ hed --config <file> [options] # Short alias
768
797
  | `--show-config` | | Display resolved config after inheritance |
769
798
  | `--format-only` | | Format destination files only (source not required) |
770
799
  | `--skip-format` | `-S` | Skip YAML formatting during sync |
771
- | `--filter <string>` | `-f` | Filter files by filename or content (case-insensitive) |
800
+ | `--filter <string>` | `-f` | Filter files by filename/content (supports `\|` OR, `&` AND) |
772
801
  | `--mode <type>` | `-m` | Filter by change type: new, modified, deleted, all (default: all) |
773
802
  | `--no-color` | | Disable colored output (CI/accessibility) |
774
803
  | `--verbose` | | Show detailed debug info |
@@ -810,6 +839,12 @@ hed -c config.yaml --force
810
839
  # Filter to only process files matching 'prod'
811
840
  hed -c config.yaml -f prod -d
812
841
 
842
+ # Filter with OR: match files containing 'prod' OR 'staging'
843
+ hed -c config.yaml -f "prod|staging" -l
844
+
845
+ # Filter with AND: match files containing BOTH 'values' AND 'prod'
846
+ hed -c config.yaml -f "values&prod" -d
847
+
813
848
  # Sync only new files
814
849
  hed -c config.yaml -m new
815
850
 
@@ -26,7 +26,7 @@ const parseCommandLine = (argv) => {
26
26
  .option('--suggest', 'Analyze differences and suggest transforms and stop rules', false)
27
27
  .option('--suggest-threshold <number>', 'Minimum confidence for suggestions (0-1, default: 0.3)', '0.3')
28
28
  .option('--no-color', 'Disable colored output')
29
- .option('-f, --filter <string>', 'Filter files by filename or content (case-insensitive)')
29
+ .option('-f, --filter <string>', 'Filter files by filename or content (supports | for OR, & for AND)')
30
30
  .option('-m, --mode <type>', 'Filter by change type: new, modified, deleted, all', 'all')
31
31
  .option('--verbose', 'Show detailed debug information', false)
32
32
  .option('--quiet', 'Suppress all output except critical errors', false)
@@ -50,6 +50,12 @@ Examples:
50
50
  # Filter to only process files matching 'prod'
51
51
  $ helm-env-delta --config config.yaml -f prod --diff
52
52
 
53
+ # Filter with OR: files matching 'prod' OR 'staging'
54
+ $ helm-env-delta --config config.yaml -f "prod|staging" --diff
55
+
56
+ # Filter with AND: files matching 'values' AND 'prod'
57
+ $ helm-env-delta --config config.yaml -f "values&prod" --diff
58
+
53
59
  # Sync only new files
54
60
  $ helm-env-delta --config config.yaml --mode new
55
61
 
package/dist/index.js CHANGED
@@ -355,6 +355,8 @@ const main = async () => {
355
355
  console.error(error.message);
356
356
  else if ((0, suggestionEngine_1.isSuggestionEngineError)(error))
357
357
  console.error(error.message);
358
+ else if ((0, fileFilter_1.isFilterParseError)(error))
359
+ console.error(error.message);
358
360
  else if (error instanceof Error)
359
361
  console.error('Unexpected error:', error.message);
360
362
  else
@@ -1,6 +1,38 @@
1
1
  import type { FileDiffResult } from '../fileDiff';
2
2
  import type { FileMap } from '../fileLoader';
3
3
  export type ChangeMode = 'new' | 'modified' | 'deleted' | 'all';
4
+ export type FilterLogicalOperator = 'AND' | 'OR' | 'NONE';
5
+ export interface ParsedFilter {
6
+ operator: FilterLogicalOperator;
7
+ terms: string[];
8
+ }
9
+ export declare const FilterParseError: {
10
+ new (message: string, options?: import("./errors").ErrorOptions): {
11
+ [key: string]: unknown;
12
+ readonly code?: string;
13
+ readonly path?: string;
14
+ readonly cause?: Error;
15
+ readonly hints?: string[];
16
+ name: string;
17
+ message: string;
18
+ stack?: string;
19
+ };
20
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
21
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
22
+ stackTraceLimit: number;
23
+ };
24
+ export declare const isFilterParseError: (error: unknown) => error is {
25
+ [key: string]: unknown;
26
+ readonly code?: string;
27
+ readonly path?: string;
28
+ readonly cause?: Error;
29
+ readonly hints?: string[];
30
+ name: string;
31
+ message: string;
32
+ stack?: string;
33
+ };
34
+ export declare const parseFilterExpression: (filter: string | undefined) => ParsedFilter;
35
+ export declare const fileMatchesFilter: (filePath: string, content: string, parsedFilter: ParsedFilter) => boolean;
4
36
  export declare const filterDiffResultByMode: (diffResult: FileDiffResult, mode: ChangeMode) => FileDiffResult;
5
37
  export declare const filterFileMap: (fileMap: FileMap, filter: string | undefined) => FileMap;
6
38
  export declare const filterFileMaps: (sourceFiles: FileMap, destinationFiles: FileMap, filter: string | undefined) => {
@@ -1,6 +1,86 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.filterFileMaps = exports.filterFileMap = exports.filterDiffResultByMode = void 0;
3
+ exports.filterFileMaps = exports.filterFileMap = exports.filterDiffResultByMode = exports.fileMatchesFilter = exports.parseFilterExpression = exports.isFilterParseError = exports.FilterParseError = void 0;
4
+ const errors_1 = require("./errors");
5
+ const filterParseErrorCodes = {
6
+ MIXED_OPERATORS: 'Cannot combine AND (&) and OR (|) operators in a single filter expression'
7
+ };
8
+ exports.FilterParseError = (0, errors_1.createErrorClass)('FilterParseError', filterParseErrorCodes);
9
+ exports.isFilterParseError = (0, errors_1.createErrorTypeGuard)(exports.FilterParseError);
10
+ const parseFilterExpression = (filter) => {
11
+ if (!filter)
12
+ return { operator: 'NONE', terms: [] };
13
+ let hasUnescapedOr = false;
14
+ let hasUnescapedAnd = false;
15
+ for (let index = 0; index < filter.length; index++) {
16
+ const char = filter[index];
17
+ const isEscaped = index > 0 && filter[index - 1] === '\\';
18
+ if (char === '|' && !isEscaped)
19
+ hasUnescapedOr = true;
20
+ if (char === '&' && !isEscaped)
21
+ hasUnescapedAnd = true;
22
+ }
23
+ if (hasUnescapedOr && hasUnescapedAnd)
24
+ throw new exports.FilterParseError('Mixed operators detected', {
25
+ code: 'MIXED_OPERATORS',
26
+ hints: [
27
+ 'Use only | (OR) or only & (AND) in a single filter',
28
+ String.raw `Escape literal characters with backslash: \| or \&`
29
+ ]
30
+ });
31
+ const operator = hasUnescapedOr ? 'OR' : hasUnescapedAnd ? 'AND' : 'NONE';
32
+ let terms;
33
+ if (operator === 'NONE')
34
+ terms = [filter];
35
+ else {
36
+ const splitChar = operator === 'OR' ? '|' : '&';
37
+ terms = [];
38
+ let currentTerm = '';
39
+ for (let index = 0; index < filter.length; index++) {
40
+ const char = filter[index];
41
+ const isEscaped = index > 0 && filter[index - 1] === '\\';
42
+ if (char === splitChar && !isEscaped) {
43
+ terms.push(currentTerm);
44
+ currentTerm = '';
45
+ }
46
+ else if (char === '\\' && index + 1 < filter.length && (filter[index + 1] === '|' || filter[index + 1] === '&'))
47
+ continue;
48
+ else
49
+ currentTerm += char;
50
+ }
51
+ terms.push(currentTerm);
52
+ }
53
+ const processedTerms = terms
54
+ .map((term) => term
55
+ .replaceAll(/\\([&|])/g, '$1')
56
+ .trim()
57
+ .toLowerCase())
58
+ .filter((term) => term.length > 0);
59
+ const finalOperator = processedTerms.length <= 1 ? 'NONE' : operator;
60
+ return { operator: finalOperator, terms: processedTerms };
61
+ };
62
+ exports.parseFilterExpression = parseFilterExpression;
63
+ const fileMatchesFilter = (filePath, content, parsedFilter) => {
64
+ const { operator, terms } = parsedFilter;
65
+ if (terms.length === 0)
66
+ return true;
67
+ const lowerPath = filePath.toLowerCase();
68
+ const lowerContent = content.toLowerCase();
69
+ const termMatches = (term) => lowerPath.includes(term) || lowerContent.includes(term);
70
+ switch (operator) {
71
+ case 'OR': {
72
+ return terms.some((term) => termMatches(term));
73
+ }
74
+ case 'AND': {
75
+ return terms.every((term) => termMatches(term));
76
+ }
77
+ case 'NONE': {
78
+ const firstTerm = terms[0];
79
+ return terms.length === 0 || (firstTerm !== undefined && termMatches(firstTerm));
80
+ }
81
+ }
82
+ };
83
+ exports.fileMatchesFilter = fileMatchesFilter;
4
84
  const filterDiffResultByMode = (diffResult, mode) => {
5
85
  if (mode === 'all')
6
86
  return diffResult;
@@ -13,29 +93,26 @@ const filterDiffResultByMode = (diffResult, mode) => {
13
93
  };
14
94
  exports.filterDiffResultByMode = filterDiffResultByMode;
15
95
  const filterFileMap = (fileMap, filter) => {
16
- if (!filter)
96
+ const parsedFilter = (0, exports.parseFilterExpression)(filter);
97
+ if (parsedFilter.terms.length === 0)
17
98
  return fileMap;
18
- const lowerFilter = filter.toLowerCase();
19
99
  const filteredMap = new Map();
20
- for (const [filePath, content] of fileMap) {
21
- const filenameMatches = filePath.toLowerCase().includes(lowerFilter);
22
- const contentMatches = content.toLowerCase().includes(lowerFilter);
23
- if (filenameMatches || contentMatches)
100
+ for (const [filePath, content] of fileMap)
101
+ if ((0, exports.fileMatchesFilter)(filePath, content, parsedFilter))
24
102
  filteredMap.set(filePath, content);
25
- }
26
103
  return filteredMap;
27
104
  };
28
105
  exports.filterFileMap = filterFileMap;
29
106
  const filterFileMaps = (sourceFiles, destinationFiles, filter) => {
30
- if (!filter)
107
+ const parsedFilter = (0, exports.parseFilterExpression)(filter);
108
+ if (parsedFilter.terms.length === 0)
31
109
  return { sourceFiles, destinationFiles };
32
- const lowerFilter = filter.toLowerCase();
33
110
  const matchingPaths = new Set();
34
111
  for (const [filePath, content] of sourceFiles)
35
- if (filePath.toLowerCase().includes(lowerFilter) || content.toLowerCase().includes(lowerFilter))
112
+ if ((0, exports.fileMatchesFilter)(filePath, content, parsedFilter))
36
113
  matchingPaths.add(filePath);
37
114
  for (const [filePath, content] of destinationFiles)
38
- if (filePath.toLowerCase().includes(lowerFilter) || content.toLowerCase().includes(lowerFilter))
115
+ if ((0, exports.fileMatchesFilter)(filePath, content, parsedFilter))
39
116
  matchingPaths.add(filePath);
40
117
  const filteredSource = new Map();
41
118
  const filteredDestination = new Map();
@@ -20,4 +20,5 @@ export { applyFixedValues, getFixedValuesForFile, setValueAtPath } from './fixed
20
20
  export type { ApplicableFilter } from './arrayMerger';
21
21
  export { findMatchingTargetItem, getApplicableArrayFilters, itemMatchesAnyFilter, shouldPreserveItem } from './arrayMerger';
22
22
  export { isCommentOnlyContent } from './commentOnlyDetector';
23
- export { filterFileMap, filterFileMaps } from './fileFilter';
23
+ export type { FilterLogicalOperator, ParsedFilter } from './fileFilter';
24
+ export { fileMatchesFilter, filterFileMap, filterFileMaps, FilterParseError, isFilterParseError, parseFilterExpression } from './fileFilter';
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
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.filterFileMaps = exports.filterFileMap = 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;
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;
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; } });
@@ -85,5 +85,9 @@ Object.defineProperty(exports, "shouldPreserveItem", { enumerable: true, get: fu
85
85
  var commentOnlyDetector_1 = require("./commentOnlyDetector");
86
86
  Object.defineProperty(exports, "isCommentOnlyContent", { enumerable: true, get: function () { return commentOnlyDetector_1.isCommentOnlyContent; } });
87
87
  var fileFilter_1 = require("./fileFilter");
88
+ Object.defineProperty(exports, "fileMatchesFilter", { enumerable: true, get: function () { return fileFilter_1.fileMatchesFilter; } });
88
89
  Object.defineProperty(exports, "filterFileMap", { enumerable: true, get: function () { return fileFilter_1.filterFileMap; } });
89
90
  Object.defineProperty(exports, "filterFileMaps", { enumerable: true, get: function () { return fileFilter_1.filterFileMaps; } });
91
+ Object.defineProperty(exports, "FilterParseError", { enumerable: true, get: function () { return fileFilter_1.FilterParseError; } });
92
+ Object.defineProperty(exports, "isFilterParseError", { enumerable: true, get: function () { return fileFilter_1.isFilterParseError; } });
93
+ Object.defineProperty(exports, "parseFilterExpression", { enumerable: true, get: function () { return fileFilter_1.parseFilterExpression; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/hogan.js": "^3.0.5",
71
- "@types/node": "^25.1.0",
71
+ "@types/node": "^25.2.0",
72
72
  "@types/picomatch": "^4.0.2",
73
73
  "@typescript-eslint/eslint-plugin": "^8.54.0",
74
74
  "@typescript-eslint/parser": "^8.54.0",
@@ -84,9 +84,9 @@
84
84
  },
85
85
  "dependencies": {
86
86
  "chalk": "^5.6.2",
87
- "commander": "^14.0.2",
87
+ "commander": "^14.0.3",
88
88
  "diff": "^8.0.3",
89
- "diff2html": "3.4.52",
89
+ "diff2html": "3.4.56",
90
90
  "open": "^11.0.0",
91
91
  "picomatch": "^4.0.3",
92
92
  "tinyglobby": "^0.2.15",