permachine 0.2.0 → 0.3.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 +230 -103
- package/dist/cli.js +177 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -77,6 +77,20 @@ EXAMPLES:
|
|
|
77
77
|
|
|
78
78
|
### File Naming Convention
|
|
79
79
|
|
|
80
|
+
**New Advanced Syntax (Recommended):**
|
|
81
|
+
|
|
82
|
+
Filter-based syntax for precise control:
|
|
83
|
+
|
|
84
|
+
| Purpose | Filename | In Git? |
|
|
85
|
+
| --------------------- | ----------------------------- | ------------------ |
|
|
86
|
+
| Base config (shared) | `config.base.json` | ✅ Yes |
|
|
87
|
+
| OS-specific | `config.{os=windows}.json` | ✅ Yes |
|
|
88
|
+
| Machine-specific | `config.{machine=laptop}.json`| ✅ Yes |
|
|
89
|
+
| Multi-filter | `secrets.{machine=laptop}{user=josxa}.json` | ✅ Yes |
|
|
90
|
+
| Final output (merged) | `config.json` | ❌ No (gitignored) |
|
|
91
|
+
|
|
92
|
+
**Legacy Syntax (Still Supported):**
|
|
93
|
+
|
|
80
94
|
Given machine name `my-laptop` (auto-detected from hostname):
|
|
81
95
|
|
|
82
96
|
| Purpose | Filename | In Git? |
|
|
@@ -85,13 +99,25 @@ Given machine name `my-laptop` (auto-detected from hostname):
|
|
|
85
99
|
| Machine-specific | `config.my-laptop.json` | ✅ Yes |
|
|
86
100
|
| Final output (merged) | `config.json` | ❌ No (gitignored) |
|
|
87
101
|
|
|
102
|
+
**Supported Filters:**
|
|
103
|
+
|
|
104
|
+
- `{os=windows}`, `{os=macos}`, `{os=linux}` - Operating system
|
|
105
|
+
- `{arch=x64}`, `{arch=arm64}` - CPU architecture
|
|
106
|
+
- `{machine=hostname}` - Machine/hostname (same as legacy)
|
|
107
|
+
- `{user=username}` - Username
|
|
108
|
+
- `{env=prod}`, `{env=dev}` - Environment (from NODE_ENV)
|
|
109
|
+
- Multiple filters: `{os=windows}{arch=x64}` (AND logic)
|
|
110
|
+
- OR logic: `{os=windows,macos,linux}` (comma-separated)
|
|
111
|
+
|
|
112
|
+
**📚 See [File Filters Documentation](docs/FILE_FILTERS.md) for complete guide and examples.**
|
|
113
|
+
|
|
88
114
|
Same pattern works for `.env` files:
|
|
89
115
|
|
|
90
|
-
| Purpose | Filename
|
|
91
|
-
| ---------------- |
|
|
92
|
-
| Base config | `.env.base`
|
|
93
|
-
| Machine-specific | `.env.
|
|
94
|
-
| Final output | `.env`
|
|
116
|
+
| Purpose | Filename | In Git? |
|
|
117
|
+
| ---------------- | -------------------------- | ------------------ |
|
|
118
|
+
| Base config | `.env.base` | ✅ Yes |
|
|
119
|
+
| Machine-specific | `.env.{machine=laptop}` | ✅ Yes |
|
|
120
|
+
| Final output | `.env` | ❌ No (gitignored) |
|
|
95
121
|
|
|
96
122
|
### Basic Commands
|
|
97
123
|
|
|
@@ -186,12 +212,13 @@ Different settings for work laptop vs home desktop:
|
|
|
186
212
|
└── settings.json # ← Merged output (gitignored)
|
|
187
213
|
```
|
|
188
214
|
|
|
189
|
-
**
|
|
215
|
+
**settings.base.json:**
|
|
190
216
|
|
|
191
217
|
```json
|
|
192
218
|
{
|
|
193
219
|
"editor.fontSize": 14,
|
|
194
|
-
"workbench.colorTheme": "Dark+"
|
|
220
|
+
"workbench.colorTheme": "Dark+",
|
|
221
|
+
"terminal.integrated.shell.windows": "powershell.exe"
|
|
195
222
|
}
|
|
196
223
|
```
|
|
197
224
|
|
|
@@ -200,34 +227,92 @@ Different settings for work laptop vs home desktop:
|
|
|
200
227
|
```json
|
|
201
228
|
{
|
|
202
229
|
"http.proxy": "http://proxy.company.com:8080",
|
|
203
|
-
"terminal.integrated.cwd": "C:/Projects"
|
|
230
|
+
"terminal.integrated.cwd": "C:/Projects/Work"
|
|
204
231
|
}
|
|
205
232
|
```
|
|
206
233
|
|
|
207
|
-
|
|
234
|
+
**settings.desktop.json:**
|
|
208
235
|
|
|
209
|
-
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"terminal.integrated.cwd": "C:/Code/Personal",
|
|
239
|
+
"git.path": "C:/Program Files/Git/bin/git.exe"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Recipe 2: OpenCode Config (AI Assistant)
|
|
244
|
+
|
|
245
|
+
[OpenCode](https://opencode.ai) supports machine-specific MCP servers and model preferences:
|
|
210
246
|
|
|
211
247
|
```bash
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
~/.config/opencode/
|
|
249
|
+
├── config.base.json # Shared: agents, themes, keybinds
|
|
250
|
+
├── config.worklaptop.json # Work: Google Sheets MCP
|
|
251
|
+
├── config.homezone.json # Home: Telegram MCP, local paths
|
|
252
|
+
└── config.json # ← Merged output (gitignored)
|
|
253
|
+
```
|
|
216
254
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
**config.base.json:**
|
|
256
|
+
|
|
257
|
+
```json
|
|
258
|
+
{
|
|
259
|
+
"$schema": "https://opencode.ai/config.json",
|
|
260
|
+
"theme": "nightowl-transparent",
|
|
261
|
+
"keybinds": {
|
|
262
|
+
"input_newline": "shift+return"
|
|
263
|
+
},
|
|
264
|
+
"mcp": {
|
|
265
|
+
"perplexity-mcp": {
|
|
266
|
+
"enabled": true,
|
|
267
|
+
"type": "local",
|
|
268
|
+
"command": ["uvx", "perplexity-mcp"]
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**config.homezone.json:**
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{
|
|
278
|
+
"mcp": {
|
|
279
|
+
"telegram-mcp": {
|
|
280
|
+
"enabled": true,
|
|
281
|
+
"type": "local",
|
|
282
|
+
"command": ["uv", "--directory", "D:\\git\\telegram-mcp", "run", "main.py"]
|
|
283
|
+
},
|
|
284
|
+
"google-sheets": {
|
|
285
|
+
"enabled": true,
|
|
286
|
+
"environment": {
|
|
287
|
+
"SERVICE_ACCOUNT_PATH": "C:/Users/josch/.config/opencode/secrets/service-account.json"
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
220
293
|
|
|
221
|
-
|
|
222
|
-
DATABASE_URL=postgresql://prod.db.com:5432/myapp
|
|
223
|
-
API_KEY=prod_key_xyz
|
|
294
|
+
**config.worklaptop.json:**
|
|
224
295
|
|
|
225
|
-
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"mcp": {
|
|
299
|
+
"telegram-mcp": {
|
|
300
|
+
"enabled": false
|
|
301
|
+
},
|
|
302
|
+
"google-sheets": {
|
|
303
|
+
"enabled": true,
|
|
304
|
+
"environment": {
|
|
305
|
+
"SERVICE_ACCOUNT_PATH": "/work/credentials/google-service-account.json",
|
|
306
|
+
"DRIVE_FOLDER_ID": "work-folder-id-123"
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
226
311
|
```
|
|
227
312
|
|
|
228
|
-
### Recipe 3: Package.json Scripts
|
|
313
|
+
### Recipe 3: Package.json Scripts (Platform-Specific)
|
|
229
314
|
|
|
230
|
-
Different
|
|
315
|
+
Different scripts for macOS vs Windows development:
|
|
231
316
|
|
|
232
317
|
```bash
|
|
233
318
|
# package.base.json
|
|
@@ -235,127 +320,167 @@ Different build scripts for different machines:
|
|
|
235
320
|
"name": "my-app",
|
|
236
321
|
"version": "1.0.0",
|
|
237
322
|
"scripts": {
|
|
238
|
-
"test": "jest"
|
|
323
|
+
"test": "jest",
|
|
324
|
+
"lint": "eslint src/"
|
|
239
325
|
},
|
|
240
326
|
"dependencies": {
|
|
241
327
|
"express": "^4.18.0"
|
|
242
328
|
}
|
|
243
329
|
}
|
|
244
330
|
|
|
245
|
-
# package.
|
|
331
|
+
# package.macos.json
|
|
246
332
|
{
|
|
247
333
|
"scripts": {
|
|
248
|
-
"dev": "nodemon src/index.js",
|
|
249
|
-
"build": "
|
|
334
|
+
"dev": "NODE_ENV=development nodemon src/index.js",
|
|
335
|
+
"build": "rm -rf dist && webpack",
|
|
336
|
+
"open": "open http://localhost:3000"
|
|
250
337
|
}
|
|
251
338
|
}
|
|
252
339
|
|
|
253
|
-
# package.
|
|
340
|
+
# package.windows.json
|
|
254
341
|
{
|
|
255
342
|
"scripts": {
|
|
256
|
-
"
|
|
257
|
-
"
|
|
343
|
+
"dev": "set NODE_ENV=development && nodemon src/index.js",
|
|
344
|
+
"build": "rmdir /s /q dist && webpack",
|
|
345
|
+
"open": "start http://localhost:3000"
|
|
258
346
|
}
|
|
259
347
|
}
|
|
260
348
|
|
|
261
349
|
# package.json ← Merged output
|
|
262
|
-
# Each
|
|
350
|
+
# Each OS gets appropriate shell commands!
|
|
263
351
|
```
|
|
264
352
|
|
|
265
|
-
### Recipe 4:
|
|
353
|
+
### Recipe 4: Git Config
|
|
266
354
|
|
|
267
|
-
|
|
355
|
+
Personal vs work Git settings:
|
|
268
356
|
|
|
269
357
|
```bash
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
358
|
+
# .gitconfig.base
|
|
359
|
+
[core]
|
|
360
|
+
editor = code --wait
|
|
361
|
+
autocrlf = true
|
|
362
|
+
[pull]
|
|
363
|
+
rebase = true
|
|
364
|
+
[init]
|
|
365
|
+
defaultBranch = main
|
|
366
|
+
|
|
367
|
+
# .gitconfig.worklaptop
|
|
368
|
+
[user]
|
|
369
|
+
name = John Doe
|
|
370
|
+
email = john.doe@company.com
|
|
371
|
+
[url "https://"]
|
|
372
|
+
insteadOf = git://
|
|
373
|
+
[http]
|
|
374
|
+
proxy = http://proxy.company.com:8080
|
|
375
|
+
|
|
376
|
+
# .gitconfig.homezone
|
|
377
|
+
[user]
|
|
378
|
+
name = JohnD
|
|
379
|
+
email = johndoe@personal.com
|
|
380
|
+
[github]
|
|
381
|
+
user = johnd-personal
|
|
382
|
+
```
|
|
280
383
|
|
|
281
|
-
|
|
282
|
-
{
|
|
283
|
-
"connection": {
|
|
284
|
-
"host": "localhost",
|
|
285
|
-
"port": 5432,
|
|
286
|
-
"database": "myapp_dev",
|
|
287
|
-
"user": "dev",
|
|
288
|
-
"password": "dev123"
|
|
289
|
-
}
|
|
290
|
-
}
|
|
384
|
+
### Recipe 5: Multi-File Dotfiles
|
|
291
385
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
386
|
+
Complete dotfiles setup across machines:
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
~/.config/
|
|
390
|
+
├── nvim/
|
|
391
|
+
│ ├── init.base.vim # Shared vim config
|
|
392
|
+
│ ├── init.worklaptop.vim # Work-specific plugins
|
|
393
|
+
│ ├── init.homezone.vim # Personal plugins
|
|
394
|
+
│ └── init.vim # ← Merged
|
|
395
|
+
├── alacritty/
|
|
396
|
+
│ ├── alacritty.base.yml # Shared terminal config
|
|
397
|
+
│ ├── alacritty.macos.yml # macOS font paths
|
|
398
|
+
│ ├── alacritty.windows.yml # Windows font paths
|
|
399
|
+
│ └── alacritty.yml # ← Merged
|
|
400
|
+
├── opencode/
|
|
401
|
+
│ ├── config.base.json
|
|
402
|
+
│ ├── config.worklaptop.json
|
|
403
|
+
│ └── config.json # ← Merged
|
|
404
|
+
└── .env.base
|
|
405
|
+
.env.worklaptop
|
|
406
|
+
.env # ← Merged
|
|
407
|
+
|
|
408
|
+
# After `permachine init`, sync your dotfiles repo across machines!
|
|
409
|
+
# Each machine automatically gets the right config.
|
|
306
410
|
```
|
|
307
411
|
|
|
308
|
-
### Recipe
|
|
412
|
+
### Recipe 6: Advanced Filters - Cross-Platform Development
|
|
309
413
|
|
|
310
|
-
|
|
414
|
+
**NEW**: Use the advanced filter syntax for precise control:
|
|
311
415
|
|
|
312
416
|
```bash
|
|
417
|
+
# Project structure
|
|
313
418
|
project/
|
|
314
|
-
├── config.base.json
|
|
315
|
-
├── config.
|
|
316
|
-
├──
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
├── .
|
|
322
|
-
└── .
|
|
323
|
-
|
|
324
|
-
# After `permachine init`, all files auto-merge:
|
|
325
|
-
# - config.json
|
|
326
|
-
# - settings/app.json
|
|
327
|
-
# - settings/database.json
|
|
328
|
-
# - .env
|
|
419
|
+
├── config.base.json # Shared config
|
|
420
|
+
├── config.{os=windows}.json # Windows-specific paths
|
|
421
|
+
├── config.{os=macos}.json # macOS-specific paths
|
|
422
|
+
├── config.{os=linux}.json # Linux-specific paths
|
|
423
|
+
├── secrets.{machine=work}{user=alice}.json # Alice's work secrets
|
|
424
|
+
├── secrets.{machine=home}{user=alice}.json # Alice's home secrets
|
|
425
|
+
├── build.{os=windows}{arch=x64}.json # Windows x64 build config
|
|
426
|
+
├── build.{os=windows}{arch=arm64}.json # Windows ARM build config
|
|
427
|
+
└── config.json # ← Merged (gitignored)
|
|
329
428
|
```
|
|
330
429
|
|
|
430
|
+
**Example use cases:**
|
|
431
|
+
|
|
432
|
+
```bash
|
|
433
|
+
# Multiple platforms with OR logic
|
|
434
|
+
package.{os=windows,macos}.json # Matches Windows OR macOS
|
|
435
|
+
|
|
436
|
+
# Specific environment AND machine
|
|
437
|
+
secrets.{env=prod}{machine=server-us-east}.json
|
|
438
|
+
|
|
439
|
+
# User-specific on specific machine
|
|
440
|
+
.vscode/settings.{machine=laptop}{user=josxa}.json
|
|
441
|
+
|
|
442
|
+
# Multiple users on shared machine
|
|
443
|
+
preferences.{user=alice}.json
|
|
444
|
+
preferences.{user=bob}.json
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
**See [File Filters Documentation](docs/FILE_FILTERS.md) for complete guide and examples.**
|
|
448
|
+
|
|
331
449
|
## How It Works
|
|
332
450
|
|
|
333
|
-
`permachine` uses a simple
|
|
451
|
+
`permachine` uses a simple process:
|
|
334
452
|
|
|
335
|
-
1. **Machine Detection** - Automatically detects your machine name from hostname (Windows: `COMPUTERNAME`, Linux/Mac: `hostname()`)
|
|
453
|
+
1. **Machine Detection** - Automatically detects your machine name from hostname (Windows: `COMPUTERNAME`, Linux/Mac: `hostname()`) and other system properties (OS, architecture, username, environment)
|
|
336
454
|
|
|
337
|
-
2. **File Discovery** - Scans your repository for files matching
|
|
455
|
+
2. **File Discovery** - Scans your repository for files matching patterns:
|
|
456
|
+
- **New**: `{key=value}` syntax (e.g., `config.{os=windows}.json`, `secrets.{machine=laptop}{user=josxa}.json`)
|
|
457
|
+
- **Legacy**: `*.{machine}.*` syntax (e.g., `config.laptop.json`, `.env.desktop`)
|
|
338
458
|
|
|
339
|
-
3. **
|
|
459
|
+
3. **Filter Matching** - For new syntax, evaluates filters against current system context:
|
|
460
|
+
- AND logic: ALL filters must match (e.g., `{os=windows}{arch=x64}`)
|
|
461
|
+
- OR logic: ANY value in list matches (e.g., `{os=windows,macos}`)
|
|
340
462
|
|
|
463
|
+
4. **Smart Merging** - Merges base and machine-specific configs:
|
|
341
464
|
- **JSON**: Deep recursive merge (machine values override base)
|
|
342
465
|
- **ENV**: Key-value merge with comment preservation
|
|
343
466
|
|
|
344
|
-
|
|
467
|
+
5. **Gitignore Management** - Automatically adds output files to `.gitignore` and removes already-tracked files from git
|
|
345
468
|
|
|
346
|
-
|
|
469
|
+
6. **Git Hooks** - Installs hooks to auto-merge on checkout, merge, and commit operations
|
|
347
470
|
|
|
348
|
-
For detailed implementation information, see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
471
|
+
For detailed implementation information, see [CONTRIBUTING.md](CONTRIBUTING.md) and [File Filters Documentation](docs/FILE_FILTERS.md).
|
|
349
472
|
|
|
350
473
|
## Supported File Types
|
|
351
474
|
|
|
352
|
-
| Type
|
|
353
|
-
|
|
|
354
|
-
| JSON
|
|
355
|
-
| JSONC
|
|
356
|
-
| ENV
|
|
357
|
-
|
|
|
358
|
-
|
|
|
475
|
+
| Type | Extensions | Merge Strategy | Status |
|
|
476
|
+
| -------- | --------------------- | --------------------------------- | ---------------------------------------------- |
|
|
477
|
+
| JSON | `.json` | Deep recursive merge | ✅ Supported |
|
|
478
|
+
| JSONC | `.json` with comments | Deep merge + comment preservation | ✅ Supported |
|
|
479
|
+
| ENV | `.env`, `.env.*` | Key-value upsert | ✅ Supported |
|
|
480
|
+
| Markdown | `.md` | Append (base + machine) | 🔜 [Planned](#3) |
|
|
481
|
+
| YAML | `.yaml`, `.yml` | Deep recursive merge | 🔜 [Planned](#1) |
|
|
482
|
+
| TOML | `.toml` | Deep recursive merge | 🔜 [Planned](#2) |
|
|
483
|
+
| Patch | `.patch` | Apply git-style patch to base | 💡 [Proposed](#4) |
|
|
359
484
|
|
|
360
485
|
## Troubleshooting
|
|
361
486
|
|
|
@@ -460,11 +585,13 @@ MIT © [JosXa](https://github.com/JosXa)
|
|
|
460
585
|
- [x] Comprehensive tests (81 tests)
|
|
461
586
|
- [x] npm package publication
|
|
462
587
|
- [x] Watch mode for development
|
|
463
|
-
- [ ] YAML support
|
|
464
|
-
- [ ] TOML support
|
|
465
|
-
- [ ]
|
|
466
|
-
- [ ]
|
|
467
|
-
- [ ]
|
|
588
|
+
- [ ] YAML support ([#1](https://github.com/JosXa/permachine/issues/1))
|
|
589
|
+
- [ ] TOML support ([#2](https://github.com/JosXa/permachine/issues/2))
|
|
590
|
+
- [ ] Markdown support ([#3](https://github.com/JosXa/permachine/issues/3))
|
|
591
|
+
- [ ] Patch file support ([#4](https://github.com/JosXa/permachine/issues/4))
|
|
592
|
+
- [ ] Custom merge strategies ([#5](https://github.com/JosXa/permachine/issues/5))
|
|
593
|
+
- [ ] Config file for patterns ([#6](https://github.com/JosXa/permachine/issues/6))
|
|
594
|
+
- [ ] Dry-run mode ([#7](https://github.com/JosXa/permachine/issues/7))
|
|
468
595
|
|
|
469
596
|
## Credits
|
|
470
597
|
|
package/dist/cli.js
CHANGED
|
@@ -5801,9 +5801,144 @@ glob.glob = glob;
|
|
|
5801
5801
|
|
|
5802
5802
|
// src/core/file-scanner.ts
|
|
5803
5803
|
import path2 from "node:path";
|
|
5804
|
+
|
|
5805
|
+
// src/core/file-filters.ts
|
|
5806
|
+
import os2 from "node:os";
|
|
5807
|
+
var cachedContext = null;
|
|
5808
|
+
function getFilterContext() {
|
|
5809
|
+
if (cachedContext) {
|
|
5810
|
+
return cachedContext;
|
|
5811
|
+
}
|
|
5812
|
+
const platform = os2.platform();
|
|
5813
|
+
let osName;
|
|
5814
|
+
switch (platform) {
|
|
5815
|
+
case "win32":
|
|
5816
|
+
osName = "windows";
|
|
5817
|
+
break;
|
|
5818
|
+
case "darwin":
|
|
5819
|
+
osName = "macos";
|
|
5820
|
+
break;
|
|
5821
|
+
case "linux":
|
|
5822
|
+
osName = "linux";
|
|
5823
|
+
break;
|
|
5824
|
+
case "freebsd":
|
|
5825
|
+
osName = "freebsd";
|
|
5826
|
+
break;
|
|
5827
|
+
case "openbsd":
|
|
5828
|
+
osName = "openbsd";
|
|
5829
|
+
break;
|
|
5830
|
+
default:
|
|
5831
|
+
osName = platform;
|
|
5832
|
+
}
|
|
5833
|
+
cachedContext = {
|
|
5834
|
+
os: osName,
|
|
5835
|
+
arch: os2.arch(),
|
|
5836
|
+
machine: getMachineName(),
|
|
5837
|
+
user: os2.userInfo().username.toLowerCase(),
|
|
5838
|
+
env: "development"?.toLowerCase() || null,
|
|
5839
|
+
platform
|
|
5840
|
+
};
|
|
5841
|
+
return cachedContext;
|
|
5842
|
+
}
|
|
5843
|
+
function createCustomContext(overrides) {
|
|
5844
|
+
const base = getFilterContext();
|
|
5845
|
+
return { ...base, ...overrides };
|
|
5846
|
+
}
|
|
5847
|
+
var FILTER_REGEX = /\{([a-zA-Z0-9_-]+)(=|!=|~|\^)([a-zA-Z0-9_*.,\-]+)\}/g;
|
|
5848
|
+
function parseFilters(filename) {
|
|
5849
|
+
const filters = [];
|
|
5850
|
+
let match2;
|
|
5851
|
+
FILTER_REGEX.lastIndex = 0;
|
|
5852
|
+
while ((match2 = FILTER_REGEX.exec(filename)) !== null) {
|
|
5853
|
+
const [raw, key, operator, value] = match2;
|
|
5854
|
+
filters.push({
|
|
5855
|
+
key: key.toLowerCase(),
|
|
5856
|
+
operator,
|
|
5857
|
+
value: value.toLowerCase(),
|
|
5858
|
+
raw
|
|
5859
|
+
});
|
|
5860
|
+
}
|
|
5861
|
+
let baseFilename = filename.replace(/\.?\{[^}]+\}/g, "");
|
|
5862
|
+
baseFilename = baseFilename.replace(/\.{2,}/g, ".");
|
|
5863
|
+
return { filters, baseFilename };
|
|
5864
|
+
}
|
|
5865
|
+
function hasFilters(filename) {
|
|
5866
|
+
FILTER_REGEX.lastIndex = 0;
|
|
5867
|
+
return FILTER_REGEX.test(filename);
|
|
5868
|
+
}
|
|
5869
|
+
function evaluateFilter(filter2, context) {
|
|
5870
|
+
const contextValue = context[filter2.key];
|
|
5871
|
+
if (contextValue === null || contextValue === undefined) {
|
|
5872
|
+
return false;
|
|
5873
|
+
}
|
|
5874
|
+
switch (filter2.operator) {
|
|
5875
|
+
case "=":
|
|
5876
|
+
return evaluateEquals(filter2.value, contextValue);
|
|
5877
|
+
case "!=":
|
|
5878
|
+
return !evaluateEquals(filter2.value, contextValue);
|
|
5879
|
+
case "~":
|
|
5880
|
+
return evaluateWildcard(filter2.value, contextValue);
|
|
5881
|
+
case "^":
|
|
5882
|
+
return evaluateRange(filter2.value, contextValue);
|
|
5883
|
+
default:
|
|
5884
|
+
return false;
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
function evaluateEquals(filterValue, contextValue) {
|
|
5888
|
+
const options = filterValue.split(",").map((v) => v.trim().toLowerCase());
|
|
5889
|
+
return options.includes(contextValue.toLowerCase());
|
|
5890
|
+
}
|
|
5891
|
+
function evaluateWildcard(filterValue, contextValue) {
|
|
5892
|
+
const regexPattern = filterValue.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
5893
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
5894
|
+
return regex.test(contextValue);
|
|
5895
|
+
}
|
|
5896
|
+
function evaluateRange(filterValue, contextValue) {
|
|
5897
|
+
const [min, max] = filterValue.split("-").map((v) => v.trim());
|
|
5898
|
+
const numContext = parseFloat(contextValue);
|
|
5899
|
+
const numMin = parseFloat(min);
|
|
5900
|
+
const numMax = parseFloat(max);
|
|
5901
|
+
if (!isNaN(numContext) && !isNaN(numMin) && !isNaN(numMax)) {
|
|
5902
|
+
return numContext >= numMin && numContext <= numMax;
|
|
5903
|
+
}
|
|
5904
|
+
return contextValue >= min && contextValue <= max;
|
|
5905
|
+
}
|
|
5906
|
+
function matchFilters(filename, context) {
|
|
5907
|
+
const ctx = context || getFilterContext();
|
|
5908
|
+
const { filters } = parseFilters(filename);
|
|
5909
|
+
if (filters.length === 0) {
|
|
5910
|
+
return {
|
|
5911
|
+
matches: true,
|
|
5912
|
+
failedFilters: [],
|
|
5913
|
+
context: ctx
|
|
5914
|
+
};
|
|
5915
|
+
}
|
|
5916
|
+
const failedFilters = [];
|
|
5917
|
+
for (const filter2 of filters) {
|
|
5918
|
+
if (!evaluateFilter(filter2, ctx)) {
|
|
5919
|
+
failedFilters.push(filter2);
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5922
|
+
return {
|
|
5923
|
+
matches: failedFilters.length === 0,
|
|
5924
|
+
failedFilters,
|
|
5925
|
+
context: ctx
|
|
5926
|
+
};
|
|
5927
|
+
}
|
|
5928
|
+
function getBaseFilename(filename) {
|
|
5929
|
+
return parseFilters(filename).baseFilename;
|
|
5930
|
+
}
|
|
5931
|
+
function isLegacyFilename(filename, machineName) {
|
|
5932
|
+
const middlePattern = new RegExp(`\\.${machineName}\\.`, "i");
|
|
5933
|
+
const endPattern = new RegExp(`\\.${machineName}$`, "i");
|
|
5934
|
+
return (middlePattern.test(filename) || endPattern.test(filename)) && !hasFilters(filename);
|
|
5935
|
+
}
|
|
5936
|
+
|
|
5937
|
+
// src/core/file-scanner.ts
|
|
5804
5938
|
async function scanForMergeOperations(machineName, cwd = process.cwd()) {
|
|
5805
5939
|
const operations = [];
|
|
5806
5940
|
const patterns = [
|
|
5941
|
+
"**/*{*}*",
|
|
5807
5942
|
`**/*.${machineName}.*`,
|
|
5808
5943
|
`**/.*.${machineName}`,
|
|
5809
5944
|
`**/.*.${machineName}.*`
|
|
@@ -5813,7 +5948,7 @@ async function scanForMergeOperations(machineName, cwd = process.cwd()) {
|
|
|
5813
5948
|
try {
|
|
5814
5949
|
const files = await glob(pattern, {
|
|
5815
5950
|
cwd,
|
|
5816
|
-
ignore: ["node_modules/**", ".git/**", "dist/**"],
|
|
5951
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "**/*.base.*", "**/.*base*"],
|
|
5817
5952
|
dot: true,
|
|
5818
5953
|
nodir: true
|
|
5819
5954
|
});
|
|
@@ -5821,10 +5956,24 @@ async function scanForMergeOperations(machineName, cwd = process.cwd()) {
|
|
|
5821
5956
|
} catch (error) {}
|
|
5822
5957
|
}
|
|
5823
5958
|
const uniqueFiles = [...new Set(foundFiles)];
|
|
5959
|
+
const context = createCustomContext({ machine: machineName });
|
|
5824
5960
|
for (const file of uniqueFiles) {
|
|
5825
|
-
const
|
|
5826
|
-
if (
|
|
5827
|
-
|
|
5961
|
+
const basename = path2.basename(file);
|
|
5962
|
+
if (basename.includes(".base.") || basename.includes(".base")) {
|
|
5963
|
+
continue;
|
|
5964
|
+
}
|
|
5965
|
+
let shouldProcess = false;
|
|
5966
|
+
if (hasFilters(basename)) {
|
|
5967
|
+
const result = matchFilters(basename, context);
|
|
5968
|
+
shouldProcess = result.matches;
|
|
5969
|
+
} else if (isLegacyFilename(basename, machineName)) {
|
|
5970
|
+
shouldProcess = true;
|
|
5971
|
+
}
|
|
5972
|
+
if (shouldProcess) {
|
|
5973
|
+
const operation = createMergeOperation(file, machineName, cwd);
|
|
5974
|
+
if (operation) {
|
|
5975
|
+
operations.push(operation);
|
|
5976
|
+
}
|
|
5828
5977
|
}
|
|
5829
5978
|
}
|
|
5830
5979
|
return operations;
|
|
@@ -5847,21 +5996,35 @@ function createMergeOperation(machineFile, machineName, cwd) {
|
|
|
5847
5996
|
if (type === "unknown") {
|
|
5848
5997
|
return null;
|
|
5849
5998
|
}
|
|
5850
|
-
const basename = type === "env" ? fullBasename : path2.basename(machineFile, ext2);
|
|
5851
|
-
const machinePattern = `.${machineName}`;
|
|
5852
|
-
const basePattern = ".base";
|
|
5853
5999
|
let baseName;
|
|
5854
6000
|
let outputName;
|
|
5855
|
-
if (
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
6001
|
+
if (hasFilters(fullBasename)) {
|
|
6002
|
+
outputName = getBaseFilename(fullBasename);
|
|
6003
|
+
if (type === "env") {
|
|
6004
|
+
const nameWithoutExt = outputName;
|
|
6005
|
+
baseName = nameWithoutExt + ".base";
|
|
6006
|
+
} else {
|
|
6007
|
+
const nameWithoutExt = outputName.replace(ext2, "");
|
|
6008
|
+
baseName = nameWithoutExt + ".base" + ext2;
|
|
6009
|
+
}
|
|
5859
6010
|
} else {
|
|
5860
|
-
|
|
6011
|
+
const basename = type === "env" ? fullBasename : path2.basename(machineFile, ext2);
|
|
6012
|
+
const machinePattern = `.${machineName}`;
|
|
6013
|
+
if (basename.endsWith(machinePattern)) {
|
|
6014
|
+
const withoutMachine = basename.substring(0, basename.length - machinePattern.length);
|
|
6015
|
+
baseName = withoutMachine + ".base";
|
|
6016
|
+
outputName = withoutMachine;
|
|
6017
|
+
if (type !== "env") {
|
|
6018
|
+
baseName = baseName + ext2;
|
|
6019
|
+
outputName = outputName + ext2;
|
|
6020
|
+
}
|
|
6021
|
+
} else {
|
|
6022
|
+
return null;
|
|
6023
|
+
}
|
|
5861
6024
|
}
|
|
5862
|
-
const basePath = path2.join(cwd, dir, baseName
|
|
6025
|
+
const basePath = path2.join(cwd, dir, baseName);
|
|
5863
6026
|
const machinePath = path2.join(cwd, machineFile);
|
|
5864
|
-
const outputPath = path2.join(cwd, dir, outputName
|
|
6027
|
+
const outputPath = path2.join(cwd, dir, outputName);
|
|
5865
6028
|
return {
|
|
5866
6029
|
basePath,
|
|
5867
6030
|
machinePath,
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "permachine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Automatically merge machine-specific config files with base configs using git hooks",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"permachine": "
|
|
7
|
+
"permachine": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bun build src/cli.ts --outdir dist --target node --format esm",
|