Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 90 additions & 65 deletions .chezmoiscripts/run_onchange_after_setup-shared-symlinks.sh.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,83 +3,104 @@ set -e

{{ if .include_defaults -}}

# Check if running inside a Coder workspace
if [ "$CODER" = "true" ]; then
# Enable nullglob to prevent glob patterns from being treated as literals when no files match
shopt -s nullglob

# ========================================
# Function: Handle .claude directory symlinking
# ========================================
symlink_claude() {
local source_claude_dir="$1"

# If ~/.claude doesn't exist OR is a symlink, symlink the entire directory
# Use -n flag to treat destination as normal file (prevents creating symlink inside directory)
if [ ! -e "$HOME/.claude" ] || [ -L "$HOME/.claude" ]; then
ln -sfn "$source_claude_dir" "$HOME/.claude"
else
# If ~/.claude is a real directory, only symlink specific subdirectories and files
for subdir in agents commands skills; do
if [ -d "$source_claude_dir/$subdir" ]; then
ln -sfn "$source_claude_dir/$subdir" "$HOME/.claude/$subdir"
fi
done
# Also symlink CLAUDE.md if it exists
if [ -f "$source_claude_dir/CLAUDE.md" ]; then
ln -sfn "$source_claude_dir/CLAUDE.md" "$HOME/.claude/CLAUDE.md"
fi
fi
}

# ========================================
# Setup shared home directory structure
# ========================================
# ========================================
# Function: Symlink items to home directory
# ========================================
# Used only in Coder workspaces to symlink from /shared/ directories
symlink_to_home() {
local source_dir="$1"
local force_overwrite="${2:-false}" # If true, overwrite existing symlinks

# Use DOTFILES_SHARED_HOME env var, default to /shared/home/default
SHARED_HOME="${DOTFILES_SHARED_HOME:-/shared/home/default}"
for item in "$source_dir"/.*; do
[ -e "$item" ] || continue
[ "$(basename "$item")" = "." ] && continue
[ "$(basename "$item")" = ".." ] && continue

basename_item=$(basename "$item")

# Ensure shared home exists
# Skip .config (managed by chezmoi)
[ "$basename_item" = ".config" ] && continue

# Handle .claude specially
if [ "$basename_item" = ".claude" ]; then
symlink_claude "$item"
continue
fi

# For other items
target="$HOME/$basename_item"

if [ "$force_overwrite" = "true" ]; then
# Force symlink (overrides existing)
ln -sf "$item" "$target"
else
# Only create symlink if target doesn't exist
if [ ! -e "$target" ]; then
ln -sf "$item" "$target"
fi
fi
done
}

# ========================================
# CODER WORKSPACE SETUP
# ========================================
# Uses persistent /shared/ directories for profile-based configuration
# that survives workspace rebuilds

if [ "$CODER" = "true" ]; then
SHARED_HOME="${DOTFILES_SHARED_HOME:-/shared/home/default}"
mkdir -p "$SHARED_HOME"

