npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

permachine

v0.9.2

Published

Automatically merge machine-specific config files with base configs using git hooks

Readme

permachine

Per-machine config management with git for tools that don't support it natively. Automatically merge machine-specific configurations with a base config.

npm version License: MIT

Problem

When syncing dotfiles across multiple machines, you often need:

  • Shared configuration - Settings that work across all machines
  • Machine-specific overrides - Local paths, API keys, ports, etc.
  • Automatic merging - No manual copy-paste or merge steps

Solution

permachine automatically:

  1. Detects your machine name
  2. Finds machine-specific config files (e.g. config.my-laptop.json, config.workstation.json)
  3. Merges them with base configs (e.g., config.base.json)
  4. Outputs the final config (e.g., config.json)
  5. Manages .gitignore - Adds output files and removes from git tracking
  6. Runs automatically on git operations via hooks

Quick Start

# Install globally
npm install -g permachine

# In your repository
cd /path/to/your/repo

# Initialize (one-time setup)
permachine init

# That's it! Your configs will now auto-merge on git operations when a file ends with `.<machine-name>.<ext>`

CLI Reference

permachine - Automatically merge machine-specific config files

USAGE:
  permachine <command> [options]

COMMANDS:
  init                Initialize permachine in current repository
  merge               Manually trigger merge operation
  info                Show information about current setup
  uninstall           Uninstall git hooks
  watch               Watch for file changes and auto-merge

OPTIONS:
  --help, -h          Show this help message
  --version, -v       Show version number
  --silent, -s        Suppress all output except errors (for merge command)
  --legacy            Use legacy .git/hooks wrapping (for init command)
  --auto              Auto-detect best installation method (for init command)
  --no-gitignore      Don't manage .gitignore or git tracking (for init/merge commands)
  --debounce <ms>     Debounce delay in milliseconds (for watch command, default: 300)
  --verbose           Show detailed file change events (for watch command)

EXAMPLES:
  permachine init
  permachine merge --silent
  permachine info
  permachine uninstall
  permachine watch
  permachine watch --debounce 500 --verbose

Usage

File Naming Convention

New Advanced Syntax (Recommended):

Filter-based syntax for precise control:

| Purpose | Filename | In Git? | | --------------------- | ----------------------------- | ------------------ | | Base config (shared) | config.base.json | ✅ Yes | | OS-specific | config.{os=windows}.json | ✅ Yes | | Machine-specific | config.{machine=laptop}.json| ✅ Yes | | Multi-filter | secrets.{machine=laptop}{user=josxa}.json | ✅ Yes | | Final output (merged) | config.json | ❌ No (gitignored) |

Legacy Syntax (Still Supported):

Given machine name my-laptop (auto-detected from hostname):

| Purpose | Filename | In Git? | | --------------------- | ----------------------- | ------------------ | | Base config (shared) | config.base.json | ✅ Yes | | Machine-specific | config.my-laptop.json | ✅ Yes | | Final output (merged) | config.json | ❌ No (gitignored) |

Supported Filters:

  • {os=windows}, {os=macos}, {os=linux} - Operating system
  • {arch=x64}, {arch=arm64} - CPU architecture
  • {machine=hostname} - Machine/hostname (same as legacy)
  • {user=username} - Username
  • {env=prod}, {env=dev} - Environment (from NODE_ENV)
  • Multiple filters: {os=windows}{arch=x64} (AND logic)
  • OR logic: {os=windows,macos,linux} (comma-separated)

📚 See File Filters Documentation for complete guide and examples.

Same pattern works for .env files:

| Purpose | Filename | In Git? | | ---------------- | -------------------------- | ------------------ | | Base config | .env.base | ✅ Yes | | Machine-specific | .env.{machine=laptop} | ✅ Yes | | Final output | .env | ❌ No (gitignored) |

Directory Matching (NEW)

In addition to file-level merging, you can apply filters to entire directories. When a directory matches, all its contents are copied as-is to the output directory (without further filter processing or merging).

Example:

