helm-env-delta 1.14.2 β†’ 1.15.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
@@ -58,7 +58,7 @@ HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows
58
58
 
59
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, collapse/expand controls, jump-to-sidebar navigation, and auto-collapse for large file sets. Empty categories are automatically hidden.
60
60
 
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.
61
+ πŸ” **Discovery Tools** - Preview files (`-l`), inspect config (`--show-config`), filter by filename/content (`-f`), filter by change type (`-m`), filter to your own git changes (`--my`), validate with comprehensive warnings including unused pattern detection.
62
62
 
63
63
  πŸ’‘ **Smart Suggestions** - Heuristic analysis (`--suggest`) detects patterns and recommends transforms and stop rules automatically. Control sensitivity with `--suggest-threshold`.
64
64
 
@@ -753,6 +753,29 @@ hed -c config.yaml -f "foo\,bar" --list-files
753
753
 
754
754
  ---
755
755
 
756
+ ### πŸ‘€ Git Author Filter (`--my`)
757
+
758
+ Filter the sync to only source files **you** modified in git. Your identity is read automatically from `git config user.name` (falls back to `user.email`).
759
+
760
+ ```bash
761
+ # Sync only files you modified in the last 30 days (default)
762
+ hed -c config.yaml --my
763
+
764
+ # Sync only files you modified in the last 7 days
765
+ hed -c config.yaml --my 7
766
+
767
+ # Preview first
768
+ hed -c config.yaml --my --dry-run --diff
769
+ ```
770
+
771
+ **How it works:** Queries `git log` for commits by your git identity within the time window, collects modified file paths, and filters source files to that set. The matching destination files are filtered in tandem.
772
+
773
+ **Requirements:** Must be run inside a git repository with a configured `user.name` or `user.email`.
774
+
775
+ **Use case:** In a shared monorepo with many services, use `--my` to focus syncs on just the files you touched during your current sprintβ€”without needing to know their exact paths.
776
+
777
+ ---
778
+
756
779
  ### πŸ”— Config Inheritance
757
780
 
758
781
  Reuse base configurations across environment pairs.
@@ -813,26 +836,27 @@ hed --config <file> [options] # Short alias
813
836
 
814
837
  ### Options
815
838
 
816
- | Flag | Short | Description |
817
- | --------------------------- | ----- | ------------------------------------------------------------------- |
818
- | `--config <path>` | `-c` | **Required** - Configuration file |
819
- | `--validate` | | Validate config and pattern usage (shows warnings) |
820
- | `--suggest` | | Analyze differences and suggest config updates |
821
- | `--suggest-threshold <0-1>` | | Minimum confidence for suggestions (default: 0.3) |
822
- | `--dry-run` | `-D` | Preview changes without writing files |
823
- | `--force` | | Override stop rules |
824
- | `--diff` | `-d` | Show console diff |
825
- | `--diff-html` | `-H` | Generate HTML report (opens in browser) |
826
- | `--diff-json` | `-J` | Output JSON to stdout (pipe to jq) |
827
- | `--list-files` | `-l` | List files without processing (takes precedence over --format-only) |
828
- | `--show-config` | | Display resolved config after inheritance |
829
- | `--format-only` | | Format destination files only (source not required) |
830
- | `--skip-format` | `-S` | Skip YAML formatting during sync |
831
- | `--filter <string>` | `-f` | Filter files by filename/content (supports `,` OR, `+` AND) |
832
- | `--mode <type>` | `-m` | Filter by change type: new, modified, deleted, all (default: all) |
833
- | `--no-color` | | Disable colored output (CI/accessibility) |
834
- | `--verbose` | | Show detailed debug info |
835
- | `--quiet` | | Suppress output except errors |
839
+ | Flag | Short | Description |
840
+ | --------------------------- | ----- | --------------------------------------------------------------------------- |
841
+ | `--config <path>` | `-c` | **Required** - Configuration file |
842
+ | `--validate` | | Validate config and pattern usage (shows warnings) |
843
+ | `--suggest` | | Analyze differences and suggest config updates |
844
+ | `--suggest-threshold <0-1>` | | Minimum confidence for suggestions (default: 0.3) |
845
+ | `--dry-run` | `-D` | Preview changes without writing files |
846
+ | `--force` | | Override stop rules |
847
+ | `--diff` | `-d` | Show console diff |
848
+ | `--diff-html` | `-H` | Generate HTML report (opens in browser) |
849
+ | `--diff-json` | `-J` | Output JSON to stdout (pipe to jq) |
850
+ | `--list-files` | `-l` | List files without processing (takes precedence over --format-only) |
851
+ | `--show-config` | | Display resolved config after inheritance |
852
+ | `--format-only` | | Format destination files only (source not required) |
853
+ | `--skip-format` | `-S` | Skip YAML formatting during sync |
854
+ | `--filter <string>` | `-f` | Filter files by filename/content (supports `,` OR, `+` AND) |
855
+ | `--mode <type>` | `-m` | Filter by change type: new, modified, deleted, all (default: all) |
856
+ | `--my [days]` | | Filter to source files you modified in git in the last N days (default: 30) |
857
+ | `--no-color` | | Disable colored output (CI/accessibility) |
858
+ | `--verbose` | | Show detailed debug info |
859
+ | `--quiet` | | Suppress output except errors |
836
860
 