# Seed missing files/directories from dotfiles/shared/ as defaults
# Seed /shared/home/{profile}/ from dotfiles if empty
if [ -d "{{ .chezmoi.sourceDir }}/shared" ]; then
for item in {{ .chezmoi.sourceDir }}/shared/*; do
[ -e "$item" ] || continue

basename_item=$(basename "$item")
# Add dot prefix for home directory
target="$SHARED_HOME/.$basename_item"

# Only copy if it doesn't exist in shared home
if [ ! -e "$target" ]; then
cp -r "$item" "$target"
fi
done
fi

# First, symlink from /shared/home/shared/ (universal for all profiles)
# Symlink from /shared/home/shared/ (universal for all profiles)
if [ -d "/shared/home/shared" ]; then
for item in /shared/home/shared/.*; do
[ -e "$item" ] || continue
[ "$(basename "$item")" = "." ] && continue
[ "$(basename "$item")" = ".." ] && continue

basename_item=$(basename "$item")

# Skip .config (managed by chezmoi)
[ "$basename_item" = ".config" ] && continue

# Target path in home directory
target="$HOME/$basename_item"

# Create symlink only if target doesn't exist
if [ ! -e "$target" ]; then
ln -sf "$item" "$target"
fi
done
symlink_to_home "/shared/home/shared" false
fi

# Then, symlink from profile-specific shared home (can override shared)
for item in "$SHARED_HOME"/.*; do
[ -e "$item" ] || continue
[ "$(basename "$item")" = "." ] && continue
[ "$(basename "$item")" = ".." ] && continue

basename_item=$(basename "$item")
# Symlink from /shared/home/{profile}/ (profile-specific, can override)
symlink_to_home "$SHARED_HOME" true

# Skip .config (managed by chezmoi)
[ "$basename_item" = ".config" ] && continue

# Target path in home directory
target="$HOME/$basename_item"

# Force symlink (overrides shared if exists)
ln -sf "$item" "$target"
done

# ========================================
# Setup GitHub CLI shared authentication
# ========================================

# Define shared GitHub CLI config path for maintainability
# GitHub CLI: Symlink ~/.config/gh to /shared/.config/gh
GH_SHARED_CONFIG="/shared/.config/gh"

# Seed /shared/.config/gh from dotfiles/shared/gh if it doesn't exist
if [ -d "{{ .chezmoi.sourceDir }}/shared/gh" ] && [ ! -d "$GH_SHARED_CONFIG" ]; then
echo "Seeding $GH_SHARED_CONFIG from dotfiles..."
mkdir -p /shared/.config
Expand All @@ -88,38 +109,42 @@ if [ "$CODER" = "true" ]; then
echo "✓ Created $GH_SHARED_CONFIG (update hosts.yml with your token)"
fi

# Create symlink from ~/.config/gh to /shared/.config/gh
if [ -d "$GH_SHARED_CONFIG" ]; then
echo "Setting up GitHub CLI shared authentication..."

# Backup and remove existing gh config if it exists and is not a symlink
if [ -e "$HOME/.config/gh" ] && [ ! -L "$HOME/.config/gh" ]; then
echo "Backing up existing GitHub CLI config to $GH_SHARED_CONFIG.backup before removal..."
timestamp=$(date +"%Y%m%d_%H%M%S")
backup_dir="${GH_SHARED_CONFIG}.backup_$timestamp"
cp -r "$HOME/.config/gh" "$backup_dir"
echo "✓ Backed up to $backup_dir"
rm -rf "$HOME/.config/gh"
fi

# Create the symlink (ensure parent directory exists)
mkdir -p "$HOME/.config"
ln -sfn "$GH_SHARED_CONFIG" "$HOME/.config/gh"
echo "✓ GitHub CLI symlinked to $GH_SHARED_CONFIG"
fi

# ========================================
# NON-CODER ENVIRONMENT SETUP
# ========================================
# Symlinks directly from dotfiles repo (no /shared/ directory exists)

else
# Outside of Coder, symlink from dotfiles shared/ directory
if [ -d "{{ .chezmoi.sourceDir }}/shared" ]; then
for item in {{ .chezmoi.sourceDir }}/shared/*; do
[ -e "$item" ] || continue
basename_item=$(basename "$item")

# Only symlink if doesn't exist in home (with dot prefix)
# Handle claude specially (no dot prefix in source)
if [ "$basename_item" = "claude" ]; then
symlink_claude "$item"
continue
fi

# Symlink other items if they don't exist (add dot prefix)
if [ ! -e "$HOME/.$basename_item" ]; then
ln -sf "$item" "$HOME/.$basename_item"
fi
done
fi
fi
{{ end -}}
{{ end -}}
8 changes: 8 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# GitHub Copilot Instructions

## Code Reviews

### Test Suite Conventions

- Do not suggest refactoring test scripts to use `install.sh` when the test intentionally bypasses it to test specific chezmoi behavior (e.g., selective symlinking with pre-existing directories)
- Tests may directly invoke chezmoi commands when they need to control the environment state before installation
20 changes: 20 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Test Dotfiles

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Run dotfiles tests
run: |
chmod +x test/test-dotfiles.sh
./test/test-dotfiles.sh
28 changes: 28 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,34 @@ To manually sync Claude configuration:
- Work-specific agents, commands, or workflows
- Personal information, credentials, or internal infrastructure details

## Testing

### Local Testing

The dotfiles include automated tests that verify both Coder and non-Coder installation scenarios using Docker.

**Prerequisites:**
- Docker installed and running
- If Docker is not running: `sudo service docker start`

**Run tests:**
```bash
./test/test-dotfiles.sh
```

**What it tests:**
1. **Non-Coder Environment**: Verifies direct symlinking from dotfiles repo
2. **Coder Fresh Install**: Verifies `/shared/` seeding and full directory symlink
3. **Coder Existing ~/.claude**: Verifies selective subdirectory symlinking (agents, commands, skills, CLAUDE.md)

### CI Testing

Tests run automatically on:
- Push to `main` branch
- Pull requests to `main` branch

The CI workflow (`.github/workflows/test.yml`) uses GitHub Actions and runs the same test script in an Ubuntu environment.

## Ultimate Goal

Host a shell script on GitHub Pages (e.g., fx.github.io/dotfiles/install.sh) that can be run via:
Expand Down
1 change: 1 addition & 0 deletions shared/claude/skills/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Skills directory
75 changes: 75 additions & 0 deletions test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Dotfiles Testing

This directory contains automated tests for the dotfiles installation system.

## Test Suite

The `test-dotfiles.sh` script validates dotfiles installation in three scenarios:

1. **Non-Coder Environment**: Verifies direct symlinking from dotfiles repository
2. **Coder Fresh Install**: Verifies `/shared/` directory seeding and full directory symlink
3. **Coder Existing ~/.claude**: Verifies selective subdirectory symlinking when `~/.claude` already exists

## Requirements

### Docker Image

The tests use a custom Docker image: `ghcr.io/fx/docker/devcontainer:latest`

This image is built from the [fx/docker](https://github.com/fx/docker) repository and provides a consistent test environment with:
- Base Linux distribution for testing
- Common development tools
- User environment similar to actual workspaces

**Note**: The image is publicly available from GitHub Container Registry. If you need to build it locally or use a different image, modify the `IMAGE` variable in `test-dotfiles.sh`.

### Docker

Docker must be installed and running:

```bash
# Check if Docker is running
docker info

# Start Docker if needed
sudo service docker start
```

## Running Tests

### Local Testing

```bash
# From repository root
./test/test-dotfiles.sh

# Or with sudo if needed
sudo ./test/test-dotfiles.sh
```

### CI Testing

Tests run automatically via GitHub Actions on:
- Push to `main` branch
- Pull requests to `main` branch

See `.github/workflows/test.yml` for CI configuration.

## Test Scenarios Explained

### Test 1: Non-Coder Environment

Simulates installation on a personal machine where `/shared/` directory doesn't exist. Verifies that `~/.claude` becomes a symlink directly to the dotfiles repository.

### Test 2: Coder Fresh Install

Simulates first-time installation in a Coder workspace. Verifies:
- `/shared/home/default/.claude` is seeded from dotfiles
- `~/.claude` is a symlink to `/shared/home/default/.claude`

### Test 3: Coder Existing ~/.claude

Simulates installation when user already has a `~/.claude` directory with custom content. Verifies:
- `~/.claude` remains a real directory (not symlink)
- User files are preserved
- Only subdirectories (`agents`, `commands`, `skills`) and `CLAUDE.md` are symlinked
Loading