permachine 0.2.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +230 -103
  2. package/dist/cli.js +183 -14
  3. 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 | In Git? |
91
- | ---------------- | ---------------- | ------------------ |
92
- | Base config | `.env.base` | ✅ Yes |
93
- | Machine-specific | `.env.my-laptop` | ✅ Yes |
94
- | Final output | `.env` | ❌ No (gitignored) |
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
- **setup.base.json:**
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
- ### Recipe 2: Environment Variables
234
+ **settings.desktop.json:**
208
235
 
209
- Different database credentials per environment:
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
- # .env.base (shared defaults)
213
- NODE_ENV=development
214
- LOG_LEVEL=info
215
- API_PORT=3000
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
- # .env.laptop (local dev)
218
- DATABASE_URL=postgresql://localhost:5432/myapp_dev
219
- API_KEY=dev_key_123
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
- # .env.prodserver (production)
222
- DATABASE_URL=postgresql://prod.db.com:5432/myapp
223
- API_KEY=prod_key_xyz
294
+ **config.worklaptop.json:**
224
295
 
225
- # .env ← Merged output (gitignored)
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 build scripts for different machines:
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.laptop.json (local development)
331
+ # package.macos.json
246
332
  {
247
333
  "scripts": {
248
- "dev": "nodemon src/index.js",
249
- "build": "webpack --mode development"
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.buildserver.json (CI/CD)
340
+ # package.windows.json
254
341
  {
255
342
  "scripts": {
256
- "build": "webpack --mode production",
257
- "deploy": "aws s3 sync dist/ s3://my-bucket"
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 machine gets appropriate scripts!
350
+ # Each OS gets appropriate shell commands!
263
351
  ```
264
352
 
265
- ### Recipe 4: Database Configuration
353
+ ### Recipe 4: Git Config
266
354
 
267
- Multi-environment database setup:
355
+ Personal vs work Git settings:
268
356
 
269
357
  ```bash
270
- # config/database.base.json
271
- {
272
- "pool": {
273
- "min": 2,
274
- "max": 10
275
- },
276
- "migrations": {
277
- "directory": "./migrations"
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
- # config/database.laptop.json
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
- # config/database.prodserver.json
293
- {
294
- "connection": {
295
- "host": "db.production.com",
296
- "port": 5432,
297
- "database": "myapp_production",
298
- "user": "produser",
299
- "password": "secure_password_from_vault"
300
- },
301
- "pool": {
302
- "min": 10,
303
- "max": 50
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 5: Multi-File Projects
412
+ ### Recipe 6: Advanced Filters - Cross-Platform Development
309
413
 
310
- Complex projects with multiple config files:
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.laptop.json
316
- ├── settings/
317
- ├── app.base.json
318
- ├── app.laptop.json
319
- ├── database.base.json
320
- │ └── database.laptop.json
321
- ├── .env.base
322
- └── .env.laptop
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 three-step process:
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 the pattern `*.{machine}.*` (e.g., `config.laptop.json`, `.env.desktop`)
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. **Smart Merging** - Merges base and machine-specific configs:
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
- 4. **Gitignore Management** - Automatically adds output files to `.gitignore` and removes already-tracked files from git
467
+ 5. **Gitignore Management** - Automatically adds output files to `.gitignore` and removes already-tracked files from git
345
468
 
346
- 5. **Git Hooks** - Installs hooks to auto-merge on checkout, merge, and commit operations
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 | Extensions | Merge Strategy | Status |
353
- | ----- | --------------------- | --------------------------------- | ------------ |
354
- | JSON | `.json` | Deep recursive merge | ✅ Supported |
355
- | JSONC | `.json` with comments | Deep merge + comment preservation | ✅ Supported |
356
- | ENV | `.env`, `.env.*` | Key-value override | ✅ Supported |
357
- | YAML | `.yaml`, `.yml` | Deep recursive merge | 🔜 Planned |
358
- | TOML | `.toml` | Deep recursive merge | 🔜 Planned |
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
- - [ ] Custom merge strategies
466
- - [ ] Config file for patterns
467
- - [ ] Dry-run mode
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,150 @@ 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
+ var BASE_PLACEHOLDER_REGEX = /\{base\}/gi;
5849
+ function parseFilters(filename) {
5850
+ const filters = [];
5851
+ let match2;
5852
+ BASE_PLACEHOLDER_REGEX.lastIndex = 0;
5853
+ const hasBasePlaceholder = BASE_PLACEHOLDER_REGEX.test(filename);
5854
+ FILTER_REGEX.lastIndex = 0;
5855
+ while ((match2 = FILTER_REGEX.exec(filename)) !== null) {
5856
+ const [raw, key, operator, value] = match2;
5857
+ filters.push({
5858
+ key: key.toLowerCase(),
5859
+ operator,
5860
+ value: value.toLowerCase(),
5861
+ raw
5862
+ });
5863
+ }
5864
+ let baseFilename = filename.replace(/\.?\{[^}]+\}/g, "");
5865
+ baseFilename = baseFilename.replace(/\.{2,}/g, ".");
5866
+ return { filters, baseFilename, hasBasePlaceholder };
5867
+ }
5868
+ function hasFilters(filename) {
5869
+ FILTER_REGEX.lastIndex = 0;
5870
+ const hasFilterSyntax = FILTER_REGEX.test(filename);
5871
+ BASE_PLACEHOLDER_REGEX.lastIndex = 0;
5872
+ const hasBasePlaceholder = BASE_PLACEHOLDER_REGEX.test(filename);
5873
+ return hasFilterSyntax || hasBasePlaceholder;
5874
+ }
5875
+ function evaluateFilter(filter2, context) {
5876
+ const contextValue = context[filter2.key];
5877
+ if (contextValue === null || contextValue === undefined) {
5878
+ return false;
5879
+ }
5880
+ switch (filter2.operator) {
5881
+ case "=":
5882
+ return evaluateEquals(filter2.value, contextValue);
5883
+ case "!=":
5884
+ return !evaluateEquals(filter2.value, contextValue);
5885
+ case "~":
5886
+ return evaluateWildcard(filter2.value, contextValue);
5887
+ case "^":
5888
+ return evaluateRange(filter2.value, contextValue);
5889
+ default:
5890
+ return false;
5891
+ }
5892
+ }
5893
+ function evaluateEquals(filterValue, contextValue) {
5894
+ const options = filterValue.split(",").map((v) => v.trim().toLowerCase());
5895
+ return options.includes(contextValue.toLowerCase());
5896
+ }
5897
+ function evaluateWildcard(filterValue, contextValue) {
5898
+ const regexPattern = filterValue.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
5899
+ const regex = new RegExp(`^${regexPattern}$`, "i");
5900
+ return regex.test(contextValue);
5901
+ }
5902
+ function evaluateRange(filterValue, contextValue) {
5903
+ const [min, max] = filterValue.split("-").map((v) => v.trim());
5904
+ const numContext = parseFloat(contextValue);
5905
+ const numMin = parseFloat(min);
5906
+ const numMax = parseFloat(max);
5907
+ if (!isNaN(numContext) && !isNaN(numMin) && !isNaN(numMax)) {
5908
+ return numContext >= numMin && numContext <= numMax;
5909
+ }
5910
+ return contextValue >= min && contextValue <= max;
5911
+ }
5912
+ function matchFilters(filename, context) {
5913
+ const ctx = context || getFilterContext();
5914
+ const { filters } = parseFilters(filename);
5915
+ if (filters.length === 0) {
5916
+ return {
5917
+ matches: true,
5918
+ failedFilters: [],
5919
+ context: ctx
5920
+ };
5921
+ }
5922
+ const failedFilters = [];
5923
+ for (const filter2 of filters) {
5924
+ if (!evaluateFilter(filter2, ctx)) {
5925
+ failedFilters.push(filter2);
5926
+ }
5927
+ }
5928
+ return {
5929
+ matches: failedFilters.length === 0,
5930
+ failedFilters,
5931
+ context: ctx
5932
+ };
5933
+ }
5934
+ function getBaseFilename(filename) {
5935
+ return parseFilters(filename).baseFilename;
5936
+ }
5937
+ function isLegacyFilename(filename, machineName) {
5938
+ const middlePattern = new RegExp(`\\.${machineName}\\.`, "i");
5939
+ const endPattern = new RegExp(`\\.${machineName}$`, "i");
5940
+ return (middlePattern.test(filename) || endPattern.test(filename)) && !hasFilters(filename);
5941
+ }
5942
+
5943
+ // src/core/file-scanner.ts
5804
5944
  async function scanForMergeOperations(machineName, cwd = process.cwd()) {
5805
5945
  const operations = [];
5806
5946
  const patterns = [
5947
+ "**/*{*}*",
5807
5948
  `**/*.${machineName}.*`,
5808
5949
  `**/.*.${machineName}`,
5809
5950
  `**/.*.${machineName}.*`
@@ -5813,7 +5954,7 @@ async function scanForMergeOperations(machineName, cwd = process.cwd()) {
5813
5954
  try {
5814
5955
  const files = await glob(pattern, {
5815
5956
  cwd,
5816
- ignore: ["node_modules/**", ".git/**", "dist/**"],
5957
+ ignore: ["node_modules/**", ".git/**", "dist/**", "**/*.base.*", "**/.*base*"],
5817
5958
  dot: true,
5818
5959
  nodir: true
5819
5960
  });
@@ -5821,10 +5962,24 @@ async function scanForMergeOperations(machineName, cwd = process.cwd()) {
5821
5962
  } catch (error) {}
5822
5963
  }
5823
5964
  const uniqueFiles = [...new Set(foundFiles)];
5965
+ const context = createCustomContext({ machine: machineName });
5824
5966
  for (const file of uniqueFiles) {
5825
- const operation = createMergeOperation(file, machineName, cwd);
5826
- if (operation) {
5827
- operations.push(operation);
5967
+ const basename = path2.basename(file);
5968
+ if (basename.includes(".base.") || basename.includes(".base")) {
5969
+ continue;
5970
+ }
5971
+ let shouldProcess = false;
5972
+ if (hasFilters(basename)) {
5973
+ const result = matchFilters(basename, context);
5974
+ shouldProcess = result.matches;
5975
+ } else if (isLegacyFilename(basename, machineName)) {
5976
+ shouldProcess = true;
5977
+ }
5978
+ if (shouldProcess) {
5979
+ const operation = createMergeOperation(file, machineName, cwd);
5980
+ if (operation) {
5981
+ operations.push(operation);
5982
+ }
5828
5983
  }
5829
5984
  }
5830
5985
  return operations;
@@ -5847,21 +6002,35 @@ function createMergeOperation(machineFile, machineName, cwd) {
5847
6002
  if (type === "unknown") {
5848
6003
  return null;
5849
6004
  }
5850
- const basename = type === "env" ? fullBasename : path2.basename(machineFile, ext2);
5851
- const machinePattern = `.${machineName}`;
5852
- const basePattern = ".base";
5853
6005
  let baseName;
5854
6006
  let outputName;
5855
- if (basename.endsWith(machinePattern)) {
5856
- const withoutMachine = basename.substring(0, basename.length - machinePattern.length);
5857
- baseName = withoutMachine + basePattern;
5858
- outputName = withoutMachine;
6007
+ if (hasFilters(fullBasename)) {
6008
+ outputName = getBaseFilename(fullBasename);
6009
+ if (type === "env") {
6010
+ const nameWithoutExt = outputName;
6011
+ baseName = nameWithoutExt + ".base";
6012
+ } else {
6013
+ const nameWithoutExt = outputName.replace(ext2, "");
6014
+ baseName = nameWithoutExt + ".base" + ext2;
6015
+ }
5859
6016
  } else {
5860
- return null;
6017
+ const basename = type === "env" ? fullBasename : path2.basename(machineFile, ext2);
6018
+ const machinePattern = `.${machineName}`;
6019
+ if (basename.endsWith(machinePattern)) {
6020
+ const withoutMachine = basename.substring(0, basename.length - machinePattern.length);
6021
+ baseName = withoutMachine + ".base";
6022
+ outputName = withoutMachine;
6023
+ if (type !== "env") {
6024
+ baseName = baseName + ext2;
6025
+ outputName = outputName + ext2;
6026
+ }
6027
+ } else {
6028
+ return null;
6029
+ }
5861
6030
  }
5862
- const basePath = path2.join(cwd, dir, baseName + ext2);
6031
+ const basePath = path2.join(cwd, dir, baseName);
5863
6032
  const machinePath = path2.join(cwd, machineFile);
5864
- const outputPath = path2.join(cwd, dir, outputName + ext2);
6033
+ const outputPath = path2.join(cwd, dir, outputName);
5865
6034
  return {
5866
6035
  basePath,
5867
6036
  machinePath,
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "permachine",
3
- "version": "0.2.1",
3
+ "version": "0.4.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": "./dist/cli.js"
7
+ "permachine": "dist/cli.js"
8
8
  },
9
9
  "scripts": {
10
10
  "build": "bun build src/cli.ts --outdir dist --target node --format esm",