A faster, more reliable alternative to yalc for local package development.
Note: This project was built with AI assistance (Claude by Anthropic).
- Speed - Go for fast startup, hard links for instant syncing
- Visibility - bbolt-backed state, always know what's linked where
- Reliability - Hard links avoid symlink resolution issues
- Simplicity - Minimal commands, sensible defaults
- Universal - Works with npm, yarn, pnpm, and bun
┌─────────────────────────────────────────────────────────────────────┐
│ lnpm CLI (Go) │
│ Commands: publish | add | remove | push | status │
└──────────────────────────────────┬──────────────────────────────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌───────────────┐ ┌─────────────────┐
│ Package Store │ │ Link Manager │
│ ~/.lnpm/store │ │ (hard links) │
└───────┬───────┘ └─────────────────┘
│
▼
┌───────────────┐
│ bbolt DB │
│ ~/.lnpm/lnpm.db│
└───────────────┘
~/.lnpm/
├── lnpm.db # bbolt database (key-value store)
├── config.yaml # Global config (optional)
├── store/ # Package store
│ └── {package-name}/
│ └── {content-hash}/ # Versioned by content hash
│ ├── package.json
│ ├── dist/
│ └── ...
project/
├── .lnpm/ # Local package cache (hard linked)
│ └── {package-name}/
│ └── ...
├── lnpm.lock # Lock file (YAML)
├── node_modules/
│ └── {package-name} -> ../.lnpm/{package-name} # Symlink to .lnpm
└── package.json # Modified with file: protocol
lnpm uses a sophisticated multi-tier approach for maximum performance:
| Method | Speed | Platform Support | Use Case | Disk Usage |
|---|---|---|---|---|
| Reflink (CoW) | Instant (~1ms) | macOS APFS, Linux Btrfs/XFS/OCFS2 | Primary method | Zero (copy-on-write) |
| Hard Link | Instant (~1ms) | Same filesystem (all platforms) | Fallback #1 | Zero (shared inodes) |
| Parallel Copy | Fast (~100ms for 10k files) | All platforms, cross-filesystem | Fallback #2 | Full copy |
New in v1.2.0 — Revolutionary performance for modern filesystems.
- Creates an instant "clone" of a file without copying data
- Uses copy-on-write (CoW): data is only copied when modified
- Provides copy semantics with link performance
- Safe: modifications don't affect the original
- macOS APFS: Uses
clonefile()syscall (SYS_CLONEFILE #462) - Linux Btrfs/XFS: Uses
FICLONEioctl (#0x40049409) - Other filesystems: Gracefully falls back to hard links
Hard Link: source.js ════► [inode 12345] ◄════ dest.js
(both point to same inode - modifications affect both)
Reflink: source.js ──► [inode 12345: blocks 1-100]
dest.js ──► [inode 67890: blocks 1-100] (CoW clone)
(separate inodes, shared blocks until modified)
- Detect if source and store are on same filesystem
- Try reflink first (works even across directories on same FS)
- If reflink unsupported, try hard link (requires same filesystem)
- If hard link fails, use parallel copy with 8 worker goroutines
- Display progress and warnings for user visibility
- Check user config for
link_modepreference - Try reflink from store to project (instant, safe)
- If reflink unsupported, try hard link (instant, requires same FS)
- If hard link fails, fall back to parallel copy
- Warn user about fallback with optimization tips
Source Package Store Project
────────────── ───── ───────
src/index.ts ──► (reflink/hardlink)
dist/index.js ──► ~/.lnpm/store/ ══► .lnpm/pkg/ ──► node_modules/pkg
package.json ──► pkg/abc123/ (reflink/hardlink) (symlink)
(CoW or shared) (CoW or shared)
10,000 file package:
- Reflink (APFS/Btrfs): ~5ms ⚡
- Hard link (same FS): ~10ms ⚡
- Sequential copy: ~5000ms 🐌
- Parallel copy (8 workers): ~800ms 🚀
Result: Up to 1000x faster for large packages!
lnpm intelligently handles edge cases:
- Same filesystem: Try reflink → hard link → parallel copy
- Different filesystem: Try reflink → parallel copy (skip hard link)
- User config
link_mode: copy: Skip linking, use parallel copy - User config
link_mode: hardlink: Try reflink → hard link → parallel copy
User Feedback:
# Same filesystem, linking works
✓ Linked 10,000 files instantly
# Linking not supported
⚠ Linking not supported, copying files instead
Copying... 10000/10000 files
# Cross-filesystem scenario
ℹ Store and source on different filesystems - files were copied
💡 Tip: Move your store to the same filesystem for instant linkinglnpm uses bbolt, an embedded key-value database (same as used by etcd).
packages - Package data (key: ID, value: JSON)
packages_by_name - Index (key: name, value: ID)
projects - Project data (key: ID, value: JSON)
projects_by_path - Index (key: path, value: ID)
links - Link data (key: ID, value: JSON)
links_by_package - Index (key: package_id, value: [link_ids])
links_by_project - Index (key: project_id, value: [link_ids])
files - File manifest (key: package_id, value: [FileEntry])
meta - Metadata (next_id counter)
// Package
{
"id": 1,
"name": "my-package",
"version": "1.0.0",
"content_hash": "abc123...",
"source_path": "/path/to/source",
"store_path": "/home/user/.lnpm/store/my-package/abc123",
"files_count": 42,
"total_size": 123456,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
// Project
{
"id": 1,
"path": "/path/to/project",
"name": "my-app",
"package_manager": "pnpm",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
// Link
{
"id": 1,
"package_id": 1,
"project_id": 1,
"link_type": "hardlink",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}Publish a package to the local store.
# Basic usage (in package directory)
lnpm publish
# Publish and push to all linked projects
lnpm publish --push
# Publish with custom tag
lnpm publish --tag betaProcess:
- Read
package.jsonfor name/version - Determine files to include (respects
.npmignore,filesfield) - Calculate content hash of all files
- Copy files to
~/.lnpm/store/{name}/{hash}/ - Record in SQLite database
- If
--push, update all linked projects
Add a package from the store to current project.
# Add latest published version
lnpm add my-package
# Add specific version/tag
lnpm add my-package@1.0.0
lnpm add my-package@beta
# Add without modifying package.json
lnpm add my-package --pure
# Add with dev dependency
lnpm add my-package --devProcess:
- Find package in store (latest or specified version)
- Create
.lnpm/{package}/directory - Hard link all files from store
- Create symlink
node_modules/{package}→.lnpm/{package} - Update
package.jsonwithfile:.lnpm/{package} - Update
lnpm.lock - Register link in SQLite
Remove a linked package.
lnpm remove my-package
# Remove all linked packages
lnpm remove --allProcess:
- Remove
.lnpm/{package}/directory - Remove
node_modules/{package}symlink - Restore original
package.jsondependency (if any) - Update
lnpm.lock - Remove link from SQLite
Push updates to all linked projects.
# Push current package to all consumers
lnpm push
# Push with force (re-link all files)
lnpm push --forceProcess:
- Calculate new content hash
- If unchanged and not
--force, skip - Update store with new version
- For each linked project:
- Remove old hard links
- Create new hard links
- Preserve
node_modulessymlink
Show current state of all links.
lnpm status
# Output:
# 📦 Published Packages
# ┌──────────────┬─────────┬──────────┬─────────────────────┐
# │ Package │ Version │ Hash │ Published │
# ├──────────────┼─────────┼──────────┼─────────────────────┤
# │ my-package │ 1.0.0 │ abc123 │ 2 minutes ago │
# │ other-pkg │ 2.1.0 │ def456 │ 1 hour ago │
# └──────────────┴─────────┴──────────┴─────────────────────┘
#
# 🔗 Active Links
# ┌──────────────┬─────────────────────────┬──────────┐
# │ Package │ Project │ Type │
# ├──────────────┼─────────────────────────┼──────────┤
# │ my-package │ ~/code/my-app │ hardlink │
# │ my-package │ ~/code/other-app │ hardlink │
# └──────────────┴─────────────────────────┴──────────┘List packages in a project or the store.
# List packages linked in current project
lnpm list
# List all packages in store
lnpm list --store
# List all projects using a package
lnpm list my-package --projectsGarbage collect unused packages from store.
# Dry run - show what would be removed
lnpm gc --dry-run
# Remove packages with no active links
lnpm gc
# Remove packages older than 30 days
lnpm gc --older-than 30dDiagnose and fix common issues.
lnpm doctor
# Output:
# 🔍 Checking lnpm health...
#
# ✓ Store directory exists
# ✓ Database is valid
# ✓ 3 packages in store
# ✓ 2 active links
# ⚠ 1 orphaned link found (project deleted)
# Run: lnpm gc --fix-links
# ✓ No cross-filesystem issues detected# Store location (default: ~/.lnpm)
store_path = "~/.lnpm"
# Default link type: "hardlink" | "copy"
link_type = "hardlink"{
"lnpm": {
"publishDir": "dist",
"ignore": ["**/*.test.ts"],
"prePublish": "npm run build",
"postAdd": "npm install"
}
}version: 1
packages:
my-package:
version: 1.0.0
hash: abc123def456
source: ~/code/my-package
linked: 2024-01-15T10:30:00Z
other-pkg:
version: 2.1.0
hash: 789xyz000111
source: ~/code/other-pkg
linked: 2024-01-15T09:00:00Zlnpm auto-detects package manager by checking for:
bun.lockb→ bunpnpm-lock.yaml→ pnpmyarn.lock→ yarnpackage-lock.json→ npm
{
"dependencies": {
"my-package": "file:.lnpm/my-package"
}
}The file: protocol is universally supported by all package managers.
Before publishing to npm registry, run:
lnpm retreat # or: lnpm remove --allThis restores original version specifiers in package.json.