helm-env-delta 1.9.1 β†’ 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'
@@ -374,6 +386,8 @@ prune: false # Optional: Delete dest files not in source
374
386
  confirmationDelay: 3000 # Optional: Delay in ms before sync (default: 3000, 0 to disable)
375
387
  ```
376
388
 
389
+ **Note:** Source and destination paths cannot resolve to the same folder.
390
+
377
391
  ---
378
392
 
379
393
  ### πŸ”’ Path Filtering (skipPath)
@@ -717,24 +731,24 @@ hed --config <file> [options] # Short alias
717
731
 
718
732
  ### Options
719
733
 
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 |
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 |
738
752
 
739
753
  ### Examples
740
754
 
@@ -769,11 +783,16 @@ hed --config config.yaml
769
783
  # Force override stop rules
770
784
  hed --config config.yaml --force
771
785
 
772
- # Format destination files only (no sync)
786
+ # Format destination files only (no sync, source not required in config)
773
787
  hed --config config.yaml --format-only
774
788
 
775
789
  # Preview format changes
776
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
777
796
  ```
778
797
 
779
798
  ---
@@ -1017,6 +1036,14 @@ spec:
1017
1036
 
1018
1037
  ## πŸ†˜ Common Issues
1019
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
+
1020
1047
  ### ❓ Stop rule violations blocking sync
1021
1048
 
1022
1049
  **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/index.js CHANGED
@@ -84,15 +84,20 @@ const main = async () => {
84
84
  (0, node_fs_1.mkdirSync)(configDirectory, { recursive: true });
85
85
  (0, node_fs_1.writeFileSync)(firstRunMarker, new Date().toISOString());
86
86
  }
87
- 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 });
88
88
  if (command.showConfig) {
89
89
  console.log(chalk_1.default.cyan('\nβš™οΈ Resolved Configuration:\n'));
90
90
  console.log(YAML.stringify(config, { indent: 2 }));
91
91
  return;
92
92
  }
93
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;
94
99
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating configuration...', 'info'));
95
- const warningResult = (0, configWarnings_1.validateConfigWarnings)(config);
100
+ const warningResult = (0, configWarnings_1.validateConfigWarnings)(validationConfig);
96
101
  let hasAnyWarnings = warningResult.hasWarnings;
97
102
  if (warningResult.hasWarnings) {
98
103
  console.warn(chalk_1.default.yellow('\n⚠️ Configuration Warnings (non-fatal):\n'));
@@ -101,23 +106,23 @@ const main = async () => {
101
106
  }
102
107
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Loading files for validation...', 'loading'));
103
108
  const sourceResult = await (0, fileLoader_1.loadFiles)({
104
- baseDirectory: config.source,
105
- include: config.include,
106
- exclude: config.exclude,
107
- transforms: config.transforms,
109
+ baseDirectory: validationConfig.source,
110
+ include: validationConfig.include,
111
+ exclude: validationConfig.exclude,
112
+ transforms: validationConfig.transforms,
108
113
  skipExclude: true
109
114
  }, logger);
110
115
  const sourceFiles = sourceResult.fileMap;
111
116
  const destinationResult = await (0, fileLoader_1.loadFiles)({
112
- baseDirectory: config.destination,
113
- include: config.include,
114
- exclude: config.exclude,
117
+ baseDirectory: validationConfig.destination,
118
+ include: validationConfig.include,
119
+ exclude: validationConfig.exclude,
115
120
  skipExclude: true
116
121
  }, logger);
117
122
  const destinationFiles = destinationResult.fileMap;
118
123
  logger.progress(`Loaded ${sourceFiles.size} source, ${destinationFiles.size} destination file(s)`, 'success');
119
124
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Validating pattern usage...', 'info'));
120
- const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(config, sourceFiles, destinationFiles);
125
+ const usageResult = (0, patternUsageValidator_1.validatePatternUsage)(validationConfig, sourceFiles, destinationFiles);
121
126
  hasAnyWarnings = hasAnyWarnings || usageResult.hasWarnings;
122
127
  if (usageResult.hasWarnings) {
123
128
  console.warn(chalk_1.default.yellow('\n⚠️ Pattern Usage Warnings (non-fatal):\n'));
@@ -134,52 +139,27 @@ const main = async () => {
134
139
  }
135
140
  if (logger.shouldShow('debug')) {
136
141
  logger.debug('\nConfig details:');
137
- logger.debug(` Source: ${config.source}`);
142
+ if (config.source)
143
+ logger.debug(` Source: ${config.source}`);
138
144
  logger.debug(` Destination: ${config.destination}`);
139
145
  logger.debug(` Include patterns: ${config.include.join(', ')}`);
140
146
  logger.debug(` Exclude patterns: ${config.exclude.join(', ')}`);
141
147
  logger.debug(` Transforms: ${Object.keys(config.transforms || {}).length} pattern(s)`);
142
148
  logger.debug(` Prune enabled: ${config.prune}`);
143
149
  }
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
150
  if (command.formatOnly) {
179
151
  if (!config.outputFormat) {
180
152
  logger.log(chalk_1.default.yellow('\n⚠️ No outputFormat configured. Nothing to format.'));
181
153
  return;
182
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');
183
163
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Formatting files...', 'info'));
184
164
  let formattedCount = 0;
185
165
  const errors = [];
@@ -215,12 +195,51 @@ const main = async () => {
215
195
  }
216
196
  return;
217
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
+ }
218
237
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Computing differences...', 'info'));
219
- const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, config, logger, originalPaths);
238
+ const diffResult = (0, fileDiff_1.computeFileDiff)(sourceFiles, destinationFiles, syncConfig, logger, originalPaths);
220
239
  if (logger.shouldShow('debug'))
221
240
  logger.debug('Diff pipeline: parse β†’ transforms β†’ skipPath β†’ normalize β†’ compare');
222
241
  if (command.diff && !command.quiet)
223
- (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, config);
242
+ (0, consoleDiffReporter_1.showConsoleDiff)(diffResult, syncConfig);
224
243
  else {
225
244
  logger.log(` New files: ${diffResult.addedFiles.length}`);
226
245
  logger.log(` Deleted files: ${diffResult.deletedFiles.length}`);
@@ -230,7 +249,7 @@ const main = async () => {
230
249
  if (command.suggest) {
231
250
  logger.log('\n' + (0, consoleFormatter_1.formatProgressMessage)('Analyzing differences for suggestions...', 'info'));
232
251
  try {
233
- const suggestions = (0, suggestionEngine_1.analyzeDifferencesForSuggestions)(diffResult, config, command.suggestThreshold);
252
+ const suggestions = (0, suggestionEngine_1.analyzeDifferencesForSuggestions)(diffResult, syncConfig, command.suggestThreshold);
234
253
  const yaml = (0, suggestionEngine_1.formatSuggestionsAsYaml)(suggestions);
235
254
  console.log(chalk_1.default.cyan('\nπŸ’‘ Suggested Configuration:\n'));
236
255
  console.log(yaml);
@@ -251,7 +270,7 @@ const main = async () => {
251
270
  }
252
271
  }
253
272
  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);
273
+ const validationResult = (0, stopRulesValidator_1.validateStopRules)(diffResult, syncConfig.stopRules, configFileDirectory, logger);
255
274
  if (validationResult.violations.length > 0)
256
275
  if (command.force)
257
276
  for (const violation of validationResult.violations)
@@ -270,20 +289,20 @@ const main = async () => {
270
289
  console.log(chalk_1.default.dim('─'.repeat(60)));
271
290
  console.log(` ${chalk_1.default.green('Added:')} ${diffResult.addedFiles.length} files`);
272
291
  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'})`);
292
+ console.log(` ${chalk_1.default.red('Deleted:')} ${diffResult.deletedFiles.length} files (${syncConfig.prune ? 'prune enabled' : 'prune disabled'})`);
274
293
  console.log(` ${chalk_1.default.blue('Unchanged:')} ${diffResult.unchangedFiles.length} files`);
275
294
  console.log(chalk_1.default.dim('─'.repeat(60)));
276
- if (diffResult.deletedFiles.length > 0 && config.prune)
295
+ if (diffResult.deletedFiles.length > 0 && syncConfig.prune)
277
296
  console.warn(chalk_1.default.red('⚠️ Warning: Prune is enabled. Files will be permanently deleted!'));
278
297
  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));
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.1",
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",