837
861
  ### Examples
838
862
 
@@ -885,6 +909,12 @@ hed -c config.yaml -m modified -D -d
885
909
  # Combine filter and mode
886
910
  hed -c config.yaml -f deployment -m modified -D -d
887
911
 
912
+ # Sync only files you modified in the last 30 days (auto-detects your git identity)
913
+ hed -c config.yaml --my -D -d
914
+
915
+ # Sync only files you modified in the last 7 days
916
+ hed -c config.yaml --my 7 -D -d
917
+
888
918
  # Format destination files only (no sync, source not required in config)
889
919
  hed -c config.yaml --format-only
890
920
 
@@ -18,5 +18,7 @@ export type SyncCommand = {
18
18
  suggestThreshold: number;
19
19
  filter?: string;
20
20
  mode: ChangeMode;
21
+ my: boolean;
22
+ myDays: number;
21
23
  };
22
24
  export declare const parseCommandLine: (argv?: string[]) => SyncCommand;
@@ -28,6 +28,7 @@ const parseCommandLine = (argv) => {
28
28
  .option('--no-color', 'Disable colored output')
29
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
+ .option('--my [days]', 'Filter source files to those you modified in the last N days (default: 30)')
31
32
  .option('--verbose', 'Show detailed debug information', false)
32
33
  .option('--quiet', 'Suppress all output except critical errors', false)
33
34
  .addHelpText('after', `
@@ -62,6 +63,12 @@ Examples:
62
63
  # Preview modified files only
63
64
  $ helm-env-delta --config config.yaml --mode modified --dry-run --diff
64
65
 
66
+ # Sync only files you modified in the last 30 days
67
+ $ helm-env-delta --config config.yaml --my --dry-run --diff
68
+
69
+ # Sync only files you modified in the last 7 days
70
+ $ helm-env-delta --config config.yaml --my 7 --diff
71
+
65
72
  Documentation: https://github.com/balazscsaba2006/helm-env-delta
66
73
  `);
67
74
  program.showSuggestionAfterError(true);
@@ -85,6 +92,15 @@ Documentation: https://github.com/balazscsaba2006/helm-env-delta
85
92
  console.error('Error: --mode must be one of: ' + validModes.join(', '));
86
93
  process.exit(1);
87
94
  }
95
+ let myDays = 30;
96
+ if (options['my'] !== undefined && options['my'] !== true) {
97
+ const parsed = Number.parseInt(options['my'], 10);
98
+ if (Number.isNaN(parsed) || parsed < 1) {
99
+ console.error('Error: --my days must be a positive integer');
100
+ process.exit(1);
101
+ }
102
+ myDays = parsed;
103
+ }
88
104
  return {
89
105
  config: options['config'],
90
106
  dryRun: options['dryRun'],
@@ -103,7 +119,9 @@ Documentation: https://github.com/balazscsaba2006/helm-env-delta
103
119
  suggest: options['suggest'],
104
120
  suggestThreshold: threshold,
105
121
  filter: options['filter'],
106
- mode: options['mode']
122
+ mode: options['mode'],
123
+ my: options['my'] !== undefined && options['my'] !== false,
124
+ myDays
107
125
  };
