helm-env-delta 1.9.1 β†’ 1.9.3

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,34 +1,40 @@
1
1
  # πŸš€ HelmEnvDelta
2
2
 
3
+ **GitOps-safe YAML sync for Helm values across environments (Dev β†’ UAT β†’ Prod)**
4
+
3
5
  [![npm version](https://img.shields.io/npm/v/helm-env-delta.svg)](https://www.npmjs.com/package/helm-env-delta)
4
6
  [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
5
7
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
6
8
 
7
9
  **Sync YAML configs across environments in seconds, not hours.**
8
10
 
9
- Stop copying files manually. Stop worrying about accidental overwrites. Stop production incidents from configuration drift.
11
+ **πŸ›‘οΈ Stop production incidents from configuration driftβ€”before they happen.**
10
12
 
11
13
  HelmEnvDelta (`hed`) automates environment synchronization for GitOps workflows while protecting your production-specific settings and preventing dangerous changes.
12
14
 
13
15
  ---
14
16
 
15
- ## πŸ’‘ Why Teams Love HelmEnvDelta
17
+ ## πŸ‘€ Who Is This For?
16
18
 
17
- **Before:**
19
+ - **Platform/SRE teams** owning multiple environment repos
20
+ - **Teams** with repeated UAT β†’ Prod copy-paste workflows
21
+ - **Helm/GitOps users** burned by config drift and sync errors
18
22
 
19
- - ⏰ 30+ minutes manually copying files between UAT β†’ Prod
20
- - 😰 Accidentally overwrite production namespaces and replica counts
21
- - πŸ› Major version upgrades slip through to production
22
- - πŸ“ Inconsistent YAML formatting across environments
23
- - πŸ” Noisy git diffs make code review painful
23
+ > **Not a Helm plugin** Β· **Not a live cluster diff** Β· **Not a general YAML linter**
24
+ >
25
+ > HelmEnvDelta syncs _files_, not running clusters. Use alongside Helm, ArgoCD, or Fluxβ€”not instead of them.
24
26
 
25
- **After:**
27
+ ---
26
28
 
27
- - ⚑ 1 minute automated sync with safety guarantees
28
- - πŸ›‘οΈ Production-specific values automatically preserved
29
- - 🚦 Stop rules block dangerous changes before deployment
30
- - ✨ Consistent formatting across all environments
31
- - πŸ“Š Clean, structural diffs that show what actually changed
29
+ ## πŸ’‘ Why Teams Love HelmEnvDelta
30
+
31
+ | Problem | Impact | HelmEnvDelta Outcome |
32
+ | ---------------------------- | ----------------------------------------- | ------------------------------------------------ |
33
+ | Copying UAT β†’ Prod by hand | 30+ minutes, human errors | **1 min** scripted sync for 50+ files |
34
+ | Hidden config drift | Incidents in Prod | Stop rules & fixed values prevent unsafe changes |
35
+ | Accidental overwrites | Production namespaces, replicas corrupted | `skipPath` preserves env-specific values |
36
+ | Major version slips | Breaking changes in Prod | `semverMajorUpgrade` rule blocks v1β†’v2 |
37
+ | Inconsistent YAML formatting | Noisy git diffs, painful reviews | Standardized formatting, clean diffs |
32
38
 
33
39
  ---
34
40
 
@@ -132,6 +138,8 @@ Analyzes differences and suggests transforms and stop rules automatically with c
132
138
 
133
139
  **Challenge:** 20+ microservices across Dev β†’ UAT β†’ Prod. Each has environment-specific namespaces, resource limits, and URLs.
134
140
 
141
+ _A platform team managing 25 microservices across 3 environments used to spend 2 hours every release copying and tweaking values files. Now 2 engineers sync everything in under 5 minutes with zero manual edits._
142
+
135
143
  **Solution:**
136
144
 
137
145
  ```yaml
@@ -159,6 +167,8 @@ transforms:
159
167
 
160
168
  **Challenge:** Production incidents from accidental major version upgrades or scaling beyond cluster capacity.
161
169
 
170
+ _After a v1β†’v2 image upgrade slipped through and caused a 4-hour outage, the team added semverMajorUpgrade rules. They've blocked 12 accidental major bumps sinceβ€”and zero incidents._
171
+
162
172
  **Solution:**
163
173
 
164
174
  ```yaml
@@ -180,6 +190,8 @@ stopRules:
180
190
 
181
191
  **Challenge:** Different editors, different formatting. Git diffs full of noise.
182
192
 
193
+ _Code reviews were painful: 80% of diff lines were formatting noise. After enabling outputFormat, PRs became clean structural diffs that reviewers actually read._
194
+
183
195
  **Solution:**
184
196
 
185
197
  ```yaml
@@ -269,6 +281,28 @@ helm-env-delta --config example/6-fixed-values/config.yaml --dry-run --diff
269
281
  - Array filter operators (`env[name=LOG_LEVEL].value`)
270
282
  - Combining with skipPath and transforms
271
283
 
284
+ ### 🎨 Example 7: Format-Only Mode
285
+
286
+ Format YAML files without syncing. No source directory required - perfect for standardizing existing files.
287
+
288
+ ```bash
289
+ # Preview which files would be formatted
290
+ helm-env-delta --config example/7-format-only/config.yaml --format-only --list-files
291
+
292
+ # Preview formatting changes
293
+ helm-env-delta --config example/7-format-only/config.yaml --format-only --dry-run
294
+
295
+ # Apply formatting
296
+ helm-env-delta --config example/7-format-only/config.yaml --format-only
297
+ ```
298
+
299
+ **Features shown:**
300
+
301
+ - Format-only mode (no source required)
302
+ - Combining `--format-only` with `--list-files` to preview files
303
+ - Key ordering, array sorting, indentation standardization
304
+ - Minimal config for formatting existing files
305
+
272
306
  ---
273
307
 
274
308
  ## πŸ’‘ Smart Configuration Suggestions (Heuristic)
@@ -362,8 +396,8 @@ helm-env-delta --config config.yaml
362
396
  ### 🎯 Core Settings
363
397
 
364
398
  ```yaml
365
- source: './uat' # Required: Source folder
366
- destination: './prod' # Required: Destination folder
399
+ source: './uat' # Required: Source folder (optional with --format-only)
400
+ destination: './prod' # Required: Destination folder (must differ from source)
367
401
 
368
402
  include: # Optional: File patterns (default: all)
369
403
  - '**/*.yaml'
@@ -374,6 +408,8 @@ prune: false # Optional: Delete dest files not in source
374
408
  confirmationDelay: 3000 # Optional: Delay in ms before sync (default: 3000, 0 to disable)
375
409
  ```
376
410
 
411
+ **Note:** Source and destination paths cannot resolve to the same folder.
412
+
377
413
  ---
378
414
 
379
415
  ### πŸ”’ Path Filtering (skipPath)
@@ -633,7 +669,7 @@ Standardize YAML across all environments.
633
669
  ```yaml
634
670
  outputFormat:
635
671
  indent: 2 # Indentation size
636
- keySeparator: true # Blank line between top-level keys
672
+ keySeparator: true # Blank line between top-level keys (or second-level keys when single top-level key)
637
673
 
638
674
  keyOrders: # Custom key ordering
639
675
  'apps/*.yaml':
@@ -717,24 +753,24 @@ hed --config <file> [options] # Short alias
717
753
 
718
754
  ### Options
719
755
 
720
- | Flag | Description |
721
- | --------------------------- | -------------------------------------------------- |
722
- | `--config <path>` | **Required** - Configuration file |
723
- | `--validate` | Validate config and pattern usage (shows warnings) |
724
- | `--suggest` | Analyze differences and suggest config updates |
725
- | `--suggest-threshold <0-1>` | Minimum confidence for suggestions (default: 0.3) |
726
- | `--dry-run` | Preview changes without writing files |
727
- | `--force` | Override stop rules |
728
- | `--diff` | Show console diff |
729
- | `--diff-html` | Generate HTML report (opens in browser) |
730
- | `--diff-json` | Output JSON to stdout (pipe to jq) |
731
- | `--list-files` | List source/destination files without processing |
732
- | `--show-config` | Display resolved config after inheritance |
733
- | `--format-only` | Format destination files without syncing |
734
- | `--skip-format` | Skip YAML formatting during sync |
735
- | `--no-color` | Disable colored output (CI/accessibility) |
736
- | `--verbose` | Show detailed debug info |
737
- | `--quiet` | Suppress output except errors |
756
+ | Flag | Description |
757
+ | --------------------------- | ------------------------------------------------------------------- |
758
+ | `--config <path>` | **Required** - Configuration file |
759
+ | `--validate` | Validate config and pattern usage (shows warnings) |
760
+ | `--suggest` | Analyze differences and suggest config updates |
761
+ | `--suggest-threshold <0-1>` | Minimum confidence for suggestions (default: 0.3) |
762
+ | `--dry-run` | Preview changes without writing files |
763
+ | `--force` | Override stop rules |
764
+ | `--diff` | Show console diff |
765
+ | `--diff-html` | Generate HTML report (opens in browser) |
766
+ | `--diff-json` | Output JSON to stdout (pipe to jq) |
767
+ | `--list-files` | List files without processing (takes precedence over --format-only) |
768
+ | `--show-config` | Display resolved config after inheritance |
769
+ | `--format-only` | Format destination files only (source not required) |
770
+ | `--skip-format` | Skip YAML formatting during sync |
771
+ | `--no-color` | Disable colored output (CI/accessibility) |
772
+ | `--verbose` | Show detailed debug info |
773
+ | `--quiet` | Suppress output except errors |
738
774
 
739
775
  ### Examples
740
776
 
@@ -769,11 +805,19 @@ hed --config config.yaml
769
805
  # Force override stop rules
770
806
  hed --config config.yaml --force
771
807
 
772
- # Format destination files only (no sync)
808
+ # Format destination files only (no sync, source not required in config)
773
809
  hed --config config.yaml --format-only
774
810
 
775
811
  # Preview format changes
776
812
  hed --config config.yaml --format-only --dry-run
813
+
814
+ # List files that would be formatted (--list-files takes precedence)
815
+ hed --config config.yaml --format-only --list-files
816
+
817
+ # Format-only config example (no source needed):
818
+ # destination: './prod'
819
+ # outputFormat:
820
+ # indent: 2
777
821
  ```
778
822
 
779
823
  ---
@@ -1017,6 +1061,14 @@ spec:
1017
1061
 
1018
1062
  ## πŸ†˜ Common Issues
1019
1063
 
1064
+ ### ❓ Source and destination folders cannot be the same
1065
+
1066
+ **Error:** `Source and destination folders cannot be the same`
1067
+
1068
+ **Fix:** Ensure source and destination paths resolve to different directories. Paths like `./envs/../envs/prod` and `./envs/prod` will be detected as identical.
1069
+
1070
+ ---
1071
+
1020
1072
  ### ❓ Stop rule violations blocking sync
1021
1073
 
1022
1074
  **Error:** `πŸ›‘ Stop Rule Violation (semverMajorUpgrade)`
@@ -240,8 +240,80 @@ declare const finalConfigSchema: z.ZodObject<{
240
240
  }, z.core.$strip>>>>;
241
241
  }, z.core.$strip>>>;
242
242
  }, z.core.$strip>;
243
+ declare const formatOnlyConfigSchema: z.ZodObject<{
244
+ transforms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
245
+ content: z.ZodOptional<z.ZodArray<z.ZodObject<{
246
+ find: z.ZodString;
247
+ replace: z.ZodString;
248
+ }, z.core.$strip>>>;
249
+ filename: z.ZodOptional<z.ZodArray<z.ZodObject<{
250
+ find: z.ZodString;
251
+ replace: z.ZodString;
252
+ }, z.core.$strip>>>;
253
+ contentFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
254
+ filenameFile: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
255
+ }, z.core.$strip>>>;
256
+ source: z.ZodOptional<z.ZodString>;
257
+ destination: z.ZodNonOptional<z.ZodOptional<z.ZodString>>;
258
+ skipPath: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
259
+ stopRules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
260
+ type: z.ZodLiteral<"semverMajorUpgrade">;
261
+ path: z.ZodString;
262
+ }, z.core.$strip>, z.ZodObject<{
263
+ type: z.ZodLiteral<"semverDowngrade">;
264
+ path: z.ZodString;
265
+ }, z.core.$strip>, z.ZodObject<{
266
+ type: z.ZodLiteral<"numeric">;
267
+ path: z.ZodString;
268
+ min: z.ZodOptional<z.ZodNumber>;
269
+ max: z.ZodOptional<z.ZodNumber>;
270
+ }, z.core.$strip>, z.ZodObject<{
271
+ type: z.ZodLiteral<"regex">;
272
+ path: z.ZodOptional<z.ZodString>;
273
+ regex: z.ZodString;
274
+ }, z.core.$strip>, z.ZodObject<{
275
+ type: z.ZodLiteral<"regexFile">;
276
+ path: z.ZodOptional<z.ZodString>;
277
+ file: z.ZodString;
278
+ }, z.core.$strip>, z.ZodObject<{
279
+ type: z.ZodLiteral<"regexFileKey">;
280
+ path: z.ZodOptional<z.ZodString>;
281
+ file: z.ZodString;
282
+ }, z.core.$strip>, z.ZodObject<{
283
+ type: z.ZodLiteral<"versionFormat">;
284
+ path: z.ZodString;
285
+ vPrefix: z.ZodDefault<z.ZodEnum<{
286
+ required: "required";
287
+ allowed: "allowed";
288
+ forbidden: "forbidden";
289
+ }>>;
290
+ }, z.core.$strict>], "type">>>>;
291
+ fixedValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
292
+ path: z.ZodString;
293
+ value: z.ZodUnknown;
294
+ }, z.core.$strip>>>>;
295
+ include: z.ZodDefault<z.ZodArray<z.ZodString>>;
296
+ exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
297
+ prune: z.ZodDefault<z.ZodBoolean>;
298
+ confirmationDelay: z.ZodDefault<z.ZodNumber>;
299
+ outputFormat: z.ZodDefault<z.ZodOptional<z.ZodObject<{
300
+ indent: z.ZodDefault<z.ZodNumber>;
301
+ keySeparator: z.ZodDefault<z.ZodBoolean>;
302
+ quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
303
+ keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
304
+ arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
305
+ path: z.ZodString;
306
+ sortBy: z.ZodString;
307
+ order: z.ZodDefault<z.ZodEnum<{
308
+ asc: "asc";
309
+ desc: "desc";
310
+ }>>;
311
+ }, z.core.$strip>>>>;
312
+ }, z.core.$strip>>>;
313
+ }, z.core.$strip>;
243
314
  export type BaseConfig = z.infer<typeof baseConfigSchema>;
244
315
  export type FinalConfig = z.infer<typeof finalConfigSchema>;
316
+ export type FormatOnlyConfig = z.infer<typeof formatOnlyConfigSchema>;
245
317
  export type Config = FinalConfig;
246
318
  export type StopRule = z.infer<typeof stopRuleSchema>;
247
319
  export type SemverMajorUpgradeRule = z.infer<typeof semverMajorUpgradeRuleSchema>;
@@ -260,5 +332,6 @@ export type FixedValueRule = z.infer<typeof fixedValueRuleSchema>;
260
332
  export type FixedValueConfig = Record<string, FixedValueRule[]>;
261
333
  export declare const parseBaseConfig: (data: unknown, configPath?: string) => BaseConfig;
262
334
  export declare const parseFinalConfig: (data: unknown, configPath?: string) => FinalConfig;
335
+ export declare const parseFormatOnlyConfig: (data: unknown, configPath?: string) => FormatOnlyConfig;
263
336
  export declare const parseConfig: (data: unknown, configPath?: string) => FinalConfig;
264
337
  export { ZodValidationError as ConfigValidationError, isZodValidationError as isConfigValidationError } from './ZodError';
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isConfigValidationError = exports.ConfigValidationError = exports.parseConfig = exports.parseFinalConfig = exports.parseBaseConfig = void 0;
6
+ exports.isConfigValidationError = exports.ConfigValidationError = exports.parseConfig = exports.parseFormatOnlyConfig = exports.parseFinalConfig = exports.parseBaseConfig = void 0;
7
+ const node_path_1 = __importDefault(require("node:path"));
4
8
  const zod_1 = require("zod");
5
9
  const ZodError_1 = require("./ZodError");
6
10
  const semverMajorUpgradeRuleSchema = zod_1.z.object({
@@ -149,6 +153,33 @@ const finalConfigSchema = baseConfigSchema
149
153
  })
150
154
  .optional()
151
155
  .default({ indent: 2, keySeparator: false })
156
+ })
157
+ .refine((data) => {
158
+ const normalizedSource = node_path_1.default.resolve(data.source);
159
+ const normalizedDestination = node_path_1.default.resolve(data.destination);
160
+ return normalizedSource !== normalizedDestination;
161
+ }, {
162
+ message: 'Source and destination folders cannot be the same',
163
+ path: ['destination']
164
+ });
165
+ const formatOnlyConfigSchema = baseConfigSchema
166
+ .omit({ extends: true })
167
+ .required({ destination: true })
168
+ .extend({
169
+ include: zod_1.z.array(zod_1.z.string().min(1)).default(['**/*']),
170
+ exclude: zod_1.z.array(zod_1.z.string().min(1)).default([]),
171
+ prune: zod_1.z.boolean().default(false),
172
+ confirmationDelay: zod_1.z.number().int().min(0).default(3000),
173
+ outputFormat: zod_1.z
174
+ .object({
175
+ indent: zod_1.z.number().int().min(1).max(10).default(2),
176
+ keySeparator: zod_1.z.boolean().default(false),
177
+ quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
178
+ keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
179
+ arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
180
+ })
181
+ .optional()
182
+ .default({ indent: 2, keySeparator: false })
152
183
  });
153
184
  const parseBaseConfig = (data, configPath) => {
154
185
  const result = baseConfigSchema.safeParse(data);
@@ -169,6 +200,18 @@ const parseFinalConfig = (data, configPath) => {
169
200
  return result.data;
170
201
  };
171
202
  exports.parseFinalConfig = parseFinalConfig;
203
+ const parseFormatOnlyConfig = (data, configPath) => {
204
+ const result = formatOnlyConfigSchema.safeParse(data);
205
+ if (!result.success) {
206
+ const error = new ZodError_1.ZodValidationError(result.error, configPath);
207
+ const destinationMissing = result.error.issues.some((issue) => issue.path[0] === 'destination' && issue.code === 'invalid_type');
208
+ if (destinationMissing)
209
+ error.message += '\n\n Hint: Format-only mode requires destination to be specified.';
210
+ throw error;
211
+ }
212
+ return result.data;
213
+ };
214
+ exports.parseFormatOnlyConfig = parseFormatOnlyConfig;
172
215
  exports.parseConfig = exports.parseFinalConfig;
173
216
  var ZodError_2 = require("./ZodError");
174
217
  Object.defineProperty(exports, "ConfigValidationError", { enumerable: true, get: function () { return ZodError_2.ZodValidationError; } });
@@ -1,3 +1,6 @@
1
- import { type FinalConfig } from './configFile';
1
+ import { type FinalConfig, type FormatOnlyConfig } from './configFile';
2
2
  export type Config = FinalConfig;
3
- export declare const loadConfigFile: (configPath: string, quiet?: boolean, logger?: import("./logger").Logger) => FinalConfig;
3
+ export type LoadConfigOptions = {
4
+ formatOnly?: boolean;
5
+ };
6
+ export declare const loadConfigFile: (configPath: string, quiet?: boolean, logger?: import("./logger").Logger, options?: LoadConfigOptions) => FinalConfig | FormatOnlyConfig;
@@ -30,13 +30,17 @@ const expandTransformFiles = (config, configDirectory) => {
30
30
  }
31
31
  return { ...config, transforms: expandedTransforms };
32
32
  };
33
- const loadConfigFile = (configPath, quiet = false, logger) => {
33
+ const loadConfigFile = (configPath, quiet = false, logger, options = {}) => {
34
34
  const configDirectory = node_path_1.default.dirname(node_path_1.default.resolve(configPath));
35
35
  const mergedConfig = (0, configMerger_1.resolveConfigWithExtends)(configPath, new Set(), 0, logger);
36
36
  const expandedConfig = expandTransformFiles(mergedConfig, configDirectory);
37
- const config = (0, configFile_1.parseFinalConfig)(expandedConfig, configPath);
38
- if (!quiet)
39
- console.log(`\nConfiguration loaded: ${config.source} -> ${config.destination}` + (config.prune ? ' [prune!]' : ''));
37
+ const config = options.formatOnly
38
+ ? (0, configFile_1.parseFormatOnlyConfig)(expandedConfig, configPath)
39
+ : (0, configFile_1.parseFinalConfig)(expandedConfig, configPath);
40
+ if (!quiet) {
41
+ const sourceInfo = config.source ? `${config.source} -> ` : '';
42
+ console.log(`\nConfiguration loaded: ${sourceInfo}${config.destination}` + (config.prune ? ' [prune!]' : ''));
43
+ }
40
44
  return config;
41
45
  };
42
46
  exports.loadConfigFile = loadConfigFile;
package/dist/fileDiff.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.computeFileDiff = exports.getSkipPathsForFile = exports.isFileDiffError = exports.FileDiffError = void 0;
7
7
  const yaml_1 = __importDefault(require("yaml"));
8
+ const commentOnlyDetector_1 = require("./utils/commentOnlyDetector");
8
9
  const deepEqual_1 = require("./utils/deepEqual");
9
10
  const errors_1 = require("./utils/errors");
10
11
  const fileType_1 = require("./utils/fileType");
@@ -143,6 +144,8 @@ const processYamlFile = (options) => {
143
144
  parseError.message += destinationHints;
144
145
  throw parseError;
145
146
  }
147
+ if ((0, commentOnlyDetector_1.isCommentOnlyContent)(destinationContent))
148
+ return undefined;
146
149
  const sourceTransformed = (0, transformer_1.applyTransforms)(sourceParsed, filePath, transforms);
147
150
  const fixedValueRules = (0, fixedValues_1.getFixedValuesForFile)(filePath, fixedValues);
148
151
  if (fixedValueRules.length > 0)
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const yaml_1 = __importDefault(require("yaml"));
10
10
  const consoleFormatter_1 = require("./consoleFormatter");
11
11
  const arrayMerger_1 = require("./utils/arrayMerger");
12
+ const commentOnlyDetector_1 = require("./utils/commentOnlyDetector");
12
13
  const errors_1 = require("./utils/errors");
13
14
  const fileType_1 = require("./utils/fileType");
14
15
  const fixedValues_1 = require("./utils/fixedValues");
@@ -289,6 +290,8 @@ const formatUnchangedFiles = async (files, destinationFiles, context) => {
289
290
  if ((0, fileType_1.isYamlFile)(relativePath))
290
291
  try {
291
292
  const content = destinationFiles.get(relativePath);
293
+ if ((0, commentOnlyDetector_1.isCommentOnlyContent)(content))
294
+ continue;
292
295
  const effectiveOutputFormat = context.skipFormat ? undefined : context.config.outputFormat;
293
296
  const formatted = (0, yamlFormatter_1.formatYaml)(content, relativePath, effectiveOutputFormat);
294
297
  if (formatted !== content) {
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ const patternUsageValidator_1 = require("./patternUsageValidator");
59
59
  const stopRulesValidator_1 = require("./stopRulesValidator");
60
60
  const suggestionEngine_1 = require("./suggestionEngine");
61
61
  const collisionDetector_1 = require("./utils/collisionDetector");
62
+ const commentOnlyDetector_1 = require("./utils/commentOnlyDetector");
62
63
  const filenameTransformer_1 = require("./utils/filenameTransformer");
63
64
  const fileType_1 = require("./utils/fileType");
64
65
  const versionChecker_1 = require("./utils/versionChecker");
@@ -84,15 +85,20 @@ const main = async () => {
84
85
  (0, node_fs_1.mkdirSync)(configDirectory, { recursive: true });
85
86
  (0, node_fs_1.writeFileSync)(firstRunMarker, new Date().toISOString());
86
87
  }
87
- const config = (0, configLoader_1.loadConfigFile)(command.config, command.quiet, logger);
88
+ const config = (0, configLoader_1.loadConfigFile)(command.config, command.quiet, logger, { formatOnly: command.formatOnly });
88
89
  if (command.showConfig) {
89
90
  console.log(chalk_1.default.cyan('\nβš™οΈ Resolved Configuration:\n'));
90
91
  console.log(YAML.stringify(config, { indent: 2 }));
91
92
  return;
92
93
  }
93
94
  if (command.validate) {
95
+ if (!config.source) {
96
+ logger.error('\nSource folder is required for validation mode.', 'critical');
97
+ process.exit(1);
98
+ }
99
+ const validationConfig = config;
94
100
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating configuration...', 'info'));
95
- const warningResult = (0, configWarnings_1.validateConfigWarnings)(config);
101
+ const warningResult = (0, configWarnings_1.validateConfigWarnings)(validationConfig);
96
102
  let hasAnyWarnings = warningResult.hasWarnings;
97
103
  if (warningResult.hasWarnings) {
98
104
  console.warn(chalk_1.default.yellow('\n⚠️ Configuration Warnings (non-fatal):\n'));
@@ -101,23 +107,23 @@ const main = async () => {
101
107
  }
102
108
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files for validation...', 'loading'));
103
109
  const sourceResult = await (0, fileLoader_1.loadFiles)({
104
- baseDirectory: config.source,
105
- include: config.include,
106
- exclude: config.exclude,
107
- transforms: config.transforms,
110
+ baseDirectory: validationConfig.source,
111
+ include: validationConfig.include,
112
+ exclude: validationConfig.exclude,
113
+ transforms: validationConfig.transforms,
108
114
  skipExclude: true
109
115
  }, logger);
110
116
  const sourceFiles = sourceResult.fileMap;
111
117
  const destinationResult = await (0, fileLoader_1.loadFiles)({
112
- baseDirectory: config.destination,
113
- include: config.include,
114
- exclude: config.exclude,
118
+ baseDirectory: validationConfig.destination,
119
+ include: validationConfig.include,
120
+ exclude: validationConfig.exclude,
115
121
  skipExclude: true
116
122
  }, logger);
117
123
  const destinationFiles = destinationResult.fileMap;
118
124
  logger.progress(`Loaded ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'success');
119
125
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating pattern usage...', 'info'));
120
- const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(config, sourceFiles, destinationFiles);
126
+ const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(validationConfig, sourceFiles, destinationFiles);
121
127
  hasAnyWarnings = hasAnyWarnings || usageResult.hasWarnings;
122
128
  if (usageResult.hasWarnings) {
123
129
  console.warn(chalk_1.default.yellow('\n⚠️ Pattern Usage Warnings (non-fatal):\n'));
@@ -134,58 +140,43 @@ const main = async () => {
134
140
  }
135
141
  if (logger.shouldShow('debug')) {
136
142
  logger.debug('\nConfig details:');
137
- logger.debug(` Source: ${config.source}`);
143
+ if (config.source)
144
+ logger.debug(` Source: ${config.source}`);
138
145
  logger.debug(` Destination: ${config.destination}`);
139
146
  logger.debug(` Include patterns: ${config.include.join(', ')}`);
140
147
  logger.debug(` Exclude patterns: ${config.exclude.join(', ')}`);
141
148
  logger.debug(` Transforms: ${Object.keys(config.transforms || {}).length} pattern(s)`);
142
149
  logger.debug(` Prune enabled: ${config.prune}`);
143
150
  }
144
- logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
145
- const sourceResult = await (0, fileLoader_1.loadFiles)({
146
- baseDirectory: config.source,
147
- include: config.include,
148
- exclude: config.exclude,
149
- transforms: config.transforms
150
- }, logger);
151
- const sourceFiles = sourceResult.fileMap;
152
- const originalPaths = sourceResult.originalPaths;
153
- logger.progress(`Loaded ${sourceFiles.size} source file(s)`, 'success');
154
- const collisions = (0, collisionDetector_1.detectCollisions)(sourceFiles, config.transforms);
155
- if (collisions.length > 0)
156
- (0, collisionDetector_1.validateNoCollisions)(collisions);
157
- if (logger.shouldShow('debug'))
158
- logger.debug('Filename collision check: passed');
159
- const destinationResult = await (0, fileLoader_1.loadFiles)({
160
- baseDirectory: config.destination,
161
- include: config.include,
162
- exclude: config.exclude
163
- }, logger);
164
- const destinationFiles = destinationResult.fileMap;
165
- logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
166
- if (command.listFiles) {
167
- const sourceFilesList = [...sourceFiles.keys()].toSorted();
168
- const destinationFilesList = [...destinationFiles.keys()].toSorted();
169
- console.log(chalk_1.default.cyan('\nπŸ“‹ Files to be synced:\n'));
170
- console.log(chalk_1.default.green(`Source files: ${sourceFilesList.length}`));
171
- for (const file of sourceFilesList)
172
- console.log(` ${chalk_1.default.dim(file)}`);
173
- console.log(chalk_1.default.yellow(`\nDestination files: ${destinationFilesList.length}`));
174
- for (const file of destinationFilesList)
175
- console.log(` ${chalk_1.default.dim(file)}`);
176
- return;
177
- }
178
151
  if (command.formatOnly) {
179
152
  if (!config.outputFormat) {
180
153
  logger.log(chalk_1.default.yellow('\n⚠️ No outputFormat configured. Nothing to format.'));
181
154
  return;
182
155
  }
156
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading destination files...', 'loading'));
157
+ const destinationResult = await (0, fileLoader_1.loadFiles)({
158
+ baseDirectory: config.destination,
159
+ include: config.include,
160
+ exclude: config.exclude
161
+ }, logger);
162
+ const destinationFiles = destinationResult.fileMap;
163
+ logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
164
+ if (command.listFiles) {
165
+ const filesList = [...destinationFiles.keys()].toSorted();
166
+ console.log(chalk_1.default.cyan('\nπŸ“‹ Files to be formatted:\n'));
167
+ console.log(chalk_1.default.yellow(`Destination files: ${filesList.length}`));
168
+ for (const file of filesList)
169
+ console.log(` ${chalk_1.default.dim(file)}`);
170
+ return;
171
+ }
183
172
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Formatting files...', 'info'));
184
173
  let formattedCount = 0;
185
174
  const errors = [];
186
175
  for (const [relativePath, content] of destinationFiles) {
187
176
  if (!(0, fileType_1.isYamlFile)(relativePath))
188
177
  continue;
178
+ if ((0, commentOnlyDetector_1.isCommentOnlyContent)(content))
179
+ continue;
189
180
  try {
190
181
  const formatted = (0, yamlFormatter_1.formatYaml)(content, relativePath, config.outputFormat);
191
182
  if (formatted !== content) {
@@ -215,12 +206,51 @@ const main = async () => {
215
206
  }
216
207
  return;
217
208
  }
209
+ if (!config.source) {
210
+ logger.error('\nSource folder is required for sync operations.', 'critical');
211
+ process.exit(1);
212
+ }
213
+ const syncConfig = config;
214
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
215
+ const sourceResult = await (0, fileLoader_1.loadFiles)({
216
+ baseDirectory: syncConfig.source,
217
+ include: syncConfig.include,
218
+ exclude: syncConfig.exclude,
219
+ transforms: syncConfig.transforms
220
+ }, logger);
221
+ const sourceFiles = sourceResult.fileMap;
222
+ const originalPaths = sourceResult.originalPaths;
223
+ logger.progress(`Loaded ${sourceFiles.size} source file(s)`, 'success');
224
+ const collisions = (0, collisionDetector_1.detectCollisions)(sourceFiles, syncConfig.transforms);
225
+ if (collisions.length > 0)
226
+ (0, collisionDetector_1.validateNoCollisions)(collisions);
227
+ if (logger.shouldShow('debug'))
228
+ logger.debug('Filename collision check: passed');
229
+ const destinationResult = await (0, fileLoader_1.loadFiles)({
230
+ baseDirectory: syncConfig.destination,
231
+ include: syncConfig.include,
232
+ exclude: syncConfig.exclude
233
+ }, logger);
234
+ const destinationFiles = destinationResult.fileMap;
235
+ logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
236
+ if (command.listFiles) {
237
+ const sourceFilesList = [...sourceFiles.keys()].toSorted();
238
+ const destinationFilesList = [...destinationFiles.keys()].toSorted();
239
+ console.log(chalk_1.default.cyan('\nπŸ“‹ Files to be synced:\n'));
240
+ console.log(chalk_1.default.green(`Source files: ${sourceFilesList.length}`));
241
+ for (const file of sourceFilesList)
242
+ console.log(` ${chalk_1.default.dim(file)}`);
243
+ console.log(chalk_1.default.yellow(`\nDestination files: ${destinationFilesList.length}`));
244
+ for (const file of destinationFilesList)
245
+ console.log(` ${chalk_1.default.dim(file)}`);
246
+ return;
247
+ }
218
248
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
219
- const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger, originalPaths);
249
+ const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, syncConfig, logger, originalPaths);
220
250
  if (logger.shouldShow('debug'))
221
251
  logger.debug('Diff pipeline: parse β†’ transforms β†’ skipPath β†’ normalize β†’ compare');
222
252
  if (command.diff && !command.quiet)
223
- (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, config);
253
+ (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, syncConfig);
224
254
  else {
225
255
  logger.log(` New files: ${diffResult.addedFiles.length}`);
226
256
  logger.log(` Deleted files: ${diffResult.deletedFiles.length}`);
@@ -230,7 +260,7 @@ const main = async () => {
230
260
  if (command.suggest) {
231
261
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Analyzing differences for suggestions...', 'info'));
232
262
  try {
233
- const suggestions = (0, suggestionEngine_1.analyzeDifferencesForSuggestions)(diffResult, config, command.suggestThreshold);
263
+ const suggestions = (0, suggestionEngine_1.analyzeDifferencesForSuggestions)(diffResult, syncConfig, command.suggestThreshold);
234
264
  const yaml = (0, suggestionEngine_1.formatSuggestionsAsYaml)(suggestions);
235
265
  console.log(chalk_1.default.cyan('\nπŸ’‘ Suggested Configuration:\n'));
236
266
  console.log(yaml);
@@ -251,7 +281,7 @@ const main = async () => {
251
281
  }
252
282
  }
253
283
  const configFileDirectory = node_path_1.default.dirname(node_path_1.default.resolve(command.config));
254
- const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, config.stopRules, configFileDirectory, logger);
284
+ const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, syncConfig.stopRules, configFileDirectory, logger);
255
285
  if (validationResult.violations.length > 0)
256
286
  if (command.force)
257
287
  for (const violation of validationResult.violations)
@@ -270,20 +300,20 @@ const main = async () => {
270
300
  console.log(chalk_1.default.dim('─'.repeat(60)));
271
301
  console.log(` ${chalk_1.default.green('Added:')} ${diffResult.addedFiles.length} files`);
272
302
  console.log(` ${chalk_1.default.yellow('Changed:')} ${diffResult.changedFiles.length} files`);
273
- console.log(` ${chalk_1.default.red('Deleted:')} ${diffResult.deletedFiles.length} files (${config.prune ? 'prune enabled' : 'prune disabled'})`);
303
+ console.log(` ${chalk_1.default.red('Deleted:')} ${diffResult.deletedFiles.length} files (${syncConfig.prune ? 'prune enabled' : 'prune disabled'})`);
274
304
  console.log(` ${chalk_1.default.blue('Unchanged:')} ${diffResult.unchangedFiles.length} files`);
275
305
  console.log(chalk_1.default.dim('─'.repeat(60)));
276
- if (diffResult.deletedFiles.length > 0 && config.prune)
306
+ if (diffResult.deletedFiles.length > 0 && syncConfig.prune)
277
307
  console.warn(chalk_1.default.red('⚠️ Warning: Prune is enabled. Files will be permanently deleted!'));
278
308
  console.log(chalk_1.default.dim('\nPress Ctrl+C to cancel, or use --dry-run to preview changes first.\n'));
279
- if (config.confirmationDelay > 0)
280
- await new Promise((resolve) => setTimeout(resolve, config.confirmationDelay));
309
+ if (syncConfig.confirmationDelay > 0)
310
+ await new Promise((resolve) => setTimeout(resolve, syncConfig.confirmationDelay));
281
311
  }
282
- const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, config, command.dryRun, command.skipFormat, logger);
312
+ const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, syncConfig, command.dryRun, command.skipFormat, logger);
283
313
  if (command.diffHtml && !command.quiet)
284
- await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, config, command.dryRun, logger);
314
+ await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, syncConfig, command.dryRun, logger);
285
315
  if (command.diffJson)
286
- (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, config, command.dryRun, package_json_1.default.version);
316
+ (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, syncConfig, command.dryRun, package_json_1.default.version);
287
317
  };
