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
34 changes: 30 additions & 4 deletions .claude/tasks/2025-07-07-bash-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@
2. **Setup orchestration (bash)**: `setup.bash` - Calls utilities directly, professional tooling
3. **Interactive tools (zsh)**: `bin/update/*.zsh` - Thin wrappers, rich user experience

### Phase 3: Framework Cleanup (Pending)
**Goal**: Remove zsh test framework and update CI
### Phase 3: Framework Cleanup and CI Enhancement (Pending)
**Goal**: Remove zsh test framework, update CI, and add shellcheck validation

**Tasks**:
- Update `.github/workflows/test-dotfiles.yml` to use bats instead of custom runner
- Add shellcheck step to CI pipeline for all bash scripts
- Configure shellcheck with appropriate exclusions (e.g., SC1091 for sourced files)
- Remove `test/run-tests.zsh` and custom zsh test utilities
- Update documentation to reflect bash-first approach
- Archive zsh test framework with migration notes
Expand Down Expand Up @@ -79,6 +81,25 @@ test/install/test-{component}-utils.bats # Unit tests
test/install/test-{component}-installation.bats # Integration tests
```

### Shellcheck CI Configuration (Planned)
```yaml
# .github/workflows/shellcheck.yml
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
scandir: '.'
check_together: 'yes'
ignore_paths: 'test/install/lib' # Mock scripts
severity: 'error'
format: 'gcc'
```

**Shellcheck Standards**:
- All bash scripts must pass with zero warnings
- Use inline directives sparingly (prefer fixing the issue)
- Document any necessary exclusions (e.g., SC1091 for sourced files)
- Configure `.shellcheckrc` for project-wide settings if needed

### Bash Script Standards
- `#!/usr/bin/env bash` shebang
- `set -euo pipefail` strict mode
Expand All @@ -98,7 +119,11 @@ test/install/test-{component}-installation.bats # Integration tests
### CI/CD Pipeline
- GitHub Actions currently uses custom zsh test runner
- **Future**: Migrate to bats with better reporting and parallelization
- **Benefit**: Standard test output format, better integration with GitHub UI
- **Future**: Add shellcheck validation step for all bash scripts
- **Benefits**:
- Standard test output format, better integration with GitHub UI
- Automated code quality enforcement via shellcheck
- Catch shell scripting issues before merge

### Code Sharing Strategy
- **Extract to utilities**: Shared logic between setup and update scripts goes to `lib/*-utils.bash`
Expand Down Expand Up @@ -157,9 +182,10 @@ test/install/test-{component}-installation.bats # Integration tests

### Quality Gates
- All bats tests must pass
- Zero shellcheck warnings allowed
- Zero shellcheck warnings allowed (enforced in CI)
- Feature parity verification required
- Complete behavior bundle (no dead code)
- CI shellcheck validation must pass

## Current Work

Expand Down
200 changes: 200 additions & 0 deletions bin/install/symlinks.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
#!/usr/bin/env bash

# Symlink Installation Script
# Creates symbolic links for all dotfiles configurations

set -euo pipefail

# Source utility functions
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
dotfiles_root="$(cd "$script_dir/../.." && pwd)"

# shellcheck source=../../lib/symlink-utils.bash
source "$dotfiles_root/lib/symlink-utils.bash"

# Configuration paths
DOTFILES="$dotfiles_root"
DOTCONFIG="$DOTFILES/config"
HOMECONFIG="$HOME/.config"

# Main symlink creation function
create_dotfiles_symlinks() {
echo "πŸ”— Creating dotfiles symlinks..."

# Remove broken symlinks first
echo "🧹 Cleaning up broken symlinks..."
remove_broken_symlinks "$HOME"
remove_broken_symlinks "$HOMECONFIG"

echo "🏠 Creating home directory symlinks..."

# Create symlinks for home directory files
local home_files=(
"$DOTFILES/home/.claude"
"$DOTFILES/home/.hushlogin"
"$DOTFILES/home/.zshenv"
)

for file in "${home_files[@]}"; do
if [[ -e "$file" ]]; then
maybe_symlink "$file" "$HOME"
else
echo "⚠️ Skipping missing file: $file"
fi
done

echo "βš™οΈ Creating config directory symlinks..."

# Create symlinks for all config files
create_config_symlinks

echo "πŸ“š Creating Library symlinks..."

# Create VS Code symlinks
create_vscode_symlinks

# Create Yazi symlinks if available
create_yazi_symlinks

echo "βœ… All dotfiles symlinks created successfully"
}

