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 +67 -37
- package/dist/configFile.d.ts +75 -0
- package/dist/configFile.js +46 -1
- package/dist/configLoader.d.ts +5 -2
- package/dist/configLoader.js +8 -4
- package/dist/configMerger.js +5 -0
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +1 -2
- 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'
|
|
@@ -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
|
-
-
|
|
701
|
-
-
|
|
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
|
|
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)`
|
package/dist/configFile.d.ts
CHANGED
|
@@ -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';
|
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({
|
|
@@ -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; } });
|
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/configMerger.js
CHANGED
|
@@ -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;
|
package/dist/constants.d.ts
CHANGED
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 =
|
|
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)(
|
|
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:
|
|
106
|
-
include:
|
|
107
|
-
exclude:
|
|
108
|
-
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:
|
|
114
|
-
include:
|
|
115
|
-
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)(
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 (${
|
|
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 &&
|
|
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
|
-
|
|
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",
|