108
126
  };
109
127
  exports.parseCommandLine = parseCommandLine;
package/dist/index.js CHANGED
@@ -55,6 +55,7 @@ const commentOnlyDetector_1 = require("./utils/commentOnlyDetector");
55
55
  const fileFilter_1 = require("./utils/fileFilter");
56
56
  const filenameTransformer_1 = require("./utils/filenameTransformer");
57
57
  const fileType_1 = require("./utils/fileType");
58
+ const gitFilter_1 = require("./utils/gitFilter");
58
59
  const versionChecker_1 = require("./utils/versionChecker");
59
60
  const main = async () => {
60
61
  const command = (0, commandLine_1.parseCommandLine)();
@@ -117,6 +118,16 @@ const main = async () => {
117
118
  sourceFiles = filtered.sourceFiles;
118
119
  destinationFiles = filtered.destinationFiles;
119
120
  }
121
+ if (command.my) {
122
+ const absoluteSourceDirectory = node_path_1.default.isAbsolute(validationConfig.source)
123
+ ? validationConfig.source
124
+ : node_path_1.default.resolve(process.cwd(), validationConfig.source);
125
+ const author = await (0, gitFilter_1.getGitUser)();
126
+ const filtered = await (0, gitFilter_1.filterFileMapsByGitAuthor)(sourceFiles, destinationFiles, absoluteSourceDirectory, author, command.myDays);
127
+ sourceFiles = filtered.sourceFiles;
128
+ destinationFiles = filtered.destinationFiles;
129
+ logger.progress(`--my filter (${command.myDays} days, author: "${author}") matched ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'info');
130
+ }
120
131
  logger.progress(`Loaded ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'success');
121
132
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating pattern usage...', 'info'));
122
133
  const usageResult = (0, pipeline_1.validatePatternUsage)(validationConfig, sourceFiles, destinationFiles);
@@ -126,6 +137,8 @@ const main = async () => {
126
137
  for (const warning of usageResult.warnings) {
127
138
  const contextString = warning.context ? chalk_1.default.dim(` (${warning.context})`) : '';
128
139
  console.warn(chalk_1.default.yellow(` β€’ ${warning.message}${contextString}`));
140
+ if (warning.hint)
141
+ console.warn(chalk_1.default.dim(` Hint: ${warning.hint}`));
129
142
  }
130
143
  }
131
144
  if (hasAnyWarnings)
@@ -237,6 +250,16 @@ const main = async () => {
237
250
  destinationFiles = filtered.destinationFiles;
238
251
  logger.progress(`Filter '${command.filter}' matched ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'info');
239
252
  }
253
+ if (command.my) {
254
+ const absoluteSourceDirectory = node_path_1.default.isAbsolute(syncConfig.source)
255
+ ? syncConfig.source
256
+ : node_path_1.default.resolve(process.cwd(), syncConfig.source);
257
+ const author = await (0, gitFilter_1.getGitUser)();
258
+ const filtered = await (0, gitFilter_1.filterFileMapsByGitAuthor)(sourceFiles, destinationFiles, absoluteSourceDirectory, author, command.myDays);
259
+ sourceFiles = filtered.sourceFiles;
260
+ destinationFiles = filtered.destinationFiles;
261
+ logger.progress(`--my filter (${command.myDays} days, author: "${author}") matched ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'info');
262
+ }
240
263
  if (command.listFiles) {
241
264
  const sourceFilesList = [...sourceFiles.keys()].toSorted();
242
265
  const destinationFilesList = [...destinationFiles.keys()].toSorted();
@@ -308,11 +331,22 @@ const main = async () => {
308
331
  console.log(` ${chalk_1.default.red('Deleted:')} ${diffResult.deletedFiles.length} files (${syncConfig.prune ? 'prune enabled' : 'prune disabled'})`);
309
332
  console.log(` ${chalk_1.default.blue('Unchanged:')} ${diffResult.unchangedFiles.length} files`);
310
333
  console.log(chalk_1.default.dim('─'.repeat(60)));
311
- if (diffResult.deletedFiles.length > 0 && syncConfig.prune)
312
- console.warn(chalk_1.default.red('⚠️ Warning: Prune is enabled. Files will be permanently deleted!'));
313
- console.log(chalk_1.default.dim('\nPress Ctrl+C to cancel, or use --dry-run to preview changes first.\n'));
314
- if (syncConfig.confirmationDelay > 0)
315
- await new Promise((resolve) => setTimeout(resolve, syncConfig.confirmationDelay));
334
+ if (diffResult.deletedFiles.length > 0 && syncConfig.prune) {
335
+ console.warn(chalk_1.default.red('⚠️ Warning: Prune is enabled. The following files will be permanently deleted:'));
336
+ for (const f of diffResult.deletedFiles)
337
+ console.warn(chalk_1.default.red(` - ${f}`));
338
+ }
339
+ if (syncConfig.confirmationDelay > 0) {
340
+ const totalSeconds = Math.ceil(syncConfig.confirmationDelay / 1000);
341
+ console.log(chalk_1.default.dim('\nPress Ctrl+C to cancel.\n'));
342
+ for (let remaining = totalSeconds; remaining > 0; remaining--) {
343
+ process.stdout.write(chalk_1.default.dim(` Proceeding in ${remaining}s...\r`));
344
+ await new Promise((resolve) => setTimeout(resolve, 1000));
345
+ }
346
+ process.stdout.write(' '.repeat(40) + '\r');
347
+ }
348
+ else
349
+ console.log(chalk_1.default.dim('\nPress Ctrl+C to cancel, or use --dry-run to preview changes first.\n'));
316
350
  }
317
351
  const formattedFiles = await (0, pipeline_1.updateFiles)(diffResult, sourceFiles, destinationFiles, syncConfig, command.dryRun, command.skipFormat, logger);
318
352
  if (command.diffHtml && !command.quiet)
@@ -349,6 +383,8 @@ const main = async () => {
349
383
  console.error(error.message);
350
384
  else if ((0, fileFilter_1.isFilterParseError)(error))
351
385
  console.error(error.message);
386
+ else if ((0, gitFilter_1.isGitFilterError)(error))
387
+ console.error(error.message);
352
388
  else if (error instanceof Error)
353
389
  console.error('Unexpected error:', error.message);
354
390
  else
@@ -22,7 +22,7 @@ class FileDiffError extends FileDiffErrorClass {
22
22
  }
23
23
  exports.FileDiffError = FileDiffError;
24
24
  exports.isFileDiffError = (0, errors_1.createErrorTypeGuard)(FileDiffError);
25
- const processAddedFileContent = (filePath, content, transforms, fixedValues, outputFormat) => {
25
+ const processAddedFileContent = (filePath, content, transforms, fixedValues, outputFormat, logger) => {
26
26
  if (!(0, fileType_1.isYamlFile)(filePath))
27
27
  return content;
28
28
  try {
@@ -35,16 +35,17 @@ const processAddedFileContent = (filePath, content, transforms, fixedValues, out
35
35
  processed = (0, yamlFormatter_1.formatYaml)(processed, filePath, outputFormat);
36
36
  return processed;
37
37
  }
38
- catch {
38
+ catch (error) {
39
+ logger?.warn(`Warning: Could not process added file '${filePath}' (${error instanceof Error ? error.message : String(error)}). Using raw content.`, 'normal');
39
40
  return content;
40
41
  }
41
42
  };
42
- const detectAddedFiles = (sourceFiles, destinationFiles, config, originalPaths) => {
43
+ const detectAddedFiles = (sourceFiles, destinationFiles, config, originalPaths, logger) => {
43
44
  const addedFiles = [];
44
45
  for (const [path, content] of sourceFiles.entries())
45
46
  if (!destinationFiles.has(path)) {
46
47
  const originalPath = originalPaths?.get(path);
47
- const processedContent = processAddedFileContent(path, content, config.transforms, config.fixedValues, config.outputFormat);
48
+ const processedContent = processAddedFileContent(path, content, config.transforms, config.fixedValues, config.outputFormat, logger);
48
49
  addedFiles.push({
49
50
  path,
50
51
  originalPath,
@@ -279,7 +280,7 @@ const computeFileDiff = (sourceFiles, destinationFiles, config, logger, original
279
280
  if (skipPathCount > 0)
280
281
  logger.debug(` SkipPath patterns: ${skipPathCount}`);
281
282
  }
282
- const addedFiles = detectAddedFiles(sourceFiles, destinationFiles, config, originalPaths);
283
+ const addedFiles = detectAddedFiles(sourceFiles, destinationFiles, config, originalPaths, logger);
283
284
  const deletedFiles = config.prune ? detectDeletedFiles(sourceFiles, destinationFiles) : [];
284
285
  const { changedFiles, unchangedFiles } = processChangedFiles(sourceFiles, destinationFiles, config.skipPath, config.transforms, config.fixedValues, originalPaths);
285
286
  return { addedFiles, deletedFiles, changedFiles, unchangedFiles };
@@ -9,5 +9,6 @@ export interface PatternUsageWarning {
9
9
  pattern: string;
10
10
  message: string;
11
11
  context?: string;
12
+ hint?: string;
12
13
  }
13
14
  export declare const validatePatternUsage: (config: FinalConfig, sourceFiles: FileMap, destinationFiles: FileMap) => PatternUsageResult;
@@ -59,7 +59,8 @@ const validateSkipPathPatterns = (config, sourceFiles, destinationFiles) => {
59
59
  type: 'unused-skipPath-jsonpath',
60
60
  pattern,
61
61
  message: `skipPath JSONPath '${jsonPath}' not found in any matched files`,
62
- context: `Pattern: ${pattern}, matches ${yamlFiles.length} file(s)`
62
+ context: `Pattern: ${pattern}, matches ${yamlFiles.length} file(s)`,
63
+ hint: 'Run with --list-files to see which files matched, and --validate for full pattern analysis'
63
64
  });
64
65
  }
65
66
  }
@@ -93,7 +94,8 @@ const validateStopRulePatterns = (config, sourceFiles, destinationFiles) => {
93
94
  type: 'unused-stopRule-path',
94
95
  pattern: globPattern,
95
96
  message: `stopRules JSONPath '${rule.path}' not found in any matched files`,
96
- context: `Rule type: ${rule.type}, matches ${yamlFiles.length} file(s)`
97
+ context: `Rule type: ${rule.type}, matches ${yamlFiles.length} file(s)`,
98
+ hint: 'Run with --list-files to see which files are loaded'
97
99
  });
98
100
  }
99
101
  }
@@ -126,7 +128,8 @@ const validateFixedValuesPatterns = (config, sourceFiles, destinationFiles) => {
126
128
  type: 'unused-fixedValues-jsonpath',
127
129
  pattern,
128
130
  message: `fixedValues JSONPath '${rule.path}' not found in any matched files`,
129
- context: `Pattern: ${pattern}, matches ${yamlFiles.length} file(s)`
131
+ context: `Pattern: ${pattern}, matches ${yamlFiles.length} file(s)`,
132
+ hint: 'Run with --list-files to see which files matched'
130
133
  });
131
134
  }
132
135
  }
@@ -88,7 +88,16 @@ const showConsoleDiff = (diffResult, config) => {
88
88
  if (diffResult.addedFiles.length === 0 &&
89
89
  diffResult.changedFiles.length === 0 &&
90
90
  diffResult.deletedFiles.length === 0) {
91
- console.log(chalk_1.default.green.bold('\nβœ“ No differences found\n'));
91
+ const totalCompared = diffResult.unchangedFiles.length;
92
+ const hasSkipPath = config.skipPath && Object.keys(config.skipPath).length > 0;
93
+ const skipNote = hasSkipPath ? chalk_1.default.dim(' (some paths may be excluded via skipPath)') : '';
94
+ if (totalCompared === 0)
95
+ console.log(chalk_1.default.yellow.bold('\n⚠ No files matched the include/exclude patterns\n'));
96
+ else
97
+ console.log(chalk_1.default.green.bold(`\nβœ“ No differences found`) +
98
+ chalk_1.default.dim(` β€” ${totalCompared} file(s) compared, all identical`) +
99
+ skipNote +
100
+ '\n');
92
101
  return;
93
102
  }
94
103
  console.log(formatAddedFiles(diffResult.addedFiles));
@@ -0,0 +1,17 @@
1
+ import { type SimpleGit } from 'simple-git';
2
+ import type { FileMap } from '../pipeline';
3
+ export declare const isGitFilterError: (error: unknown) => error is {
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
+ export declare const getGitUser: (git?: SimpleGit) => Promise<string>;
14
+ export declare const filterFileMapsByGitAuthor: (sourceFiles: FileMap, destinationFiles: FileMap, absoluteSourceDirectory: string, author: string, days: number) => Promise<{
15
+ sourceFiles: FileMap;
16
+ destinationFiles: FileMap;
17
+ }>;
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.filterFileMapsByGitAuthor = exports.getGitUser = exports.isGitFilterError = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
8
+ const simple_git_1 = __importDefault(require("simple-git"));
9
+ const errors_1 = require("./errors");
10
+ const GitFilterError = (0, errors_1.createErrorClass)('GitFilterError', {
11
+ NOT_GIT_REPO: 'Current directory is not a git repository.',
12
+ NO_GIT_USER: 'No git user configured. Run: git config user.name "Your Name"',
13
+ GIT_COMMAND_FAILED: 'Git command failed'
14
+ });
15
+ exports.isGitFilterError = (0, errors_1.createErrorTypeGuard)(GitFilterError);
16
+ const getGitRoot = async (git) => {
17
+ const result = await git.revparse(['--show-toplevel']);
18
+ return result.trim();
19
+ };
20
+ const getGitUser = async (git) => {
21
+ const g = git ?? (0, simple_git_1.default)();
22
+ try {
23
+ const nameResult = await g.raw(['config', 'user.name']);
24
+ const name = nameResult.trim();
25
+ if (name)
26
+ return name;
27
+ const emailResult = await g.raw(['config', 'user.email']);
28
+ const email = emailResult.trim();
29
+ if (email)
30
+ return email;
31
+ }
32
+ catch (error) {
33
+ if (error instanceof Error && error.message.toLowerCase().includes('not a git repository'))
34
+ throw new GitFilterError('Current directory is not a git repository.', { code: 'NOT_GIT_REPO' });
35
+ throw new GitFilterError('Git command failed', { code: 'GIT_COMMAND_FAILED', cause: error });
36
+ }
37
+ throw new GitFilterError('No git user configured. Run: git config user.name "Your Name"', { code: 'NO_GIT_USER' });
38
+ };
39
+ exports.getGitUser = getGitUser;
40
+ const getGitModifiedPaths = async (git, author, days, absoluteSourceDirectory) => {
41
+ try {
42
+ const output = await git.raw([
43
+ 'log',
44
+ `--author=${author}`,
45
+ `--since=${days} days ago`,
46
+ '--name-only',
47
+ '--pretty=format:',
48
+ '--',
49
+ absoluteSourceDirectory
50
+ ]);
51
+ const paths = new Set();
52
+ for (const line of output.split('\n')) {
53
+ const trimmed = line.trim();
54
+ if (trimmed)
55
+ paths.add(trimmed);
56
+ }
57
+ return paths;
58
+ }
59
+ catch (error) {
60
+ if (error instanceof Error && error.message.toLowerCase().includes('not a git repository'))
61
+ throw new GitFilterError('Current directory is not a git repository.', { code: 'NOT_GIT_REPO' });
62
+ throw new GitFilterError('Git command failed', { code: 'GIT_COMMAND_FAILED', cause: error });
63
+ }
64
+ };
65
+ const filterFileMapsByGitAuthor = async (sourceFiles, destinationFiles, absoluteSourceDirectory, author, days) => {
66
+ const git = (0, simple_git_1.default)();
67
+ const gitRoot = await getGitRoot(git);
68
+ const gitModifiedPaths = await getGitModifiedPaths(git, author, days, absoluteSourceDirectory);
69
+ const matchingKeys = new Set();
70
+ for (const gitRelativePath of gitModifiedPaths) {
71
+ const absolutePath = node_path_1.default.join(gitRoot, gitRelativePath);
72
+ const fileMapKey = node_path_1.default.relative(absoluteSourceDirectory, absolutePath).replaceAll(node_path_1.default.sep, '/');
73
+ matchingKeys.add(fileMapKey);
74
+ }
75
+ const filteredSource = new Map();
76
+ const filteredDestination = new Map();
77
+ for (const [filePath, content] of sourceFiles)
78
+ if (matchingKeys.has(filePath))
79
+ filteredSource.set(filePath, content);
80
+ for (const [filePath, content] of destinationFiles)
81
+ if (matchingKeys.has(filePath))
82
+ filteredDestination.set(filePath, content);
83
+ return { sourceFiles: filteredSource, destinationFiles: filteredDestination };
84
+ };
85
+ exports.filterFileMapsByGitAuthor = filterFileMapsByGitAuthor;
@@ -20,6 +20,36 @@ const getAllValuesRecursive = (data) => {
20
20
  return values;
21
21
  };
22
22
  exports.getAllValuesRecursive = getAllValuesRecursive;
23
+ const getAllValuesWithPaths = (data) => {
24
+ const results = [];
25
+ const traverse = (node, currentPath) => {
26
+ if (node === null || node === undefined)
27
+ return;
28
+ if (typeof node === 'object')
29
+ if (Array.isArray(node))
30
+ for (const [index, item] of node.entries())
31
+ traverse(item, currentPath ? `${currentPath}.${index}` : String(index));
32
+ else
33
+ for (const [key, value] of Object.entries(node))
34
+ traverse(value, currentPath ? `${currentPath}.${key}` : key);
35
+ else
36
+ results.push({ path: currentPath, value: node });
37
+ };
38
+ traverse(data, '');
39
+ return results;
40
+ };
41
+ const getValueAtDotPath = (data, dotPath) => {
42
+ if (!dotPath)
43
+ return data;
44
+ const parts = dotPath.split('.');
45
+ let current = data;
46
+ for (const part of parts) {
47
+ if (current === null || current === undefined || typeof current !== 'object')
48
+ return undefined;
49
+ current = current[part];
50
+ }
51
+ return current;
52
+ };
23
53
  const validateTargetedRegex = (options) => {
24
54
  const valueToCheck = options.updatedValue === undefined ? options.oldValue : options.updatedValue;
25
55
  if (valueToCheck === undefined)
@@ -46,9 +76,9 @@ const validatePathlessRegex = (options) => {
46
76
  const dataToCheck = options.updatedData === undefined ? options.oldData : options.updatedData;
47
77
  if (dataToCheck === undefined)
48
78
  return undefined;
49
- const allValues = (0, exports.getAllValuesRecursive)(dataToCheck);
50
- for (const value of allValues) {
51
- const stringValue = String(value);
79
+ const allEntries = getAllValuesWithPaths(dataToCheck);
80
+ for (const entry of allEntries) {
81
+ const stringValue = String(entry.value);
52
82
  for (const patternString of options.patterns) {
53
83
  const pattern = new RegExp(patternString);
54
84
  if (pattern.test(stringValue)) {
@@ -58,9 +88,9 @@ const validatePathlessRegex = (options) => {
58
88
  return {
59
89
  file: options.filePath,
60
90
  rule: options.rule,
61
- path: '(global scan)',
62
- oldValue: options.oldData,
63
- updatedValue: options.updatedData,
91
+ path: entry.path,
92
+ oldValue: getValueAtDotPath(options.oldData, entry.path),
93
+ updatedValue: entry.value,
64
94
  message: `Value "${stringValue}" matches forbidden pattern${patternInfo} (found during global scan)`
65
95
  };
66
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.14.2",
3
+ "version": "1.15.0",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -85,6 +85,7 @@
85
85
  "diff2html": "3.4.56",
86
86
  "open": "^11.0.0",
87
87
  "picomatch": "^4.0.3",
88
+ "simple-git": "^3.33.0",
88
89
  "tinyglobby": "^0.2.15",
89
90
  "yaml": "^2.8.2",
90
91
  "zod": "^4.3.6"