.opencode/skills/
├── jira.{machine=homezone}/       # Only exists on machine "homezone"
│   ├── skill.md
│   └── templates/
│       └── issue.md
├── work-tools.{machine=laptop}/   # Only exists on machine "laptop"  
│   └── slack.md
└── shared-skill/                  # Regular directory (always present)
    └── common.md

On machine homezone:

.opencode/skills/
├── jira/                          # ← Copied from jira.{machine=homezone}/
│   ├── skill.md
│   └── templates/
│       └── issue.md
└── shared-skill/
    └── common.md

Supported filters on directories:

  • {machine=hostname} - Machine-specific directories
  • {os=windows}, {os=macos}, {os=linux} - OS-specific directories
  • {user=username} - User-specific directories
  • Multiple filters: mydir.{machine=laptop}{os=windows}/ (AND logic)

Key behaviors:

  • Files inside matched directories are copied verbatim (no recursive filter processing)
  • No base directory fallback (unlike files, there's no mydir.base/ pattern)
  • Nested filtered directories are not allowed: outer.{machine=X}/inner.{os=Y}/ → Error
  • If multiple directories would produce the same output, an error is raised
  • Stale outputs are renamed with .permachine-deleted suffix for safety

Basic Commands

Initialize in Repository

permachine init

What it does:

  • Detects your machine name (e.g., laptop, desktop, workstation)
  • Installs git hooks for automatic merging
  • Scans for existing machine-specific files
  • Prompts for confirmation if existing files will be overwritten
  • Performs initial merge
  • Adds output files to .gitignore and removes them from git tracking

Example output:

✓ Machine detected: laptop
✓ Git hooks installed via core.hooksPath
✓ Merged 2 file(s)
✓ Added 2 file(s) to .gitignore
✓ Removed 1 file(s) from git tracking

Git hooks will auto-merge on:
  - checkout (switching branches)
  - merge (git pull/merge)
  - commit

Manual Merge

permachine merge

Prompts for confirmation if existing files will be overwritten. Useful for testing or running without git hooks.

Watch Mode

permachine watch

What it does:

  • Watches all base and machine-specific files for changes
  • Automatically merges when you save any watched file

Check Setup

permachine info

Example output:

Machine name: laptop
Repository: /path/to/repo
Hooks method: core.hooksPath
Hooks path: .permachine/hooks
Tracked patterns: 2
  - config.base.json + config.laptop.json → config.json
  - .env.base + .env.laptop → .env

Output files: 2 total, 1 existing
Existing output files:
  - config.json

Cookbook / Recipes

Recipe 1: VSCode Settings Per Machine

Different settings for work laptop vs home desktop:

# On work laptop (machine: "worklaptop")
.vscode/
  ├── settings.base.json         # Shared: theme, font size
  ├── settings.worklaptop.json   # Work paths, proxy settings
  └── settings.json              # ← Merged output (gitignored)

# On home desktop (machine: "desktop")
.vscode/
  ├── settings.base.json         # Shared: theme, font size
  ├── settings.desktop.json      # Home paths, no proxy
  └── settings.json              # ← Merged output (gitignored)

settings.base.json:

{
  "editor.fontSize": 14,
  "workbench.colorTheme": "Dark+",
  "terminal.integrated.shell.windows": "powershell.exe"
}

settings.worklaptop.json:

{
  "http.proxy": "http://proxy.company.com:8080",
  "terminal.integrated.cwd": "C:/Projects/Work"
}

settings.desktop.json:

{
  "terminal.integrated.cwd": "C:/Code/Personal",
  "git.path": "C:/Program Files/Git/bin/git.exe"
}

Recipe 2: OpenCode Config (AI Assistant)

OpenCode supports machine-specific MCP servers and model preferences:

~/.config/opencode/
  ├── config.base.json         # Shared: agents, themes, keybinds
  ├── config.worklaptop.json   # Work: Google Sheets MCP
  ├── config.homezone.json     # Home: Telegram MCP, local paths
  └── config.json              # ← Merged output (gitignored)

config.base.json:

{
  "$schema": "https://opencode.ai/config.json",
  "theme": "nightowl-transparent",
  "keybinds": {
    "input_newline": "shift+return"
  },
  "mcp": {
    "perplexity-mcp": {
      "enabled": true,
      "type": "local",
      "command": ["uvx", "perplexity-mcp"]
    }
  }
}

config.homezone.json:

{
  "mcp": {
    "telegram-mcp": {
      "enabled": true,
      "type": "local",
      "command": ["uv", "--directory", "D:\\git\\telegram-mcp", "run", "main.py"]
    },
    "google-sheets": {
      "enabled": true,
      "environment": {
        "SERVICE_ACCOUNT_PATH": "C:/Users/josch/.config/opencode/secrets/service-account.json"
      }
    }
  }
}

config.worklaptop.json:

{
  "mcp": {
    "telegram-mcp": {
      "enabled": false
    },
    "google-sheets": {
      "enabled": true,
      "environment": {
        "SERVICE_ACCOUNT_PATH": "/work/credentials/google-service-account.json",
        "DRIVE_FOLDER_ID": "work-folder-id-123"
      }
    }
  }
}

Recipe 3: Package.json Scripts (Platform-Specific)

Different scripts for macOS vs Windows development:

# package.base.json
{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "test": "jest",
    "lint": "eslint src/"
  },
  "dependencies": {
    "express": "^4.18.0"
  }
}

# package.macos.json
{
  "scripts": {
    "dev": "NODE_ENV=development nodemon src/index.js",
    "build": "rm -rf dist && webpack",
    "open": "open http://localhost:3000"
  }
}

# package.windows.json
{
  "scripts": {
    "dev": "set NODE_ENV=development && nodemon src/index.js",
    "build": "rmdir /s /q dist && webpack",
    "open": "start http://localhost:3000"
  }
}

# package.json ← Merged output
# Each OS gets appropriate shell commands!

Recipe 4: Git Config

Personal vs work Git settings:

# .gitconfig.base
[core]
  editor = code --wait
  autocrlf = true
[pull]
  rebase = true
[init]
  defaultBranch = main

# .gitconfig.worklaptop
[user]
  name = John Doe
  email = [email protected]
[url "https://"]
  insteadOf = git://
[http]
  proxy = http://proxy.company.com:8080

# .gitconfig.homezone
[user]
  name = JohnD
  email = [email protected]
[github]
  user = johnd-personal

Recipe 5: Multi-File Dotfiles

Complete dotfiles setup across machines:

~/.config/
├── nvim/
│   ├── init.base.vim        # Shared vim config
│   ├── init.worklaptop.vim  # Work-specific plugins
│   ├── init.homezone.vim    # Personal plugins
│   └── init.vim             # ← Merged
├── alacritty/
│   ├── alacritty.base.yml   # Shared terminal config
│   ├── alacritty.macos.yml  # macOS font paths
│   ├── alacritty.windows.yml # Windows font paths
│   └── alacritty.yml        # ← Merged
├── opencode/
│   ├── config.base.json
│   ├── config.worklaptop.json
│   └── config.json          # ← Merged
└── .env.base
    .env.worklaptop
    .env                     # ← Merged

# After `permachine init`, sync your dotfiles repo across machines!
# Each machine automatically gets the right config.

Recipe 6: Advanced Filters - Cross-Platform Development

NEW: Use the advanced filter syntax for precise control:

# Project structure
project/
├── config.base.json                    # Shared config
├── config.{os=windows}.json            # Windows-specific paths
├── config.{os=macos}.json              # macOS-specific paths
├── config.{os=linux}.json              # Linux-specific paths
├── secrets.{machine=work}{user=alice}.json   # Alice's work secrets
├── secrets.{machine=home}{user=alice}.json   # Alice's home secrets
├── build.{os=windows}{arch=x64}.json   # Windows x64 build config
├── build.{os=windows}{arch=arm64}.json # Windows ARM build config
└── config.json                         # ← Merged (gitignored)

Example use cases:

# Multiple platforms with OR logic
package.{os=windows,macos}.json   # Matches Windows OR macOS

# Specific environment AND machine
secrets.{env=prod}{machine=server-us-east}.json

