diff --git a/.chezmoidata/claude.yaml b/.chezmoidata/claude.yaml new file mode 100644 index 0000000..7455866 --- /dev/null +++ b/.chezmoidata/claude.yaml @@ -0,0 +1,16 @@ +--- +# Claude Code marketplace configuration +# This data is merged into ~/.claude/settings.json via run_onchange_after_ script + +marketplace: + extraKnownMarketplaces: + fx-cc: + source: + source: git + url: git@github.com:fx/cc.git + +settings: + includeCoAuthoredBy: false + enableAllProjectMcpServers: true + enabledPlugins: + fx-dev@fx-cc: true diff --git a/.chezmoiignore b/.chezmoiignore index 3b51790..ab811b0 100644 --- a/.chezmoiignore +++ b/.chezmoiignore @@ -11,4 +11,8 @@ test-*.sh *.bak # Note: shared directory stays in source for symlink targets, not installed to home -shared/ \ No newline at end of file +shared/ + +# .claude directory is managed by run_onchange_after_update-claude-settings.sh script +# Not by chezmoi's file management (allows symlinks to work in Coder) +.claude \ No newline at end of file diff --git a/.chezmoiscripts/run_before_00_remove-claude-symlink.sh.tmpl b/.chezmoiscripts/run_before_00_remove-claude-symlink.sh.tmpl deleted file mode 100644 index bd5200a..0000000 --- a/.chezmoiscripts/run_before_00_remove-claude-symlink.sh.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -# Remove any existing ~/.claude symlink before chezmoi applies dot_claude/ - -{{ if .include_defaults -}} -if [ -L "$HOME/.claude" ]; then - rm -f "$HOME/.claude" -fi -{{ end -}} diff --git a/.chezmoiscripts/run_onchange_after_update-claude-settings.sh.tmpl b/.chezmoiscripts/run_onchange_after_update-claude-settings.sh.tmpl new file mode 100755 index 0000000..b548515 --- /dev/null +++ b/.chezmoiscripts/run_onchange_after_update-claude-settings.sh.tmpl @@ -0,0 +1,36 @@ +#!/bin/bash +# Update ~/.claude/settings.json with marketplace configuration +# Works with both real directories and symlinks + +{{ if .include_defaults -}} +set -e + +# Ensure ~/.claude exists (as directory or symlink) +if [ ! -e "$HOME/.claude" ]; then + mkdir -p "$HOME/.claude" +fi + +SETTINGS_FILE="$HOME/.claude/settings.json" + +# Read existing settings or start with empty object +if [ -f "$SETTINGS_FILE" ]; then + existing=$(cat "$SETTINGS_FILE") +else + existing='{}' +fi + +# Our required settings from .chezmoidata/claude.yaml +required_settings='{{ .settings | toJson }}' +marketplace_config='{{ .marketplace | toJson }}' + +# Merge: existing settings + our required settings + marketplace config +# Our settings take precedence (using * operator for recursive merge) +result=$(echo "$existing" | jq --argjson required "$required_settings" \ + --argjson marketplace "$marketplace_config" \ + '. * $required * $marketplace') + +# Write the merged result +echo "$result" > "$SETTINGS_FILE" + +echo "✓ Updated $SETTINGS_FILE" +{{ end -}} diff --git a/.claude/skills/chezmoi-development/SKILL.md b/.claude/skills/chezmoi-development/SKILL.md new file mode 100644 index 0000000..1519136 --- /dev/null +++ b/.claude/skills/chezmoi-development/SKILL.md @@ -0,0 +1,587 @@ +--- +name: chezmoi-development +description: This skill should be used when developing or modifying dotfiles using chezmoi. Covers using .chezmoidata for configuration data and modify_ scripts (or run_onchange_after_ scripts for symlinked directories) for non-destructive file merging. Particularly useful when needing to configure application settings without overwriting user preferences. +--- + +# Chezmoi Development + +## Overview + +Develop dotfiles using chezmoi's advanced features for non-destructive configuration management. This skill focuses on two key patterns: +1. Using `.chezmoidata/` to store structured configuration data +2. Using `modify_` scripts (or `run_onchange_after_` scripts for symlinked directories) to merge configuration into existing files without overwriting user settings + +## When to Use This Skill + +Use this skill when: +- Developing dotfiles with chezmoi that need to configure applications +- Need to enforce required settings while preserving user preferences +- Managing JSON/YAML/TOML configuration files in dotfiles +- Application config files already exist and shouldn't be overwritten +- Working with dotfiles in a team where users may have custom settings + +## Core Concepts + +### .chezmoidata Directory + +Store structured configuration as data files (YAML, JSON, or TOML) in `.chezmoidata/` at the root of the chezmoi source directory. + +**Purpose:** Separate configuration data from templates, making it reusable across multiple files and easier to maintain. + +**Location:** `~/.local/share/chezmoi/.chezmoidata/` + +**Access in templates:** Reference data via dot notation (e.g., `{{ .app.config.setting }}`) + +### modify_ Scripts vs run_onchange_ Scripts + +There are two approaches for non-destructive configuration management in chezmoi: + +#### modify_ Scripts (Preferred for regular directories) + +Create executable scripts that transform existing files by reading them via stdin and outputting the modified version to stdout. + +**Purpose:** Update existing files non-destructively by merging new config with existing content. + +**Naming:** `modify_` + target file path + `.tmpl` +- Example: `modify_dot_config/app/settings.json.tmpl` → modifies `~/.config/app/settings.json` + +**Execution:** chezmoi runs the script, captures stdout, and writes it to the target file. + +**How it works:** Chezmoi provides file contents via **stdin** (use `cat -` to read), script outputs merged result to **stdout**. + +**Limitation:** Cannot be used when the target directory is a symlink (chezmoi requires managing the directory). + +#### run_onchange_after_ Scripts (For symlinked directories) + +Create executable scripts in `.chezmoiscripts/` that read from disk and write back to disk. + +**Purpose:** Update files in directories that may be symlinks (common in Coder workspaces). + +**Naming:** `run_onchange_after_.sh.tmpl` in `.chezmoiscripts/` +- Example: `.chezmoiscripts/run_onchange_after_update-claude-settings.sh.tmpl` + +**Execution:** Script runs after other chezmoi operations, whenever the rendered script content changes. + +**How it works:** Script reads from disk (`cat "$FILE"`), merges config, writes back to disk (`echo "$result" > "$FILE"`). + +**Use when:** Target directory is or may be a symlink, preventing chezmoi from managing individual files within it. + +## Workflow: Implementing Non-Destructive Config Management + +Follow this workflow when implementing configuration management for an application: + +### Step 1: Define Configuration Data + +Create a data file in `.chezmoidata/` with the configuration to enforce. + +**Choose file format based on data complexity:** +- YAML for nested structures with comments +- JSON for simple data or when templates need JSON output +- TOML for flat key-value pairs + +**Example - `.chezmoidata/myapp.yaml`:** +```yaml +--- +# Application configuration to enforce +required_settings: + feature_flag: true + api_endpoint: "https://api.example.com" + +optional_defaults: + theme: "dark" + timeout: 30 +``` + +**Access in templates:** +```bash +{{ .myapp.required_settings.feature_flag }} +{{ .myapp.optional_defaults.theme }} +``` + +### Step 2: Create modify_ Script + +Create a script that merges configuration data into the target file. + +**Script requirements:** +1. Read existing file from stdin (NOT from disk!) +2. Load configuration from `.chezmoidata` via template +3. Merge configuration (required settings take precedence) +4. Output merged result to stdout + +**File naming for modify_ scripts:** +- For `~/.config/myapp/settings.json` → `dot_config/myapp/modify_settings.json.tmpl` +- The directory structure mirrors the target path +- The `modify_` prefix goes on the **filename** (not the directory) +- The file must be executable (`chmod +x`) + +**Example - `dot_config/myapp/modify_settings.json.tmpl`:** +```bash +{{- if .include_defaults -}} +#!/bin/bash +set -e + +# Read existing settings from stdin (chezmoi provides current file contents) +# If stdin is empty (file doesn't exist), use empty object +existing=$(cat - || echo '{}') +if [ -z "$existing" ]; then + existing='{}' +fi + +# Load configuration from .chezmoidata +required='{{ .myapp.required_settings | toJson }}' +defaults='{{ .myapp.optional_defaults | toJson }}' + +# Merge: existing + defaults + required (right side wins) +echo "$existing" | jq --argjson defaults "$defaults" \ + --argjson required "$required" \ + '. * $defaults * $required' +{{- end }} +``` + +**CRITICAL for modify_ scripts:** Chezmoi provides file contents via **stdin**, not by file path. Always use `cat -` to read from stdin. (Note: `run_onchange_` scripts read from disk instead - see the distinction in Core Concepts above.) + +**For YAML files, use `yq` instead of `jq`:** +```bash +# Merge YAML files +existing=$(cat "$HOME/.config/app/config.yaml" || echo '{}') +required='{{ .app.config | toYaml }}' +echo "$existing" | yq eval-all '. as $item ireduce ({}; . * $item)' - <(echo "$required") +``` + +### Step 3: Make Script Executable (Optional - if needed) + +Ensure the modify script is executable in the source directory: + +```bash +chmod +x ~/.local/share/chezmoi/dot_config/myapp/modify_settings.json.tmpl +``` + +**Note:** Chezmoi automatically creates parent directories when writing files, so you typically don't need `run_before_` scripts just to create directories. + +**Only use `run_before_` scripts when you need to:** +- Remove old symlinks that would conflict with new files +- Set special directory permissions +- Install dependencies (like `jq` for JSON processing) + +### Step 4: Test the Implementation + +Preview changes before applying: + +```bash +# View what would be written to the file +chezmoi cat ~/.config/myapp/settings.json + +# Show diff between current and new state +chezmoi diff + +# Apply with dry run +chezmoi apply --dry-run --verbose +``` + +Test merge logic manually: + +```bash +# Extract and test the modify script +chezmoi execute-template < modify_dot_config/myapp/settings.json.tmpl > /tmp/test_modify.sh +chmod +x /tmp/test_modify.sh + +# Test with sample input +echo '{"userSetting":"value"}' | /tmp/test_modify.sh +``` + +## Common Patterns + +### Pattern: Merge with jq + +Merge JSON objects where required settings override existing ones: + +```bash +echo "$existing" | jq --argjson required "$required_settings" \ + '. * $required' +``` + +The `*` operator performs recursive merge with right-side precedence. + +### Pattern: Conditional Configuration + +Apply different config based on environment or profile: + +```yaml +# .chezmoidata/app.yaml +{{ if eq .profile "work" -}} +config: + api_url: "https://work.api.com" +{{ else if eq .profile "personal" -}} +config: + api_url: "https://personal.api.com" +{{ end -}} +``` + +### Pattern: Environment Variable References + +Include environment variables in configuration: + +```yaml +# .chezmoidata/app.yaml +config: + api_key: "{{ env "APP_API_KEY" }}" + debug: {{ env "DEBUG" | default "false" }} +``` + +### Pattern: Multi-File Configuration + +Use same data across multiple files: + +``` +.chezmoidata/ + brand.yaml # Logo paths, colors, fonts + +modify_dot_config/app1/settings.json.tmpl # References {{ .brand.logo }} +modify_dot_config/app2/config.toml.tmpl # References {{ .brand.colors }} +dot_bashrc.tmpl # References {{ .brand.theme }} +``` + +## Symlinks and Execution Order + +### Using symlink_ Prefix + +Chezmoi creates symlinks declaratively using the `symlink_` prefix in the source state. + +**Naming:** `symlink_` + target path + `.tmpl` (template optional) +- Example: `symlink_dot_config/app.tmpl` → creates symlink at `~/.config/app` + +**Content:** The file content (with trailing newline stripped) becomes the symlink target. + +**Example - `symlink_dot_config/myapp.tmpl`:** +```bash +{{ if eq .profile "work" -}} +/shared/work/.config/myapp +{{ else -}} +/shared/default/.config/myapp +{{ end -}} +``` + +**Conditional symlinks:** +```bash +{{- if .is_coder -}} +/shared/.config/app +{{- end -}} +``` + +If the content is empty or whitespace-only after template processing, the symlink is removed. + +### Execution Order + +Understanding execution order is critical to avoid race conditions: + +1. **Read source state** - Parse all files in chezmoi source directory +2. **Read destination state** - Check current state of target files +3. **Compute target state** - Determine what changes are needed +4. **Run `run_before_` scripts** - Execute in alphabetical order +5. **Update entries** - Process all entries in alphabetical order by target name: + - Regular files (`dot_`, `private_`, etc.) + - Symlinks (`symlink_`) + - Modified files (`modify_`) + - Directories + - Scripts (`run_`) +6. **Run `run_after_` scripts** - Execute in alphabetical order + +**Key insight:** All entry types (files, symlinks, modify scripts) are processed together in step 5, sorted alphabetically by their final target path. + +### Avoiding Race Conditions + +**❌ WRONG - Creating directory in run_before prevents symlinking:** +```bash +# .chezmoiscripts/run_before_00_setup.sh.tmpl +mkdir -p "$HOME/.config/app" + +# Later, this fails because ~/.config/app already exists as a directory +# symlink_dot_config/app.tmpl +/shared/.config/app +``` + +**✅ CORRECT - Use symlink_ declaratively:** +```bash +# symlink_dot_config/app.tmpl +{{ if .is_coder -}} +/shared/.config/app +{{ end -}} + +# modify_dot_config/app/settings.json.tmpl +# This works because symlink is created first (alphabetically) +``` + +**✅ ALSO CORRECT - Let chezmoi create directories automatically:** +```bash +# No run_before script needed! +# modify_dot_config/app/settings.json.tmpl +# Chezmoi automatically creates ~/.config/app/ when writing the file +``` + +### When to Use Each Approach + +| Approach | Use When | Example | +|----------|----------|---------| +| `symlink_` | Entire directory should point elsewhere | Link `~/.config/app` → `/shared/.config/app` | +| `modify_` | Merge config into existing file | Merge marketplace config into `settings.json` | +| `dot_` regular file | Fully manage file content | Template `~/.bashrc` from scratch | +| `run_before_` | Install dependencies, clean up old state | Install `jq`, remove old symlinks | +| `run_after_` | Post-install tasks, restart services | Run `systemctl --user daemon-reload` | + +**IMPORTANT:** Chezmoi automatically creates parent directories when writing files. You do NOT need `run_before_` scripts to create directories for `modify_` scripts or regular files. + +### Pattern: Conditional Symlinking in Coder + +For Coder workspaces with persistent `/shared/` storage: + +```bash +# symlink_dot_config/gh.tmpl - Link to shared GitHub CLI config +{{- if .is_coder -}} +/shared/.config/gh +{{- end -}} +``` + +If `.is_coder` is false, the symlink won't be created. If it's true, the symlink points to persistent storage. + +### Pattern: Symlink vs Modify Decision + +**Use symlink when:** +- Entire directory managed externally (e.g., `/shared/`) +- Content is already in a persistent location +- No need to merge with existing content + +**Use modify when:** +- Need to merge with existing user settings +- Want to preserve user customizations +- Enforcing required settings while allowing optional ones + +**Example scenario - Claude Code config:** +```bash +# ❌ BAD - Symlink loses user settings +symlink_dot_claude.tmpl → /shared/.claude + +# ✅ GOOD - Modify merges marketplace config with user settings +modify_dot_claude/settings.json.tmpl → merges settings +``` + +## Best Practices + +### Parent Directories Are Created Automatically + +Chezmoi creates parent directories automatically. Do NOT create directories in `run_before_` scripts unless you have a specific reason (like setting permissions). + +**❌ Unnecessary:** +```bash +# .chezmoiscripts/run_before_setup.sh.tmpl +mkdir -p "$HOME/.config/app" # Chezmoi will do this! +``` + +**✅ Only when needed:** +```bash +# .chezmoiscripts/run_before_setup.sh.tmpl +# Only if you need special permissions +mkdir -p "$HOME/.config/app" +chmod 700 "$HOME/.config/app" +``` + +### Always Handle Missing Files + +Check if target file exists before reading: + +```bash +if [ -f "$HOME/.config/app/settings.json" ]; then + existing=$(cat "$HOME/.config/app/settings.json") +else + existing='{}' # Sensible default +fi +``` + +### Validate JSON/YAML Before Writing + +Ensure output is valid before chezmoi writes it: + +```bash +# Validate JSON +result=$(echo "$existing" | jq --argjson required "$required" '. * $required') +echo "$result" | jq empty # Will fail if invalid +echo "$result" +``` + +### Use Template Guards + +Control when scripts execute based on configuration: + +```bash +{{ if .include_defaults -}} +# Only execute when include_defaults is true +{{ end -}} + +{{ if eq .profile "work" -}} +# Only execute for work profile +{{ end -}} +``` + +### Separate Data from Logic + +**❌ Bad - Hardcode config in template:** +```bash +echo '{"api":"https://api.com","timeout":30}' > ~/.app/config.json +``` + +**✅ Good - Reference .chezmoidata:** +```yaml +# .chezmoidata/app.yaml +config: + api: "https://api.com" + timeout: 30 +``` + +```bash +# modify_dot_app/config.json.tmpl +echo '{{ .app.config | toJson }}' +``` + +### Document Configuration Structure + +Add comments to data files explaining what each setting does: + +```yaml +--- +# Database configuration for application +database: + # Maximum number of connections in the pool + max_connections: 100 + + # Connection timeout in seconds + timeout: 30 + + # Enable query logging (set to false in production) + log_queries: true +``` + +### Script Ordering Matters + +Use clear numeric prefixes to control execution order: + +``` +.chezmoiscripts/ + run_before_00_install-dependencies.sh.tmpl + run_before_10_setup-directories.sh.tmpl + run_before_20_remove-old-symlinks.sh.tmpl + run_onchange_after_50_configure-apps.sh.tmpl +``` + +## Troubleshooting + +### Race Condition: Directory Created Before Symlink + +**Problem:** Want to symlink a directory, but it already exists as a real directory. + +**Cause:** A `run_before_` script or another entry creates the directory before the symlink is processed. + +**Solution 1 - Remove directory creation:** +```bash +# Delete the run_before script that creates the directory +# Let chezmoi handle it via symlink_ or modify_ +``` + +**Solution 2 - Use symlink_ declaratively:** +```bash +# symlink_dot_config/app.tmpl +/shared/.config/app + +# Don't create ~/.config/app anywhere else! +``` + +**Solution 3 - Remove existing directory:** +```bash +# .chezmoiscripts/run_before_00_cleanup.sh.tmpl +if [ -d "$HOME/.config/app" ] && [ ! -L "$HOME/.config/app" ]; then + # Backup if needed + [ -n "$(ls -A "$HOME/.config/app")" ] && \ + mv "$HOME/.config/app" "$HOME/.config/app.backup.$(date +%s)" + rm -rf "$HOME/.config/app" +fi +``` + +### modify_ Script Not Running + +**Check template guard:** +```bash +# View rendered script to see if template guard blocked it +chezmoi execute-template < modify_dot_app/settings.json.tmpl +``` + +**Verify script is executable:** +```bash +chmod +x ~/.local/share/chezmoi/modify_dot_app/settings.json.tmpl +``` + +### Data Not Available in Template + +**List all available template data:** +```bash +chezmoi data | jq +``` + +**Check .chezmoidata file is valid:** +```bash +# For YAML +yq eval .chezmoidata/app.yaml + +# For JSON +jq . .chezmoidata/app.json +``` + +### Merge Produces Incorrect Result + +**Test jq merge manually:** +```bash +existing='{"user":"setting"}' +required='{"new":"value"}' + +echo "$existing" | jq --argjson required "$required" '. * $required' +``` + +**Check operator precedence:** +- `*` recursive merge (right side wins) +- `+` concatenate (arrays append, objects merge) + +### Script Fails with "command not found" + +**Ensure dependencies are installed in run_before script:** +```bash +# .chezmoiscripts/run_before_00_install-jq.sh.tmpl +#!/bin/bash +if ! command -v jq &> /dev/null; then + if [ "$(uname)" = "Darwin" ]; then + brew install jq + else + sudo apt-get install -y jq + fi +fi +``` + +## Reference Documentation + +For a complete, working example of this pattern, see: +- `references/chezmoidata-modify-example.md` - Real-world Claude Code marketplace configuration + +## Quick Reference + +| Task | Command | +|------|---------| +| Preview file output | `chezmoi cat ~/.config/app/settings.json` | +| Show changes | `chezmoi diff` | +| Test template | `chezmoi execute-template < file.tmpl` | +| View template data | `chezmoi data` | +| Apply changes | `chezmoi apply` | +| Dry run | `chezmoi apply --dry-run --verbose` | + +| Pattern | Purpose | +|---------|---------| +| `.chezmoidata/app.yaml` | Store structured configuration data | +| `modify_dot_app/config.json.tmpl` | Merge config into existing file | +| `run_before_00_setup.sh.tmpl` | Ensure prerequisites before applying | +| `{{ .app.setting \| toJson }}` | Convert data to JSON in template | +| `jq '. * $required'` | Merge JSON with right-side precedence | diff --git a/.claude/skills/chezmoi-development/references/chezmoidata-modify-example.md b/.claude/skills/chezmoi-development/references/chezmoidata-modify-example.md new file mode 100644 index 0000000..78f8b62 --- /dev/null +++ b/.claude/skills/chezmoi-development/references/chezmoidata-modify-example.md @@ -0,0 +1,237 @@ +# Real-World Example: Claude Code Marketplace Configuration + +This is a complete, working example of using `.chezmoidata/` and `run_onchange_after_` scripts to manage Claude Code's `settings.json` without overwriting user preferences. + +## The Problem + +Need to configure Claude Code marketplace settings in dotfiles, but: +- Can't overwrite user's existing `~/.claude/settings.json` +- Must enforce required marketplace configuration +- Must handle fresh installs (no existing settings file) +- Must handle existing installations (preserve user settings) + +## The Solution + +Use `.chezmoidata/` to store configuration data, and a `run_onchange_after_` script to merge it into existing settings. + +### File Structure + +``` +~/.local/share/chezmoi/ +├── .chezmoidata/ +│ └── claude.yaml # Configuration data +├── modify_dot_claude/ +│ └── settings.json.tmpl # Merge script +└── .chezmoiscripts/ + └── run_before_00_setup-claude.sh.tmpl # Ensure directory exists +``` + +### 1. Configuration Data (.chezmoidata/claude.yaml) + +```yaml +--- +# Claude Code marketplace configuration +# This data is merged into ~/.claude/settings.json via run_onchange_after_ script + +marketplace: + extraKnownMarketplaces: + fx-cc: + source: + source: git + url: git@github.com:fx/cc.git + +settings: + includeCoAuthoredBy: false + enableAllProjectMcpServers: true +``` + +### 2. Merge Script (.chezmoiscripts/run_onchange_after_update-claude-settings.sh.tmpl) + +```bash +{{- if .include_defaults -}} +#!/bin/bash +# Update ~/.claude/settings.json with marketplace configuration +# Works with both real directories and symlinks + +set -e + +# Ensure ~/.claude exists (as directory or symlink) +if [ ! -e "$HOME/.claude" ]; then + mkdir -p "$HOME/.claude" +fi + +SETTINGS_FILE="$HOME/.claude/settings.json" + +# Read existing settings or start with empty object +if [ -f "$SETTINGS_FILE" ]; then + existing=$(cat "$SETTINGS_FILE") +else + existing='{}' +fi + +# Our required settings from .chezmoidata/claude.yaml +required_settings='{{ .settings | toJson }}' +marketplace_config='{{ .marketplace | toJson }}' + +# Merge: existing settings + our required settings + marketplace config +# Our settings take precedence (using * operator for recursive merge) +result=$(echo "$existing" | jq --argjson required "$required_settings" \ + --argjson marketplace "$marketplace_config" \ + '. * $required * $marketplace') + +# Write the merged result +echo "$result" > "$SETTINGS_FILE" + +echo "✓ Updated $SETTINGS_FILE" +{{- end }} +``` + +**Why use `run_onchange_after_` instead of `modify_`?** +- The `modify_` approach requires chezmoi to manage `~/.claude` as a directory +- This conflicts with Coder workspaces where `~/.claude` is a symlink +- Using a `run_onchange_after_` script allows us to update files inside symlinked directories +- The script runs whenever `.chezmoidata/claude.yaml` changes (because the rendered script changes) + +## How It Works + +When `chezmoi apply` runs: + +1. **Other setup scripts execute** (like `run_onchange_after_setup-shared-symlinks.sh`): + - May create `~/.claude` as a symlink to `/shared/home/default/.claude` in Coder workspaces + - Or it remains a regular directory + +2. **run_onchange_after_update-claude-settings.sh** executes after: + - Checks if `~/.claude` exists (as directory or symlink) + - Reads current `~/.claude/settings.json` from disk (or defaults to `{}`) + - Loads configuration from `.chezmoidata/claude.yaml` + - Merges using `jq`: existing + required settings + marketplace config + - Writes merged JSON back to `~/.claude/settings.json` + - Works whether `~/.claude` is a real directory or a symlink! + +## Example Scenarios + +### Scenario 1: Fresh Install (no existing settings.json) + +**Before:** +``` +~/.claude/ # doesn't exist +``` + +**After:** +```json +{ + "includeCoAuthoredBy": false, + "enableAllProjectMcpServers": true, + "extraKnownMarketplaces": { + "fx-cc": { + "source": { + "source": "git", + "url": "git@github.com:fx/cc.git" + } + } + } +} +``` + +### Scenario 2: Existing Installation (user has custom settings) + +**Before:** +```json +{ + "alwaysThinkingEnabled": true, + "someUserSetting": "user-value" +} +``` + +**After:** +```json +{ + "alwaysThinkingEnabled": true, + "someUserSetting": "user-value", + "includeCoAuthoredBy": false, + "enableAllProjectMcpServers": true, + "extraKnownMarketplaces": { + "fx-cc": { + "source": { + "source": "git", + "url": "git@github.com:fx/cc.git" + } + } + } +} +``` + +User settings preserved, required config enforced! + +### Scenario 3: Coder Workspace (symlinked ~/.claude) + +**Before:** +``` +~/.claude -> /shared/home/default/.claude # symlink +/shared/home/default/.claude/settings.json: +{ + "userWorkspaceSetting": "value" +} +``` + +**After:** +``` +~/.claude -> /shared/home/default/.claude # symlink preserved! +/shared/home/default/.claude/settings.json: +{ + "userWorkspaceSetting": "value", + "includeCoAuthoredBy": false, + "enableAllProjectMcpServers": true, + "extraKnownMarketplaces": { + "fx-cc": { + "source": { + "source": "git", + "url": "git@github.com:fx/cc.git" + } + } + } +} +``` + +Symlink preserved, settings merged inside the symlinked directory! + +## Testing + +To test the merge logic manually: + +```bash +# Simulate the jq merge +existing='{"alwaysThinkingEnabled":true}' +required_settings='{"includeCoAuthoredBy":false,"enableAllProjectMcpServers":true}' +marketplace_config='{"extraKnownMarketplaces":{"fx-cc":{"source":{"source":"git","url":"git@github.com:fx/cc.git"}}}}' + +echo "$existing" | jq --argjson required "$required_settings" \ + --argjson marketplace "$marketplace_config" \ + '. * $required * $marketplace' +``` + +Output: +```json +{ + "alwaysThinkingEnabled": true, + "includeCoAuthoredBy": false, + "enableAllProjectMcpServers": true, + "extraKnownMarketplaces": { + "fx-cc": { + "source": { + "source": "git", + "url": "git@github.com:fx/cc.git" + } + } + } +} +``` + +## Key Takeaways + +1. **`.chezmoidata/` for structured config** - Store configuration as YAML/JSON/TOML data +2. **`run_onchange_after_` scripts for merging** - Non-destructive updates to existing files (use `modify_` when not dealing with symlinks) +3. **`jq` for JSON merging** - Use `*` operator for recursive merge (right side wins) +4. **Template guards** - Use `{{ if .include_defaults -}}` to control when scripts run +5. **Prerequisite scripts** - Use `run_before_` to ensure dependencies exist +6. **Handle all cases** - Script must work for: no file, empty file, existing file with data diff --git a/CLAUDE.md b/CLAUDE.md index 98c00f5..c45b6aa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,6 +70,12 @@ Claude Code plugins, skills, and agents are distributed via the `fx/cc` marketpl ### How It Works 1. **Automatic Configuration**: Dotfiles configure `fx/cc` marketplace via `~/.claude/settings.json`: + - Marketplace configuration is stored in `.chezmoidata/claude.yaml` + - A `modify_` script merges this config into existing settings.json (or creates it if missing) + - User settings are preserved; only marketplace config and required settings are enforced + - Uses chezmoi's template system with `jq` for JSON merging + + Example configuration: ```json { "extraKnownMarketplaces": { diff --git a/dot_claude/settings.json b/dot_claude/settings.json deleted file mode 100644 index 9d1520b..0000000 --- a/dot_claude/settings.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-settings.json", - "includeCoAuthoredBy": false, - "enableAllProjectMcpServers": true, - "extraKnownMarketplaces": { - "fx-cc": { - "source": { - "source": "git", - "url": "git@github.com:fx/cc.git" - } - } - } -} diff --git a/install.sh b/install.sh index 0e2a0c0..ef9f208 100755 --- a/install.sh +++ b/install.sh @@ -41,20 +41,31 @@ run_or_fail() { # Set Git to automatically accept new SSH keys while preserving existing GIT_SSH_COMMAND export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh} -o StrictHostKeyChecking=accept-new" -# Purge cached chezmoi directory if it exists +# Purge cached chezmoi directory and state if they exist if [ -d "$HOME/.local/share/chezmoi" ]; then print_info "Removing cached chezmoi directory..." rm -rf "$HOME/.local/share/chezmoi" fi +if [ -f "$HOME/.config/chezmoi/chezmoistate.boltdb" ]; then + print_info "Removing chezmoi state database..." + rm -f "$HOME/.config/chezmoi/chezmoistate.boltdb" +fi -# Test git SSH access by attempting to ls-remote -print_step "Checking Git access..." -if git ls-remote "git@github.com:${DOTFILES_REPO}.git" >/dev/null 2>&1; then - DOTFILES_URL="git@github.com:${DOTFILES_REPO}.git" - print_success "Using SSH for dotfiles repository" +# Detect if we're running from within the dotfiles repository +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/.chezmoiignore" ] && [ -d "$SCRIPT_DIR/.chezmoiscripts" ]; then + DOTFILES_URL="$SCRIPT_DIR" + print_success "Using local dotfiles repository at $SCRIPT_DIR" else - DOTFILES_URL="https://github.com/${DOTFILES_REPO}.git" - print_warning "Using HTTPS for dotfiles repository" + # Test git SSH access by attempting to ls-remote + print_step "Checking Git access..." + if git ls-remote "git@github.com:${DOTFILES_REPO}.git" >/dev/null 2>&1; then + DOTFILES_URL="git@github.com:${DOTFILES_REPO}.git" + print_success "Using SSH for dotfiles repository" + else + DOTFILES_URL="https://github.com/${DOTFILES_REPO}.git" + print_warning "Using HTTPS for dotfiles repository" + fi fi echo "" @@ -82,12 +93,24 @@ print_success "chezmoi installed" # Initialize dotfiles with selected profile print_step "Initializing dotfiles..." -run_or_fail "Failed to initialize dotfiles" mise exec -- chezmoi init --promptString profile="$PROFILE" "$DOTFILES_URL" +# Capture output and filter noise while preserving exit status +INIT_OUTPUT=$(mktemp) +if mise exec -- chezmoi init --promptString profile="$PROFILE" "$DOTFILES_URL" >"$INIT_OUTPUT" 2>&1; then + grep -v "Cloning into" "$INIT_OUTPUT" | grep -v "^done\.$" || true + rm -f "$INIT_OUTPUT" +else + grep -v "Cloning into" "$INIT_OUTPUT" | grep -v "^done\.$" || true + rm -f "$INIT_OUTPUT" + print_error "Failed to initialize dotfiles" + exit 1 +fi print_success "Dotfiles initialized" # Apply the dotfiles (this automatically runs .chezmoiscripts) print_step "Applying dotfiles..." -run_or_fail "Failed to apply dotfiles" mise exec -- chezmoi apply +# --force is used because we've already cleared state above (lines 45-52) +# This ensures a clean apply without prompts for conflicts +run_or_fail "Failed to apply dotfiles" mise exec -- chezmoi apply --force print_success "Dotfiles applied" echo ""