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 +62 -35
- package/dist/configFile.d.ts +73 -0
- package/dist/configFile.js +44 -1
- package/dist/configLoader.d.ts +5 -2
- package/dist/configLoader.js +8 -4
- package/dist/index.js +75 -56
- package/package.json +3 -3
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
|
[](https://www.npmjs.com/package/helm-env-delta)
|
|
4
6
|
[](https://opensource.org/licenses/ISC)
|
|
5
7
|
[](https://nodejs.org/)
|
|
6
8
|
|
|
7
9
|
**Sync YAML configs across environments in seconds, not hours.**
|
|
8
10
|
|
|
9
|
-
|
|
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
|
-
##
|
|
17
|
+
## π€ Who Is This For?
|
|
16
18
|
|
|
17
|
-
**
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
+
## π‘ Why Teams Love HelmEnvDelta
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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)`
|
package/dist/configFile.d.ts
CHANGED
|
@@ -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';
|
package/dist/configFile.js
CHANGED
|
@@ -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; } });
|
package/dist/configLoader.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/configLoader.js
CHANGED
|
@@ -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 =
|
|
38
|
-
|
|
39
|
-
|
|
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)(
|
|
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:
|
|
105
|
-
include:
|
|
106
|
-
exclude:
|
|
107
|
-
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:
|
|
113
|
-
include:
|
|
114
|
-
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)(
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 (${
|
|
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 &&
|
|
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 (
|
|
280
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
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,
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
74
|
-
"@typescript-eslint/parser": "^8.
|
|
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",
|