gravity.nvim: Smart Dotfile Sync
Status: Planning Phase Repository: Fork of kickstart.nvim
Overview
The Name
gravity.nvim captures the core concept: machines pulled into alignment with a central manifest. Like gravity pulling objects into orbit, gravity.nvim brings all development machines into synchronized configuration around a central manifest - fitting naturally within the Sao/Neptune space theme.
The Problem
Development environments suffer from configuration drift - gradual divergence as manual changes accumulate:
- Dotfile chaos: Local configs drift from repository versions
- Manual sync pain: Copying configs between machines causes errors
- Setup friction: Fresh machines require extensive manual configuration
- No conflict detection: Accidentally overwrite local changes
- Template hell: .gitconfig needs different emails per machine
Current Reality
- Machine A: Perfect .tmux.conf, .gitconfig has wrong email
- Machine B: .tmux.conf outdated, breaks muscle memory
- Machine C: Fresh install, configs manually copied, some forgotten
The Solution
A smart dotfile sync plugin for Neovim that handles one thing exceptionally well: keeping configuration files synchronized across machines with intelligent conflict detection, backups, and machine-specific overrides.
Core Philosophy: Do dotfile sync perfectly. Let AI handle everything else.
What This Is
gravity.nvim v1.0:
- ✅ Sync dotfiles from
dotfiles/to~/with hash-based change detection - ✅ Two override systems for different purposes:
- Manifest overrides (
manifest.overrides.json) for dependency/metadata changes - Dotfile overrides (
dotfiles.overrides/) for machine-specific config files - ✅ Conflict detection with diffs before overwriting
- ✅ Automatic backups with timestamps
- ✅ Fast status checks
What This Is NOT
Out of scope (handled by separate AI agent):
- ❌ Installing packages (apt/brew/pacman)
- ❌ Installing languages (Go, Node.js, Python)
- ❌ Installing tools (Docker, tmux, ripgrep)
- ❌ Platform detection and adaptation
- ❌ System-level configuration
The AI Agent Approach
Instead of hardcoding installers for every platform, a separate AI bootstrap agent reads the manifest and figures out how to install dependencies on any platform.
Architecture
Separation of Concerns
graph TB
subgraph "gravity.nvim (Core Plugin)"
A[Load Manifests] --> B[Detect Changes]
B --> C[Show Diffs]
C --> D[User Approval]
D --> E[Sync Files]
E --> F[Update State]
end
subgraph "AI Bootstrap Agent (Separate)"
G[Read Dependencies] --> H[Detect Platform]
H --> I[Research Install Methods]
I --> J[Generate Script]
J --> K[User Approves]
K --> L[Execute]
end
M[manifest.json] --> A
M --> G
style A fill:#2d5
style G fill:#25d
Usage Flow: Parallel to Mason
graph TB
subgraph "Mason (Language Tooling)"
M1[User opens Neovim]
M2[Mason reads ensure_installed]
M3[Checks what's missing]
M4[Auto-installs LSPs/formatters]
M5[gopls, gofumpt, stylua, etc.]
M6["User can update: :Mason"]
M1 --> M2
M2 --> M3
M3 --> M4
M4 --> M5
M5 -.-> M6
end
subgraph "gravity.nvim (System Configuration)"
G1[User opens Neovim]
G2[gravity reads manifest.json]
G3[Checks dotfile status]
G4[Shows status/diffs]
G5[.tmux.conf, .bash_aliases, etc.]
G6["User syncs: :GravitySync"]
G1 --> G2
G2 --> G3
G3 --> G4
G4 --> G5
G5 -.-> G6
end
subgraph "Shared Patterns"
P1[Declarative manifest]
P2[Automatic dependency handling]
P3[Update on git pull]
P4[Part of kickstart fork]
end
M2 -.-> P1
G2 -.-> P1
M4 -.-> P2
G6 -.-> P2
M2 -.-> P3
G2 -.-> P3
M1 -.-> P4
G1 -.-> P4
style M1 fill:#4a9
style G1 fill:#4a9
style P1 fill:#fa4
style P2 fill:#fa4
style P3 fill:#fa4
style P4 fill:#fa4
Key Insight: gravity.nvim extends the kickstart.nvim pattern to system-level configuration. Mason handles dev tools inside Neovim, gravity handles dotfiles outside Neovim. Both use the same declarative, manifest-driven approach that makes kickstart.nvim so effective.
Key Design Decisions
Decision: Pure Lua Plugin vs. Shell Scripts
Pure Lua for core plugin, AI agent for system setup. Clean separation prevents feature creep.
Decision: Declarative Dependencies
Manifest declares what to install, not how. AI figures out the "how" based on current platform.
Decision: Hash-Based Change Detection
SHA256 hashes in .sync_state.json for three-way merge detection (repo vs. system vs. previous). Accurate change detection with minimal storage.
Directory Structure
~/.config/nvim/
├── init.lua # Entry point
├── .gitignore # Ignores overrides & state files
├── manifest.json # Base config (always tracked)
├── manifest.overrides.json # Manifest overrides (gitignored)
├── .sync_state.json # State tracking (gitignored)
├── dotfiles/ # Base dotfiles (committed)
│ ├── .tmux.conf
│ ├── .bash_aliases
│ ├── .gitmux.conf
│ └── .gitconfig
├── dotfiles.overrides/ # Machine-specific dotfile overrides (gitignored)
│ └── .gitconfig # Example: work machine with different email
├── lua/custom/gravity/
│ ├── init.lua # Commands
│ ├── manifest.lua # Load manifest with deep merge
│ ├── sync.lua # Dotfile sync logic with override precedence
│ ├── check.lua # Dependency checks
│ └── utils.lua # Hashing, backups, diffs, deep merge
└── .claude/
└── bootstrap-agent.md # AI agent prompt
.gitignore Contents:
# Machine-specific overrides (never tracked)
manifest.overrides.json
dotfiles.overrides/
# State files (never tracked)
.sync_state.json
backups/
Two Override Systems
gravity.nvim provides two complementary override mechanisms for different purposes:
1. Manifest Overrides (Structured Data)
File: manifest.overrides.json (gitignored)
Purpose: Override specific fields in manifest (dependencies, package versions, metadata)
Mechanism: Deep merge - can change individual fields without redefining entire sections
Use cases:
- Change Node version from 18 → 16 on this machine
- Add one extra system package without duplicating entire list
- Override Go version for testing
Why deep merge?: We control the manifest JSON schema. Structured data with known fields is naturally mergeable.
Example:
// manifest.overrides.json
{
"version": "1.0.0",
"dependencies": {
"languages": {
"node": "16" // Override just Node version, inherit rest
},
"system_packages": ["postgresql-client"] // Add to base list
}
}
2. Dotfile Overrides (File Replacement)
Directory: dotfiles.overrides/ (gitignored)
Purpose: Replace entire dotfiles per machine
Mechanism: File precedence - if override exists, use it; else use base
Use cases:
- Work machine needs completely different
.gitconfig - Development machine has custom
.tmux.confkeybindings - Production machine requires specific
.bash_aliases
Why file replacement?: External file formats (.conf, .ini, shell scripts) can't be partially merged without understanding their syntax. Safer and simpler to replace entire file.
Example:
dotfiles/.gitconfig # Base: Mike Bros <noreply@example.com>
dotfiles.overrides/.gitconfig # Work: Mike Bros <mike@work.com>
When to Use Which System
| Scenario | Use | Reason |
|---|---|---|
| Change Node version | Manifest override | Single field change in structured data |
| Add one system package | Manifest override | Append to existing list |
| Work machine Git email | Dotfile override | Email appears in multiple sections of .gitconfig |
| Custom tmux prefix | Dotfile override | Keybinding change affects multiple config lines |
| Test with older Go | Manifest override | Version number swap |
| Production bash aliases | Dotfile override | Completely different command set |
Manifest Schema
Base Structure
Just declares what, not how:
{
"version": "1.0.0",
"dependencies": {
"system_packages": ["build-essential", "git", "curl", "tmux", "ripgrep"],
"languages": {
"go": "1.25.3",
"node": "18"
},
"tools": ["docker", "neovim"]
},
"dotfiles": {
".tmux.conf": {
"source": "dotfiles/.tmux.conf",
"target": "~/.tmux.conf"
},
".bash_aliases": {
"source": "dotfiles/.bash_aliases",
"target": "~/.bash_aliases"
},
".gitconfig": {
"source": "dotfiles/.gitconfig",
"target": "~/.gitconfig"
}
}
}
Understanding the Version Field
The version field is a schema format version, not a project version:
- Purpose: Ensures manifest compatibility between base and overrides
- Not for project versioning: Git commits provide project versioning
- For format versioning: Breaking changes to manifest structure require version bump
Version validation: Plugin checks that manifest.overrides.json version matches manifest.json version. Mismatched versions produce clear error message.
When version changes: Breaking changes to manifest structure (e.g., dependencies becomes deps, new required fields) require version bump.
Manifest Deep Merge
How override merging works:
function merge_manifests(base, override)
-- Validate versions match
if base.version ~= override.version then
error("Version mismatch: base=" .. base.version .. ", override=" .. override.version)
end
-- Deep merge logic
return deep_merge(base, override)
end
Deep merge rules:
- Objects: Merge keys recursively
- Arrays: Override replaces base (append would cause duplicates)
- Primitives: Override value replaces base value
Example merge:
-- Base manifest.json
{
"dependencies": {
"languages": { "go": "1.25.3", "node": "18" },
"system_packages": ["git", "curl"]
}
}
-- Override manifest.overrides.json
{
"dependencies": {
"languages": { "node": "16" },
"system_packages": ["git", "curl", "postgresql-client"]
}
}
-- Result after merge
{
"dependencies": {
"languages": { "go": "1.25.3", "node": "16" }, -- node overridden, go inherited
"system_packages": ["git", "curl", "postgresql-client"] -- array replaced
}
}
Dotfile Override Precedence
File-based override system:
- Base dotfiles in
dotfiles/(committed to git) - Machine-specific versions in
dotfiles.overrides/(gitignored) - Sync checks override directory first, falls back to base
Sync logic:
function get_source_file(dotfile_name)
local override = "dotfiles.overrides/" .. dotfile_name
local base = "dotfiles/" .. dotfile_name
if file_exists(override) then
return override
else
return base
end
end
Result: Simple file precedence for configs that can't be easily merged.
Shipping Strategy
How Overrides Work
Initial Repository State:
manifest.json- Always tracked by Gitdotfiles/- Base dotfiles tracked by Gitmanifest.overrides.json.example- Starter template (tracked, renamed by user)dotfiles.overrides/- Created by user, always gitignored.gitignore- Contains bothmanifest.overrides.jsonanddotfiles.overrides/
Workflow:
- User clones repo → gets base manifest and dotfiles
- For manifest overrides (optional):
- Copy starter template:
cp manifest.overrides.json.example manifest.overrides.json - Edit to override specific fields (Node version, extra packages, etc.)
- For dotfile overrides (optional):
- Create directory:
mkdir -p dotfiles.overrides/ - Copy base file:
cp dotfiles/.gitconfig dotfiles.overrides/.gitconfig - Edit for machine-specific values
- Git never tracks overrides (both in
.gitignore) - Updates to base files pull cleanly (no conflicts)
Commands
Core Commands (v1.0)
| Command | Description | Use Case |
|---|---|---|
:GravitySync |
Sync dotfiles with prompts/diffs | After git pull, apply config changes |
:GravityStatus |
Show which dotfiles differ | Quick check before syncing |
:GravityDiff <file> |
Show diff for specific file | Review changes before applying |
Optional Commands (v1.1)
| Command | Description | Use Case |
|---|---|---|
:GravityCheck |
Check if dependencies installed | Verify system state (read-only) |
:GravityBootstrap |
Launch AI agent with prompt | Fresh machine setup |
Implementation Timeline
Day 1 Morning
Goal: Basic plugin structure and status checking
Tasks:
- Fork kickstart.nvim, create directory structure
- Implement
manifest.lua(load base + override manifests, validate version, deep merge) - Implement
utils.lua(SHA256 hashing, file operations, deep merge utility) - Implement
sync.lua(load state, compute hashes, detect changes, dotfile override precedence) - Implement
:GravityStatuscommand
Deliverables: :GravityStatus shows which dotfiles differ from repo, handles both override systems
Day 1 Afternoon
Goal: File syncing with safety
Tasks:
- Implement backup system (
~/.config/nvim/backups/with timestamps) - Implement sync operations (repo → system with prompts)
- Update
.sync_state.jsonafter sync - Implement
:GravitySynccommand with user confirmations - Test on real dotfiles
Deliverables: :GravitySync copies files with backups and updates state
Day 2 Morning
Goal: Conflict detection and diffs
Tasks:
- Implement three-way conflict detection (repo vs. system vs. previous)
- Implement
:GravityDiffcommand (show changes before applying) - Test conflict scenarios with overrides
Deliverables: Detects conflicts, shows diffs, handles overrides correctly
Day 2 Afternoon
Goal: AI agent and polish
Tasks:
- Write AI agent prompt (
.claude/bootstrap-agent.md) - Implement
:GravityCheck(check dependencies exist, don't install) - Implement
:GravityBootstrap(launch Claude with prompt) - Improve output formatting (clear status tables, colored diffs)
- Write README with workflows and examples
Deliverables: Complete plugin with AI agent integration
User Workflows
Workflow 1: Fresh Machine Setup
# 1. Clone repo
git clone https://github.com/Mike-Bros/gravity.nvim.git ~/.config/nvim
# 2. Open Neovim
nvim
# 3. Bootstrap system (AI agent figures out how to install on this platform)
:GravityBootstrap
# Claude: "I see you need Go 1.25.3, Node 18, Docker, ripgrep..."
# Claude: "Detected Ubuntu 22.04. Here's the install script:"
# Shows commands, user approves, executes
# 4. Sync dotfiles
:GravitySync
# Shows what will be copied (uses base dotfiles)
# User confirms, files synced
# 5. Create overrides if needed (optional)
mkdir -p ~/.config/nvim/dotfiles.overrides
cp ~/.config/nvim/dotfiles/.gitconfig ~/.config/nvim/dotfiles.overrides/.gitconfig
nvim ~/.config/nvim/dotfiles.overrides/.gitconfig
# Edit for machine-specific values
Workflow 2: Sync Config Changes (Machine A → B)
Machine A (make and push change):
nvim ~/.config/nvim/dotfiles/.tmux.conf # Change prefix to Ctrl+a
cd ~/.config/nvim
git add dotfiles/.tmux.conf
git commit -m "Update tmux prefix"
git push
Machine B (pull and apply):
cd ~/.config/nvim && git pull
nvim
:GravityStatus
Output:
Dotfile Status:
✓ .bash_aliases - unchanged
✓ .gitmux.conf - unchanged
→ .tmux.conf - repo changed (2 minutes ago)
✓ .gitconfig - unchanged
:GravitySync
Output:
Sync Plan:
→ .tmux.conf
- Backing up to ~/.config/nvim/backups/.tmux.conf.2025-11-04_143022
- Copying dotfiles/.tmux.conf → ~/.tmux.conf
Apply changes? [y/N]: y
✓ Synced 1 file, 3 unchanged
Workflow 3: Manifest Override (Dependency Version)
Scenario: Testing machine needs older Node version
# Copy starter template
cp ~/.config/nvim/manifest.overrides.json.example ~/.config/nvim/manifest.overrides.json
# Edit to override Node version
nvim ~/.config/nvim/manifest.overrides.json
Content of manifest.overrides.json:
{
"version": "1.0.0",
"dependencies": {
"languages": {
"node": "16" // Override just Node version
}
}
}
Run bootstrap:
nvim
:GravityBootstrap
# Claude sees merged manifest: Node 16 (from override), Go 1.25.3 (from base)
# Installs Node 16 instead of 18
Result: Testing machine uses Node 16, production machines use Node 18 (base). No git conflicts.
Workflow 4: Dotfile Override (Configuration File)
Scenario: Work machine needs different Git email
# Create overrides directory
mkdir -p ~/.config/nvim/dotfiles.overrides
# Copy base file to override
cp ~/.config/nvim/dotfiles/.gitconfig ~/.config/nvim/dotfiles.overrides/.gitconfig
# Edit override for work email
nvim ~/.config/nvim/dotfiles.overrides/.gitconfig
# Change email to mike@work.com
# Sync dotfiles
nvim
:GravitySync
# Uses dotfiles.overrides/.gitconfig instead of dotfiles/.gitconfig
# Copies to ~/.gitconfig with work email
Result: Work machine has work email, personal machine uses base email. No git conflicts (overrides gitignored).
AI Bootstrap Agent
Purpose
Read manifest.json, detect platform, research current installation methods, generate platform-specific install script.
Implementation
Location: .claude/bootstrap-agent.md
Process:
- Read
manifest.json - Detect platform (
uname -s,/etc/os-release, package managers) - For each dependency, search for current install method
- Generate idempotent install script
- Show commands to user for approval
- Execute approved script
- Verify installations
Platform Support
AI adapts to any platform:
- Ubuntu/Debian:
apt, PPAs,.debpackages - Arch:
pacman, AUR helpers - macOS: Homebrew,
curlinstallers - NixOS: Nix expressions
- Unknown: AI researches and adapts
User Control
- Shows all commands before running
- User approves/rejects each step
- Can edit script before execution
- Clear explanations of what each command does
Example Interaction
User: :GravityBootstrap
Claude: I'll set up your development environment based on manifest.json.
Detected platform: Ubuntu 22.04 LTS
Dependencies to install:
- System packages: build-essential, git, curl, tmux, ripgrep
- Languages: Go 1.25.3, Node.js 18
- Tools: Docker
Generated install script:
```bash
#!/bin/bash
set -e
# Update package list
sudo apt-get update
# Install system packages
sudo apt-get install -y build-essential git curl tmux ripgrep
# Install Go 1.25.3
wget https://go.dev/dl/go1.25.3.linux-amd64.tar.gz
sudo rm -rf /usr/local/go
sudo tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz
rm go1.25.3.linux-amd64.tar.gz
# Install Node.js 18 via NVM
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.bashrc
nvm install 18
# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
rm get-docker.sh
State Tracking
Three-Way Conflict Detection
Core Concept: Track the state at last sync to detect what changed since then. Without this, you only know "repo ≠ system" but can't tell which side changed.
stateDiagram-v2
[*] --> LastSync: Initial sync
LastSync --> Unchanged: No changes
LastSync --> RepoChanged: Only repo changed
LastSync --> SystemChanged: Only system changed
LastSync --> Conflict: Both changed
Unchanged --> [*]: Skip
RepoChanged --> [*]: Copy repo → system
SystemChanged --> [*]: Warn before overwrite
Conflict --> [*]: Show diff, user decides
note right of LastSync
Remembers state at last sync:
- repo_hash
- system_hash
end note
note right of RepoChanged
Safe: System unchanged,
repo has updates
end note
note right of SystemChanged
Warning: User edited locally,
don't lose work
end note
note right of Conflict
Both sides changed since
last sync - needs resolution
end note
Four States Explained
Tracking last sync state enables detection of four distinct states:
| State | Last Sync | Repo Now | System Now | Action |
|---|---|---|---|---|
| Unchanged | A |
A |
A |
Skip |
| Repo Changed | A |
B |
A |
Copy repo → system (safe) |
| System Changed | A |
A |
B |
Warn before overwrite |
| Conflict | A |
B |
C |
Show diff, user decides |
Why this matters: Without tracking last sync (only comparing repo vs. system), you can't distinguish "system changed" from "repo changed" - you just see they differ and don't know which to trust.
State File Structure
.sync_state.json - Complete structure with all tracked fields:
{
"manifest_hash": "abc123...",
"dotfiles": {
".tmux.conf": {
"source_hash": "def456...",
"system_hash": "def456...",
"last_sync": "2025-11-04T14:30:22Z"
},
".gitconfig": {
"source_hash": "ghi789...",
"system_hash": "jkl012...",
"used_override": true,
"last_sync": "2025-11-04T14:30:22Z"
}
}
}
Field Descriptions:
- manifest_hash: SHA256 of merged manifest (base + overrides) - detects changes to dependencies or dotfile definitions
- source_hash: SHA256 of source file (either
dotfiles/ordotfiles.overrides/) - system_hash: SHA256 of target file in
~/ - used_override: Boolean flag indicating if dotfile override was used (optional, for debugging)
- last_sync: ISO 8601 timestamp of last successful sync
Override Detection
How the plugin detects override changes:
The plugin tracks which source file was used (source_hash) regardless of whether it came from dotfiles/ or dotfiles.overrides/. Change detection works the same way:
- Determine source file (check override first, fall back to base)
- Compute source file hash
- Compare to
source_hashin state
Override switching detection:
If user creates an override file after syncing base, the source path changes:
- Previous sync: dotfiles/.gitconfig → hash A
- Current sync: dotfiles.overrides/.gitconfig exists → hash B
- Plugin detects source changed, marks as repo_changed
Hashing Performance
Why hashing instead of full file comparison?
Without hashing:
- Must read and compare full file contents
- State file stores entire file snapshots
- Large state files for many dotfiles
With hashing:
- Hash file contents to 64-byte SHA256 fingerprint
- Compare short fingerprints instead
- Minimal state file storage
Conflict Detection Implementation
Complete three-way detection with override support:
function detect_change_type(file)
local state = load_sync_state()
-- Determine source file (override or base)
local source_file = get_source_file(file)
local source_hash = hash_file(source_file)
local system_hash = hash_file(expand_path("~/" .. file))
-- Get previous state
local prev = state.dotfiles[file] or {}
-- Unchanged: all match
if source_hash == system_hash and source_hash == prev.source_hash then
return "unchanged"
end
-- Detect what changed
local source_changed = (source_hash ~= prev.source_hash)
local system_changed = (system_hash ~= prev.system_hash)
-- Three-way detection
if source_changed and system_changed then
return "conflict"
elseif source_changed then
return "source_changed"
elseif system_changed then
return "system_changed"
end
end
Change Types
| Type | Description | Action |
|---|---|---|
unchanged |
All hashes match | Skip |
source_changed |
Source file changed (base or override) | Copy source → system (safe) |
system_changed |
Only system changed | Warn (or copy system → repo in future) |
conflict |
Both changed | Show diff, require user decision |
Testing
Overview
gravity.nvim includes a comprehensive test suite to validate all core functionality. The testing approach focuses on isolated test environments that prevent side effects on the real system while ensuring fast execution.
Test Framework: plenary.nvim (standard Neovim testing framework)
Test Statistics:
- Total Tests: 13 unit tests
- Execution Time: ~2 seconds for full suite
- Coverage: Status detection, override precedence, state tracking
- Isolation: Each test uses temporary directories (no real file modifications)
Test Coverage
The test suite covers three critical areas of gravity.nvim functionality:
Status Detection Tests (7 tests)
Complete validation of three-way merge detection logic:
| Test Case | Symbol | Description |
|---|---|---|
| Missing System | ○ |
File exists in repo but not yet on system |
| Out of Sync | ◌ |
File exists on system but never synced (no state tracking) |
| Unchanged | ✓ |
Source and system files identical, hashes match state |
| Source Changed | → |
Source file modified since last sync, system unchanged |
| System Changed | ← |
System file modified since last sync, source unchanged |
| Conflict | ⚠ |
Both source and system changed since last sync |
| Missing Source | ✗ |
Source file deleted but system file remains |
Why these matter: Three-way detection prevents data loss by distinguishing between safe updates (source changed), dangerous overwrites (system changed), and conflicts requiring user intervention.
Override Precedence Tests (3 tests)
Validates the dotfile override mechanism works correctly:
| Test Case | Validates |
|---|---|
| Base file used | When no override exists, sync uses dotfiles/ file |
| Override file used | When override exists, sync uses dotfiles.overrides/ file |
| Override content synced | Override file content correctly copied to system |
Why these matter: Override precedence ensures machine-specific configurations (work email, custom keybindings) take precedence over base configurations without git conflicts.
State Tracking Tests (3 tests)
Verifies state persistence and hash tracking:
| Test Case | Validates |
|---|---|
| State file creation | .sync_state.json created after first sync |
| Hash tracking | Source and system hashes stored correctly |
| Override tracking | used_override field records which source was used |
Why these matter: Accurate state tracking is the foundation of three-way merge detection. Without it, conflict detection fails.
Test Architecture
The test suite creates isolated environments to ensure tests don't modify real system files:
graph TB
subgraph "Test Suite"
T1[tests/sync_spec.lua]
T2[plenary test runner]
end
subgraph "Test Environment (Per Test)"
E1[Temp Directory]
E2[Mock dotfiles/]
E3[Mock dotfiles.overrides/]
E4[Mock home/]
E5[Test manifest.json]
E6[Test .sync_state.json]
end
subgraph "Test Execution Flow"
F1[Setup: Create temp dirs]
F2[Arrange: Write test files]
F3[Act: Call gravity function]
F4[Assert: Check result]
F5[Cleanup: Delete temp dirs]
end
subgraph "Coverage Areas"
C1[Status Detection<br/>7 tests]
C2[Override Precedence<br/>3 tests]
C3[State Tracking<br/>3 tests]
end
T1 --> T2
T2 --> E1
E1 --> E2
E1 --> E3
E1 --> E4
E1 --> E5
E1 --> E6
T2 --> F1
F1 --> F2
F2 --> F3
F3 --> F4
F4 --> F5
T1 --> C1
T1 --> C2
T1 --> C3
style T1 fill:#4a9
style C1 fill:#2d5
style C2 fill:#2d5
style C3 fill:#2d5
Test Isolation Strategy
Each test runs in complete isolation from the real system:
Mock Functions:
vim.fn.stdpath('config')→ Points to temp directoryHOMEenvironment variable → Points to temp home directory- All file operations → Contained within temp directories
Test Structure:
- Setup: Create isolated temp directory structure
- Arrange: Write test files with specific content and hashes
- Act: Execute gravity.nvim sync/status functions
- Assert: Verify correct status detection, file operations, state updates
- Cleanup: Delete temp directories (automatic via temp dir library)
Benefits:
- Tests run in parallel without conflicts
- No risk of corrupting real dotfiles
- Fast execution (no real file I/O overhead)
- Repeatable test environments
Running Tests
Quick Run (recommended):
cd ~/.config/nvim
./tests/run_tests.sh
Manual Run (with full command):
nvim --headless -c "PlenaryBustedDirectory tests/ { minimal_init = 'tests/minimal_init.lua' }"
Run Specific Test File:
nvim --headless -c "PlenaryBustedFile tests/sync_spec.lua { minimal_init = 'tests/minimal_init.lua' }"
Test Output Format:
Testing: /home/user/.config/nvim/tests/sync_spec.lua
Success: sync status detection > detects unchanged files
Success: sync status detection > detects source changed
Success: sync status detection > detects system changed
Success: sync status detection > detects conflicts
...
13 tests passed in 1.8s
Test File Location
Test files follow standard Neovim plugin structure:
~/.config/nvim/
├── lua/custom/gravity/
│ ├── init.lua # Production code
│ ├── sync.lua
│ ├── manifest.lua
│ └── utils.lua
├── tests/
│ ├── minimal_init.lua # Minimal Neovim config for testing
│ ├── sync_spec.lua # All 13 unit tests
│ └── run_tests.sh # Convenience script
└── README.md
Example Test Case
Test: Detect source changed
it("detects source changed status", function()
-- Setup: Create temp environment
local temp_dir = create_temp_dir()
local dotfiles_dir = temp_dir .. "/dotfiles"
local home_dir = temp_dir .. "/home"
-- Arrange: Create files with initial state
write_file(dotfiles_dir .. "/.tmux.conf", "original content")
write_file(home_dir .. "/.tmux.conf", "original content")
-- Simulate first sync (create state)
local state = {
dotfiles = {
[".tmux.conf"] = {
source_hash = hash("original content"),
system_hash = hash("original content")
}
}
}
write_state(state)
-- Act: Modify source file
write_file(dotfiles_dir .. "/.tmux.conf", "new content")
local status = detect_change_type(".tmux.conf")
-- Assert: Should detect source changed
assert.equals("source_changed", status)
end)
Continuous Integration
Future enhancement: Run tests automatically on git push via GitHub Actions.
Success Criteria
| Criterion | Measurement |
|---|---|
| No data loss | Backups before every overwrite |
| Conflict detection | Catch all three-way conflicts |
| Cross-machine consistency | Hash verification |
Future Enhancements (v1.1+)
| Enhancement | Description | Effort |
|---|---|---|
| System → Repo sync | Copy local changes back to repo | 1 day |
| Auto-commit | Commit changes with formatted messages | 4 hours |
| Backup rotation | Keep only last N backups per file | 2 hours |
| Encrypted vars | Integration with password managers | 2 days |
| Visual diff | Side-by-side diff in floating window | 1 day |
| Multi-platform testing | Test AI agent on Arch, macOS, NixOS | 2 days |
Risks and Mitigations
| Risk | Impact | Mitigation | Status |
|---|---|---|---|
| Data loss | High | Always backup, require confirmation | ✅ Mitigated |
| Overwrite conflicts | Medium | Three-way detection, show diffs | ✅ Mitigated |
| AI agent failures | Medium | Show commands before running, user approval | ✅ Mitigated |
| Missing dependencies | Low | :GravityCheck detects before use |
✅ Mitigated |
| Template errors | Low | Validate before writing, clear error messages | ✅ Mitigated |
Comparison: Both Override Systems
| Aspect | Manifest Overrides | Dotfile Overrides |
|---|---|---|
| File/Directory | manifest.overrides.json |
dotfiles.overrides/ |
| Mechanism | Deep merge of JSON structures | File precedence (check override first) |
| Use case | Dependency versions, package lists | Complete config files (.gitconfig, .tmux.conf) |
| Granularity | Change one field, inherit rest | Replace entire file |
| User workflow | Edit JSON to override specific fields | Copy base file, edit override version |
| Git conflicts | None (overrides gitignored) | None (overrides gitignored) |
| Implementation | ~100 LOC deep merge + validation | ~50 LOC file precedence check |
| Mental model | Field-level inheritance | File replacement |
| Why this way? | Structured data we control → merge safely | External formats → can't parse, replace safely |
Next Steps
- Fork kickstart.nvim
- Build Day 1 (manifest loading + status checking + override precedence)
- Build Day 2 (syncing + conflict detection + AI agent)
- Test on multiple machines
- Ship v1.0