helm-env-delta 1.1.2 → 1.2.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
@@ -1,4 +1,4 @@
1
- # HelmEnvDelta v1.1.0
1
+ # HelmEnvDelta
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/helm-env-delta.svg)](https://www.npmjs.com/package/helm-env-delta)
4
4
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
@@ -248,6 +248,7 @@ flowchart LR
248
248
  - **Multiple Reporting Formats**: Console diff, HTML report (visual side-by-side), JSON output (CI/CD integration)
249
249
  - **Prune Mode**: Remove destination files not present in source
250
250
  - **Dry-Run Preview**: Review all changes before applying them
251
+ - **Automatic Update Notifications**: Notifies when newer versions are available on npm (skips in CI/CD environments)
251
252
 
252
253
  ---
253
254
 
@@ -256,9 +257,6 @@ flowchart LR
256
257
  ```bash
257
258
  # Global installation
258
259
  npm install -g helm-env-delta
259
-
260
- # Verify installation
261
- helm-env-delta --version
262
260
  ```
263
261
 
264
262
  **Prerequisites:**
@@ -266,6 +264,8 @@ helm-env-delta --version
266
264
  - Node.js >= 22
267
265
  - npm >= 9
268
266
 
267
+ **Note:** The tool automatically checks for updates on every run and displays a notification if a newer version is available. This check is skipped in CI/CD environments and fails silently if the npm registry is unreachable.
268
+
269
269
  ---
270
270
 
271
271
  ## Quick Start
@@ -666,21 +666,28 @@ hed --config <file> [options]
666
666
 
667
667
  ### Options
668
668
 
669
- | Option | Short | Description | Default |
670
- | ----------------- | ----- | ------------------------------------------- | ------------ |
671
- | `--config <path>` | `-c` | Path to YAML configuration file | **required** |
672
- | `--dry-run` | | Preview changes without writing files | `false` |
673
- | `--force` | | Override stop rules and proceed | `false` |
674
- | `--diff` | | Display console diff for changed files | `false` |
675
- | `--diff-html` | | Generate HTML report and open in browser | `false` |
676
- | `--diff-json` | | Output diff as JSON to stdout | `false` |
677
- | `--skip-format` | | Skip YAML formatting (outputFormat section) | `false` |
678
- | `--version` | `-V` | Show version number | |
679
- | `--help` | `-h` | Display help | |
669
+ | Option | Short | Description | Default |
670
+ | ----------------- | ----- | ---------------------------------------------------------- | ------------ |
671
+ | `--config <path>` | `-c` | Path to YAML configuration file | **required** |
672
+ | `--validate` | | Validate configuration file and exit | `false` |
673
+ | `--dry-run` | | Preview changes without writing files | `false` |
674
+ | `--force` | | Override stop rules and proceed | `false` |
675
+ | `--diff` | | Display console diff for changed files | `false` |
676
+ | `--diff-html` | | Generate HTML report and open in browser | `false` |
677
+ | `--diff-json` | | Output diff as JSON to stdout | `false` |
678
+ | `--skip-format` | | Skip YAML formatting (outputFormat section) | `false` |
679
+ | `--verbose` | | Show detailed debug information (config, transforms, etc.) | `false` |
680
+ | `--quiet` | | Suppress all output except critical errors | `false` |
681
+ | `--help` | `-h` | Display help | |
682
+
683
+ **Note:** `--verbose` and `--quiet` are mutually exclusive. Machine-readable output (`--diff-json`) always outputs regardless of verbosity.
680
684
 
681
685
  ### Examples
682
686
 
683
687
  ```bash
688
+ # Validate configuration file
689
+ helm-env-delta --config config.yaml --validate
690
+
684
691
  # Basic sync
685
692
  helm-env-delta --config config.yaml
686
693
 
@@ -6,5 +6,8 @@ export type SyncCommand = {
6
6
  diffHtml: boolean;
7
7
  diffJson: boolean;
8
8
  skipFormat: boolean;
9
+ validate: boolean;
10
+ verbose: boolean;
11
+ quiet: boolean;
9
12
  };
10
13
  export declare const parseCommandLine: (argv?: string[]) => SyncCommand;
@@ -18,9 +18,16 @@ const parseCommandLine = (argv) => {
18
18
  .option('--diff', 'Display console diff for changed files', false)
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
- .option('--skip-format', 'Skip YAML formatting (outputFormat section)', false);
21
+ .option('--skip-format', 'Skip YAML formatting (outputFormat section)', false)
22
+ .option('--validate', 'Validate configuration file and exit', false)
23
+ .option('--verbose', 'Show detailed debug information', false)
24
+ .option('--quiet', 'Suppress all output except critical errors', false);
22
25
  program.parse(argv || process.argv);
23
26
  const options = program.opts();
27
+ if (options['verbose'] && options['quiet']) {
28
+ console.error('Error: --verbose and --quiet flags are mutually exclusive');
29
+ process.exit(1);
30
+ }
24
31
  return {
25
32
  config: options['config'],
26
33
  dryRun: options['dryRun'],
@@ -28,7 +35,10 @@ const parseCommandLine = (argv) => {
28
35
  diff: options['diff'],
29
36
  diffHtml: options['diffHtml'],
30
37
  diffJson: options['diffJson'],
31
- skipFormat: options['skipFormat']
38
+ skipFormat: options['skipFormat'],
39
+ validate: options['validate'],
40
+ verbose: options['verbose'],
41
+ quiet: options['quiet']
32
42
  };
33
43
  };
34
44
  exports.parseCommandLine = parseCommandLine;
@@ -1,3 +1,3 @@
1
1
  import { type FinalConfig } from './configFile';
2
2
  export type Config = FinalConfig;
3
- export declare const loadConfigFile: (configPath: string) => FinalConfig;
3
+ export declare const loadConfigFile: (configPath: string, quiet?: boolean, logger?: import("./logger").Logger) => FinalConfig;
@@ -3,10 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadConfigFile = void 0;
4
4
  const configFile_1 = require("./configFile");
5
5
  const configMerger_1 = require("./configMerger");
6
- const loadConfigFile = (configPath) => {
7
- const mergedConfig = (0, configMerger_1.resolveConfigWithExtends)(configPath);
6
+ const loadConfigFile = (configPath, quiet = false, logger) => {
7
+ const mergedConfig = (0, configMerger_1.resolveConfigWithExtends)(configPath, new Set(), 0, logger);
8
8
  const config = (0, configFile_1.parseFinalConfig)(mergedConfig, configPath);
9
- console.log(`\nConfiguration loaded: ${config.source} -> ${config.destination}` + (config.prune ? ' [prune!]' : ''));
9
+ if (!quiet)
10
+ console.log(`\nConfiguration loaded: ${config.source} -> ${config.destination}` + (config.prune ? ' [prune!]' : ''));
10
11
  return config;
11
12
  };
12
13
  exports.loadConfigFile = loadConfigFile;
@@ -17,5 +17,5 @@ export declare class ConfigMergerError extends ConfigMergerErrorClass {
17
17
  }
18
18
  export declare const isConfigMergerError: (error: unknown) => error is ConfigMergerError;
19
19
  export declare const mergeConfigs: (parent: BaseConfig, child: BaseConfig) => BaseConfig;
20
- export declare const resolveConfigWithExtends: (configPath: string, visited?: Set<string>, depth?: number) => BaseConfig;
20
+ export declare const resolveConfigWithExtends: (configPath: string, visited?: Set<string>, depth?: number, logger?: import("./logger").Logger) => BaseConfig;
21
21
  export {};
@@ -133,7 +133,7 @@ const mergePerFileRecords = (parent, child) => {
133
133
  merged[pattern] = merged[pattern] === undefined ? [...rules] : [...merged[pattern], ...rules];
134
134
  return merged;
135
135
  };
136
- const resolveConfigWithExtends = (configPath, visited = new Set(), depth = 0) => {
136
+ const resolveConfigWithExtends = (configPath, visited = new Set(), depth = 0, logger) => {
137
137
  if (depth > MAX_EXTENDS_DEPTH) {
138
138
  const depthError = new ConfigMergerError('Extends chain exceeds maximum depth of 5', {
139
139
  code: 'MAX_DEPTH_EXCEEDED',
@@ -202,6 +202,10 @@ const resolveConfigWithExtends = (configPath, visited = new Set(), depth = 0) =>
202
202
  });
203
203
  }
204
204
  const config = (0, configFile_1.parseBaseConfig)(rawConfig, absolutePath);
205
+ if (logger?.shouldShow('debug')) {
206
+ const filename = absolutePath.split('/').pop();
207
+ logger.debug(`Loading config: ${filename} (depth: ${depth})`);
208
+ }
205
209
  if (config.extends === undefined)
206
210
  return config;
207
211
  const configDirectory = node_path_1.default.dirname(absolutePath);
@@ -232,7 +236,9 @@ const resolveConfigWithExtends = (configPath, visited = new Set(), depth = 0) =>
232
236
  cause: error
233
237
  });
234
238
  }
235
- const parentConfig = (0, exports.resolveConfigWithExtends)(parentPath, visitedWithCurrent, depth + 1);
239
+ if (logger?.shouldShow('debug'))
240
+ logger.debug(` Extends: ${config.extends} → ${parentPath.split('/').pop()}`);
241
+ const parentConfig = (0, exports.resolveConfigWithExtends)(parentPath, visitedWithCurrent, depth + 1, logger);
236
242
  return (0, exports.mergeConfigs)(parentConfig, config);
237
243
  };
238
244
  exports.resolveConfigWithExtends = resolveConfigWithExtends;
@@ -1,10 +1,9 @@
1
1
  import { StopRuleViolation } from './stopRulesValidator';
2
- type BoxStyle = 'success' | 'warning' | 'error' | 'info';
3
- type ProgressStyle = 'loading' | 'success' | 'info';
4
- type ViolationMode = 'error' | 'warning' | 'force';
5
- type FileOperation = 'add' | 'update' | 'delete' | 'format';
2
+ export type BoxStyle = 'success' | 'warning' | 'error' | 'info';
3
+ export type ProgressStyle = 'loading' | 'success' | 'info';
4
+ export type ViolationMode = 'error' | 'warning' | 'force';
5
+ export type FileOperation = 'add' | 'update' | 'delete' | 'format';
6
6
  export declare const formatBox: (title: string, content: string[], style?: BoxStyle, width?: number) => string;
7
7
  export declare const formatStopRuleViolation: (violation: StopRuleViolation, mode: ViolationMode) => string;
8
8
  export declare const colorizeFileOperation: (operation: FileOperation, filePath: string, isDryRun: boolean, alreadyDeleted?: boolean) => string;
9
9
  export declare const formatProgressMessage: (message: string, style: ProgressStyle) => string;
10
- export {};
@@ -37,5 +37,5 @@ export declare class FileDiffError extends FileDiffErrorClass {
37
37
  }
38
38
  export declare const isFileDiffError: (error: unknown) => error is FileDiffError;
39
39
  export declare const getSkipPathsForFile: (filePath: string, skipPath?: Record<string, string[]>) => string[];
40
- export declare const computeFileDiff: (sourceFiles: FileMap, destinationFiles: FileMap, config: Config) => FileDiffResult;
40
+ export declare const computeFileDiff: (sourceFiles: FileMap, destinationFiles: FileMap, config: Config, logger?: import("./logger").Logger) => FileDiffResult;
41
41
  export {};
package/dist/fileDiff.js CHANGED
@@ -162,7 +162,18 @@ const processChangedFiles = (sourceFiles, destinationFiles, skipPath, transforms
162
162
  }
163
163
  return { changedFiles, unchangedFiles };
164
164
  };
165
- const computeFileDiff = (sourceFiles, destinationFiles, config) => {
165
+ const computeFileDiff = (sourceFiles, destinationFiles, config, logger) => {
166
+ if (logger?.shouldShow('debug')) {
167
+ logger.debug('Computing file differences:');
168
+ logger.debug(` Source files: ${sourceFiles.size}`);
169
+ logger.debug(` Destination files: ${destinationFiles.size}`);
170
+ const transformCount = Object.keys(config.transforms || {}).length;
171
+ if (transformCount > 0)
172
+ logger.debug(` Content transform patterns: ${transformCount}`);
173
+ const skipPathCount = Object.keys(config.skipPath || {}).length;
174
+ if (skipPathCount > 0)
175
+ logger.debug(` SkipPath patterns: ${skipPathCount}`);
176
+ }
166
177
  const addedFiles = detectAddedFiles(sourceFiles, destinationFiles);
167
178
  const deletedFiles = config.prune ? detectDeletedFiles(sourceFiles, destinationFiles) : [];
168
179
  const { changedFiles, unchangedFiles } = processChangedFiles(sourceFiles, destinationFiles, config.skipPath, config.transforms);
@@ -23,5 +23,5 @@ declare const FileLoaderErrorClass: {
23
23
  export declare class FileLoaderError extends FileLoaderErrorClass {
24
24
  }
25
25
  export declare const isFileLoaderError: (error: unknown) => error is FileLoaderError;
26
- export declare const loadFiles: (options: FileLoaderOptions) => Promise<FileMap>;
26
+ export declare const loadFiles: (options: FileLoaderOptions, logger?: import("./logger").Logger) => Promise<FileMap>;
27
27
  export {};
@@ -147,13 +147,33 @@ const readFilesIntoMap = async (baseDirectory, absoluteFilePaths) => {
147
147
  });
148
148
  }
149
149
  };
150
- const loadFiles = async (options) => {
150
+ const loadFiles = async (options, logger) => {
151
151
  const absoluteBaseDirectory = await validateAndResolveBaseDirectory(options.baseDirectory);
152
152
  const includePatterns = options.include ?? ['**/*'];
153
153
  const excludePatterns = options.exclude ?? [];
154
154
  const files = await findMatchingFiles(absoluteBaseDirectory, includePatterns, excludePatterns, options.transforms);
155
+ if (logger?.shouldShow('debug')) {
156
+ logger.debug('Glob matching:');
157
+ logger.debug(` Directory: ${absoluteBaseDirectory}`);
158
+ logger.debug(` Include patterns: ${includePatterns.join(', ')}`);
159
+ logger.debug(` Exclude patterns: ${excludePatterns.join(', ')}`);
160
+ logger.debug(` Matched: ${files.length} file(s)`);
161
+ }
155
162
  const fileMap = await readFilesIntoMap(absoluteBaseDirectory, files);
156
163
  const transformedMap = options.transforms ? (0, filenameTransformer_1.transformFilenameMap)(fileMap, options.transforms) : fileMap;
164
+ if (options.transforms && logger?.shouldShow('debug')) {
165
+ logger.debug(`Filename transforms applied: ${fileMap.size} → ${transformedMap.size} files`);
166
+ let exampleCount = 0;
167
+ for (const [transformed, content] of transformedMap.entries()) {
168
+ const original = [...fileMap.entries()].find(([_key, c]) => c === content)?.[0];
169
+ if (original && original !== transformed) {
170
+ logger.debug(` ${original} → ${transformed}`);
171
+ exampleCount++;
172
+ if (exampleCount >= 3)
173
+ break;
174
+ }
175
+ }
176
+ }
157
177
  return sortMapByKeys(transformedMap);
158
178
  };
159
179
  exports.loadFiles = loadFiles;
@@ -1,6 +1,7 @@
1
1
  import { Config } from './configFile';
2
2
  import { FileDiffResult } from './fileDiff';
3
3
  import { FileMap } from './fileLoader';
4
+ import { Logger } from './logger';
4
5
  export interface FileUpdateError {
5
6
  operation: 'add' | 'update' | 'delete';
6
7
  path: string;
@@ -23,5 +24,5 @@ declare const FileUpdaterErrorClass: {
23
24
  export declare class FileUpdaterError extends FileUpdaterErrorClass {
24
25
  }
25
26
  export declare const isFileUpdaterError: (error: unknown) => error is FileUpdaterError;
26
- export declare const updateFiles: (diffResult: FileDiffResult, sourceFiles: FileMap, destinationFiles: FileMap, config: Config, dryRun: boolean, skipFormat?: boolean) => Promise<string[]>;
27
+ export declare const updateFiles: (diffResult: FileDiffResult, sourceFiles: FileMap, destinationFiles: FileMap, config: Config, dryRun: boolean, skipFormat: boolean, logger: Logger) => Promise<string[]>;
27
28
  export {};
@@ -117,10 +117,10 @@ const mergeYamlContent = (destinationContent, processedSourceContent, filePath)
117
117
  });
118
118
  }
119
119
  };
120
- const addFile = async (relativePath, content, absoluteDestinationDirectory, config, dryRun, skipFormat = false) => {
120
+ const addFile = async (relativePath, content, absoluteDestinationDirectory, config, dryRun, skipFormat, logger) => {
121
121
  const absolutePath = node_path_1.default.join(absoluteDestinationDirectory, relativePath);
122
122
  if (dryRun) {
123
- console.log((0, consoleFormatter_1.colorizeFileOperation)('add', relativePath, true));
123
+ logger.fileOp('add', relativePath, true);
124
124
  return;
125
125
  }
126
126
  let contentToWrite = content;
@@ -142,7 +142,7 @@ const addFile = async (relativePath, content, absoluteDestinationDirectory, conf
142
142
  try {
143
143
  await ensureParentDirectory(absolutePath);
144
144
  await (0, promises_1.writeFile)(absolutePath, contentToWrite, 'utf8');
145
- console.log((0, consoleFormatter_1.colorizeFileOperation)('add', relativePath, false));
145
+ logger.fileOp('add', relativePath, false);
146
146
  }
147
147
  catch (error) {
148
148
  throw new FileUpdaterError('Failed to add file', {
@@ -152,10 +152,10 @@ const addFile = async (relativePath, content, absoluteDestinationDirectory, conf
152
152
  });
153
153
  }
154
154
  };
155
- const updateFile = async (changedFile, absoluteDestinationDirectory, config, dryRun, skipFormat = false) => {
155
+ const updateFile = async (changedFile, absoluteDestinationDirectory, config, dryRun, skipFormat, logger) => {
156
156
  const absolutePath = node_path_1.default.join(absoluteDestinationDirectory, changedFile.path);
157
157
  if (dryRun) {
158
- console.log((0, consoleFormatter_1.colorizeFileOperation)('update', changedFile.path, true));
158
+ logger.fileOp('update', changedFile.path, true);
159
159
  return;
160
160
  }
161
161
  let contentToWrite = (0, fileType_1.isYamlFile)(changedFile.path)
@@ -168,7 +168,7 @@ const updateFile = async (changedFile, absoluteDestinationDirectory, config, dry
168
168
  try {
169
169
  await ensureParentDirectory(absolutePath);
170
170
  await (0, promises_1.writeFile)(absolutePath, contentToWrite, 'utf8');
171
- console.log((0, consoleFormatter_1.colorizeFileOperation)('update', changedFile.path, false));
171
+ logger.fileOp('update', changedFile.path, false);
172
172
  }
173
173
  catch (error) {
174
174
  throw new FileUpdaterError('Failed to update file', {
@@ -178,19 +178,19 @@ const updateFile = async (changedFile, absoluteDestinationDirectory, config, dry
178
178
  });
179
179
  }
180
180
  };
181
- const deleteFile = async (relativePath, absoluteDestinationDirectory, dryRun) => {
181
+ const deleteFile = async (relativePath, absoluteDestinationDirectory, dryRun, logger) => {
182
182
  const absolutePath = node_path_1.default.join(absoluteDestinationDirectory, relativePath);
183
183
  if (dryRun) {
184
- console.log((0, consoleFormatter_1.colorizeFileOperation)('delete', relativePath, true));
184
+ logger.fileOp('delete', relativePath, true);
185
185
  return;
186
186
  }
187
187
  try {
188
188
  await (0, promises_1.unlink)(absolutePath);
189
- console.log((0, consoleFormatter_1.colorizeFileOperation)('delete', relativePath, false));
189
+ logger.fileOp('delete', relativePath, false);
190
190
  }
191
191
  catch (error) {
192
192
  if (error.code === 'ENOENT') {
193
- console.log((0, consoleFormatter_1.colorizeFileOperation)('delete', relativePath, false, true));
193
+ logger.fileOp('delete', relativePath, false, true);
194
194
  return;
195
195
  }
196
196
  throw new FileUpdaterError('Failed to delete file', {
@@ -200,21 +200,25 @@ const deleteFile = async (relativePath, absoluteDestinationDirectory, dryRun) =>
200
200
  });
201
201
  }
202
202
  };
203
- const updateFiles = async (diffResult, sourceFiles, destinationFiles, config, dryRun, skipFormat = false) => {
204
- console.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Updating files...', 'info'));
203
+ const updateFiles = async (diffResult, sourceFiles, destinationFiles, config, dryRun, skipFormat, logger) => {
204
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Updating files...', 'info'));
205
205
  const absoluteDestinationDirectory = await validateDestinationDirectory(config.destination);
206
206
  const errors = [];
207
+ if (logger.shouldShow('debug'))
208
+ logger.debug(`Processing ${diffResult.addedFiles.length} new files`);
207
209
  for (const relativePath of diffResult.addedFiles)
208
210
  try {
209
211
  const content = sourceFiles.get(relativePath);
210
- await addFile(relativePath, content, absoluteDestinationDirectory, config, dryRun, skipFormat);
212
+ await addFile(relativePath, content, absoluteDestinationDirectory, config, dryRun, skipFormat, logger);
211
213
  }
212
214
  catch (error) {
213
215
  errors.push({ operation: 'add', path: relativePath, error: error });
214
216
  }
217
+ if (logger.shouldShow('debug'))
218
+ logger.debug(`Updating ${diffResult.changedFiles.length} changed files`);
215
219
  for (const changedFile of diffResult.changedFiles)
216
220
  try {
217
- await updateFile(changedFile, absoluteDestinationDirectory, config, dryRun, skipFormat);
221
+ await updateFile(changedFile, absoluteDestinationDirectory, config, dryRun, skipFormat, logger);
218
222
  }
219
223
  catch (error) {
220
224
  errors.push({ operation: 'update', path: changedFile.path, error: error });
@@ -229,11 +233,11 @@ const updateFiles = async (diffResult, sourceFiles, destinationFiles, config, dr
229
233
  if (formatted !== content) {
230
234
  const absolutePath = node_path_1.default.join(absoluteDestinationDirectory, relativePath);
231
235
  if (dryRun)
232
- console.log((0, consoleFormatter_1.colorizeFileOperation)('format', relativePath, true));
236
+ logger.fileOp('format', relativePath, true);
233
237
  else {
234
238
  await ensureParentDirectory(absolutePath);
235
239
  await (0, promises_1.writeFile)(absolutePath, formatted, 'utf8');
236
- console.log((0, consoleFormatter_1.colorizeFileOperation)('format', relativePath, false));
240
+ logger.fileOp('format', relativePath, false);
237
241
  }
238
242
  formattedFiles.push(relativePath);
239
243
  }
@@ -243,29 +247,29 @@ const updateFiles = async (diffResult, sourceFiles, destinationFiles, config, dr
243
247
  }
244
248
  for (const relativePath of diffResult.deletedFiles)
245
249
  try {
246
- await deleteFile(relativePath, absoluteDestinationDirectory, dryRun);
250
+ await deleteFile(relativePath, absoluteDestinationDirectory, dryRun, logger);
247
251
  }
248
252
  catch (error) {
249
253
  errors.push({ operation: 'delete', path: relativePath, error: error });
250
254
  }
251
255
  if (dryRun) {
252
- console.log('\n[DRY RUN] Would perform:');
253
- console.log(` ${diffResult.addedFiles.length} files would be added`);
254
- console.log(` ${diffResult.changedFiles.length} files would be updated`);
255
- console.log(` ${formattedFiles.length} files would be formatted`);
256
- console.log(` ${diffResult.deletedFiles.length} files would be deleted`);
256
+ logger.log('\n[DRY RUN] Would perform:');
257
+ logger.log(` ${diffResult.addedFiles.length} files would be added`);
258
+ logger.log(` ${diffResult.changedFiles.length} files would be updated`);
259
+ logger.log(` ${formattedFiles.length} files would be formatted`);
260
+ logger.log(` ${diffResult.deletedFiles.length} files would be deleted`);
257
261
  }
258
262
  else {
259
- console.log('\n✓ Files updated successfully:');
260
- console.log(` ${diffResult.addedFiles.length} files added`);
261
- console.log(` ${diffResult.changedFiles.length} files updated`);
262
- console.log(` ${formattedFiles.length} files formatted`);
263
- console.log(` ${diffResult.deletedFiles.length} files deleted`);
263
+ logger.log('\n✓ Files updated successfully:');
264
+ logger.log(` ${diffResult.addedFiles.length} files added`);
265
+ logger.log(` ${diffResult.changedFiles.length} files updated`);
266
+ logger.log(` ${formattedFiles.length} files formatted`);
267
+ logger.log(` ${diffResult.deletedFiles.length} files deleted`);
264
268
  }
265
269
  if (errors.length > 0) {
266
- console.error(`\n❌ Encountered ${errors.length} error(s):`);
270
+ logger.error(`\n❌ Encountered ${errors.length} error(s):`, 'critical');
267
271
  for (const { operation, path: errorPath, error } of errors)
268
- console.error(` [${operation}] ${errorPath}: ${error.message}`);
272
+ logger.error(` [${operation}] ${errorPath}: ${error.message}`, 'critical');
269
273
  throw new FileUpdaterError(`Failed to update ${errors.length} file(s)`, { code: 'UPDATE_FAILED' });
270
274
  }
271
275
  return formattedFiles;
@@ -23,5 +23,5 @@ declare const HtmlReporterErrorClass: {
23
23
  export declare class HtmlReporterError extends HtmlReporterErrorClass {
24
24
  }
25
25
  export declare const isHtmlReporterError: (error: unknown) => error is HtmlReporterError;
26
- export declare const generateHtmlReport: (diffResult: FileDiffResult, formattedFiles: string[], config: Config, dryRun: boolean) => Promise<void>;
26
+ export declare const generateHtmlReport: (diffResult: FileDiffResult, formattedFiles: string[], config: Config, dryRun: boolean, logger?: import("./logger").Logger) => Promise<void>;
27
27
  export {};
@@ -585,7 +585,7 @@ const openInBrowser = async (filePath) => {
585
585
  throw openError;
586
586
  }
587
587
  };
588
- const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun) => {
588
+ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun, logger) => {
589
589
  const reportPath = generateTemporaryFilePath();
590
590
  const metadata = {
591
591
  timestamp: new Date().toISOString(),
@@ -598,14 +598,14 @@ const generateHtmlReport = async (diffResult, formattedFiles, config, dryRun) =>
598
598
  const changedSections = diffResult.changedFiles.map((file) => generateChangedFileSection(file));
599
599
  const htmlContent = generateHtmlTemplate(diffResult, formattedFiles, trulyUnchangedFiles, metadata, changedSections);
600
600
  await writeHtmlFile(htmlContent, reportPath);
601
- console.log(`✓ HTML report generated: ${reportPath}, opening in browser...`);
601
+ logger?.log(`✓ HTML report generated: ${reportPath}, opening in browser...`);
602
602
  try {
603
603
  await openInBrowser(reportPath);
604
604
  }
605
605
  catch {
606
606
  const absolutePath = node_path_1.default.resolve(reportPath);
607
- console.log('⚠ Could not open browser automatically. Please open manually:');
608
- console.log(` file://${absolutePath}`);
607
+ logger?.log('⚠ Could not open browser automatically. Please open manually:');
608
+ logger?.log(` file://${absolutePath}`);
609
609
  }
