A Python-based, idempotent symlink deployment engine designed to manage environment configurations, batch scripts, and dotfiles. It operates by reading a centralized JSON configuration and mapping atomic files from a host repository to target directories.
This tool is designed to be used as a Git Submodule inside a parent repository. The manager is completely blind to your personal files; it locates configurations and payload files relative to the parent repository root - the current directory by default, or an explicit --repo-root.
Each profile's source files can be siloed under a domain directory via its source_root (recommended once you mix domains). A typical layout:
parent-repo/
├── config.json # Your profiles (private to the parent repo)
├── skyrim/ # A skyrim_batch domain (source_root: "skyrim")
│ ├── manifest.json # Routing logic for variant files
│ ├── manifest.md # Auto-generated documentation
│ ├── core/ # Universal files deployed to all variants
│ ├── builds/ # Character-build batch scripts (Skyrim-only)
│ └── variants/ # Variant-specific files
├── dotfiles/ # A dotfiles domain (referenced by links paths)
└── symlink-manager/ # THIS SUBMODULE (installable package)
└── src/symlink_manager/
source_root defaults to ".", so if you don't silo, core//builds//variants//manifest.json are expected at the repository root.
To use this manager, create a config.json in the root of your parent repository. Each skyrim_batch profile also needs a manifest.json under its source_root (the repository root when source_root is ".").
Each entry is a profile keyed by name. Its type selects how files are formatted and how sources map to link targets. type defaults to skyrim_batch when omitted, so existing configs keep working.
skyrim_batch - formats batch files, then broadcasts every gathered source flat into a single target_dir. The optional source_root (default ".") names the subdirectory its core//builds//variants//manifest.json live under:
{
"my_environment": {
"type": "skyrim_batch",
"source_root": "skyrim",
"target_dir": "C:/Path/To/Target/Directory",
"include_core": true,
"include_builds": true,
"variant_folder": "variants/my_environment"
}
}dotfiles - no formatting; each repo-relative source links to its own explicit target (~ and environment variables are expanded). A link value may be a bare target string, or an object/list conditioned on platforms (windows/macos/linux) and/or hosts, so one config serves multiple machines:
{
"home": {
"type": "dotfiles",
"links": {
"dotfiles/git/gitconfig": "~/.gitconfig",
"dotfiles/profile/bashrc": { "target": "~/.bashrc", "platforms": ["linux", "macos"] },
"dotfiles/nvim/init.lua": [
{ "target": "~/AppData/Local/nvim/init.lua", "platforms": "windows" },
{ "target": "~/.config/nvim/init.lua", "platforms": ["linux", "macos"] }
]
}
}
}Any profile may set "relative": true (or pass --relative) to store links as paths relative to each target's directory instead of absolute, which stays valid if the whole tree is relocated.
Preview a host's selection with symlink-deploy <profile> --dry-run [--platform <p>] [--host <h>].
Acts as the routing table for files in your variants/ directory.
{
"specific_script.txt": [
"my_environment",
"another_environment"
],
"experimental_feature.txt": [
"my_environment"
]
}Running symlink-audit --source-root <dir> will automatically audit your physical files against your manifest.json and generate a manifest.md alongside it (under the profile's source_root; omit the flag when files live at the repository root).
Here is an example of the generated output:
# Repository Manifest
> *Auto-generated by `symlink-audit`. Do not edit this file directly.*
> *To update the variants list, edit `manifest.json`. Core scripts are auto-discovered.*
## Universal Core Scripts
These scripts are deployed to all configurations automatically.
* `common_aliases.txt`
* `global_settings.txt`
## Variant-Specific Scripts
These scripts are routed only to the specified modlists.
| Script Name | Active Variants |
| :--- | :--- |
| `experimental_feature.txt` | `my_environment` |
| `specific_script.txt` | `another_environment`, `my_environment` |Install the engine once as a global uv tool, from the parent repository root:
uv tool install --editable symlink-managerThe symlink-* commands land on PATH in every shell with no activation step, and --editable means engine source changes are live. (pip install -e symlink-manager into a Python of your choosing still works if you'd rather not use uv.)
Then run these commands from the root of your parent repository (or pass --repo-root <path>).
Universal (any profile type):
- Deploy Links:
symlink-deploy <profile> - Remove Links (Teardown):
symlink-deploy <profile> --remove - Adopt Existing Files:
symlink-adopt <profile>(move machine files into the repo, then link back) - Inspect Link State:
symlink-status <profile>(read-only; honors--platform/--host)
Formatting profiles only (Skyrim today):
- Format Only (no deploy):
symlink-format <profile> - Audit & Generate Docs:
symlink-audit(add--source-root <dir>for the siloed domain) - Full Pipeline (Format -> Audit -> Deploy):
symlink-build <profile>(equivalent todeployfor non-formatting profiles)
Each is also a subcommand of python -m symlink_manager (e.g. python -m symlink_manager deploy <profile>) if you would rather not rely on the console scripts being on PATH.
See USAGE.md for the full command reference - every flag, examples, and common workflows.
Note: The engine aggressively protects real files. It will not overwrite existing non-symlink files in the target directory. Use symlink-deploy <profile> --backup to rename a blocking real file aside (<name>.<timestamp>.bak) and link anyway - handy when onboarding a machine that already has the files.
Windows note: creating symlinks needs the privilege a normal account lacks by default. Enable Developer Mode (Settings → System → For developers) or run from an Administrator shell; otherwise the engine reports a clear error (WinError 1314) rather than failing cryptically. macOS and Linux need nothing extra.
Requirements: Python 3.10+ (the deploy engine uses structural pattern matching). The engine has no third-party runtime dependencies - it runs on the standard library alone, so deployment needs nothing beyond a Python install.
The engine ships with a pytest suite covering the formatters, link resolvers, the symlink engine, status/adopt, and config/audit logic. pytest is the only external dependency, and it is needed for tests only:
uv run --extra test pytest # from symlink-manager/ -- uv creates/syncs the venv on the flySymlink-creation tests self-skip when the host lacks the privilege (Windows without Developer Mode / admin); every other test runs unconditionally. To run the full suite locally on Windows, enable Developer Mode (Settings → System → For developers) or run from an elevated shell; CI runs them on both Linux and Windows.
Linting and type-checking use ruff and mypy, wired into CI:
uv run --extra lint ruff check src tests
uv run --extra lint mypy srcMIT - see LICENSE.