helm-env-delta 1.9.0 β†’ 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.
26
+
27
+ ---
24
28
 
25
- **After:**
29
+ ## πŸ’‘ Why Teams Love HelmEnvDelta
26
30
 
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
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
@@ -362,8 +374,8 @@ helm-env-delta --config config.yaml
362
374
  ### 🎯 Core Settings
363
375
 
364
376
  ```yaml
365
- source: './uat' # Required: Source folder
366
- destination: './prod' # Required: Destination folder
377
+ source: './uat' # Required: Source folder (optional with --format-only)
378
+ destination: './prod' # Required: Destination folder (must differ from source)
367
379
 
368
380
  include: # Optional: File patterns (default: all)
369
381
  - '**/*.yaml'
@@ -371,8 +383,11 @@ exclude: # Optional: Exclude patterns
371
383
  - '**/test*.yaml'
372
384
 
373
385
  prune: false # Optional: Delete dest files not in source
386
+ confirmationDelay: 3000 # Optional: Delay in ms before sync (default: 3000, 0 to disable)
374
387
  ```
375
388
 
389
+ **Note:** Source and destination paths cannot resolve to the same folder.
390
+
376
391
  ---
377
392
 
378
393
  ### πŸ”’ Path Filtering (skipPath)
@@ -697,8 +712,10 @@ stopRules: # Add production safety rules
697
712
 
698
713
  **Merging:**
699
714
 
700
- - Arrays: Concatenated (child adds to parent)
701
- - Objects: Deep merged (child overrides parent)
715
+ - Primitives (`source`, `destination`, `prune`, `confirmationDelay`): Child overrides parent
716
+ - Arrays (`include`, `exclude`): Concatenated (parent + child)
717
+ - Per-file Records (`skipPath`, `transforms`, `stopRules`, `fixedValues`): Keys merged, arrays concatenated
718
+ - `outputFormat`: Shallow merged (child fields override parent)
702
719
  - Max depth: 5 levels
703
720
 
704
721
  ---
@@ -714,24 +731,24 @@ hed --config <file> [options] # Short alias
714
731
 
715
732
  ### Options
716
733
 
717
- | Flag | Description |
718
- | --------------------------- | -------------------------------------------------- |
719
- | `--config <path>` | **Required** - Configuration file |
720
- | `--validate` | Validate config and pattern usage (shows warnings) |
721
- | `--suggest` | Analyze differences and suggest config updates |
722
- | `--suggest-threshold <0-1>` | Minimum confidence for suggestions (default: 0.3) |
723
- | `--dry-run` | Preview changes without writing files |
724
- | `--force` | Override stop rules |
725
- | `--diff` | Show console diff |
726
- | `--diff-html` | Generate HTML report (opens in browser) |
727
- | `--diff-json` | Output JSON to stdout (pipe to jq) |
728
- | `--list-files` | List source/destination files without processing |
729
- | `--show-config` | Display resolved config after inheritance |
730
- | `--format-only` | Format destination files without syncing |
731
- | `--skip-format` | Skip YAML formatting during sync |
732
- | `--no-color` | Disable colored output (CI/accessibility) |
733
- | `--verbose` | Show detailed debug info |
734
- | `--quiet` | Suppress output except errors |
734
+ | Flag | Description |
735
+ | --------------------------- | --------------------------------------------------- |
736
+ | `--config <path>` | **Required** - Configuration file |
737
+ | `--validate` | Validate config and pattern usage (shows warnings) |
738
+ | `--suggest` | Analyze differences and suggest config updates |
739
+ | `--suggest-threshold <0-1>` | Minimum confidence for suggestions (default: 0.3) |
740
+ | `--dry-run` | Preview changes without writing files |
741
+ | `--force` | Override stop rules |
742
+ | `--diff` | Show console diff |
743
+ | `--diff-html` | Generate HTML report (opens in browser) |
744
+ | `--diff-json` | Output JSON to stdout (pipe to jq) |
745
+ | `--list-files` | List source/destination files without processing |
746
+ | `--show-config` | Display resolved config after inheritance |
747
+ | `--format-only` | Format destination files only (source not required) |
748
+ | `--skip-format` | Skip YAML formatting during sync |
749
+ | `--no-color` | Disable colored output (CI/accessibility) |
750
+ | `--verbose` | Show detailed debug info |
751
+ | `--quiet` | Suppress output except errors |
735
752
 
736
753
  ### Examples
737
754
 
@@ -766,11 +783,16 @@ hed --config config.yaml
766
783
  # Force override stop rules
767
784
  hed --config config.yaml --force
768
785
 
769
- # Format destination files only (no sync)
786
+ # Format destination files only (no sync, source not required in config)
770
787
  hed --config config.yaml --format-only
771
788
 
772
789
  # Preview format changes
773
790
  hed --config config.yaml --format-only --dry-run
791
+
792
+ # Format-only config example (no source needed):
793
+ # destination: './prod'
794
+ # outputFormat:
795
+ # indent: 2
774
796
  ```
775
797
 
776
798
  ---
@@ -1014,6 +1036,14 @@ spec:
1014
1036
 
1015
1037
  ## πŸ†˜ Common Issues
1016
1038
 
1039
+ ### ❓ Source and destination folders cannot be the same
1040
+
1041
+ **Error:** `Source and destination folders cannot be the same`
1042
+
1043
+ **Fix:** Ensure source and destination paths resolve to different directories. Paths like `./envs/../envs/prod` and `./envs/prod` will be detected as identical.
1044
+
1045
+ ---
1046
+
1017
1047
  ### ❓ Stop rule violations blocking sync
1018
1048
 
1019
1049
  **Error:** `πŸ›‘ Stop Rule Violation (semverMajorUpgrade)`
@@ -104,6 +104,7 @@ declare const baseConfigSchema: z.ZodObject<{
104
104
  include: z.ZodOptional<z.ZodArray<z.ZodString>>;
105
105
  exclude: z.ZodOptional<z.ZodArray<z.ZodString>>;
106
106
  prune: z.ZodOptional<z.ZodBoolean>;
107
+ confirmationDelay: z.ZodOptional<z.ZodNumber>;
107
108
  skipPath: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
108
109
  outputFormat: z.ZodOptional<z.ZodObject<{
109
110
  indent: z.ZodOptional<z.ZodNumber>;
@@ -223,6 +224,78 @@ declare const finalConfigSchema: z.ZodObject<{
223
224
  include: z.ZodDefault<z.ZodArray<z.ZodString>>;
224
225
  exclude: z.ZodDefault<z.ZodArray<z.ZodString>>;
225
226
  prune: z.ZodDefault<z.ZodBoolean>;
227
+ confirmationDelay: z.ZodDefault<z.ZodNumber>;
228
+ outputFormat: z.ZodDefault<z.ZodOptional<z.ZodObject<{
229
+ indent: z.ZodDefault<z.ZodNumber>;
230
+ keySeparator: z.ZodDefault<z.ZodBoolean>;
231
+ quoteValues: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
232
+ keyOrders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
233
+ arraySort: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodObject<{
234
+ path: z.ZodString;
235
+ sortBy: z.ZodString;
236
+ order: z.ZodDefault<z.ZodEnum<{
237
+ asc: "asc";
238
+ desc: "desc";
239
+ }>>;
240
+ }, z.core.$strip>>>>;
241
+ }, z.core.$strip>>>;
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>;
226
299
  outputFormat: z.ZodDefault<z.ZodOptional<z.ZodObject<{
227
300
  indent: z.ZodDefault<z.ZodNumber>;
228
301
  keySeparator: z.ZodDefault<z.ZodBoolean>;
@@ -240,6 +313,7 @@ declare const finalConfigSchema: z.ZodObject<{
240
313
  }, z.core.$strip>;
241
314
  export type BaseConfig = z.infer<typeof baseConfigSchema>;
242
315
  export type FinalConfig = z.infer<typeof finalConfigSchema>;
316
+ export type FormatOnlyConfig = z.infer<typeof formatOnlyConfigSchema>;
243
317
  export type Config = FinalConfig;
244
318
  export type StopRule = z.infer<typeof stopRuleSchema>;
245
319
  export type SemverMajorUpgradeRule = z.infer<typeof semverMajorUpgradeRuleSchema>;
@@ -258,5 +332,6 @@ export type FixedValueRule = z.infer<typeof fixedValueRuleSchema>;
258
332
  export type FixedValueConfig = Record<string, FixedValueRule[]>;
259
333
  export declare const parseBaseConfig: (data: unknown, configPath?: string) => BaseConfig;
260
334
  export declare const parseFinalConfig: (data: unknown, configPath?: string) => FinalConfig;
335
+ export declare const parseFormatOnlyConfig: (data: unknown, configPath?: string) => FormatOnlyConfig;
261
336
  export declare const parseConfig: (data: unknown, configPath?: string) => FinalConfig;
262
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({
@@ -116,6 +120,7 @@ const baseConfigSchema = zod_1.z.object({
116
120
  include: zod_1.z.array(zod_1.z.string().min(1)).optional(),
117
121
  exclude: zod_1.z.array(zod_1.z.string().min(1)).optional(),
118
122
  prune: zod_1.z.boolean().optional(),
123
+ confirmationDelay: zod_1.z.number().int().min(0).optional(),
119
124
  skipPath: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
120
125
  outputFormat: zod_1.z
121
126
  .object({
@@ -137,6 +142,34 @@ const finalConfigSchema = baseConfigSchema
137
142
  include: zod_1.z.array(zod_1.z.string().min(1)).default(['**/*']),
138
143
  exclude: zod_1.z.array(zod_1.z.string().min(1)).default([]),
139
144
  prune: zod_1.z.boolean().default(false),
145
+ confirmationDelay: zod_1.z.number().int().min(0).default(3000),
146
+ outputFormat: zod_1.z
147
+ .object({
148
+ indent: zod_1.z.number().int().min(1).max(10).default(2),
149
+ keySeparator: zod_1.z.boolean().default(false),
150
+ quoteValues: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
151
+ keyOrders: zod_1.z.record(zod_1.z.string(), zod_1.z.array(zod_1.z.string())).optional(),
152
+ arraySort: zod_1.z.record(zod_1.z.string(), zod_1.z.array(arraySortRuleSchema)).optional()
153
+ })
154
+ .optional()
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),
140
173
  outputFormat: zod_1.z
141
174
  .object({
142
175
  indent: zod_1.z.number().int().min(1).max(10).default(2),
@@ -167,6 +200,18 @@ const parseFinalConfig = (data, configPath) => {
167
200
  return result.data;
168
201
  };
169
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;
170
215
  exports.parseConfig = exports.parseFinalConfig;
171
216
  var ZodError_2 = require("./ZodError");
172
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;
@@ -84,6 +84,10 @@ const mergeConfigs = (parent, child) => {
84
84
  merged.prune = child.prune;
85
85
  else if (parent.prune !== undefined)
86
86
  merged.prune = parent.prune;
87
+ if (child.confirmationDelay !== undefined)
88
+ merged.confirmationDelay = child.confirmationDelay;
89
+ else if (parent.confirmationDelay !== undefined)
90
+ merged.confirmationDelay = parent.confirmationDelay;
87
91
  const parentInclude = parent.include ?? [];
88
92
  const childInclude = child.include ?? [];
89
93
  if (parentInclude.length > 0 || childInclude.length > 0)
@@ -100,6 +104,7 @@ const mergeConfigs = (parent, child) => {
100
104
  merged.skipPath = mergePerFileRecords(parent.skipPath, child.skipPath);
101
105
  merged.transforms = mergeTransformRecords(parent.transforms, child.transforms);
102
106
  merged.stopRules = mergePerFileRecords(parent.stopRules, child.stopRules);
107
+ merged.fixedValues = mergePerFileRecords(parent.fixedValues, child.fixedValues);
103
108
  return merged;
104
109
  };
105
110
  exports.mergeConfigs = mergeConfigs;
@@ -1,4 +1,3 @@
1
- export declare const SYNC_CONFIRMATION_DELAY_MS = 2000;
2
1
  export declare const YAML_LINE_WIDTH_UNLIMITED = 0;
3
2
  export declare const YAML_DEFAULT_INDENT = 2;
4
3
  export declare const MAX_CONFIG_EXTENDS_DEPTH = 5;
package/dist/constants.js CHANGED
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SEMVER_STRICT_PATTERN = exports.MAX_CONFIG_EXTENDS_DEPTH = exports.YAML_DEFAULT_INDENT = exports.YAML_LINE_WIDTH_UNLIMITED = exports.SYNC_CONFIRMATION_DELAY_MS = void 0;
4
- exports.SYNC_CONFIRMATION_DELAY_MS = 2000;
3
+ exports.SEMVER_STRICT_PATTERN = exports.MAX_CONFIG_EXTENDS_DEPTH = exports.YAML_DEFAULT_INDENT = exports.YAML_LINE_WIDTH_UNLIMITED = void 0;
5
4
  exports.YAML_LINE_WIDTH_UNLIMITED = 0;
6
5
  exports.YAML_DEFAULT_INDENT = 2;
7
6
  exports.MAX_CONFIG_EXTENDS_DEPTH = 5;
package/dist/index.js CHANGED
@@ -49,7 +49,6 @@ const configMerger_1 = require("./configMerger");
49
49
  const configWarnings_1 = require("./configWarnings");
50
50
  const consoleDiffReporter_1 = require("./consoleDiffReporter");
51
51
  const consoleFormatter_1 = require("./consoleFormatter");
52
- const constants_1 = require("./constants");
53
52
  const fileDiff_1 = require("./fileDiff");
54
53
  const fileLoader_1 = require("./fileLoader");
55
54
  const fileUpdater_1 = require("./fileUpdater");
@@ -85,15 +84,20 @@ const main = async () => {
85
84
  (0, node_fs_1.mkdirSync)(configDirectory, { recursive: true });
86
85
  (0, node_fs_1.writeFileSync)(firstRunMarker, new Date().toISOString());
87
86
  }
88
- const config = (0, configLoader_1.loadConfigFile)(command.config, command.quiet, logger);
87
+ const config = (0, configLoader_1.loadConfigFile)(command.config, command.quiet, logger, { formatOnly: command.formatOnly });
89
88
  if (command.showConfig) {
90
89
  console.log(chalk_1.default.cyan('\nβš™οΈ Resolved Configuration:\n'));
91
90
  console.log(YAML.stringify(config, { indent: 2 }));
92
91
  return;
93
92
  }
94
93
  if (command.validate) {
94
+ if (!config.source) {
95
+ logger.error('\nSource folder is required for validation mode.', 'critical');
96
+ process.exit(1);
97
+ }
98
+ const validationConfig = config;
95
99
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating configuration...', 'info'));
96
- const warningResult = (0, configWarnings_1.validateConfigWarnings)(config);
100
+ const warningResult = (0, configWarnings_1.validateConfigWarnings)(validationConfig);
97
101
  let hasAnyWarnings = warningResult.hasWarnings;
98
102
  if (warningResult.hasWarnings) {
99
103
  console.warn(chalk_1.default.yellow('\n⚠️ Configuration Warnings (non-fatal):\n'));
@@ -102,23 +106,23 @@ const main = async () => {
102
106
  }
103
107
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files for validation...', 'loading'));
104
108
  const sourceResult = await (0, fileLoader_1.loadFiles)({
105
- baseDirectory: config.source,
106
- include: config.include,
107
- exclude: config.exclude,
108
- transforms: config.transforms,
109
+ baseDirectory: validationConfig.source,
110
+ include: validationConfig.include,
111
+ exclude: validationConfig.exclude,
112
+ transforms: validationConfig.transforms,
109
113
  skipExclude: true
110
114
  }, logger);
111
115
  const sourceFiles = sourceResult.fileMap;
112
116
  const destinationResult = await (0, fileLoader_1.loadFiles)({
113
- baseDirectory: config.destination,
114
- include: config.include,
115
- exclude: config.exclude,
117
+ baseDirectory: validationConfig.destination,
118
+ include: validationConfig.include,
119
+ exclude: validationConfig.exclude,
116
120
  skipExclude: true
117
121
  }, logger);
118
122
  const destinationFiles = destinationResult.fileMap;
119
123
  logger.progress(`Loaded ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'success');
120
124
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating pattern usage...', 'info'));
121
- const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(config, sourceFiles, destinationFiles);
125
+ const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(validationConfig, sourceFiles, destinationFiles);
122
126
  hasAnyWarnings = hasAnyWarnings || usageResult.hasWarnings;
123
127
  if (usageResult.hasWarnings) {
124
128
  console.warn(chalk_1.default.yellow('\n⚠️ Pattern Usage Warnings (non-fatal):\n'));
@@ -135,52 +139,27 @@ const main = async () => {
135
139
  }
136
140
  if (logger.shouldShow('debug')) {
137
141
  logger.debug('\nConfig details:');
138
- logger.debug(` Source: ${config.source}`);
142
+ if (config.source)
143
+ logger.debug(` Source: ${config.source}`);
139
144
  logger.debug(` Destination: ${config.destination}`);
140
145
  logger.debug(` Include patterns: ${config.include.join(', ')}`);
141
146
  logger.debug(` Exclude patterns: ${config.exclude.join(', ')}`);
142
147
  logger.debug(` Transforms: ${Object.keys(config.transforms || {}).length} pattern(s)`);
143
148
  logger.debug(` Prune enabled: ${config.prune}`);
144
149
  }
145
- logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
146
- const sourceResult = await (0, fileLoader_1.loadFiles)({
147
- baseDirectory: config.source,
148
- include: config.include,
149
- exclude: config.exclude,
150
- transforms: config.transforms
151
- }, logger);
152
- const sourceFiles = sourceResult.fileMap;
153
- const originalPaths = sourceResult.originalPaths;
154
- logger.progress(`Loaded ${sourceFiles.size} source file(s)`, 'success');
155
- const collisions = (0, collisionDetector_1.detectCollisions)(sourceFiles, config.transforms);
156
- if (collisions.length > 0)
157
- (0, collisionDetector_1.validateNoCollisions)(collisions);
158
- if (logger.shouldShow('debug'))
159
- logger.debug('Filename collision check: passed');
160
- const destinationResult = await (0, fileLoader_1.loadFiles)({
161
- baseDirectory: config.destination,
162
- include: config.include,
163
- exclude: config.exclude
164
- }, logger);
165
- const destinationFiles = destinationResult.fileMap;
166
- logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
167
- if (command.listFiles) {
168
- const sourceFilesList = [...sourceFiles.keys()].toSorted();
169
- const destinationFilesList = [...destinationFiles.keys()].toSorted();
170
- console.log(chalk_1.default.cyan('\nπŸ“‹ Files to be synced:\n'));
171
- console.log(chalk_1.default.green(`Source files: ${sourceFilesList.length}`));
172
- for (const file of sourceFilesList)
173
- console.log(` ${chalk_1.default.dim(file)}`);
174
- console.log(chalk_1.default.yellow(`\nDestination files: ${destinationFilesList.length}`));
175
- for (const file of destinationFilesList)
176
- console.log(` ${chalk_1.default.dim(file)}`);
177
- return;
178
- }
179
150
  if (command.formatOnly) {
180
151
  if (!config.outputFormat) {
181
152
  logger.log(chalk_1.default.yellow('\n⚠️ No outputFormat configured. Nothing to format.'));
182
153
  return;
183
154
  }
155
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading destination files...', 'loading'));
156
+ const destinationResult = await (0, fileLoader_1.loadFiles)({
157
+ baseDirectory: config.destination,
158
+ include: config.include,
159
+ exclude: config.exclude
160
+ }, logger);
161
+ const destinationFiles = destinationResult.fileMap;
162
+ logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
184
163
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Formatting files...', 'info'));
185
164
  let formattedCount = 0;
186
165
  const errors = [];
@@ -216,12 +195,51 @@ const main = async () => {
216
195
  }
217
196
  return;
218
197
  }
198
+ if (!config.source) {
199
+ logger.error('\nSource folder is required for sync operations.', 'critical');
200
+ process.exit(1);
201
+ }
202
+ const syncConfig = config;
203
+ logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files...', 'loading'));
204
+ const sourceResult = await (0, fileLoader_1.loadFiles)({
205
+ baseDirectory: syncConfig.source,
206
+ include: syncConfig.include,
207
+ exclude: syncConfig.exclude,
208
+ transforms: syncConfig.transforms
209
+ }, logger);
210
+ const sourceFiles = sourceResult.fileMap;
211
+ const originalPaths = sourceResult.originalPaths;
212
+ logger.progress(`Loaded ${sourceFiles.size} source file(s)`, 'success');
213
+ const collisions = (0, collisionDetector_1.detectCollisions)(sourceFiles, syncConfig.transforms);
214
+ if (collisions.length > 0)
215
+ (0, collisionDetector_1.validateNoCollisions)(collisions);
216
+ if (logger.shouldShow('debug'))
217
+ logger.debug('Filename collision check: passed');
218
+ const destinationResult = await (0, fileLoader_1.loadFiles)({
219
+ baseDirectory: syncConfig.destination,
220
+ include: syncConfig.include,
221
+ exclude: syncConfig.exclude
222
+ }, logger);
223
+ const destinationFiles = destinationResult.fileMap;
224
+ logger.progress(`Loaded ${destinationFiles.size} destination file(s)`, 'success');
225
+ if (command.listFiles) {
226
+ const sourceFilesList = [...sourceFiles.keys()].toSorted();
227
+ const destinationFilesList = [...destinationFiles.keys()].toSorted();
228
+ console.log(chalk_1.default.cyan('\nπŸ“‹ Files to be synced:\n'));
229
+ console.log(chalk_1.default.green(`Source files: ${sourceFilesList.length}`));
230
+ for (const file of sourceFilesList)
231
+ console.log(` ${chalk_1.default.dim(file)}`);
232
+ console.log(chalk_1.default.yellow(`\nDestination files: ${destinationFilesList.length}`));
233
+ for (const file of destinationFilesList)
234
+ console.log(` ${chalk_1.default.dim(file)}`);
235
+ return;
236
+ }
219
237
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
220
- const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger, originalPaths);
238
+ const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, syncConfig, logger, originalPaths);
221
239
  if (logger.shouldShow('debug'))
222
240
  logger.debug('Diff pipeline: parse β†’ transforms β†’ skipPath β†’ normalize β†’ compare');
223
241
  if (command.diff && !command.quiet)
224
- (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, config);
242
+ (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, syncConfig);
225
243
  else {
226
244
  logger.log(` New files: ${diffResult.addedFiles.length}`);
227
245
  logger.log(` Deleted files: ${diffResult.deletedFiles.length}`);
@@ -231,7 +249,7 @@ const main = async () => {
231
249
  if (command.suggest) {
232
250
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Analyzing differences for suggestions...', 'info'));
233
251
  try {
234
- const suggestions = (0, suggestionEngine_1.analyzeDifferencesForSuggestions)(diffResult, config, command.suggestThreshold);
252
+ const suggestions = (0, suggestionEngine_1.analyzeDifferencesForSuggestions)(diffResult, syncConfig, command.suggestThreshold);
235
253
  const yaml = (0, suggestionEngine_1.formatSuggestionsAsYaml)(suggestions);
236
254
  console.log(chalk_1.default.cyan('\nπŸ’‘ Suggested Configuration:\n'));
237
255
  console.log(yaml);
@@ -252,7 +270,7 @@ const main = async () => {
252
270
  }
253
271
  }
254
272
  const configFileDirectory = node_path_1.default.dirname(node_path_1.default.resolve(command.config));
255
- const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, config.stopRules, configFileDirectory, logger);
273
+ const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, syncConfig.stopRules, configFileDirectory, logger);
256
274
  if (validationResult.violations.length > 0)
257
275
  if (command.force)
258
276
  for (const violation of validationResult.violations)
@@ -271,19 +289,20 @@ const main = async () => {
271
289
  console.log(chalk_1.default.dim('─'.repeat(60)));
272
290
  console.log(` ${chalk_1.default.green('Added:')} ${diffResult.addedFiles.length} files`);
273
291
  console.log(` ${chalk_1.default.yellow('Changed:')} ${diffResult.changedFiles.length} files`);
274
- console.log(` ${chalk_1.default.red('Deleted:')} ${diffResult.deletedFiles.length} files (${config.prune ? 'prune enabled' : 'prune disabled'})`);
292
+ console.log(` ${chalk_1.default.red('Deleted:')} ${diffResult.deletedFiles.length} files (${syncConfig.prune ? 'prune enabled' : 'prune disabled'})`);
275
293
  console.log(` ${chalk_1.default.blue('Unchanged:')} ${diffResult.unchangedFiles.length} files`);
276
294
  console.log(chalk_1.default.dim('─'.repeat(60)));
277
- if (diffResult.deletedFiles.length > 0 && config.prune)
295
+ if (diffResult.deletedFiles.length > 0 && syncConfig.prune)
278
296
  console.warn(chalk_1.default.red('⚠️ Warning: Prune is enabled. Files will be permanently deleted!'));
279
297
  console.log(chalk_1.default.dim('\nPress Ctrl+C to cancel, or use --dry-run to preview changes first.\n'));
280
- await new Promise((resolve) => setTimeout(resolve, constants_1.SYNC_CONFIRMATION_DELAY_MS));
298
+ if (syncConfig.confirmationDelay > 0)
299
+ await new Promise((resolve) => setTimeout(resolve, syncConfig.confirmationDelay));
281
300
  }
282
- const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, config, command.dryRun, command.skipFormat, logger);
301
+ const formattedFiles = await (0, fileUpdater_1.updateFiles)(diffResult, sourceFiles, destinationFiles, syncConfig, command.dryRun, command.skipFormat, logger);
283
302
  if (command.diffHtml && !command.quiet)
284
- await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, config, command.dryRun, logger);
303
+ await (0, htmlReporter_1.generateHtmlReport)(diffResult, formattedFiles, syncConfig, command.dryRun, logger);
285
304
  if (command.diffJson)
286
- (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, config, command.dryRun, package_json_1.default.version);
305
+ (0, jsonReporter_1.generateJsonReport)(diffResult, formattedFiles, validationResult, syncConfig, command.dryRun, package_json_1.default.version);
287
306
  };
288
307
  (async () => {
289
308
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm-env-delta",
3
- "version": "1.9.0",
3
+ "version": "1.9.2",
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",