610
610
  };
611
611
  exports.generateHtmlReport = generateHtmlReport;
package/dist/index.js CHANGED
@@ -14,58 +14,79 @@ const fileLoader_1 = require("./fileLoader");
14
14
  const fileUpdater_1 = require("./fileUpdater");
15
15
  const htmlReporter_1 = require("./htmlReporter");
16
16
  const jsonReporter_1 = require("./jsonReporter");
17
+ const logger_1 = require("./logger");
17
18
  const stopRulesValidator_1 = require("./stopRulesValidator");
18
19
  const collisionDetector_1 = require("./utils/collisionDetector");
19
20
  const filenameTransformer_1 = require("./utils/filenameTransformer");
21
+ const versionChecker_1 = require("./utils/versionChecker");
20
22
  const ZodError_1 = require("./ZodError");
21
23
  const main = async () => {
22
- console.log(`Now you run ${package_json_1.default.name} v${package_json_1.default.version}...`);
23
24
  const command = (0, commandLine_1.parseCommandLine)();
24
- const config = (0, configLoader_1.loadConfigFile)(command.config);
25
- console.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
25
+ const verbosityLevel = command.verbose ? 'verbose' : command.quiet ? 'quiet' : 'normal';
26
+ const logger = new logger_1.Logger({ level: verbosityLevel, isDiffJson: command.diffJson });
27
+ logger.log(`Now you run ${package_json_1.default.name} v${package_json_1.default.version}...`);
28
+ const config = (0, configLoader_1.loadConfigFile)(command.config, command.quiet, logger);
29
+ if (command.validate) {
30
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Configuration is valid', 'success'));
31
+ return;
32
+ }
33
+ if (logger.shouldShow('debug')) {
34
+ logger.debug('\nConfig details:');
35
+ logger.debug(` Source: ${config.source}`);
36
+ logger.debug(` Destination: ${config.destination}`);
37
+ logger.debug(` Include patterns: ${config.include.join(', ')}`);
38
+ logger.debug(` Exclude patterns: ${config.exclude.join(', ')}`);
39
+ logger.debug(` Transforms: ${Object.keys(config.transforms || {}).length} pattern(s)`);
40
+ logger.debug(` Prune enabled: ${config.prune}`);
41
+ }
42
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
26
43
  const sourceFiles = await (0, fileLoader_1.loadFiles)({
27
44
  baseDirectory: config.source,
28
45
  include: config.include,
29
46
  exclude: config.exclude,
30
47
  transforms: config.transforms
31
- });
32
- console.log((0, consoleFormatter_1.formatProgressMessage)(`Loaded ${sourceFiles.size} source file(s)`, 'success'));
48
+ }, logger);
49
+ logger.progress(`Loaded ${sourceFiles.size} source file(s)`, 'success');
33
50
  const collisions = (0, collisionDetector_1.detectCollisions)(sourceFiles, config.transforms);
34
51
  if (collisions.length > 0)
35
52
  (0, collisionDetector_1.validateNoCollisions)(collisions);
53
+ if (logger.shouldShow('debug'))
54
+ logger.debug('Filename collision check: passed');
36
55
  const destinationFiles = await (0, fileLoader_1.loadFiles)({
37
56
  baseDirectory: config.destination,
38
57
  include: config.include,
39
58
  exclude: config.exclude
40
- });
41
- console.log((0, consoleFormatter_1.formatProgressMessage)(`Loaded ${destinationFiles.size} destination file(s)`, 'success'));
42
- console.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
43
- const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config);
44
- if (command.diff)
59
+ }, logger);
60
+ logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
61
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
62
+ const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger);
63
+ if (logger.shouldShow('debug'))
64
+ logger.debug('Diff pipeline: parse → transforms → skipPath → normalize → compare');
65
+ if (command.diff && !command.quiet)
45
66
  (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, config);
46
67
  else {
47
- console.log(` New files: ${diffResult.addedFiles.length}`);
48
- console.log(` Deleted files: ${diffResult.deletedFiles.length}`);
49
- console.log(` Changed files: ${diffResult.changedFiles.length}`);
50
- console.log(` Unchanged files: ${diffResult.unchangedFiles.length}`);
68
+ logger.log(` New files: ${diffResult.addedFiles.length}`);
69
+ logger.log(` Deleted files: ${diffResult.deletedFiles.length}`);
70
+ logger.log(` Changed files: ${diffResult.changedFiles.length}`);
71
+ logger.log(` Unchanged files: ${diffResult.unchangedFiles.length}`);
51
72
  }
