@aspruyt/json-config-sync 1.0.0 → 2.0.0
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 +408 -58
- package/dist/config.d.ts +14 -2
- package/dist/config.js +81 -9
- package/dist/env.d.ts +24 -0
- package/dist/env.js +88 -0
- package/dist/index.js +4 -4
- package/dist/merge.d.ts +27 -0
- package/dist/merge.js +147 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,16 +5,70 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@aspruyt/json-config-sync)
|
|
6
6
|
[](https://www.npmjs.com/package/@aspruyt/json-config-sync)
|
|
7
7
|
|
|
8
|
-
A CLI tool that syncs JSON configuration files across multiple
|
|
8
|
+
A CLI tool that syncs JSON or YAML configuration files across multiple GitHub and Azure DevOps repositories by creating pull requests. Output format is automatically detected from the target filename extension.
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Quick Start](#quick-start)
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [How It Works](#how-it-works)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Prerequisites](#prerequisites)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [Configuration Format](#configuration-format)
|
|
19
|
+
- [Examples](#examples)
|
|
20
|
+
- [Supported Git URL Formats](#supported-git-url-formats)
|
|
21
|
+
- [CI/CD Integration](#cicd-integration)
|
|
22
|
+
- [Output Examples](#output-examples)
|
|
23
|
+
- [Troubleshooting](#troubleshooting)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Install
|
|
31
|
+
npm install -g @aspruyt/json-config-sync
|
|
32
|
+
|
|
33
|
+
# Authenticate (GitHub)
|
|
34
|
+
gh auth login
|
|
35
|
+
|
|
36
|
+
# Create config.yaml
|
|
37
|
+
cat > config.yaml << 'EOF'
|
|
38
|
+
fileName: .prettierrc.json
|
|
39
|
+
|
|
40
|
+
# Base configuration inherited by all repos
|
|
41
|
+
content:
|
|
42
|
+
semi: false
|
|
43
|
+
singleQuote: true
|
|
44
|
+
tabWidth: 2
|
|
45
|
+
trailingComma: es5
|
|
46
|
+
|
|
47
|
+
repos:
|
|
48
|
+
# Multiple repos can share the same config
|
|
49
|
+
- git:
|
|
50
|
+
- git@github.com:your-org/frontend-app.git
|
|
51
|
+
- git@github.com:your-org/backend-api.git
|
|
52
|
+
- git@github.com:your-org/shared-lib.git
|
|
53
|
+
EOF
|
|
54
|
+
|
|
55
|
+
# Run
|
|
56
|
+
json-config-sync --config ./config.yaml
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Result:** PRs are created in all three repos with identical `.prettierrc.json` files.
|
|
9
60
|
|
|
10
61
|
## Features
|
|
11
62
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
63
|
+
- **JSON/YAML Output** - Automatically outputs JSON or YAML based on filename extension
|
|
64
|
+
- **Content Inheritance** - Define base config once, override per-repo as needed
|
|
65
|
+
- **Multi-Repo Targeting** - Apply same config to multiple repos with array syntax
|
|
66
|
+
- **Environment Variables** - Use `${VAR}` syntax for dynamic values
|
|
67
|
+
- **Merge Strategies** - Control how arrays merge (replace, append, prepend)
|
|
68
|
+
- **Override Mode** - Skip merging entirely for specific repos
|
|
69
|
+
- **GitHub & Azure DevOps** - Works with both platforms
|
|
70
|
+
- **Dry-Run Mode** - Preview changes without creating PRs
|
|
71
|
+
- **Error Resilience** - Continues processing if individual repos fail
|
|
18
72
|
|
|
19
73
|
## Installation
|
|
20
74
|
|
|
@@ -71,88 +125,384 @@ json-config-sync --config ./config.yaml --work-dir ./my-temp
|
|
|
71
125
|
|
|
72
126
|
### Options
|
|
73
127
|
|
|
74
|
-
| Option
|
|
75
|
-
|
|
76
|
-
| `--config`
|
|
77
|
-
| `--dry-run`
|
|
78
|
-
| `--work-dir` | `-w`
|
|
128
|
+
| Option | Alias | Description | Required |
|
|
129
|
+
| ------------ | ----- | -------------------------------------------------- | -------- |
|
|
130
|
+
| `--config` | `-c` | Path to YAML config file | Yes |
|
|
131
|
+
| `--dry-run` | `-d` | Show what would be done without making changes | No |
|
|
132
|
+
| `--work-dir` | `-w` | Temporary directory for cloning (default: `./tmp`) | No |
|
|
79
133
|
|
|
80
134
|
## Configuration Format
|
|
81
135
|
|
|
82
|
-
|
|
136
|
+
### Basic Structure
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
fileName: my.config.json # Target file (.json outputs JSON, .yaml/.yml outputs YAML)
|
|
140
|
+
mergeStrategy: replace # Default array merge strategy (optional)
|
|
141
|
+
|
|
142
|
+
content: # Base config content (optional)
|
|
143
|
+
key: value
|
|
144
|
+
|
|
145
|
+
repos: # List of repositories
|
|
146
|
+
- git: git@github.com:org/repo.git
|
|
147
|
+
content: # Per-repo overlay (optional if base content exists)
|
|
148
|
+
key: override
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Root-Level Fields
|
|
152
|
+
|
|
153
|
+
| Field | Description | Required |
|
|
154
|
+
| --------------- | --------------------------------------------------------------------- | -------- |
|
|
155
|
+
| `fileName` | Target file name (`.json` → JSON output, `.yaml`/`.yml` → YAML output)| Yes |
|
|
156
|
+
| `content` | Base config inherited by all repos | No* |
|
|
157
|
+
| `mergeStrategy` | Default array merge strategy: `replace`, `append`, `prepend` | No |
|
|
158
|
+
| `repos` | Array of repository configurations | Yes |
|
|
159
|
+
|
|
160
|
+
\* Required if any repo entry omits the `content` field.
|
|
161
|
+
|
|
162
|
+
### Per-Repo Fields
|
|
163
|
+
|
|
164
|
+
| Field | Description | Required |
|
|
165
|
+
| ---------- | --------------------------------------------------------- | -------- |
|
|
166
|
+
| `git` | Git URL (string) or array of URLs | Yes |
|
|
167
|
+
| `content` | Content overlay merged onto base (optional if base exists)| No* |
|
|
168
|
+
| `override` | If `true`, ignore base content and use only this repo's | No |
|
|
169
|
+
|
|
170
|
+
\* Required if no root-level `content` is defined.
|
|
171
|
+
|
|
172
|
+
### Environment Variables
|
|
173
|
+
|
|
174
|
+
Use `${VAR}` syntax in string values:
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
content:
|
|
178
|
+
apiUrl: ${API_URL} # Required - errors if not set
|
|
179
|
+
environment: ${ENV:-development} # With default value
|
|
180
|
+
secretKey: ${SECRET:?Secret required} # Required with custom error message
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Merge Directives
|
|
184
|
+
|
|
185
|
+
Control array merging with the `$arrayMerge` directive:
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
content:
|
|
189
|
+
features:
|
|
190
|
+
- core
|
|
191
|
+
- monitoring
|
|
192
|
+
|
|
193
|
+
repos:
|
|
194
|
+
- git: git@github.com:org/repo.git
|
|
195
|
+
content:
|
|
196
|
+
features:
|
|
197
|
+
$arrayMerge: append # append | prepend | replace
|
|
198
|
+
values:
|
|
199
|
+
- custom-feature # Results in: [core, monitoring, custom-feature]
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Examples
|
|
203
|
+
|
|
204
|
+
### Shared Config Across Teams
|
|
205
|
+
|
|
206
|
+
Define common settings once, customize per team:
|
|
207
|
+
|
|
208
|
+
```yaml
|
|
209
|
+
fileName: service.config.json
|
|
210
|
+
|
|
211
|
+
content:
|
|
212
|
+
version: "2.0"
|
|
213
|
+
logging:
|
|
214
|
+
level: info
|
|
215
|
+
format: json
|
|
216
|
+
features:
|
|
217
|
+
- health-check
|
|
218
|
+
- metrics
|
|
219
|
+
|
|
220
|
+
repos:
|
|
221
|
+
# Platform team repos - add extra features
|
|
222
|
+
- git:
|
|
223
|
+
- git@github.com:org/api-gateway.git
|
|
224
|
+
- git@github.com:org/auth-service.git
|
|
225
|
+
content:
|
|
226
|
+
team: platform
|
|
227
|
+
features:
|
|
228
|
+
$arrayMerge: append
|
|
229
|
+
values:
|
|
230
|
+
- tracing
|
|
231
|
+
- rate-limiting
|
|
232
|
+
|
|
233
|
+
# Data team repos - different logging
|
|
234
|
+
- git:
|
|
235
|
+
- git@github.com:org/data-pipeline.git
|
|
236
|
+
- git@github.com:org/analytics.git
|
|
237
|
+
content:
|
|
238
|
+
team: data
|
|
239
|
+
logging:
|
|
240
|
+
level: debug
|
|
241
|
+
|
|
242
|
+
# Legacy service - completely different config
|
|
243
|
+
- git: git@github.com:org/legacy-api.git
|
|
244
|
+
override: true
|
|
245
|
+
content:
|
|
246
|
+
version: "1.0"
|
|
247
|
+
legacy: true
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Environment-Specific Values
|
|
251
|
+
|
|
252
|
+
Use environment variables for secrets and environment-specific values:
|
|
83
253
|
|
|
84
254
|
```yaml
|
|
85
|
-
fileName:
|
|
255
|
+
fileName: app.config.json
|
|
256
|
+
|
|
257
|
+
content:
|
|
258
|
+
database:
|
|
259
|
+
host: ${DB_HOST:-localhost}
|
|
260
|
+
port: ${DB_PORT:-5432}
|
|
261
|
+
password: ${DB_PASSWORD:?Database password required}
|
|
262
|
+
|
|
263
|
+
api:
|
|
264
|
+
baseUrl: ${API_BASE_URL}
|
|
265
|
+
timeout: 30000
|
|
266
|
+
|
|
86
267
|
repos:
|
|
87
|
-
- git: git@github.com:
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
268
|
+
- git: git@github.com:org/backend.git
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Simple Multi-Repo Sync
|
|
272
|
+
|
|
273
|
+
When all repos need identical config:
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
fileName: .eslintrc.json
|
|
277
|
+
|
|
278
|
+
content:
|
|
279
|
+
extends: ["@org/eslint-config"]
|
|
280
|
+
rules:
|
|
281
|
+
no-console: warn
|
|
282
|
+
|
|
283
|
+
repos:
|
|
284
|
+
- git:
|
|
285
|
+
- git@github.com:org/frontend.git
|
|
286
|
+
- git@github.com:org/backend.git
|
|
287
|
+
- git@github.com:org/shared-lib.git
|
|
288
|
+
- git@github.com:org/cli-tool.git
|
|
289
|
+
```
|
|
106
290
|
|
|
107
291
|
## Supported Git URL Formats
|
|
108
292
|
|
|
109
293
|
### GitHub
|
|
294
|
+
|
|
110
295
|
- SSH: `git@github.com:owner/repo.git`
|
|
111
296
|
- HTTPS: `https://github.com/owner/repo.git`
|
|
112
297
|
|
|
113
298
|
### Azure DevOps
|
|
299
|
+
|
|
114
300
|
- SSH: `git@ssh.dev.azure.com:v3/organization/project/repo`
|
|
115
301
|
- HTTPS: `https://dev.azure.com/organization/project/_git/repo`
|
|
116
302
|
|
|
117
|
-
##
|
|
303
|
+
## How It Works
|
|
304
|
+
|
|
305
|
+
```mermaid
|
|
306
|
+
flowchart TB
|
|
307
|
+
subgraph Input
|
|
308
|
+
YAML[/"YAML Config File<br/>fileName + content + repos[]"/]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
subgraph Normalization
|
|
312
|
+
EXPAND[Expand git arrays] --> MERGE[Merge base + overlay content]
|
|
313
|
+
MERGE --> ENV[Interpolate env vars]
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
subgraph Processing["For Each Repository"]
|
|
317
|
+
CLONE[Clone Repo] --> BRANCH[Create/Checkout Branch<br/>chore/sync-filename]
|
|
318
|
+
BRANCH --> WRITE[Write Config File<br/>JSON or YAML]
|
|
319
|
+
WRITE --> CHECK{Changes?}
|
|
320
|
+
CHECK -->|No| SKIP[Skip - No Changes]
|
|
321
|
+
CHECK -->|Yes| COMMIT[Commit Changes]
|
|
322
|
+
COMMIT --> PUSH[Push to Remote]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
subgraph Platform["Platform Detection"]
|
|
326
|
+
PUSH --> DETECT{GitHub or<br/>Azure DevOps?}
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
subgraph Output
|
|
330
|
+
DETECT -->|GitHub| GH_PR[Create PR via gh CLI]
|
|
331
|
+
DETECT -->|Azure DevOps| AZ_PR[Create PR via az CLI]
|
|
332
|
+
GH_PR --> GH_URL[/"GitHub PR URL"/]
|
|
333
|
+
AZ_PR --> AZ_URL[/"Azure DevOps PR URL"/]
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
YAML --> EXPAND
|
|
337
|
+
ENV --> CLONE
|
|
338
|
+
```
|
|
118
339
|
|
|
119
340
|
For each repository in the config, the tool:
|
|
120
341
|
|
|
121
|
-
1.
|
|
122
|
-
2.
|
|
123
|
-
3.
|
|
124
|
-
4.
|
|
125
|
-
5.
|
|
126
|
-
6.
|
|
127
|
-
7.
|
|
128
|
-
8.
|
|
342
|
+
1. Expands git URL arrays into individual entries
|
|
343
|
+
2. Merges base content with per-repo overlay
|
|
344
|
+
3. Interpolates environment variables
|
|
345
|
+
4. Cleans the temporary workspace
|
|
346
|
+
5. Clones the repository
|
|
347
|
+
6. Creates/checks out branch `chore/sync-{sanitized-filename}`
|
|
348
|
+
7. Generates the config file (JSON or YAML based on filename extension)
|
|
349
|
+
8. Checks for changes (skips if no changes)
|
|
350
|
+
9. Commits and pushes changes
|
|
351
|
+
10. Creates a pull request
|
|
129
352
|
|
|
130
|
-
##
|
|
353
|
+
## CI/CD Integration
|
|
131
354
|
|
|
132
|
-
|
|
355
|
+
### GitHub Actions
|
|
133
356
|
|
|
134
357
|
```yaml
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
358
|
+
name: Sync Configs
|
|
359
|
+
on:
|
|
360
|
+
push:
|
|
361
|
+
branches: [main]
|
|
362
|
+
paths: ["config.yaml"]
|
|
363
|
+
|
|
364
|
+
jobs:
|
|
365
|
+
sync:
|
|
366
|
+
runs-on: ubuntu-latest
|
|
367
|
+
steps:
|
|
368
|
+
- uses: actions/checkout@v4
|
|
369
|
+
- uses: actions/setup-node@v4
|
|
370
|
+
with:
|
|
371
|
+
node-version: "20"
|
|
372
|
+
- run: npm install -g @aspruyt/json-config-sync
|
|
373
|
+
- run: json-config-sync --config ./config.yaml
|
|
374
|
+
env:
|
|
375
|
+
GH_TOKEN: ${{ secrets.GH_PAT }}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
> **Note:** `GH_PAT` must be a Personal Access Token with `repo` scope to create PRs in target repositories.
|
|
379
|
+
|
|
380
|
+
### Azure Pipelines
|
|
381
|
+
|
|
382
|
+
```yaml
|
|
383
|
+
trigger:
|
|
384
|
+
branches:
|
|
385
|
+
include: [main]
|
|
386
|
+
paths:
|
|
387
|
+
include: ["config.yaml"]
|
|
388
|
+
|
|
389
|
+
pool:
|
|
390
|
+
vmImage: "ubuntu-latest"
|
|
391
|
+
|
|
392
|
+
steps:
|
|
393
|
+
- task: NodeTool@0
|
|
394
|
+
inputs:
|
|
395
|
+
versionSpec: "20.x"
|
|
396
|
+
- script: npm install -g @aspruyt/json-config-sync
|
|
397
|
+
displayName: "Install json-config-sync"
|
|
398
|
+
- script: json-config-sync --config ./config.yaml
|
|
399
|
+
displayName: "Sync configs"
|
|
400
|
+
env:
|
|
401
|
+
AZURE_DEVOPS_EXT_PAT: $(System.AccessToken)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
> **Note:** Ensure the build service account has permission to create PRs in target repositories.
|
|
405
|
+
|
|
406
|
+
## Output Examples
|
|
407
|
+
|
|
408
|
+
### Console Output
|
|
409
|
+
|
|
143
410
|
```
|
|
411
|
+
[1/3] Processing example-org/repo1...
|
|
412
|
+
✓ Cloned repository
|
|
413
|
+
✓ Created branch chore/sync-my-config
|
|
414
|
+
✓ Wrote my.config.json
|
|
415
|
+
✓ Committed changes
|
|
416
|
+
✓ Pushed to remote
|
|
417
|
+
✓ Created PR: https://github.com/example-org/repo1/pull/42
|
|
418
|
+
|
|
419
|
+
[2/3] Processing example-org/repo2...
|
|
420
|
+
✓ Cloned repository
|
|
421
|
+
✓ Checked out existing branch chore/sync-my-config
|
|
422
|
+
✓ Wrote my.config.json
|
|
423
|
+
⊘ No changes detected, skipping
|
|
424
|
+
|
|
425
|
+
[3/3] Processing example-org/repo3...
|
|
426
|
+
✓ Cloned repository
|
|
427
|
+
✓ Created branch chore/sync-my-config
|
|
428
|
+
✓ Wrote my.config.json
|
|
429
|
+
✓ Committed changes
|
|
430
|
+
✓ Pushed to remote
|
|
431
|
+
✓ PR already exists: https://github.com/example-org/repo3/pull/15
|
|
432
|
+
|
|
433
|
+
Summary: 2 succeeded, 1 skipped, 0 failed
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### Created PR
|
|
437
|
+
|
|
438
|
+
The tool creates PRs with:
|
|
439
|
+
|
|
440
|
+
- **Title:** `chore: sync {fileName}`
|
|
441
|
+
- **Branch:** `chore/sync-{sanitized-filename}`
|
|
442
|
+
- **Body:** Describes the sync action and links to documentation
|
|
443
|
+
|
|
444
|
+
## Troubleshooting
|
|
445
|
+
|
|
446
|
+
### Authentication Errors
|
|
144
447
|
|
|
145
|
-
|
|
448
|
+
**GitHub:**
|
|
146
449
|
|
|
147
450
|
```bash
|
|
451
|
+
# Check authentication status
|
|
452
|
+
gh auth status
|
|
453
|
+
|
|
454
|
+
# Re-authenticate if needed
|
|
455
|
+
gh auth login
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
**Azure DevOps:**
|
|
459
|
+
|
|
460
|
+
```bash
|
|
461
|
+
# Check authentication status
|
|
462
|
+
az account show
|
|
463
|
+
|
|
464
|
+
# Re-authenticate if needed
|
|
465
|
+
az login
|
|
466
|
+
az devops configure --defaults organization=https://dev.azure.com/YOUR_ORG
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Permission Denied
|
|
470
|
+
|
|
471
|
+
- Ensure your token has write access to the target repositories
|
|
472
|
+
- For GitHub, the token needs `repo` scope
|
|
473
|
+
- For Azure DevOps, ensure the user/service account has "Contribute to pull requests" permission
|
|
474
|
+
|
|
475
|
+
### Branch Already Exists
|
|
476
|
+
|
|
477
|
+
The tool automatically reuses existing branches. If you see unexpected behavior:
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Delete the remote branch to start fresh
|
|
481
|
+
git push origin --delete chore/sync-my-config
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Missing Environment Variables
|
|
485
|
+
|
|
486
|
+
If you see "Missing required environment variable" errors:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
# Set the variable before running
|
|
490
|
+
export MY_VAR=value
|
|
148
491
|
json-config-sync --config ./config.yaml
|
|
492
|
+
|
|
493
|
+
# Or use default values in config
|
|
494
|
+
# ${MY_VAR:-default-value}
|
|
149
495
|
```
|
|
150
496
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
497
|
+
### Network/Proxy Issues
|
|
498
|
+
|
|
499
|
+
If cloning fails behind a corporate proxy:
|
|
500
|
+
|
|
501
|
+
```bash
|
|
502
|
+
# Configure git proxy
|
|
503
|
+
git config --global http.proxy http://proxy.example.com:8080
|
|
504
|
+
git config --global https.proxy http://proxy.example.com:8080
|
|
505
|
+
```
|
|
156
506
|
|
|
157
507
|
## Development
|
|
158
508
|
|
package/dist/config.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
|
+
import { type ArrayMergeStrategy } from './merge.js';
|
|
2
|
+
export interface RawRepoConfig {
|
|
3
|
+
git: string | string[];
|
|
4
|
+
content?: Record<string, unknown>;
|
|
5
|
+
override?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RawConfig {
|
|
8
|
+
fileName: string;
|
|
9
|
+
content?: Record<string, unknown>;
|
|
10
|
+
mergeStrategy?: ArrayMergeStrategy;
|
|
11
|
+
repos: RawRepoConfig[];
|
|
12
|
+
}
|
|
1
13
|
export interface RepoConfig {
|
|
2
14
|
git: string;
|
|
3
|
-
|
|
15
|
+
content: Record<string, unknown>;
|
|
4
16
|
}
|
|
5
17
|
export interface Config {
|
|
6
18
|
fileName: string;
|
|
7
19
|
repos: RepoConfig[];
|
|
8
20
|
}
|
|
9
21
|
export declare function loadConfig(filePath: string): Config;
|
|
10
|
-
export declare function
|
|
22
|
+
export declare function convertContentToString(content: Record<string, unknown>, fileName: string): string;
|
package/dist/config.js
CHANGED
|
@@ -1,25 +1,97 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { parse } from 'yaml';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { parse, stringify } from 'yaml';
|
|
3
|
+
import { deepMerge, stripMergeDirectives, createMergeContext, } from './merge.js';
|
|
4
|
+
import { interpolateEnvVars } from './env.js';
|
|
5
|
+
// =============================================================================
|
|
6
|
+
// Validation
|
|
7
|
+
// =============================================================================
|
|
8
|
+
function validateRawConfig(config) {
|
|
6
9
|
if (!config.fileName) {
|
|
7
10
|
throw new Error('Config missing required field: fileName');
|
|
8
11
|
}
|
|
9
12
|
if (!config.repos || !Array.isArray(config.repos)) {
|
|
10
13
|
throw new Error('Config missing required field: repos (must be an array)');
|
|
11
14
|
}
|
|
15
|
+
const hasRootContent = config.content !== undefined;
|
|
12
16
|
for (let i = 0; i < config.repos.length; i++) {
|
|
13
17
|
const repo = config.repos[i];
|
|
14
18
|
if (!repo.git) {
|
|
15
19
|
throw new Error(`Repo at index ${i} missing required field: git`);
|
|
16
20
|
}
|
|
17
|
-
if (!repo.
|
|
18
|
-
throw new Error(`Repo at index ${i} missing required field:
|
|
21
|
+
if (!hasRootContent && !repo.content) {
|
|
22
|
+
throw new Error(`Repo at index ${i} missing required field: content (no root-level content defined)`);
|
|
23
|
+
}
|
|
24
|
+
if (repo.override && !repo.content) {
|
|
25
|
+
throw new Error(`Repo ${getGitDisplayName(repo.git)} has override: true but no content defined`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getGitDisplayName(git) {
|
|
30
|
+
if (Array.isArray(git)) {
|
|
31
|
+
return git[0] || 'unknown';
|
|
32
|
+
}
|
|
33
|
+
return git;
|
|
34
|
+
}
|
|
35
|
+
// =============================================================================
|
|
36
|
+
// Normalization Pipeline
|
|
37
|
+
// =============================================================================
|
|
38
|
+
function normalizeConfig(raw) {
|
|
39
|
+
const baseContent = raw.content ?? {};
|
|
40
|
+
const defaultStrategy = raw.mergeStrategy ?? 'replace';
|
|
41
|
+
const expandedRepos = [];
|
|
42
|
+
for (const rawRepo of raw.repos) {
|
|
43
|
+
// Step 1: Expand git arrays
|
|
44
|
+
const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
|
|
45
|
+
for (const gitUrl of gitUrls) {
|
|
46
|
+
// Step 2: Compute merged content
|
|
47
|
+
let mergedContent;
|
|
48
|
+
if (rawRepo.override) {
|
|
49
|
+
// Override mode: use only repo content
|
|
50
|
+
mergedContent = stripMergeDirectives(structuredClone(rawRepo.content));
|
|
51
|
+
}
|
|
52
|
+
else if (!rawRepo.content) {
|
|
53
|
+
// No repo content: use root content as-is
|
|
54
|
+
mergedContent = structuredClone(baseContent);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Merge mode: deep merge base + overlay
|
|
58
|
+
const ctx = createMergeContext(defaultStrategy);
|
|
59
|
+
mergedContent = deepMerge(structuredClone(baseContent), rawRepo.content, ctx);
|
|
60
|
+
mergedContent = stripMergeDirectives(mergedContent);
|
|
61
|
+
}
|
|
62
|
+
// Step 3: Interpolate env vars
|
|
63
|
+
mergedContent = interpolateEnvVars(mergedContent, { strict: true });
|
|
64
|
+
expandedRepos.push({
|
|
65
|
+
git: gitUrl,
|
|
66
|
+
content: mergedContent,
|
|
67
|
+
});
|
|
19
68
|
}
|
|
20
69
|
}
|
|
21
|
-
return
|
|
70
|
+
return {
|
|
71
|
+
fileName: raw.fileName,
|
|
72
|
+
repos: expandedRepos,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// Public API
|
|
77
|
+
// =============================================================================
|
|
78
|
+
export function loadConfig(filePath) {
|
|
79
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
80
|
+
const rawConfig = parse(content);
|
|
81
|
+
validateRawConfig(rawConfig);
|
|
82
|
+
return normalizeConfig(rawConfig);
|
|
22
83
|
}
|
|
23
|
-
|
|
24
|
-
|
|
84
|
+
function detectOutputFormat(fileName) {
|
|
85
|
+
const ext = fileName.toLowerCase().split('.').pop();
|
|
86
|
+
if (ext === 'yaml' || ext === 'yml') {
|
|
87
|
+
return 'yaml';
|
|
88
|
+
}
|
|
89
|
+
return 'json';
|
|
90
|
+
}
|
|
91
|
+
export function convertContentToString(content, fileName) {
|
|
92
|
+
const format = detectOutputFormat(fileName);
|
|
93
|
+
if (format === 'yaml') {
|
|
94
|
+
return stringify(content, { indent: 2 });
|
|
95
|
+
}
|
|
96
|
+
return JSON.stringify(content, null, 2);
|
|
25
97
|
}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable interpolation utilities.
|
|
3
|
+
* Supports ${VAR}, ${VAR:-default}, and ${VAR:?message} syntax.
|
|
4
|
+
*/
|
|
5
|
+
export interface EnvInterpolationOptions {
|
|
6
|
+
/**
|
|
7
|
+
* If true (default), throws an error when a variable is missing
|
|
8
|
+
* and has no default value. If false, leaves the placeholder as-is.
|
|
9
|
+
*/
|
|
10
|
+
strict: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Interpolate environment variables in a JSON object.
|
|
14
|
+
*
|
|
15
|
+
* Supports three syntaxes:
|
|
16
|
+
* - ${VAR} - Replace with env value, error if missing (in strict mode)
|
|
17
|
+
* - ${VAR:-default} - Replace with env value, or use default if missing
|
|
18
|
+
* - ${VAR:?message} - Replace with env value, or throw error with message if missing
|
|
19
|
+
*
|
|
20
|
+
* @param json - The JSON object to process
|
|
21
|
+
* @param options - Interpolation options (default: strict mode)
|
|
22
|
+
* @returns A new object with interpolated values
|
|
23
|
+
*/
|
|
24
|
+
export declare function interpolateEnvVars(json: Record<string, unknown>, options?: EnvInterpolationOptions): Record<string, unknown>;
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment variable interpolation utilities.
|
|
3
|
+
* Supports ${VAR}, ${VAR:-default}, and ${VAR:?message} syntax.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_OPTIONS = {
|
|
6
|
+
strict: true,
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Regex to match environment variable placeholders.
|
|
10
|
+
* Captures:
|
|
11
|
+
* - Group 1: Variable name
|
|
12
|
+
* - Group 2: Modifier (- for default, ? for required with message)
|
|
13
|
+
* - Group 3: Default value or error message
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* - ${VAR} -> varName=VAR, modifier=undefined, value=undefined
|
|
17
|
+
* - ${VAR:-default} -> varName=VAR, modifier=-, value=default
|
|
18
|
+
* - ${VAR:?message} -> varName=VAR, modifier=?, value=message
|
|
19
|
+
*/
|
|
20
|
+
const ENV_VAR_REGEX = /\$\{([^}:]+)(?::([?-])([^}]*))?\}/g;
|
|
21
|
+
/**
|
|
22
|
+
* Check if a value is a plain object (not null, not array).
|
|
23
|
+
*/
|
|
24
|
+
function isPlainObject(val) {
|
|
25
|
+
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Process a single string value, replacing environment variable placeholders.
|
|
29
|
+
*/
|
|
30
|
+
function processString(value, options) {
|
|
31
|
+
return value.replace(ENV_VAR_REGEX, (match, varName, modifier, defaultOrMsg) => {
|
|
32
|
+
const envValue = process.env[varName];
|
|
33
|
+
// Variable exists - use its value
|
|
34
|
+
if (envValue !== undefined) {
|
|
35
|
+
return envValue;
|
|
36
|
+
}
|
|
37
|
+
// Has default value (:-default)
|
|
38
|
+
if (modifier === '-') {
|
|
39
|
+
return defaultOrMsg ?? '';
|
|
40
|
+
}
|
|
41
|
+
// Required with message (:?message)
|
|
42
|
+
if (modifier === '?') {
|
|
43
|
+
const message = defaultOrMsg || `is required`;
|
|
44
|
+
throw new Error(`${varName}: ${message}`);
|
|
45
|
+
}
|
|
46
|
+
// No modifier - check strictness
|
|
47
|
+
if (options.strict) {
|
|
48
|
+
throw new Error(`Missing required environment variable: ${varName}`);
|
|
49
|
+
}
|
|
50
|
+
// Non-strict mode - leave placeholder as-is
|
|
51
|
+
return match;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Recursively process a value, interpolating environment variables in strings.
|
|
56
|
+
*/
|
|
57
|
+
function processValue(value, options) {
|
|
58
|
+
if (typeof value === 'string') {
|
|
59
|
+
return processString(value, options);
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
return value.map((item) => processValue(item, options));
|
|
63
|
+
}
|
|
64
|
+
if (isPlainObject(value)) {
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const [key, val] of Object.entries(value)) {
|
|
67
|
+
result[key] = processValue(val, options);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
// For numbers, booleans, null - return as-is
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Interpolate environment variables in a JSON object.
|
|
76
|
+
*
|
|
77
|
+
* Supports three syntaxes:
|
|
78
|
+
* - ${VAR} - Replace with env value, error if missing (in strict mode)
|
|
79
|
+
* - ${VAR:-default} - Replace with env value, or use default if missing
|
|
80
|
+
* - ${VAR:?message} - Replace with env value, or throw error with message if missing
|
|
81
|
+
*
|
|
82
|
+
* @param json - The JSON object to process
|
|
83
|
+
* @param options - Interpolation options (default: strict mode)
|
|
84
|
+
* @returns A new object with interpolated values
|
|
85
|
+
*/
|
|
86
|
+
export function interpolateEnvVars(json, options = DEFAULT_OPTIONS) {
|
|
87
|
+
return processValue(json, options);
|
|
88
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { program } from 'commander';
|
|
3
3
|
import { resolve, join } from 'node:path';
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
|
-
import { loadConfig,
|
|
5
|
+
import { loadConfig, convertContentToString } from './config.js';
|
|
6
6
|
import { parseGitUrl, getRepoDisplayName } from './repo-detector.js';
|
|
7
7
|
import { GitOps, sanitizeBranchName } from './git-ops.js';
|
|
8
8
|
import { createPR } from './pr-creator.js';
|
|
@@ -61,10 +61,10 @@ async function main() {
|
|
|
61
61
|
// Step 4: Create/checkout branch
|
|
62
62
|
logger.info(`Switching to branch: ${branchName}`);
|
|
63
63
|
gitOps.createBranch(branchName);
|
|
64
|
-
// Step 5: Write
|
|
64
|
+
// Step 5: Write config file
|
|
65
65
|
logger.info(`Writing ${config.fileName}...`);
|
|
66
|
-
const
|
|
67
|
-
gitOps.writeFile(config.fileName,
|
|
66
|
+
const fileContent = convertContentToString(repoConfig.content, config.fileName);
|
|
67
|
+
gitOps.writeFile(config.fileName, fileContent);
|
|
68
68
|
// Step 6: Check for changes
|
|
69
69
|
if (!gitOps.hasChanges()) {
|
|
70
70
|
logger.skip(current, repoName, 'No changes detected');
|
package/dist/merge.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep merge utilities for JSON configuration objects.
|
|
3
|
+
* Supports configurable array merge strategies via $arrayMerge directive.
|
|
4
|
+
*/
|
|
5
|
+
export type ArrayMergeStrategy = 'replace' | 'append' | 'prepend';
|
|
6
|
+
export interface MergeContext {
|
|
7
|
+
arrayStrategies: Map<string, ArrayMergeStrategy>;
|
|
8
|
+
defaultArrayStrategy: ArrayMergeStrategy;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Deep merge two objects with configurable array handling.
|
|
12
|
+
*
|
|
13
|
+
* @param base - The base object
|
|
14
|
+
* @param overlay - The overlay object (values override base)
|
|
15
|
+
* @param ctx - Merge context with array strategies
|
|
16
|
+
* @param path - Current path for strategy lookup (internal)
|
|
17
|
+
*/
|
|
18
|
+
export declare function deepMerge(base: Record<string, unknown>, overlay: Record<string, unknown>, ctx: MergeContext, path?: string): Record<string, unknown>;
|
|
19
|
+
/**
|
|
20
|
+
* Strip merge directive keys ($arrayMerge, $override, etc.) from an object.
|
|
21
|
+
* Works recursively on nested objects and arrays.
|
|
22
|
+
*/
|
|
23
|
+
export declare function stripMergeDirectives(obj: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* Create a default merge context.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createMergeContext(defaultStrategy?: ArrayMergeStrategy): MergeContext;
|
package/dist/merge.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep merge utilities for JSON configuration objects.
|
|
3
|
+
* Supports configurable array merge strategies via $arrayMerge directive.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Check if a value is a plain object (not null, not array).
|
|
7
|
+
*/
|
|
8
|
+
function isPlainObject(val) {
|
|
9
|
+
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Merge two arrays based on the specified strategy.
|
|
13
|
+
*/
|
|
14
|
+
function mergeArrays(base, overlay, strategy) {
|
|
15
|
+
switch (strategy) {
|
|
16
|
+
case 'replace':
|
|
17
|
+
return overlay;
|
|
18
|
+
case 'append':
|
|
19
|
+
return [...base, ...overlay];
|
|
20
|
+
case 'prepend':
|
|
21
|
+
return [...overlay, ...base];
|
|
22
|
+
default:
|
|
23
|
+
return overlay;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Extract array values from an overlay object that uses the directive syntax:
|
|
28
|
+
* { $arrayMerge: 'append', values: [1, 2, 3] }
|
|
29
|
+
*
|
|
30
|
+
* Or just return the array if it's already an array.
|
|
31
|
+
*/
|
|
32
|
+
function extractArrayFromOverlay(overlay) {
|
|
33
|
+
if (Array.isArray(overlay)) {
|
|
34
|
+
return overlay;
|
|
35
|
+
}
|
|
36
|
+
if (isPlainObject(overlay) && 'values' in overlay) {
|
|
37
|
+
const values = overlay.values;
|
|
38
|
+
if (Array.isArray(values)) {
|
|
39
|
+
return values;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get merge strategy from an overlay object's $arrayMerge directive.
|
|
46
|
+
*/
|
|
47
|
+
function getStrategyFromOverlay(overlay) {
|
|
48
|
+
if (isPlainObject(overlay) && '$arrayMerge' in overlay) {
|
|
49
|
+
const strategy = overlay.$arrayMerge;
|
|
50
|
+
if (strategy === 'replace' ||
|
|
51
|
+
strategy === 'append' ||
|
|
52
|
+
strategy === 'prepend') {
|
|
53
|
+
return strategy;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Deep merge two objects with configurable array handling.
|
|
60
|
+
*
|
|
61
|
+
* @param base - The base object
|
|
62
|
+
* @param overlay - The overlay object (values override base)
|
|
63
|
+
* @param ctx - Merge context with array strategies
|
|
64
|
+
* @param path - Current path for strategy lookup (internal)
|
|
65
|
+
*/
|
|
66
|
+
export function deepMerge(base, overlay, ctx, path = '') {
|
|
67
|
+
const result = { ...base };
|
|
68
|
+
// Check for $arrayMerge directive at this level (applies to child arrays)
|
|
69
|
+
const levelStrategy = getStrategyFromOverlay(overlay);
|
|
70
|
+
for (const [key, overlayValue] of Object.entries(overlay)) {
|
|
71
|
+
// Skip directive keys in output
|
|
72
|
+
if (key.startsWith('$'))
|
|
73
|
+
continue;
|
|
74
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
75
|
+
const baseValue = base[key];
|
|
76
|
+
// If overlay is an object with $arrayMerge directive for an array field
|
|
77
|
+
if (isPlainObject(overlayValue) && '$arrayMerge' in overlayValue) {
|
|
78
|
+
const strategy = getStrategyFromOverlay(overlayValue);
|
|
79
|
+
const overlayArray = extractArrayFromOverlay(overlayValue);
|
|
80
|
+
if (strategy && overlayArray && Array.isArray(baseValue)) {
|
|
81
|
+
result[key] = mergeArrays(baseValue, overlayArray, strategy);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Both are arrays - apply strategy
|
|
86
|
+
if (Array.isArray(baseValue) && Array.isArray(overlayValue)) {
|
|
87
|
+
// Check for level-specific strategy, then path-specific, then default
|
|
88
|
+
const strategy = levelStrategy ??
|
|
89
|
+
ctx.arrayStrategies.get(currentPath) ??
|
|
90
|
+
ctx.defaultArrayStrategy;
|
|
91
|
+
result[key] = mergeArrays(baseValue, overlayValue, strategy);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Both are plain objects - recurse
|
|
95
|
+
if (isPlainObject(baseValue) && isPlainObject(overlayValue)) {
|
|
96
|
+
// Extract $arrayMerge for child paths if present
|
|
97
|
+
if ('$arrayMerge' in overlayValue) {
|
|
98
|
+
const childStrategy = getStrategyFromOverlay(overlayValue);
|
|
99
|
+
if (childStrategy) {
|
|
100
|
+
// Apply to all immediate child arrays
|
|
101
|
+
for (const childKey of Object.keys(overlayValue)) {
|
|
102
|
+
if (!childKey.startsWith('$')) {
|
|
103
|
+
const childPath = currentPath ? `${currentPath}.${childKey}` : childKey;
|
|
104
|
+
ctx.arrayStrategies.set(childPath, childStrategy);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
result[key] = deepMerge(baseValue, overlayValue, ctx, currentPath);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
// Otherwise, overlay wins (including null values)
|
|
113
|
+
result[key] = overlayValue;
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Strip merge directive keys ($arrayMerge, $override, etc.) from an object.
|
|
119
|
+
* Works recursively on nested objects and arrays.
|
|
120
|
+
*/
|
|
121
|
+
export function stripMergeDirectives(obj) {
|
|
122
|
+
const result = {};
|
|
123
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
124
|
+
// Skip all $-prefixed keys (reserved for directives)
|
|
125
|
+
if (key.startsWith('$'))
|
|
126
|
+
continue;
|
|
127
|
+
if (isPlainObject(value)) {
|
|
128
|
+
result[key] = stripMergeDirectives(value);
|
|
129
|
+
}
|
|
130
|
+
else if (Array.isArray(value)) {
|
|
131
|
+
result[key] = value.map((item) => isPlainObject(item) ? stripMergeDirectives(item) : item);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
result[key] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create a default merge context.
|
|
141
|
+
*/
|
|
142
|
+
export function createMergeContext(defaultStrategy = 'replace') {
|
|
143
|
+
return {
|
|
144
|
+
arrayStrategies: new Map(),
|
|
145
|
+
defaultArrayStrategy: defaultStrategy,
|
|
146
|
+
};
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/json-config-sync",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "CLI tool to sync JSON configuration files across multiple repositories",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "CLI tool to sync JSON configuration files across multiple GitHub and Azure DevOps repositories",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"build": "tsc",
|
|
27
27
|
"start": "node dist/index.js",
|
|
28
28
|
"dev": "ts-node src/index.ts",
|
|
29
|
-
"test": "node --import tsx --test src/config.test.ts",
|
|
29
|
+
"test": "node --import tsx --test src/config.test.ts src/merge.test.ts src/env.test.ts",
|
|
30
30
|
"test:integration": "npm run build && node --import tsx --test src/integration.test.ts",
|
|
31
31
|
"prepublishOnly": "npm run build",
|
|
32
32
|
"release:patch": "npm version patch && git push --follow-tags",
|