288
318
  (async () => {
289
319
  try {
@@ -0,0 +1 @@
1
+ export declare const isCommentOnlyContent: (content: string) => boolean;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isCommentOnlyContent = void 0;
4
+ const isCommentOnlyContent = (content) => {
5
+ if (!content)
6
+ return false;
7
+ const lines = content.split('\n');
8
+ for (const line of lines) {
9
+ const trimmed = line.trim();
10
+ if (trimmed === '')
11
+ continue;
12
+ if (trimmed.startsWith('#'))
13
+ continue;
14
+ return false;
15
+ }
16
+ return true;
17
+ };
18
+ exports.isCommentOnlyContent = isCommentOnlyContent;
@@ -20,3 +20,4 @@ export { ANTONYM_PAIRS, ARRAY_KEY_FIELDS, CONFIDENCE_DEFAULTS, CONSTRAINT_FIELD_
20
20
  export { applyFixedValues, getFixedValuesForFile, setValueAtPath } from './fixedValues';
21
21
  export type { ApplicableFilter } from './arrayMerger';
22
22
  export { findMatchingTargetItem, getApplicableArrayFilters, itemMatchesAnyFilter, shouldPreserveItem } from './arrayMerger';
23
+ export { isCommentOnlyContent } from './commentOnlyDetector';
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
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.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 = exports.NUMERIC_MIN_MULTIPLIER = void 0;
4
+ 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 = 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; } });
@@ -84,3 +84,5 @@ Object.defineProperty(exports, "findMatchingTargetItem", { enumerable: true, get
84
84
  Object.defineProperty(exports, "getApplicableArrayFilters", { enumerable: true, get: function () { return arrayMerger_1.getApplicableArrayFilters; } });
85
85
  Object.defineProperty(exports, "itemMatchesAnyFilter", { enumerable: true, get: function () { return arrayMerger_1.itemMatchesAnyFilter; } });
86
86
  Object.defineProperty(exports, "shouldPreserveItem", { enumerable: true, get: function () { return arrayMerger_1.shouldPreserveItem; } });
87
+ var commentOnlyDetector_1 = require("./commentOnlyDetector");
88
+ Object.defineProperty(exports, "isCommentOnlyContent", { enumerable: true, get: function () { return commentOnlyDetector_1.isCommentOnlyContent; } });
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.formatYaml = exports.isYamlFormatterError = exports.YamlFormatterError = void 0;
7
7
  const yaml_1 = __importDefault(require("yaml"));
8
8
  const constants_1 = require("./constants");
9
+ const commentOnlyDetector_1 = require("./utils/commentOnlyDetector");
9
10
  const errors_1 = require("./utils/errors");
10
11
  const jsonPath_1 = require("./utils/jsonPath");
11
12
  const patternMatcher_1 = require("./utils/patternMatcher");
@@ -73,6 +74,8 @@ const formatYaml = (content, filePath, outputFormat) => {
73
74
  return content;
74
75
  if (!content || content.trim() === '')
75
76
  return content;
77
+ if ((0, commentOnlyDetector_1.isCommentOnlyContent)(content))
78
+ return content;
76
79
  try {
77
80
  const yamlDocument = yaml_1.default.parseDocument(content);
78
81
  const rules = getFormattingRules(filePath, outputFormat);
@@ -310,7 +313,7 @@ const applyKeySeparator = (yamlString, indent) => {
310
313
  const baseIndent = ' '.repeat(indent);
311
314
  let hasSeenSecondLevelKey = false;
312
315
  for (const line of lines) {
313
- if (!line)
316
+ if (!line.trim())
314
317
  continue;
315
318
  if (topLevelKeys > 1) {
316
319
  if (!line.startsWith(' ') && result.length > 0 && result.at(-1) !== '')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "HelmEnvDelta – environment-aware YAML delta and sync for GitOps",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -70,8 +70,8 @@
70
70
  "@types/hogan.js": "^3.0.5",
71
71
  "@types/node": "^25.0.10",
72
72
  "@types/picomatch": "^4.0.2",
73
- "@typescript-eslint/eslint-plugin": "^8.53.1",
74
- "@typescript-eslint/parser": "^8.53.1",
73
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
74
+ "@typescript-eslint/parser": "^8.54.0",
75
75
  "@vitest/coverage-v8": "^4.0.18",
76
76
  "eslint": "^9.39.2",
77
77
  "eslint-config-prettier": "^10.1.8",