- C# 100%
|
All checks were successful
Continous Integration / build-and-test (push) Successful in 26s
Reviewed-on: #1 |
||
|---|---|---|
| .forgejo/workflows | ||
| src | ||
| tests | ||
| .gitignore | ||
| CLAUDE.md | ||
| Directory.Build.props | ||
| Directory.Packages.props | ||
| global.json | ||
| lncfg.slnx | ||
| README.md | ||
lncfg
lncfg ("link config") manages your UNIX config files through a git repository. It moves a config file or directory into a tracked repository and leaves a symlink in its place, so your dotfiles live in version control while remaining where the apps that use them expect them.
~/.gitconfig ──symlink──▶ ~/dotfiles/.gitconfig (tracked in git)
Installation
The install script downloads the latest release for your platform, drops the binary in a
user-level location (no sudo/admin required), and—on Linux and macOS—marks it executable.
Linux / macOS — installs to ~/.local/bin/lncfg:
curl -fsSL https://git.mmtsc.de/marvinmees/lncfg/raw/branch/main/install.sh | bash
Windows (PowerShell) — installs to %LOCALAPPDATA%\Programs\lncfg\lncfg.exe:
iwr https://git.mmtsc.de/marvinmees/lncfg/raw/branch/main/install.bat -OutFile install.bat; .\install.bat
If the install directory isn't on your PATH, the script prints the line to add it. A few
environment variables tweak the behaviour: LNCFG_INSTALL_DIR (install location), LNCFG_VERSION
(a specific tag instead of the latest release) and LNCFG_TOKEN (API token for a private mirror).
You can also download a binary directly from the
releases page.
Commands
| Command | What it does |
|---|---|
lncfg init -c -d <dir> -r <remote> |
Create a new repository in <dir>, configure the git identity, make the first commit and push to <remote> (the remote is optional). |
lncfg init -d <dir> -r <remote> |
Clone <remote> into <dir> and apply its links, resolving any conflicts with files already on the machine. |
lncfg add <path> |
Move an existing file or directory into the repository, symlink it back, and commit + push. |
lncfg sync |
Pull the latest configs for the managed repository and re-apply the links. |
Conflict handling (init clone / sync)
When a config already exists on the machine, lncfg shows a diff and asks what to do:
- Replace – use the repository version (delete the local file, create the symlink).
- Keep – leave the local file untouched and skip linking it.
- Backup – move the local file aside to
*.bak, then create the symlink.
For non-interactive use, pass --force, --keep or --backup to apply one choice to every
conflict. A real directory is never silently replaced; it is always treated as a conflict.
Git identity (init -c)
If a gcm ("Git Configuration Manager") executable is on PATH, lncfg runs gcm auto. If that is
unavailable or fails, it prompts for a name and email and configures the repository directly.
How it works
Each managed item is recorded in a committed manifest, lncfg.json, at the repository root:
{
"version": 1,
"entries": [
{ "source": ".gitconfig", "target": "~/.gitconfig", "kind": "File" },
{ "source": ".config/nvim", "target": "~/.config/nvim", "kind": "Directory" }
]
}
sourceis the item's location inside the repository.targetis its location on the machine, stored relative to$HOMEas~/…so the manifest is portable across machines and users. Paths outside$HOMEare stored absolute.
lncfg's own per-machine state (which repository this machine uses) is kept separately and not
committed, at $XDG_CONFIG_HOME/lncfg/config.json (default ~/.config/lncfg/config.json).
Architecture
Clean Architecture, with dependencies pointing inward:
Lncfg.Cli ─▶ Lncfg.Application ─▶ Lncfg.Domain ◀─ Lncfg.Infrastructure
(Spectre.Console.Cli, (use cases) (entities, ports) (filesystem, git,
composition root) gcm, JSON, Spectre UI)
- Domain – entities (
LinkEntry,Manifest), pure services (PathMapper,LinkStatusEvaluator) and ports (IFileSystem,IGitClient,IGcmClient,IUserInteraction, …). No external dependencies. - Application – the use cases (
InitCreate,InitClone,Add,Sync) and the sharedLinkApplier. Depends only on the domain.addperforms its filesystem mutation with strict rollback, so a failedaddnever leaves your config missing. - Infrastructure – adapters over the real filesystem (symlink-aware), the
gitandgcmCLIs, JSON persistence, and the Spectre.Console UI. - Cli – Spectre.Console.Cli commands and the dependency-injection composition root; builds the
lncfgexecutable.
Building and testing
dotnet build # build everything (warnings are errors)
dotnet test # run all four test projects
dotnet run --project src/Lncfg.Cli -- --help
Test strategy
Because lncfg manipulates real config files, it is tested intensively:
- Domain tests – pure unit tests for path mapping, the manifest, and conflict classification.
- Infrastructure tests – the symlink semantics of the real filesystem (detection, deleting a symlink without touching its target, moves), the git client against a temporary bare remote, and JSON round-trips.
- Application tests – every use case against the real filesystem and git in a hermetic sandbox,
the full conflict-resolution matrix, and the
addrollback paths (fault injection). - Integration tests – the compiled
lncfgexecutable, end to end, with an isolated$HOMEand a local bare remote (create → add → push, and clone-onto-another-machine with a backed-up conflict).
Tech stack
.NET 10 · Spectre.Console · Clean Architecture · xUnit v3 (Microsoft Testing Platform)