diff --git a/README.md b/README.md
index 80ddd2c..9a26d37 100644
--- a/README.md
+++ b/README.md
@@ -1,387 +1,165 @@
# Lnk
-**The missing middle: Safer than simple, simpler than complex.**
+**Git-native dotfiles management that doesn't suck.**
-Git-native dotfiles management that won't break your setup. Zero config, zero bloat, zero surprises.
+Move your dotfiles to `~/.config/lnk`, symlink them back, and use Git like normal. That's it.
```bash
-# The power of Git, the safety of proper engineering
-lnk init && lnk add ~/.vimrc && lnk push
+lnk init
+lnk add ~/.vimrc ~/.bashrc
+lnk push "setup"
```
-[](./test) [](https://golang.org) [](LICENSE)
-
-## Why Lnk?
-
-**The dotfiles manager that fills the missing gap.**
-
-While chezmoi offers 100+ features and Home Manager requires learning Nix, **Lnk focuses on doing the essentials perfectly**:
-
-- 🎯 **Safe simplicity**: More robust than Dotbot, simpler than chezmoi
-- 🛡️ **Bulletproof operations**: Comprehensive edge case handling (unlike minimal tools)
-- ⚡ **Zero friction**: No YAML configs, no templates, no learning curve
-- 🔧 **Git-native**: Clean commits, standard workflow, no abstractions
-- 📦 **Zero dependencies**: Single binary vs Python/Node/Ruby runtimes
-- 🚀 **Production ready**: 20 integration tests, proper error handling
-- 🔄 **Smart sync**: Built-in status tracking and seamless multi-machine workflow
-- 📁 **Directory support**: Manage entire config directories or individual files
-
-**The market gap**: Tools are either too simple (and unsafe) or too complex (and overwhelming). Lnk is the **Goldilocks solution** – just right for developers who want reliability without complexity.
-
-## Quick Start
+## Install
```bash
-# Install (30 seconds)
-curl -sSL https://github.com/yarlson/lnk/releases/latest/download/lnk-linux-amd64 -o lnk
-chmod +x lnk && sudo mv lnk /usr/local/bin/
-
-# Use (60 seconds)
-lnk init -r git@github.com:you/dotfiles.git
-lnk add ~/.bashrc ~/.vimrc ~/.gitconfig
-lnk push "Initial dotfiles setup"
-```
-
-**That's it.** Your dotfiles are now version-controlled and synced.
-
-## Installation
-
-### Quick Install (Recommended)
-
-```bash
-# Linux/macOS
+# Quick
curl -sSL https://raw.githubusercontent.com/yarlson/lnk/main/install.sh | bash
-# Or manually download from releases
+# Manual
wget https://github.com/yarlson/lnk/releases/latest/download/lnk-$(uname -s | tr '[:upper:]' '[:lower:]')-amd64
+chmod +x lnk-* && sudo mv lnk-* /usr/local/bin/lnk
+
+# From source
+git clone https://github.com/yarlson/lnk.git && cd lnk && go build . && sudo mv lnk /usr/local/bin/
```
-### From Source
-
-```bash
-git clone https://github.com/yarlson/lnk.git && cd lnk
-go build -ldflags="-s -w" -o lnk .
-sudo mv lnk /usr/local/bin/
-```
-
-### Package Managers
-
-```bash
-# Homebrew (macOS/Linux)
-brew install yarlson/tap/lnk
-
-# Arch Linux
-yay -S lnk-git
-```
-
-## How It Works
-
-**The mental model is simple**: Lnk moves your dotfiles to `~/.config/lnk/` and replaces them with symlinks.
-
-```
-Before: ~/.vimrc (actual file)
-After: ~/.vimrc -> ~/.config/lnk/.vimrc (symlink)
-```
-
-Every change gets a Git commit with descriptive messages like `lnk: added .vimrc`.
-
## Usage
-### Initialize Once
+### Setup
```bash
-lnk init # Local repository
-lnk init -r git@github.com:username/dotfiles.git # With remote
+# Fresh start
+lnk init
+
+# With existing repo
+lnk init -r git@github.com:user/dotfiles.git
```
-**Safety features** (because your dotfiles matter):
-
-- ✅ Idempotent - run multiple times safely
-- ✅ Protects existing repositories from overwrite
-- ✅ Validates remote conflicts before changes
-
-### Manage Files & Directories
+### Daily workflow
```bash
-lnk add ~/.bashrc ~/.vimrc ~/.tmux.conf # Add multiple files
-lnk add ~/.config/nvim ~/.ssh # Add entire directories
-lnk rm ~/.bashrc # Remove from management
+# Add files/directories
+lnk add ~/.vimrc ~/.config/nvim ~/.gitconfig
+
+# Check status
+lnk status
+
+# Sync changes
+lnk push "updated vim config"
+lnk pull
```
-### Sync Commands
+## How it works
-```bash
-lnk status # Check sync status with remote
-lnk push "Update vim configuration" # Stage, commit, and push changes
-lnk pull # Pull changes and restore symlinks
+```
+Before: ~/.vimrc (file)
+After: ~/.vimrc -> ~/.config/lnk/.vimrc (symlink)
```
-**Smart sync features**:
+Your files live in `~/.config/lnk` (a Git repo). Lnk creates symlinks back to original locations. Edit files normally, use Git normally.
-- ✅ Only commits when there are actual changes
-- ✅ Automatic symlink restoration after pull
-- ✅ Clear status reporting (commits ahead/behind)
-- ✅ Graceful error handling for missing remotes
+## Why not just Git?
-### Real-World Workflow
+You could `git init ~/.config/lnk` and manually symlink everything. Lnk just automates the tedious parts:
-```bash
-# Set up on new machine
-lnk init -r git@github.com:you/dotfiles.git
-lnk pull # Get your existing dotfiles with automatic symlink restoration
-
-# Or clone existing manually for complex setups
-git clone git@github.com:you/dotfiles.git ~/.config/lnk
-cd ~/.config/lnk && find . -name ".*" -exec ln -sf ~/.config/lnk/{} ~/{} \;
-```
+- Moving files safely
+- Creating relative symlinks
+- Handling conflicts
+- Tracking what's managed
## Examples
-
-📁 Common Development Setup
+### First time setup
```bash
-# Initialize with remote (recommended)
lnk init -r git@github.com:you/dotfiles.git
-
-# Shell & terminal
-lnk add ~/.bashrc ~/.zshrc ~/.tmux.conf
-
-# Development tools
-lnk add ~/.vimrc ~/.gitconfig ~/.ssh/config
-
-# Language-specific
-lnk add ~/.npmrc ~/.cargo/config.toml ~/.pylintrc
-
-# Push to remote with sync command
-lnk push "Initial dotfiles setup"
-
-# Check what's managed and sync status
-lnk status
-cd ~/.config/lnk && git log --oneline
-# 7f3a12c lnk: Initial dotfiles setup
-# 4e8b33d lnk: added .cargo/config.toml
-# 2a9c45e lnk: added .npmrc
+lnk add ~/.bashrc ~/.vimrc ~/.gitconfig
+lnk push "initial setup"
```
-
-
-
-🔄 Multi-Machine Sync
+### On a new machine
```bash
-# Machine 1: Initial setup
lnk init -r git@github.com:you/dotfiles.git
-lnk add ~/.vimrc ~/.bashrc
-lnk push "Setup from machine 1"
-
-# Machine 2: Clone existing
-lnk init -r git@github.com:you/dotfiles.git
-lnk pull # Automatically restores symlinks
-
-# Daily workflow: Keep machines in sync
-lnk status # Check if changes need syncing
-lnk push "Updated vim configuration" # Share your changes
-lnk pull # Get changes from other machines
-
-# Example sync session
-lnk status
-# Your branch is ahead of 'origin/main' by 2 commit(s)
-
-lnk push "Added new aliases and vim plugins"
-# Successfully pushed changes to remote
-
-lnk pull # On other machine
-# Successfully pulled changes and restored 0 symlink(s)
+lnk pull # auto-creates symlinks
```
-
-
-
-🔄 Smart Sync Workflow
+### Daily edits
```bash
-# Check current status
-lnk status
-# Repository is up to date with remote
-
-# Make changes to your dotfiles
-vim ~/.vimrc # Edit managed file
-
-# Check what needs syncing
-lnk status
-# Your branch is ahead of 'origin/main' by 1 commit(s)
-
-# Sync changes with descriptive message
-lnk push "Added syntax highlighting and line numbers"
-# Successfully pushed changes to remote
-
-# On another machine
-lnk pull
-# Successfully pulled changes and restored 1 symlink(s):
-# - .vimrc
-
-# Verify sync status
-lnk status
-# Repository is up to date with remote
+vim ~/.vimrc # edit normally
+lnk status # check what changed
+lnk push "new plugins" # commit & push
```
-
+## Commands
-## Technical Details
+- `lnk init [-r remote]` - Create repo
+- `lnk add ` - Move files to repo, create symlinks
+- `lnk rm ` - Move files back, remove symlinks
+- `lnk status` - Git status + sync info
+- `lnk push [msg]` - Stage all, commit, push
+- `lnk pull` - Pull + restore missing symlinks
-### Architecture
+## Technical bits
-```
-cmd/ # CLI layer (Cobra)
-├── init.go # Repository initialization
-├── add.go # File adoption & symlinking
-├── rm.go # File restoration
-├── status.go # Sync status reporting
-├── push.go # Smart commit and push
-└── pull.go # Pull with symlink restoration
+- **Single binary** (~8MB, no deps)
+- **Atomic operations** (rollback on failure)
+- **Relative symlinks** (portable)
+- **XDG compliant** (`~/.config/lnk`)
+- **20 integration tests**
-internal/
-├── core/ # Business logic
-├── fs/ # File system operations
-└── git/ # Git automation & sync
-```
+## Alternatives
-### What Makes It Robust
-
-- **20 integration tests** covering edge cases and error conditions
-- **Zero external dependencies** at runtime
-- **Atomic operations** with automatic rollback on failure
-- **Relative symlinks** for cross-platform compatibility
-- **XDG compliance** with fallback to `~/.config`
-
-### Feature Positioning
-
-| Feature | Lnk | Dotbot | yadm | chezmoi | Home Manager |
-| ----------------------- | ------- | ------- | ----- | -------- | ------------ |
-| **Simplicity** | ✅ | ✅ | ❌ | ❌ | ❌ |
-| **Safety/Edge Cases** | ✅ | ❌ | ⚠️ | ✅ | ✅ |
-| **Git Integration** | ✅ | ❌ | ✅ | ⚠️ | ❌ |
-| **Zero Dependencies** | ✅ | ❌ | ❌ | ✅ | ❌ |
-| **Cross-Platform** | ✅ | ✅ | ⚠️ | ✅ | ⚠️ |
-| **Learning Curve** | Minutes | Minutes | Hours | Days | Weeks |
-| **File Templating** | ❌ | ❌ | Basic | Advanced | Advanced |
-| **Built-in Encryption** | ❌ | ❌ | ✅ | ✅ | Plugin |
-| **Package Management** | ❌ | ❌ | ❌ | ❌ | ✅ |
-
-**Lnk's niche**: Maximum safety and Git integration with minimum complexity.
-
-### Performance
-
-- **Single binary**: ~8MB, starts in <10ms
-- **Minimal I/O**: Only touches files being managed
-- **Git efficiency**: Uses native Git commands, not libraries
+| Tool | Complexity | Why choose it |
+| ------- | ---------- | ------------------------------------- |
+| **lnk** | Minimal | Just works, no config, Git-native |
+| chezmoi | High | Templates, encryption, cross-platform |
+| yadm | Medium | Git power user, encryption |
+| dotbot | Low | YAML config, basic features |
+| stow | Low | Perl, symlink only |
## FAQ
-
-How is this different from other dotfiles managers?
+**Q: What if I already have dotfiles in Git?**
+A: `git clone your-repo ~/.config/lnk && lnk add ~/.vimrc` (adopts existing files)
-| Tool | Stars | Approach | Complexity | Learning Curve | Git Integration | Cross-Platform | Key Strength |
-| ------------ | ----- | ------------------------ | ------------- | -------------- | --------------- | -------------- | ---------------------- |
-| **Lnk** | - | Simple symlinks + safety | **Minimal** | **Minutes** | **Native** | ✅ | **Safe simplicity** |
-| chezmoi | 15k | Templates + encryption | High | Hours/Days | Abstracted | ✅ | Feature completeness |
-| Mackup | 14.9k | App config sync | Medium | Hours | Manual | macOS/Linux | GUI app settings |
-| Home Manager | 8.1k | Declarative Nix | **Very High** | **Weeks** | Manual | Linux/macOS | Package + config unity |
-| Dotbot | 7.4k | YAML symlinks | Low | Minutes | Manual | ✅ | Pure simplicity |
-| yadm | 5.7k | Git wrapper | Medium | Hours | **Native** | Unix-like | Git-centric power |
+**Q: How do I handle machine-specific configs?**
+A: Git branches, or just don't manage machine-specific files with lnk
-**Lnk fills the "safe simplicity" gap** – easier than chezmoi/yadm, safer than Dotbot, more capable than plain Git.
+**Q: Windows support?**
+A: Symlinks work on Windows 10+, but untested
-
+**Q: Production ready?**
+A: I use it daily. It won't break your files. API might change (pre-1.0).
-
-Why choose Lnk over the alternatives?
-
-**Choose Lnk if you want:**
-
-- ✅ **Safety first**: Bulletproof edge case handling, won't break existing setups
-- ✅ **Git-native workflow**: No abstractions, just clean commits with clear messages
-- ✅ **Zero learning curve**: 3 commands, works like Git, no configuration files
-- ✅ **Zero dependencies**: Single binary, no Python/Node/Ruby runtime requirements
-- ✅ **Production ready**: Comprehensive test suite, proper error handling
-
-**Choose others if you need:**
-
-- **chezmoi**: Heavy templating, password manager integration, Windows-first
-- **Mackup**: GUI app settings sync via Dropbox/iCloud (macOS focus)
-- **Home Manager**: Nix ecosystem, package management, declarative everything
-- **Dotbot**: Ultra-minimal YAML configuration (no safety features)
-- **yadm**: Git power user features, encryption, bare repo workflow
-
-**The sweet spot**: Lnk is for developers who want dotfiles management **without the ceremony** – all the safety and Git integration you need, none of the complexity you don't.
-
-
-
-
-When NOT to use Lnk?
-
-**Lnk might not be for you if you need:**
-
-❌ **File templating**: Different configs per machine → use **chezmoi**
-❌ **Built-in encryption**: Secrets in dotfiles → use **chezmoi** or **yadm**
-❌ **GUI app settings**: Mac app preferences → use **Mackup**
-❌ **Package management**: Installing software → use **Home Manager** (Nix)
-❌ **Complex workflows**: Multi-step bootstrapping → use **chezmoi** or custom scripts
-❌ **Windows-first**: Native Windows support → use **chezmoi**
-
-**Lnk's philosophy**: Do one thing (symlink management) extremely well, let other tools handle their specialties. You can always combine Lnk with other tools as needed.
-
-
-
-
-What if I already have a dotfiles repo?
+## Contributing
```bash
-# Clone your existing repo to the lnk location
-git clone your-repo ~/.config/lnk
-
-# Lnk works with any Git repo structure
-lnk add ~/.vimrc # Adopts existing files safely
+git clone https://github.com/yarlson/lnk.git
+cd lnk
+make deps # Install golangci-lint
+make check # Runs fmt, vet, lint, test
```
-
+**What we use:**
-
-Is this production ready?
+- **Runtime deps**: Only `cobra` (CLI framework)
+- **Test deps**: `testify` for assertions
+- **Build pipeline**: Standard Makefile with quality checks
-**Yes, with caveats.** Lnk is thoroughly tested and handles edge cases well, but it's actively developed.
-
-✅ **Safe to use**: Won't corrupt your files
-✅ **Well tested**: Comprehensive integration test suite
-⚠️ **API stability**: Commands may evolve (following semver)
-
-**Recommendation**: Try it on non-critical dotfiles first.
-
-
-
-## Development
-
-### Quick Dev Setup
+**Before submitting:**
```bash
-git clone https://github.com/yarlson/lnk.git && cd lnk
-make test # Run integration tests
-make build # Build binary
-make dev # Watch & rebuild
+make check # Runs all quality checks + tests
```
-### Contributing
+**Adding features:**
-We follow standard Go practices:
-
-- **Tests first**: All features need integration tests
-- **Conventional commits**: `feat:`, `fix:`, `docs:`, etc.
-- **No dependencies**: Keep the runtime dependency-free
+- Put integration tests in `test/integration_test.go`
+- Use conventional commits: `feat:`, `fix:`, `docs:`
## License
-MIT License - see [LICENSE](LICENSE) file for details.
-
----
-
-**Made by developers, for developers.** Star ⭐ if this saves you time.
+[MIT](LICENSE)
diff --git a/cmd/init.go b/cmd/init.go
index e931f16..2a204f5 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -15,15 +15,12 @@ var initCmd = &cobra.Command{
remote, _ := cmd.Flags().GetString("remote")
lnk := core.NewLnk()
- if err := lnk.Init(); err != nil {
+ if err := lnk.InitWithRemote(remote); err != nil {
return fmt.Errorf("failed to initialize lnk: %w", err)
}
if remote != "" {
- if err := lnk.AddRemote("origin", remote); err != nil {
- return fmt.Errorf("failed to add remote: %w", err)
- }
- fmt.Printf("Initialized lnk repository with remote: %s\n", remote)
+ fmt.Printf("Initialized lnk repository by cloning: %s\n", remote)
} else {
fmt.Println("Initialized lnk repository")
}
@@ -33,6 +30,6 @@ var initCmd = &cobra.Command{
}
func init() {
- initCmd.Flags().StringP("remote", "r", "", "Add origin remote URL to the repository")
+ initCmd.Flags().StringP("remote", "r", "", "Clone from remote URL instead of creating empty repository")
rootCmd.AddCommand(initCmd)
}
diff --git a/internal/core/lnk.go b/internal/core/lnk.go
index ac9eb43..f7bf1a1 100644
--- a/internal/core/lnk.go
+++ b/internal/core/lnk.go
@@ -45,6 +45,16 @@ func getRepoPath() string {
// Init initializes the lnk repository
func (l *Lnk) Init() error {
+ return l.InitWithRemote("")
+}
+
+// InitWithRemote initializes the lnk repository, optionally cloning from a remote
+func (l *Lnk) InitWithRemote(remoteURL string) error {
+ if remoteURL != "" {
+ // Clone from remote
+ return l.Clone(remoteURL)
+ }
+
// Create the repository directory
if err := os.MkdirAll(l.repoPath, 0755); err != nil {
return fmt.Errorf("failed to create lnk directory: %w", err)
@@ -70,6 +80,14 @@ func (l *Lnk) Init() error {
return nil
}
+// Clone clones a repository from the given URL
+func (l *Lnk) Clone(url string) error {
+ if err := l.git.Clone(url); err != nil {
+ return fmt.Errorf("failed to clone repository: %w", err)
+ }
+ return nil
+}
+
// AddRemote adds a remote to the repository
func (l *Lnk) AddRemote(name, url string) error {
if err := l.git.AddRemote(name, url); err != nil {
diff --git a/internal/git/git.go b/internal/git/git.go
index 8e801bc..9ab5f15 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -457,3 +457,43 @@ func (g *Git) Pull() error {
return nil
}
+
+// Clone clones a repository from the given URL
+func (g *Git) Clone(url string) error {
+ // Remove the directory if it exists to ensure clean clone
+ if err := os.RemoveAll(g.repoPath); err != nil {
+ return fmt.Errorf("failed to remove existing directory: %w", err)
+ }
+
+ // Create parent directory
+ parentDir := filepath.Dir(g.repoPath)
+ if err := os.MkdirAll(parentDir, 0755); err != nil {
+ return fmt.Errorf("failed to create parent directory: %w", err)
+ }
+
+ // Clone the repository
+ cmd := exec.Command("git", "clone", url, g.repoPath)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("git clone failed: %w\nOutput: %s", err, string(output))
+ }
+
+ // Set up upstream tracking for main branch
+ cmd = exec.Command("git", "branch", "--set-upstream-to=origin/main", "main")
+ cmd.Dir = g.repoPath
+ _, err = cmd.CombinedOutput()
+ if err != nil {
+ // If main doesn't exist, try master
+ cmd = exec.Command("git", "branch", "--set-upstream-to=origin/master", "master")
+ cmd.Dir = g.repoPath
+ _, err = cmd.CombinedOutput()
+ if err != nil {
+ // If that also fails, try to set upstream for current branch
+ cmd = exec.Command("git", "branch", "--set-upstream-to=origin/HEAD")
+ cmd.Dir = g.repoPath
+ _, _ = cmd.CombinedOutput() // Ignore error as this is best effort
+ }
+ }
+
+ return nil
+}