# User-specific on specific machine
.vscode/settings.{machine=laptop}{user=josxa}.json

# Multiple users on shared machine
preferences.{user=alice}.json
preferences.{user=bob}.json

See File Filters Documentation for complete guide and examples.

How It Works

permachine uses a simple process:

  1. Machine Detection - Automatically detects your machine name from hostname (Windows: COMPUTERNAME, Linux/Mac: hostname()) and other system properties (OS, architecture, username, environment)

  2. File Discovery - Scans your repository for files matching patterns:

    • New: {key=value} syntax (e.g., config.{os=windows}.json, secrets.{machine=laptop}{user=josxa}.json)
    • Legacy: *.{machine}.* syntax (e.g., config.laptop.json, .env.desktop)
  3. Filter Matching - For new syntax, evaluates filters against current system context:

    • AND logic: ALL filters must match (e.g., {os=windows}{arch=x64})
    • OR logic: ANY value in list matches (e.g., {os=windows,macos})
  4. Smart Merging - Merges base and machine-specific configs:

    • JSON: Deep recursive merge (machine values override base)
    • ENV: Key-value merge with comment preservation
  5. Gitignore Management - Automatically adds output files to .gitignore and removes already-tracked files from git

  6. Git Hooks - Installs hooks to auto-merge on checkout, merge, and commit operations

For detailed implementation information, see CONTRIBUTING.md and File Filters Documentation.

Supported File Types

| Type | Extensions | Merge Strategy | Status | | -------- | --------------------- | --------------------------------- | ---------------------------------------------- | | JSON | .json | Deep recursive merge | ✅ Supported | | JSONC | .json with comments | Deep merge + comment preservation | ✅ Supported | | ENV | .env, .env.* | Key-value upsert | ✅ Supported | | Markdown | .md | Append (base + machine) | 🔜 Planned | | YAML | .yaml, .yml | Deep recursive merge | 🔜 Planned | | TOML | .toml | Deep recursive merge | 🔜 Planned | | Patch | .patch | Apply git-style patch to base | 💡 Proposed |

Troubleshooting

Hooks not running

Check hook installation:

permachine info

Verify git config:

git config --get core.hooksPath
# Should output: .permachine/hooks

Check hook files exist:

ls .permachine/hooks/

Merge not happening

Run manually to see errors:

permachine merge

Check machine name matches your files:

permachine info
# Verify "Machine name" matches your file pattern

Wrong machine name detected

Machine names are auto-detected from your system hostname. To verify:

# Windows
echo %COMPUTERNAME%

# Linux/Mac
hostname

Files must match this name (case-insensitive).

Conflicts with other git hook tools

If you use Husky or other hook managers, use legacy mode:

permachine uninstall
permachine init --legacy

This wraps existing hooks instead of replacing them.

Output file not being gitignored

By default, permachine init and permachine merge automatically add output files to .gitignore. If this isn't working:

  1. Check if .gitignore exists and contains your output files
  2. Verify the file was removed from git tracking: git ls-files config.json (should return nothing)
  3. If you used --no-gitignore, re-run without that flag

To manually fix:

echo "config.json" >> .gitignore
git rm --cached config.json

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for:

  • Development setup
  • Architecture overview
  • Testing guidelines
  • Code standards
  • How to submit PRs

License

MIT © JosXa

Roadmap

  • [x] JSON support
  • [x] ENV support
  • [x] JSONC support (comments & trailing commas)
  • [x] Git hooks (hooksPath & legacy)
  • [x] Automatic .gitignore management
  • [x] CLI interface
  • [x] Comprehensive tests (81 tests)
  • [x] npm package publication
  • [x] Watch mode for development
  • [ ] YAML support (#1)
  • [ ] TOML support (#2)
  • [ ] Markdown support (#3)
  • [ ] Patch file support (#4)
  • [ ] Custom merge strategies (#5)
  • [ ] Config file for patterns (#6)
  • [ ] Dry-run mode (#7)

Credits

Inspired by:

  • Husky - Git hooks made easy
  • The need for machine-specific configurations across development environments