52
- const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, config.stopRules);
73
+ const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, config.stopRules, logger);
53
74
  if (validationResult.violations.length > 0)
54
75
  if (command.force)
55
76
  for (const violation of validationResult.violations)
56
- console.warn('\n' + (0, consoleFormatter_1.formatStopRuleViolation)(violation, 'force'));
77
+ logger.stopRule(violation, 'force');
57
78
  else if (command.dryRun)
58
79
  for (const violation of validationResult.violations)
59
- console.warn('\n' + (0, consoleFormatter_1.formatStopRuleViolation)(violation, 'warning'));
80
+ logger.stopRule(violation, 'warning');
60
81
  else {
61
82
  for (const violation of validationResult.violations)
62
- console.error('\n' + (0, consoleFormatter_1.formatStopRuleViolation)(violation, 'error'));
63
- console.error('\nUse --force to override stop rules or --dry-run to preview changes.');
83
+ logger.stopRule(violation, 'error');
84
+ logger.error('\nUse --force to override stop rules or --dry-run to preview changes.', 'critical');
64
85
  process.exit(1);
65
86
  }
66
- const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, config, command.dryRun, command.skipFormat);
67
- if (command.diffHtml)
68
- await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, config, command.dryRun);
87
+ const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, config, command.dryRun, command.skipFormat, logger);
88
+ if (command.diffHtml && !command.quiet)
89
+ await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, config, command.dryRun, logger);
69
90
  if (command.diffJson)
70
91
  (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, config, command.dryRun, package_json_1.default.version);
71
92
  };
@@ -98,4 +119,9 @@ const main = async () => {
98
119
  console.error('Unexpected error:', error);
99
120
  process.exit(1);
100
121
  }
122
+ finally {
123
+ const command = (0, commandLine_1.parseCommandLine)();
124
+ if (!command.quiet)
125
+ void (0, versionChecker_1.checkForUpdates)(package_json_1.default.version);
126
+ }
101
127
  })();
@@ -0,0 +1,20 @@
1
+ import { FileOperation, ProgressStyle, ViolationMode } from './consoleFormatter';
2
+ import { StopRuleViolation } from './stopRulesValidator';
3
+ export type VerbosityLevel = 'quiet' | 'normal' | 'verbose';
4
+ export type OutputCategory = 'critical' | 'normal' | 'debug' | 'special';
5
+ export type LoggerOptions = {
6
+ level: VerbosityLevel;
7
+ isDiffJson: boolean;
8
+ };
9
+ export declare class Logger {
10
+ private level;
11
+ constructor(options: LoggerOptions);
12
+ shouldShow(category: OutputCategory): boolean;
13
+ log(message: string, category?: OutputCategory): void;
14
+ warn(message: string, category?: OutputCategory): void;
15
+ error(message: string, category?: OutputCategory): void;
16
+ debug(message: string): void;
17
+ progress(message: string, style: ProgressStyle): void;
18
+ fileOp(operation: FileOperation, path: string, isDryRun: boolean, alreadyDeleted?: boolean): void;
19
+ stopRule(violation: StopRuleViolation, mode: ViolationMode): void;
20
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Logger = void 0;
4
+ const consoleFormatter_1 = require("./consoleFormatter");
5
+ class Logger {
6
+ level;
7
+ constructor(options) {
8
+ this.level = options.level;
9
+ }
10
+ shouldShow(category) {
11
+ if (category === 'special')
12
+ return true;
13
+ if (category === 'critical')
14
+ return true;
15
+ if (this.level === 'quiet')
16
+ return false;
17
+ if (this.level === 'verbose')
18
+ return true;
19
+ return category === 'normal';
20
+ }
21
+ log(message, category = 'normal') {
22
+ if (this.shouldShow(category))
23
+ console.log(message);
24
+ }
25
+ warn(message, category = 'normal') {
26
+ if (this.shouldShow(category))
27
+ console.warn(message);
28
+ }
29
+ error(message, category = 'critical') {
30
+ if (this.shouldShow(category))
31
+ console.error(message);
32
+ }
33
+ debug(message) {
34
+ if (this.shouldShow('debug'))
35
+ console.log(message);
36
+ }
37
+ progress(message, style) {
38
+ if (this.shouldShow('normal'))
39
+ console.log((0, consoleFormatter_1.formatProgressMessage)(message, style));
40
+ }
41
+ fileOp(operation, path, isDryRun, alreadyDeleted = false) {
42
+ if (this.shouldShow('normal'))
43
+ console.log((0, consoleFormatter_1.colorizeFileOperation)(operation, path, isDryRun, alreadyDeleted));
44
+ }
45
+ stopRule(violation, mode) {
46
+ const message = '\n' + (0, consoleFormatter_1.formatStopRuleViolation)(violation, mode);
47
+ if (mode === 'error')
48
+ this.error(message, 'critical');
49
+ else
50
+ this.warn(message, 'critical');
51
+ }
52
+ }
53
+ exports.Logger = Logger;
@@ -29,5 +29,5 @@ export interface ValidationResult {
29
29
  violations: StopRuleViolation[];
30
30
  isValid: boolean;
31
31
  }
32
- export declare const validateStopRules: (diffResult: FileDiffResult, stopRulesConfig?: Record<string, StopRule[]>) => ValidationResult;
32
+ export declare const validateStopRules: (diffResult: FileDiffResult, stopRulesConfig?: Record<string, StopRule[]>, logger?: import("./logger").Logger) => ValidationResult;
33
33
  export {};
@@ -26,14 +26,22 @@ class StopRulesValidatorError extends StopRulesValidatorErrorClass {
26
26
  }
27
27
  exports.StopRulesValidatorError = StopRulesValidatorError;
28
28
  exports.isStopRulesValidatorError = (0, errors_1.createErrorTypeGuard)(StopRulesValidatorError);
29
- const validateStopRules = (diffResult, stopRulesConfig) => {
29
+ const validateStopRules = (diffResult, stopRulesConfig, logger) => {
30
30
  if (!stopRulesConfig)
31
31
  return { violations: [], isValid: true };
32
+ if (logger?.shouldShow('debug')) {
33
+ const totalRules = Object.values(stopRulesConfig).reduce((sum, rules) => sum + rules.length, 0);
34
+ logger.debug('Stop rule validation:');
35
+ logger.debug(` Total rules: ${totalRules}`);
36
+ logger.debug(` Files to check: ${diffResult.changedFiles.length}`);
37
+ }
32
38
  const violations = [];
33
39
  for (const changedFile of diffResult.changedFiles) {
34
40
  const fileViolations = validateFileAgainstRules(changedFile, stopRulesConfig);
35
41
  violations.push(...fileViolations);
36
42
  }
43
+ if (logger?.shouldShow('debug'))
44
+ logger.debug(`Stop rules: ${violations.length} violation(s) found`);
37
45
  return {
38
46
  violations,
39
47
  isValid: violations.length === 0
@@ -5,3 +5,4 @@ export { normalizeForComparison, serializeForDiff } from './serialization';
5
5
  export { getValueAtPath, parseJsonPath } from './jsonPath';
6
6
  export { isYamlFile } from './fileType';
7
7
  export { generateUnifiedDiff } from './diffGenerator';
8
+ export { checkForUpdates, isVersionCheckerError, VersionCheckerError } from './versionChecker';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateUnifiedDiff = exports.isYamlFile = exports.parseJsonPath = exports.getValueAtPath = exports.serializeForDiff = exports.normalizeForComparison = exports.deepEqual = exports.createErrorTypeGuard = exports.createErrorClass = void 0;
3
+ exports.VersionCheckerError = exports.isVersionCheckerError = exports.checkForUpdates = exports.generateUnifiedDiff = exports.isYamlFile = exports.parseJsonPath = exports.getValueAtPath = exports.serializeForDiff = exports.normalizeForComparison = exports.deepEqual = exports.createErrorTypeGuard = exports.createErrorClass = void 0;
4
4
  var errors_1 = require("./errors");
5
5
  Object.defineProperty(exports, "createErrorClass", { enumerable: true, get: function () { return errors_1.createErrorClass; } });
6
6
  Object.defineProperty(exports, "createErrorTypeGuard", { enumerable: true, get: function () { return errors_1.createErrorTypeGuard; } });
@@ -16,3 +16,7 @@ var fileType_1 = require("./fileType");
16
16
  Object.defineProperty(exports, "isYamlFile", { enumerable: true, get: function () { return fileType_1.isYamlFile; } });
17
17
  var diffGenerator_1 = require("./diffGenerator");
18
18
  Object.defineProperty(exports, "generateUnifiedDiff", { enumerable: true, get: function () { return diffGenerator_1.generateUnifiedDiff; } });
19
+ var versionChecker_1 = require("./versionChecker");
20
+ Object.defineProperty(exports, "checkForUpdates", { enumerable: true, get: function () { return versionChecker_1.checkForUpdates; } });
21
+ Object.defineProperty(exports, "isVersionCheckerError", { enumerable: true, get: function () { return versionChecker_1.isVersionCheckerError; } });
22
+ Object.defineProperty(exports, "VersionCheckerError", { enumerable: true, get: function () { return versionChecker_1.VersionCheckerError; } });
@@ -0,0 +1,19 @@
1
+ declare const VersionCheckerErrorClass: {
2
+ new (message: string, options?: import("./errors").ErrorOptions): {
3
+ [key: string]: unknown;
4
+ readonly code?: string;
5
+ readonly path?: string;
6
+ readonly cause?: Error;
7
+ name: string;
8
+ message: string;
9
+ stack?: string;
10
+ };
11
+ captureStackTrace(targetObject: object, constructorOpt?: Function): void;
12
+ prepareStackTrace(err: Error, stackTraces: NodeJS.CallSite[]): any;
13
+ stackTraceLimit: number;
14
+ };
15
+ export declare class VersionCheckerError extends VersionCheckerErrorClass {
16
+ }
17
+ export declare const isVersionCheckerError: (error: unknown) => error is VersionCheckerError;
18
+ export declare const checkForUpdates: (currentVersion: string) => Promise<void>;
19
+ export {};
@@ -0,0 +1,117 @@
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.checkForUpdates = exports.isVersionCheckerError = exports.VersionCheckerError = void 0;
7
+ const node_https_1 = __importDefault(require("node:https"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const package_json_1 = __importDefault(require("../../package.json"));
10
+ const errors_1 = require("./errors");
11
+ const VersionCheckerErrorClass = (0, errors_1.createErrorClass)('Version Checker Error', {
12
+ TIMEOUT: 'Request timed out',
13
+ NETWORK: 'Network request failed',
14
+ PARSE: 'Failed to parse response',
15
+ INVALID: 'Invalid response format'
16
+ });
17
+ class VersionCheckerError extends VersionCheckerErrorClass {
18
+ }
19
+ exports.VersionCheckerError = VersionCheckerError;
20
+ exports.isVersionCheckerError = (0, errors_1.createErrorTypeGuard)(VersionCheckerError);
21
+ const isCiEnvironment = () => {
22
+ const ciEnvironmentVariables = [
23
+ 'CI',
24
+ 'CONTINUOUS_INTEGRATION',
25
+ 'BUILD_NUMBER',
26
+ 'GITHUB_ACTIONS',
27
+ 'GITLAB_CI',
28
+ 'CIRCLECI',
29
+ 'TRAVIS',
30
+ 'JENKINS_HOME',
31
+ 'TEAMCITY_VERSION',
32
+ 'TF_BUILD'
33
+ ];
34
+ return ciEnvironmentVariables.some((environmentVariable) => process.env[environmentVariable]);
35
+ };
36
+ const parseVersion = (version) => {
37
+ const semverRegex = /^v?(\d+)\.(\d+)\.(\d+)/;
38
+ const match = semverRegex.exec(version);
39
+ if (!match)
40
+ return undefined;
41
+ return {
42
+ major: Number.parseInt(match[1], 10),
43
+ minor: Number.parseInt(match[2], 10),
44
+ patch: Number.parseInt(match[3], 10)
45
+ };
46
+ };
47
+ const isNewerVersion = (current, latest) => {
48
+ const currentParts = parseVersion(current);
49
+ const latestParts = parseVersion(latest);
50
+ if (!currentParts || !latestParts)
51
+ return false;
52
+ if (latestParts.major > currentParts.major)
53
+ return true;
54
+ if (latestParts.major < currentParts.major)
55
+ return false;
56
+ if (latestParts.minor > currentParts.minor)
57
+ return true;
58
+ if (latestParts.minor < currentParts.minor)
59
+ return false;
60
+ return latestParts.patch > currentParts.patch;
61
+ };
62
+ const fetchLatestVersion = (packageName, timeout) => {
63
+ return new Promise((resolve, reject) => {
64
+ const url = `https://registry.npmjs.org/${packageName}/latest`;
65
+ const request = node_https_1.default.request(url, {
66
+ headers: {
67
+ Accept: 'application/json',
68
+ 'User-Agent': `helm-env-delta/${package_json_1.default.version}`
69
+ },
70
+ signal: AbortSignal.timeout(timeout)
71
+ }, (response) => {
72
+ let data = '';
73
+ response.on('data', (chunk) => {
74
+ data += chunk;
75
+ });
76
+ response.on('end', () => {
77
+ if (response.statusCode !== 200) {
78
+ reject(new VersionCheckerError(`HTTP ${response.statusCode}`, { code: 'NETWORK' }));
79
+ return;
80
+ }
81
+ try {
82
+ const json = JSON.parse(data);
83
+ if (!json.version) {
84
+ reject(new VersionCheckerError('Missing version field', { code: 'INVALID' }));
85
+ return;
86
+ }
87
+ resolve(json.version);
88
+ }
89
+ catch {
90
+ reject(new VersionCheckerError('Invalid JSON response', { code: 'PARSE' }));
91
+ }
92
+ });
93
+ });
94
+ request.on('error', (error) => {
95
+ const isTimeout = error.name === 'AbortError';
96
+ const code = isTimeout ? 'TIMEOUT' : 'NETWORK';
97
+ reject(new VersionCheckerError(error.message, { code, cause: error }));
98
+ });
99
+ request.end();
100
+ });
101
+ };
102
+ const displayUpdateNotification = (currentVersion, latestVersion) => {
103
+ console.log('\n' + chalk_1.default.yellow(`⚠ Update available! v${currentVersion} → v${latestVersion}`));
104
+ console.log(chalk_1.default.yellow('Run: npm install -g helm-env-delta@latest'));
105
+ };
106
+ const checkForUpdates = async (currentVersion) => {
107
+ if (isCiEnvironment())
108
+ return;
109
+ try {
110
+ const latestVersion = await fetchLatestVersion('helm-env-delta', 3000);
111
+ if (isNewerVersion(currentVersion, latestVersion))
112
+ displayUpdateNotification(currentVersion, latestVersion);
113
+ }
114
+ catch {
115
+ }
116
+ };
117
+ exports.checkForUpdates = checkForUpdates;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",