Overview & Introduction
linkd is a local-dev link daemon engineered to solve the chronic fragility of package linking in modern monorepos and multi-repository workspaces.
Traditional workflows like npm link, yarn link, composer link, pip install -e, or "file:../lib" dependencies suffer from a fundamental problem: package managers assume they own the target directory. As soon as you run npm install, composer update, or uv sync, your local symlinks are obliterated and replaced with registry packages.
linkd introduces Kubernetes-style continuous reconciliation to local paths. A lightweight background daemon maintains a registry of desired link states, watches lockfiles for reinstall events, and instantly restores your dev packages within milliseconds without touching manifest files.
"my-pkg": "file:../packages/my-pkg" frequently leak into package.json, composer.json, or pyproject.toml and get accidentally committed to Git. With linkd, your manifests stay 100% untouched and clean.
📦 1-Line Installation
linkd ships as a standalone, zero-dependency compiled binary for Linux, macOS, and Windows.
Linux & macOS (curl / bash)
curl -fsSL https://raw.githubusercontent.com/pinoox/linkd/master/install.sh | bash
Windows (PowerShell)
irm https://raw.githubusercontent.com/pinoox/linkd/master/install.ps1 | iex
Via Cargo (from source)
cargo install --path crates/linkd-cli --locked
⚡ Quickstart Tutorial
linkd offers two powerful workflows designed for modern monorepos and local package development:
Workflow 1: Global Package Workflow (2-Step, Recommended)
Register a package once globally, then link it into any project with a single command:
$ cd packages/my-ui-kit
$ linkd register
✓ Registered reusable package: @acme/ui-kit
Usage: run `linkd use @acme/ui-kit` in any consumer project directory.
$ cd apps/web-app
$ linkd use @acme/ui-kit
✓ Linked @acme/ui-kit → ./apps/web-app
sync target: ./apps/web-app/node_modules/@acme/ui-kit
$ cd ../mobile-app && linkd use @acme/ui-kit
✓ Linked @acme/ui-kit → ./apps/mobile-app
Workflow 2: Direct Path Linking
Point directly to your library source and consumer app:
$ linkd link ./packages/my-ui-kit ./apps/web-app
✓ Linked @acme/ui-kit → ./apps/web-app
🔄 Multi-Consumer Real-Time Syncing
When one package is linked into multiple apps (e.g. Web, Mobile, Desktop), changing a single file in the package automatically synchronizes all consumer apps in real time in parallel!
🔄 Architecture: The Reconciliation Loop
linkd operates similarly to a Kubernetes controller:
- Desired State: Stored globally in
~/.linkd/registry.json. - Actual State: Monitored on the filesystem via notify file watchers and
.linkd-marker.jsonfiles. - Reconciliation Loop: When source files change, changes are debounced and synced. When lockfiles change (indicating an install occurred), the target directory is atomically restored.
┌────────────────────────────────────────────────────────────────────────┐
│ ~/.linkd/registry.json │
│ (Global State Store) │
└───────────────────────────────────┬────────────────────────────────────┘
│
┌──────────▼──────────┐
│ linkd daemon │
│ (Background Engine) │
└──────────┬──────────┘
│
┌────────────────────────────┼────────────────────────────┐
│ (File Watchers) │ (Reconciliation Engine) │ (IPC Server)
▼ ▼ ▼
Source Changes Reinstall Event Live Dashboard
- Debounced sync - Lockfile markers - Subscriptions
- Cache filter - Atomic directory swap - Pause/Resume
│ │
└────────────────────────────┴────────────────────────────┐
▼
Target Project (.linkd-marker)
- node_modules/<pkg>
- vendor/<pkg>
- .venv/Lib/site-packages/<pkg>
🛡️ pnpm Global Store Safety Gate
In pnpm, packages in node_modules are hardlinks to a global content-addressable store (~/.pnpm-store). Writing directly into node_modules would contaminate the global cache for all projects on your computer.
linkd automatically detects pnpm, isolates the link by copying into a project-local shadow directory:
./apps/web/node_modules/.linkd-shadow/<package>
and repoints the package symlink safely to that shadow copy.
⚡ Atomic Swaps & Marker Provenance
To prevent build tools (webpack, vite, tsc, cargo) from crashing during writes, linkd prepares files in a temporary sibling directory and atomically swaps it using filesystem renames.
🔁 Anti-Watch-Loop Guards
When custom paths or monorepos are linked, source and target folders could inadvertently nest. linkd strictly validates that neither path is a prefix or ancestor of the other, preventing infinite watcher loops.
🟢 JavaScript & TypeScript (npm, pnpm, yarn, bun)
Automatically resolves package.json names, scoped packages (e.g. @org/pkg), and targets node_modules/<name>.
linkd link ./packages/ui-kit ./apps/nextjs-app
- Watched Markers:
package-lock.json,pnpm-lock.yaml,yarn.lock,bun.lockb - Filtered files:
node_modules,.git,dist/.cache
💙 Flutter & Dart (pubspec.yaml)
Detects pubspec.yaml, extracts the package name, and synchronizes into consumer/.dart_tool/packages/<package_name> with instant hot reload compatibility.
linkd link ./packages/flutter_ui_kit ./apps/flutter_mobile_app
- Automatic Filters:
.dart_tool,.pub-cache,build/,android/,ios/,.git - Watched Markers:
pubspec.lock,.dart_tool/package_config.json,.packages
🔷 .NET (C# / F# / NuGet)
Parses *.csproj and Directory.Build.props for <PackageId> and syncs compiled libraries into consumer/packages/<package_id> without requiring temporary <ProjectReference> edits.
linkd link ./src/Acme.Logging ./src/Acme.ApiServer
- Automatic Filters:
obj/,.vs/,TestResults/,.git - Watched Markers:
obj/project.assets.json,packages.lock.json
💎 Ruby (Gems & Bundler)
Parses *.gemspec definitions and syncs gem files into consumer/vendor/bundle/gems/<gem_name>, making local gem development painless in Rails applications.
linkd link ./gems/acme_auth ./apps/rails_app
- Automatic Filters:
.bundle/,vendor/,tmp/,.git - Watched Markers:
Gemfile.lock
🐦 Swift (Swift Package Manager)
Detects Package.swift manifests and syncs into consumer SPM checkouts at consumer/.build/checkouts/<package_name> for both command-line builds and Xcode.
linkd link ./packages/SwiftUiKit ./apps/SwiftApp
- Automatic Filters:
.build/,.swiftpm/,DerivedData/,.git - Watched Markers:
Package.resolved
💧 Elixir (Mix & Phoenix)
Parses app: :name from mix.exs and syncs directly into consumer/deps/<app_name> for Phoenix and OTP projects without modifying mix.exs.
linkd link ./plugins/elixir_auth ./apps/phoenix_web
- Automatic Filters:
_build/,deps/,.elixir_ls/,.git - Watched Markers:
mix.lock
🐘 PHP (Composer)
Detects composer.json, extracts the vendor/package namespace, and syncs into consumer/vendor/<vendor>/<package>.
linkd link ./packages/acme-logger ./apps/laravel-api
linkd alerts you if composer dump-autoload is needed to regenerate classmaps.
🐍 Python (uv / pip / poetry / flit)
Detects PEP 621 pyproject.toml, setup.py, or setup.cfg. Automatically locates virtual environments (.venv/Lib/site-packages on Windows, .venv/lib/python*/site-packages on Unix).
linkd link ./packages/ml-core ./apps/fastapi-service
- Automatic Filters:
__pycache__,*.pyc,.pytest_cache,.mypy_cache,*.egg-info - Watched Markers:
uv.lock,poetry.lock,Pipfile.lock,requirements.txt,pyvenv.cfg
🐹 Go (Go Modules & Vendor)
Extracts the module path from go.mod and syncs directly into consumer/vendor/<module_path>.
linkd link ./packages/auth-lib ./apps/go-microservice
- Watched Markers:
go.sum,go.work.sum,go.work,vendor/modules.txt - Usage: Run with standard
go build -mod=vendoror Go workspace mode.
🦀 Rust (Cargo Crates)
Parses [package.name] from Cargo.toml and vendors into consumer/vendor/<crate_name>.
linkd link ./crates/common-types ./apps/rust-server
- Filtered files:
target/,.git,.cargo - Watched Markers:
Cargo.lock,.cargo/config.toml
☕ Java & Kotlin (JVM - Maven / Gradle)
Parses Group and Artifact IDs from pom.xml, build.gradle, or build.gradle.kts and routes to consumer/libs/<artifact>.
linkd link ./packages/java-sdk ./apps/spring-app
- Filtered files:
build/,target/,.gradle/,*.class - Watched Markers:
pom.xml,build.gradle,gradle.lockfile
📁 Custom Paths (Any Framework / Asset Folder)
Use the --target flag to link any directory to any destination without package manager assumptions:
linkd link ./shared-assets ./apps/electron-app --target ./apps/electron-app/src/assets/shared
🖥️ Live TUI Monitor Dashboard
Launch the dashboard with linkd monitor (or aliases linkd top / linkd dashboard):
The monitor displays links in a hierarchical tree view: each package appears as a header with all its consumer projects listed underneath as collapsible branches.
▼ 📦 @acme/ui-kit [npm] (3 consumers)
│ Source: ~/projects/monorepo/packages/ui-kit
├── 📂 apps/web-app → node_modules/@acme/ui-kit [✓ synced]
├── 📂 apps/mobile-app → node_modules/@acme/ui-kit [✓ synced]
└── 📂 apps/admin → node_modules/@acme/ui-kit [⏸ paused]
▼ 📦 my_flutter_widget [dart/flutter] (1 consumer)
└── 📂 apps/flutter_app → .dart_tool/packages/my_flutter_widget [✓ synced]
🧙 Interactive Setup Wizard (linkd init)
If you don't want to memorize command-line flags and path arguments, linkd init provides an interactive terminal guide that asks questions step-by-step to link any package:
$ linkd init
? Link type:
> npm package (JavaScript / TypeScript)
flutter / dart package (pubspec.yaml)
.NET package (C# / F# / NuGet)
ruby gem (Bundler)
swift package (SPM)
elixir package (Mix)
composer package (PHP)
python (uv/pip/poetry)
go module
rust (cargo crate)
java/kotlin (jvm)
custom path
? Source package directory: ./packages/my-ui-kit
? Consumer project directory: ./apps/web-app
✓ Suggested sync strategy: reflink / copy
? Start background daemon after linking? Yes
? Create link now? Yes
✓ Link '@acme/my-ui-kit' successfully registered and synced!
Step-by-Step Flow:
- Step 1 (Ecosystem): Use ↑/↓ arrows and Enter to pick from all 11 supported ecosystems.
- Step 2 (Source Path): Enter the relative or absolute path to your package directory.
- Step 3 (Consumer Path): Enter the path to your consuming project (or press Enter for current directory
.). - Step 4 (Custom Target): (Custom path mode only) Specify your custom destination folder inside the consumer.
- Step 5 (Autostart & Execution): Confirms daemon autostart, performs path loop-guard validation, and executes the initial incremental sync.
⚙️ Daemon Lifecycle & Service Setup
Manage the background daemon directly via CLI:
linkd start # Start detached background daemon
linkd stop # Graceful shutdown via IPC
linkd stop --force # Terminate PID immediately
linkd watch # Run in foreground with live terminal logs
linkd status # Instant snapshot of PID & link count
linkd logs -f # Tail structured daemon logs
🩺 Running Diagnostics (linkd doctor)
Use linkd doctor to inspect environment health, socket permissions, inotify limits, and package manager availability:
linkd doctor
linkd doctor --explain pnpm-store
linkd doctor --explain autostart
linkd doctor --explain python
🐧 Systemd & 🍎 macOS Launchd Auto-Start
To run linkd automatically on system login:
macOS Launchd plist (~/Library/LaunchAgents/dev.linkd.daemon.plist):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>dev.linkd.daemon</string>
<key>ProgramArguments</key>
<array><string>/usr/local/bin/linkd</string><string>--daemon-internal</string></array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
</dict>
</plist>
Linux Systemd User Unit (~/.config/systemd/user/linkd.service):
[Unit]
Description=linkd local-dev link daemon
After=default.target
[Service]
ExecStart=/usr/local/bin/linkd --daemon-internal
Restart=always
[Install]
WantedBy=default.target
📖 CLI Command Reference
Search and explore all commands, options, and operational subcommands in linkd v0.2.2:
| Command | Flags & Arguments | Description |
|---|---|---|
linkd register |
[path] [--name <name>] [--ecosystem <type>] (aliases: pin, add) |
Register current package globally for reuse across multiple projects. |
linkd use |
<package> [consumer] [--target <path>] (aliases: on, attach) |
Link a globally registered package into the current/specified project. |
linkd packages |
none (alias: pinned) |
List all globally registered reusable packages. |
linkd unregister |
<package> (alias: unpin) |
Remove package from global registry. |
linkd link |
<source> [consumer] [--target <path>] [--ecosystem <type>] [--copy|--hardlink|--link] [--no-daemon] |
Directly register and sync local package into consumer. |
linkd unlink |
<target|name|source> |
Remove an active link and clean marker files. |
linkd list |
[--json] [--details] (aliases: ls, status) |
List all active links with sync status and health indicators. |
linkd pause |
<target|name|all> |
Temporarily pause reconciliation for target link(s). |
linkd resume |
<target|name|all> |
Resume active reconciliation and run immediate sync. |
linkd sync |
[target|name|all] [--force] (alias: reconcile) |
Manually trigger an immediate reconciliation run. |
linkd monitor |
[--interval <ms>] (aliases: top, dashboard) |
Launch interactive full-screen TUI terminal monitor. |
linkd init |
none (alias: wizard) |
Interactive setup wizard for guided linking. |
linkd start |
[--port <p>] [--foreground] |
Start the background daemon process. |
linkd stop |
[--force] |
Stop running linkd daemon. |
linkd doctor |
[--explain <topic>] |
Run environment & permission diagnostic checks. |
linkd logs |
[-f|--follow] |
Tail background daemon logs. |
linkd version |
[-v, --json] |
Print version, target OS, architecture, and supported ecosystems. |
linkd completions |
<shell> |
Generate shell completions (bash, zsh, fish, pwsh). |
❓ FAQ & Troubleshooting
Does linkd modify my package.json or composer.json?
No. linkd never modifies your source manifests. This ensures your Git working tree remains completely clean and no local file paths are accidentally committed to version control.
How does linkd detect reinstalls?
Rather than monitoring process trees which is fragile across operating systems, linkd sets up inotify/FSEvents/ReadDirectoryChanges watchers on package manager completion lockfiles (e.g. package-lock.json, pnpm-lock.yaml, composer.lock, uv.lock, go.sum, Cargo.lock). When an install completes, linkd immediately re-evaluates the target directory.
Can I sync between arbitrary folders?
Yes. Use linkd link <source> <consumer> --target <path> to sync any folder into any destination with automatic loop-guarding.
Ready to streamline your local development?
Install linkd v0.2.2 today and enjoy rock-solid local package synchronization.
Get Started with linkd