# Create symlinks for config directory files
create_config_symlinks() {
# Ensure config directory exists
mkdir -p "$HOMECONFIG"

# Find all files in config directory and create symlinks
if command -v fd >/dev/null 2>&1; then
# Use fd if available (faster and more reliable)
while IFS= read -r -d '' file; do
local relpath="${file#"$DOTCONFIG"/}"
local dirpath
dirpath="$(dirname "$relpath")"
local targetdir="$HOMECONFIG/$dirpath"

maybe_symlink "$file" "$targetdir"
done < <(fd --type file --hidden . "$DOTCONFIG" --print0 2>/dev/null)
else
# Fallback to find command
while IFS= read -r -d '' file; do
local relpath="${file#"$DOTCONFIG"/}"
local dirpath
dirpath="$(dirname "$relpath")"
local targetdir="$HOMECONFIG/$dirpath"

maybe_symlink "$file" "$targetdir"
done < <(find "$DOTCONFIG" -type f -print0 2>/dev/null)
fi
}

# Create VS Code symlinks
create_vscode_symlinks() {
local vscode_user="$HOME/Library/Application Support/Code/User"

# Check if VS Code directory exists
if [[ ! -d "$vscode_user" ]]; then
echo "πŸ“ VS Code not found, skipping VS Code symlinks"
return 0
fi

local vscode_files=(
"$DOTFILES/library/vscode/settings.json"
"$DOTFILES/library/vscode/keybindings.json"
"$DOTFILES/library/vscode/snippets"
)

for file in "${vscode_files[@]}"; do
if [[ -e "$file" ]]; then
maybe_symlink "$file" "$vscode_user"
else
echo "⚠️ Skipping missing VS Code file: $file"
fi
done
}

# Create Yazi symlinks if available
create_yazi_symlinks() {
local yazi_flavors="$HOME/Repos/yazi-rs/flavors"

if [[ ! -d "$yazi_flavors" ]]; then
echo "πŸ—‚οΈ Yazi flavors not found, skipping Yazi symlinks"
return 0
fi

# Create symlink for Catppuccin Mocha theme
local catppuccin_theme="$yazi_flavors/catppuccin-mocha.yazi"
if [[ -d "$catppuccin_theme" ]]; then
maybe_symlink "$catppuccin_theme" "$HOMECONFIG/yazi/flavors"
else
echo "⚠️ Catppuccin Mocha theme not found at $catppuccin_theme"
fi
}

# Verify symlinks were created correctly
verify_symlinks() {
echo "πŸ” Verifying symlinks..."

local verification_failed=false

# Check critical symlinks
local critical_symlinks=(
"$HOME/.zshenv:$DOTFILES/home/.zshenv"
"$HOMECONFIG/nvim/init.lua:$DOTFILES/config/nvim/init.lua"
"$HOMECONFIG/tmux/tmux.conf:$DOTFILES/config/tmux/tmux.conf"
)

for symlink_check in "${critical_symlinks[@]}"; do
local symlink_path="${symlink_check%:*}"
local expected_target="${symlink_check#*:}"

if [[ -e "$expected_target" ]]; then
if is_symlink_correct "$symlink_path" "$expected_target"; then
echo "βœ… $symlink_path β†’ $expected_target"
else
echo "❌ $symlink_path is not correctly linked to $expected_target"
verification_failed=true
fi
fi
done

if [[ "$verification_failed" == "true" ]]; then
echo "⚠️ Some symlinks verification failed"
return 1
else
echo "βœ… All critical symlinks verified successfully"
return 0
fi
}

# Main execution
main() {
local action="${1:-create}"

case "$action" in
"create")
create_dotfiles_symlinks
;;
"verify")
verify_symlinks
;;
"clean")
echo "🧹 Cleaning broken symlinks..."
remove_broken_symlinks "$HOME"
remove_broken_symlinks "$HOMECONFIG"
echo "βœ… Cleanup completed"
;;
*)
echo "Usage: $0 [create|verify|clean]" >&2
echo " create - Create all dotfiles symlinks (default)" >&2
echo " verify - Verify critical symlinks are correct" >&2
echo " clean - Remove broken symlinks only" >&2
exit 1
;;
esac
}

# Only run main if script is executed directly (not sourced)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
Loading