Skip to content

zspatter/symlink-manager

Repository files navigation

Symlink Manager

CI

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.

Architecture & Integration

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.

Expected Parent Directory Structure

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.

Setup & Configuration

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 ".").

config.json

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>].

manifest.json

Acts as the routing table for files in your variants/ directory.

{
    "specific_script.txt": [
        "my_environment",
        "another_environment"
    ],
    "experimental_feature.txt": [
        "my_environment"
    ]
}

Generated Documentation

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` |

Installation & Execution

Install the engine once as a global uv tool, from the parent repository root:

uv tool install --editable symlink-manager

The 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 to deploy for 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.

Development

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 fly

Symlink-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 src

License

MIT - see LICENSE.

About

Idempotent symlink deployment engine designed to manage environment configurations, batch scripts, and dotfiles

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages