From 45abdbd2068eb9a4af5daf947a23f34dca5dc6db Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 07:52:51 -0800 Subject: [PATCH 01/62] break out ai tooling --- docs/installation/003.md | 26 ++++++++++++++------------ docs/installation/004.md | 3 +-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/installation/003.md b/docs/installation/003.md index 04d9a05..4213db8 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -46,7 +46,9 @@ Host github.com AddKeysToAgent yes UseKeychain yes ``` -8. Clone this repo `git clone git@github.com:chrishough/my-configurations.git .myconfigurations` into the `$HOME` directory. +8. Close the private repo `git clone git@github.com:chrishough/my-configurations-ai.git .myconfigurations.ai` into the `$HOME` directory. +> I am documenting this as part of my flow, but will not be public. Also, steps in the process need this cloned, but will not be fully utilized until later steps. +9. Clone this repo `git clone git@github.com:chrishough/my-configurations.git .myconfigurations` into the `$HOME` directory. * Setup the `.myconfigurations.private` file for private keys and ENV settings. * Install [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) `sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"` theme for `zsh`. * Run the install script `sh "$HOME/.myconfigurations/lib/install.sh"` to load all of the required brew packages. @@ -54,36 +56,36 @@ Host github.com * Run the ruby script to map all of our shell settings to the dotfile repository files `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Install the latest version of node to preload nvm `nvm install --lts`. > If the `.zprofile` and or the `.zshrc` files fail to create a symlink, delete them and run the script again to configure them. -9. Create the rbenv plugin directory `mkdir $(rbenv root)/plugins`. +10. Create the rbenv plugin directory `mkdir $(rbenv root)/plugins`. * Install [rbenv-default-gems](https://github.com/rbenv/rbenv-default-gems) plugin `git clone https://github.com/rbenv/rbenv-default-gems.git $(rbenv root)/plugins/rbenv-default-gems`. * Install [rbenv/rbenv-gem-rehash](https://github.com/rbenv/rbenv-gem-rehash) plugin `git clone https://github.com/sstephenson/rbenv-gem-rehash.git $(rbenv root)/plugins/rbenv-gem-rehash`. * Install [rbenv-vars](https://github.com/rbenv/rbenv-vars) plugin `git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars`. * Install [rbenv-update](https://github.com/rkh/rbenv-update) plugin `git clone https://github.com/rkh/rbenv-update.git $(rbenv root)/plugins/rbenv-update`. -10. Verify global rbenv vars are setup correctly. Type `nano $HOME/.rbenv/vars` and confirm the following. Adjust if necessary. Once completed, type `rbenv vars` and you should see `export GEM_PATH='bundle'`. That is **all** that should be in there at this time! +11. Verify global rbenv vars are setup correctly. Type `nano $HOME/.rbenv/vars` and confirm the following. Adjust if necessary. Once completed, type `rbenv vars` and you should see `export GEM_PATH='bundle'`. That is **all** that should be in there at this time! ``` #GLOBAL GEM_PATH=.bundle ``` -11. Install [Docker Desktop](https://www.docker.com/get-started/). +12. Install [Docker Desktop](https://www.docker.com/get-started/). * `dkrup opensearch1315 pg169 pg175 redis7` install current images. * Stop all via `dkrstopall`. -12. Install the [Jet Brains Toolbox](https://www.jetbrains.com/toolbox-app/). +13. Install the [Jet Brains Toolbox](https://www.jetbrains.com/toolbox-app/). * Install `RubyMine`, and sync settings from the cloud to match previous workstation. * Install `DataGrip` for DB interactions. -13. Setup [eslint](https://eslint.org) via `npm install -g eslint`. -14. Install [puma-dev](https://github.com/puma/puma-dev) via `brew install puma/puma/puma-dev && puma-dev -install -d localhost && sudo puma-dev -setup` -15. Setup Alfred theme and workflows inside `applications/alfred` in this repo. -16. Setup the [Fira Code Fonts](https://fonts.google.com/specimen/Fira+Code) from inside `assets/fonts` in this repo. -17. Configure `iTerm2` to match previous installations. +14. Setup [eslint](https://eslint.org) via `npm install -g eslint`. +15. Install [puma-dev](https://github.com/puma/puma-dev) via `brew install puma/puma/puma-dev && puma-dev -install -d localhost && sudo puma-dev -setup` +16. Setup Alfred theme and workflows inside `applications/alfred` in this repo. +17. Setup the [Fira Code Fonts](https://fonts.google.com/specimen/Fira+Code) from inside `assets/fonts` in this repo. +18. Configure `iTerm2` to match previous installations. * Setup [Base16 iTerm2](https://github.com/chriskempson/base16-iterm2) from inside `assets/colors` in this repo, and set it to use `base16-railscasts.dark`. * In `General` set the Initial Directory to `Reuse previous session's directory`. * In `Text` change the font to `Fira Code`. * In `Terminal` check `Unlimited Scrollback`, verify the `Report Terminal Type` is `xterm-256color`, and verify `Character Encoding` is set to `Unicode( UTF-8 ) * In `Window` set the `Transparency` to `10`. -18. Setup and Install [VSCODE](https://code.visualstudio.com/). +19. Setup and Install [VSCODE](https://code.visualstudio.com/). * Install the `shell command` via the `command palette`. * Configure `sync` via `GitHub`. -19. Configure the [Keyboard Maestro](https://www.keyboardmaestro.com/main/) macro library. +20. Configure the [Keyboard Maestro](https://www.keyboardmaestro.com/main/) macro library. * Desktop and Laptops will have very different settings, for example the laptop will have the brightness controls, the desktop may not, but both will have the player controls. ### Running a Desktop? diff --git a/docs/installation/004.md b/docs/installation/004.md index 20c6af3..ec12a1e 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -1,6 +1,6 @@ ### Artificial Intelligence Preferences and Configurations : 004 -> This section builds on top of sections 1 through 4. Those must be run first! +> At this point the private repository `.myconfigurations.ai` should have been configured in previous steps as it is required at this point. 1. Install [Claude CLI](https://code.claude.com/docs/en/setup#npm). * Via NPM `npm install -g @anthropic-ai/claude-code`. @@ -10,7 +10,6 @@ 2. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. - #### Favorite Skills in Claude * frontend-design@claude-plugins-official * playwright@claude-plugins-official From 2f5f31b75a86dd37e9dab20d385e4af1fdd3c559 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 08:27:39 -0800 Subject: [PATCH 02/62] add chezmoi --- lib/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/install.sh b/lib/install.sh index 718ff73..83cbaf3 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -27,6 +27,7 @@ install_brew_packages() { "pkg-config" "libpq" "gitleaks" + "chezmoi" ) echo "Running brew update..." From 39669ce6f19179139ef9c66da4d8f858e8c247b2 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 08:29:05 -0800 Subject: [PATCH 03/62] update git ignore --- .gitignore | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 0a1a31a..ef15834 100644 --- a/.gitignore +++ b/.gitignore @@ -6,14 +6,10 @@ *.idea *.code -aitooling/cursor/workspaces/* -!aitooling/cursor/workspaces/.keep - applications/tmux/paths.json applications/vscode/workspaces/* !applications/vscode/workspaces/.keep -!applications/itermocil/.keep #-------------------------------------------- # APPLICATION From 3a90fe0af4ea832e163060bb47199383fc66ee8a Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 08:39:32 -0800 Subject: [PATCH 04/62] add claude plugins --- aitooling/claude/settings.json | 3 ++- docs/installation/004.md | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/aitooling/claude/settings.json b/aitooling/claude/settings.json index 4f23ca8..82b1548 100644 --- a/aitooling/claude/settings.json +++ b/aitooling/claude/settings.json @@ -3,7 +3,8 @@ "enabledPlugins": { "frontend-design@claude-plugins-official": true, "playwright@claude-plugins-official": true, - "code-simplifier@claude-plugins-official": true + "code-simplifier@claude-plugins-official": true, + "code-review@claude-code-plugins": true }, "feedbackSurveyState": { "lastShownTime": 1754090209216 diff --git a/docs/installation/004.md b/docs/installation/004.md index ec12a1e..c81168b 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -7,10 +7,13 @@ * Install `npx claude-plugins install @anthropics/claude-plugins-official/playwright`. * Install `npx claude-plugins install @anthropics/claude-plugins-official/code-simplifier`. * Install `npx claude-plugins install @anthropics/claude-code-plugins/frontend-design`. + * Install `npx claude-plugins install @anthropics/claude-code-plugins/code-review`. + 2. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. #### Favorite Skills in Claude -* frontend-design@claude-plugins-official -* playwright@claude-plugins-official -* code-simplifier@claude-plugins-official +* @anthropics/claude-plugins-official/playwright +* @anthropics/claude-plugins-official/playwrigcode-simplifierht +* @anthropics/claude-plugins-official/frontend-design +* @anthropics/claude-plugins-official/code-review From c52a9210b9ea91c78c1a44db830b1a293137cfa9 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 08:54:18 -0800 Subject: [PATCH 05/62] add github to brew --- lib/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/install.sh b/lib/install.sh index 83cbaf3..ab4168d 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -28,6 +28,7 @@ install_brew_packages() { "libpq" "gitleaks" "chezmoi" + "gh" ) echo "Running brew update..." From 768aabaf744b102fda209f22287f06c21af35a0a Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 09:03:33 -0800 Subject: [PATCH 06/62] add github --- docs/installation/003.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation/003.md b/docs/installation/003.md index 4213db8..221c798 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -52,6 +52,7 @@ Host github.com * Setup the `.myconfigurations.private` file for private keys and ENV settings. * Install [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) `sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"` theme for `zsh`. * Run the install script `sh "$HOME/.myconfigurations/lib/install.sh"` to load all of the required brew packages. + * Setup the `github` dependency via `gh auth login`. * Setup the latest version of Ruby `rbenv install 3.4.3`, set the local machine to run it via `rbenv local 3.4.3`, and make sure Bundler `gem install bundler`, PRY `gem install pry`, and rsense `gem install rsense` are installed. If you get error installing these gemes run `source ~/.zshrc`. * Run the ruby script to map all of our shell settings to the dotfile repository files `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Install the latest version of node to preload nvm `nvm install --lts`. From 401e02337713c3e882091477b147f99cadfb2844 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 09:14:24 -0800 Subject: [PATCH 07/62] refactor claude setup to ai tooling --- applications/claude/.keep | 0 .../ciso-application-security-analysis.md | 0 applications/claude/prompts/code-review.md | 92 +++++++++++++++++++ 3 files changed, 92 insertions(+) create mode 100644 applications/claude/.keep rename aitooling/prompts/ciso_application_security_analysis_prompt.md => applications/claude/prompts/ciso-application-security-analysis.md (100%) create mode 100644 applications/claude/prompts/code-review.md diff --git a/applications/claude/.keep b/applications/claude/.keep new file mode 100644 index 0000000..e69de29 diff --git a/aitooling/prompts/ciso_application_security_analysis_prompt.md b/applications/claude/prompts/ciso-application-security-analysis.md similarity index 100% rename from aitooling/prompts/ciso_application_security_analysis_prompt.md rename to applications/claude/prompts/ciso-application-security-analysis.md diff --git a/applications/claude/prompts/code-review.md b/applications/claude/prompts/code-review.md new file mode 100644 index 0000000..41cadcd --- /dev/null +++ b/applications/claude/prompts/code-review.md @@ -0,0 +1,92 @@ +--- +allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Bash(gh pr list:*) +description: Code review a pull request +disable-model-invocation: false +--- + +Provide a code review for the given pull request. + +To do this, follow these steps precisely: + +1. Use a Haiku agent to check if the pull request (a) is closed, (b) does not need a code review (eg. because it is an automated pull request, or is very simple and obviously ok), or (c) already has a code review from you from earlier. If so, do not proceed. +2. Use another Haiku agent to give you a list of file paths to (but not the contents of) any relevant CLAUDE.md files from the codebase: the root CLAUDE.md file (if one exists), as well as any CLAUDE.md files in the directories whose files the pull request modified +3. Use a Haiku agent to view the pull request, and ask the agent to return a summary of the change +4. Then, launch 5 parallel Sonnet agents to independently code review the change. The agents should do the following, then return a list of issues and the reason each issue was flagged (eg. CLAUDE.md adherence, bug, historical git context, etc.): + a. Agent #1: Audit the changes to make sure they compily with the CLAUDE.md. Note that CLAUDE.md is guidance for Claude as it writes code, so not all instructions will be applicable during code review. + b. Agent #2: Read the file changes in the pull request, then do a shallow scan for obvious bugs. Avoid reading extra context beyond the changes, focusing just on the changes themselves. Focus on large bugs, and avoid small issues and nitpicks. Ignore likely false positives. + c. Agent #3: Read the git blame and history of the code modified, to identify any bugs in light of that historical context + d. Agent #4: Read previous pull requests that touched these files, and check for any comments on those pull requests that may also apply to the current pull request. + e. Agent #5: Read code comments in the modified files, and make sure the changes in the pull request comply with any guidance in the comments. +5. For each issue found in #4, launch a parallel Haiku agent that takes the PR, issue description, and list of CLAUDE.md files (from step 2), and returns a score to indicate the agent's level of confidence for whether the issue is real or false positive. To do that, the agent should score each issue on a scale from 0-100, indicating its level of confidence. For issues that were flagged due to CLAUDE.md instructions, the agent should double check that the CLAUDE.md actually calls out that issue specifically. The scale is (give this rubric to the agent verbatim): + a. 0: Not confident at all. This is a false positive that doesn't stand up to light scrutiny, or is a pre-existing issue. + b. 25: Somewhat confident. This might be a real issue, but may also be a false positive. The agent wasn't able to verify that it's a real issue. If the issue is stylistic, it is one that was not explicitly called out in the relevant CLAUDE.md. + c. 50: Moderately confident. The agent was able to verify this is a real issue, but it might be a nitpick or not happen very often in practice. Relative to the rest of the PR, it's not very important. + d. 75: Highly confident. The agent double checked the issue, and verified that it is very likely it is a real issue that will be hit in practice. The existing approach in the PR is insufficient. The issue is very important and will directly impact the code's functionality, or it is an issue that is directly mentioned in the relevant CLAUDE.md. + e. 100: Absolutely certain. The agent double checked the issue, and confirmed that it is definitely a real issue, that will happen frequently in practice. The evidence directly confirms this. +6. Filter out any issues with a score less than 80. If there are no issues that meet this criteria, do not proceed. +7. Use a Haiku agent to repeat the eligibility check from #1, to make sure that the pull request is still eligible for code review. +8. Finally, use the gh bash command to comment back on the pull request with the result. When writing your comment, keep in mind to: + a. Keep your output brief + b. Avoid emojis + c. Link and cite relevant code, files, and URLs + +Examples of false positives, for steps 4 and 5: + +- Pre-existing issues +- Something that looks like a bug but is not actually a bug +- Pedantic nitpicks that a senior engineer wouldn't call out +- Issues that a linter, typechecker, or compiler would catch (eg. missing or incorrect imports, type errors, broken tests, formatting issues, pedantic style issues like newlines). No need to run these build steps yourself -- it is safe to assume that they will be run separately as part of CI. +- General code quality issues (eg. lack of test coverage, general security issues, poor documentation), unless explicitly required in CLAUDE.md +- Issues that are called out in CLAUDE.md, but explicitly silenced in the code (eg. due to a lint ignore comment) +- Changes in functionality that are likely intentional or are directly related to the broader change +- Real issues, but on lines that the user did not modify in their pull request + +Notes: + +- Do not check build signal or attempt to build or typecheck the app. These will run separately, and are not relevant to your code review. +- Use `gh` to interact with Github (eg. to fetch a pull request, or to create inline comments), rather than web fetch +- Make a todo list first +- You must cite and link each bug (eg. if referring to a CLAUDE.md, you must link it) +- For your final comment, follow the following format precisely (assuming for this example that you found 3 issues): + +--- + +### Code review + +Found 3 issues: + +1. (CLAUDE.md says "<...>") + + + +2. (some/other/CLAUDE.md says "<...>") + + + +3. (bug due to ) + + + +πŸ€– Generated with [Claude Code](https://claude.ai/code) + +- If this code review was useful, please react with πŸ‘. Otherwise, react with πŸ‘Ž. + +--- + +- Or, if you found no issues: + +--- + +### Code review + +No issues found. Checked for bugs and CLAUDE.md compliance. + +πŸ€– Generated with [Claude Code](https://claude.ai/code) + +- When linking to code, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-cli-internal/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15 + - Requires full git sha + - You must provide the full sha. Commands like `https://github.com/owner/repo/blob/$(git rev-parse HEAD)/foo/bar` will not work, since your comment will be directly rendered in Markdown. + - Repo name must match the repo you're code reviewing + - # sign after the file name + - Line range format is L[start]-L[end] + - Provide at least 1 line of context before and after, centered on the line you are commenting about (eg. if you are commenting about lines 5-6, you should link to `L4-7`) From abf14dc5893e71075061b9cabc657e1a0f5b938b Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 17 Jan 2026 10:13:47 -0800 Subject: [PATCH 08/62] refactor divvy naming conventions --- dotfiles/functions/collections/divvy | 77 ++++++++++++++++++++++++---- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/dotfiles/functions/collections/divvy b/dotfiles/functions/collections/divvy index 426c088..4c5fca3 100644 --- a/dotfiles/functions/collections/divvy +++ b/dotfiles/functions/collections/divvy @@ -16,13 +16,13 @@ _dvy_next_session_name() { echo "${base}_${i}" } -# Layout: apps +# Layout: l1_r3 # +--------+----+----+ # | | 1 | 2 | # | 0 +----+----+ # | | 3 | # +--------+---------+ -_dvy_layout_apps() { +_dvy_layout_l1_r3() { local session="$1" local dir="$2" local config="$3" @@ -47,7 +47,7 @@ _dvy_layout_apps() { tmux attach -t "$session" } -# Layout: configurations +# Layout: l1_r4 # +--------+---------+ # | | 1 | # | +---------+ @@ -57,7 +57,7 @@ _dvy_layout_apps() { # | +---------+ # | | 4 | # +--------+---------+ -_dvy_layout_configurations() { +_dvy_layout_l1_r4() { local session="$1" local dir="$2" local config="$3" @@ -82,6 +82,49 @@ _dvy_layout_configurations() { tmux attach -t "$session" } +# Layout: l2_r4 +# +--------+---------+ +# | 0 | 1 | +# | +---------+ +# | | 2 | +# +--------+---------+ +# | | 3 | +# | 5 +---------+ +# | | 4 | +# +--------+---------+ +_dvy_layout_l2_r4() { + local session="$1" + local dir="$2" + local config="$3" + + # Create session with pane 0 + tmux new-session -d -s "$session" -c "$dir" -x $(tput cols) -y $(tput lines) + + # Split horizontally: pane 1 on right (50%) + tmux split-window -h -p 50 -t "$session:0" -c "$dir" + + # Split right pane (1) vertically 3 times + tmux split-window -v -p 75 -t "$session:0.1" -c "$dir" + tmux split-window -v -p 66 -t "$session:0.2" -c "$dir" + tmux split-window -v -p 50 -t "$session:0.3" -c "$dir" + + # Split left pane (0) vertically once + tmux split-window -v -p 50 -t "$session:0.0" -c "$dir" + + tmux select-layout -t "$session:0" -E + + for pane in 0 1 2 3 4 5; do + tmux send-keys -t "$session:0.$pane" 'clear' Enter + local cmd=$(echo "$config" | jq -r --arg p "$pane" '.[$p] // empty') + if [[ -n "$cmd" ]]; then + tmux send-keys -t "$session:0.$pane" "$cmd" Enter + tmux send-keys -t "$session:0.$pane" 'clear' Enter + fi + done + + tmux attach -t "$session" +} + _dvy_list() { echo echo "-------------------------------------------" @@ -201,11 +244,14 @@ dvy() { session=$(_dvy_next_session_name "$session") case "$layout" in - apps) - _dvy_layout_apps "$session" "$dir" "$config" + l1_r3) + _dvy_layout_l1_r3 "$session" "$dir" "$config" ;; - configurations) - _dvy_layout_configurations "$session" "$dir" "$config" + l1_r4) + _dvy_layout_l1_r4 "$session" "$dir" "$config" + ;; + l2_r4) + _dvy_layout_l2_r4 "$session" "$dir" "$config" ;; *) echo "Error: Unknown layout type: $layout" @@ -233,14 +279,14 @@ dvyhelp() { echo echo " Layout types:" echo - echo " apps - 4-pane layout" + echo " l1_r3 - 4-pane layout (1 left, 3 right)" echo " +--------+----+----+" echo " | | 1 | 2 |" echo " | 0 +----+----+" echo " | | 3 |" echo " +--------+---------+" echo - echo " configurations - 5-pane layout" + echo " l1_r4 - 5-pane layout (1 left, 4 right)" echo " +--------+---------+" echo " | | 1 |" echo " | +---------+" @@ -251,6 +297,17 @@ dvyhelp() { echo " | | 4 |" echo " +--------+---------+" echo + echo " l2_r4 - 6-pane layout (2 left, 4 right)" + echo " +--------+---------+" + echo " | 0 | 1 |" + echo " | +---------+" + echo " | | 2 |" + echo " +--------+---------+" + echo " | | 3 |" + echo " | 5 +---------+" + echo " | | 4 |" + echo " +--------+---------+" + echo echo "-------------------------------------------" echo } From 20f93657e5e5f3560243400147fad2fdf4997c03 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 19 Jan 2026 18:20:23 -0800 Subject: [PATCH 09/62] update claude code review --- applications/claude/prompts/code-review.md | 124 ++++++++++++--------- 1 file changed, 69 insertions(+), 55 deletions(-) diff --git a/applications/claude/prompts/code-review.md b/applications/claude/prompts/code-review.md index 41cadcd..668c5ac 100644 --- a/applications/claude/prompts/code-review.md +++ b/applications/claude/prompts/code-review.md @@ -1,89 +1,103 @@ --- -allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Bash(gh pr list:*) +allowed-tools: Bash(gh issue view:*), Bash(gh search:*), Bash(gh issue list:*), Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Bash(gh pr list:*), mcp__github_inline_comment__create_inline_comment description: Code review a pull request -disable-model-invocation: false --- Provide a code review for the given pull request. +**Important: Draft PRs ARE allowed and should be reviewed. Do not skip a PR because it is a draft.** + +**Agent assumptions (applies to all agents and subagents):** +- All tools are functional and will work without error. Do not test tools or make exploratory calls. Make sure this is clear to every subagent that is launched. +- Only call a tool if it is required to complete the task. Every tool call should have a clear purpose. + To do this, follow these steps precisely: -1. Use a Haiku agent to check if the pull request (a) is closed, (b) does not need a code review (eg. because it is an automated pull request, or is very simple and obviously ok), or (c) already has a code review from you from earlier. If so, do not proceed. -2. Use another Haiku agent to give you a list of file paths to (but not the contents of) any relevant CLAUDE.md files from the codebase: the root CLAUDE.md file (if one exists), as well as any CLAUDE.md files in the directories whose files the pull request modified -3. Use a Haiku agent to view the pull request, and ask the agent to return a summary of the change -4. Then, launch 5 parallel Sonnet agents to independently code review the change. The agents should do the following, then return a list of issues and the reason each issue was flagged (eg. CLAUDE.md adherence, bug, historical git context, etc.): - a. Agent #1: Audit the changes to make sure they compily with the CLAUDE.md. Note that CLAUDE.md is guidance for Claude as it writes code, so not all instructions will be applicable during code review. - b. Agent #2: Read the file changes in the pull request, then do a shallow scan for obvious bugs. Avoid reading extra context beyond the changes, focusing just on the changes themselves. Focus on large bugs, and avoid small issues and nitpicks. Ignore likely false positives. - c. Agent #3: Read the git blame and history of the code modified, to identify any bugs in light of that historical context - d. Agent #4: Read previous pull requests that touched these files, and check for any comments on those pull requests that may also apply to the current pull request. - e. Agent #5: Read code comments in the modified files, and make sure the changes in the pull request comply with any guidance in the comments. -5. For each issue found in #4, launch a parallel Haiku agent that takes the PR, issue description, and list of CLAUDE.md files (from step 2), and returns a score to indicate the agent's level of confidence for whether the issue is real or false positive. To do that, the agent should score each issue on a scale from 0-100, indicating its level of confidence. For issues that were flagged due to CLAUDE.md instructions, the agent should double check that the CLAUDE.md actually calls out that issue specifically. The scale is (give this rubric to the agent verbatim): - a. 0: Not confident at all. This is a false positive that doesn't stand up to light scrutiny, or is a pre-existing issue. - b. 25: Somewhat confident. This might be a real issue, but may also be a false positive. The agent wasn't able to verify that it's a real issue. If the issue is stylistic, it is one that was not explicitly called out in the relevant CLAUDE.md. - c. 50: Moderately confident. The agent was able to verify this is a real issue, but it might be a nitpick or not happen very often in practice. Relative to the rest of the PR, it's not very important. - d. 75: Highly confident. The agent double checked the issue, and verified that it is very likely it is a real issue that will be hit in practice. The existing approach in the PR is insufficient. The issue is very important and will directly impact the code's functionality, or it is an issue that is directly mentioned in the relevant CLAUDE.md. - e. 100: Absolutely certain. The agent double checked the issue, and confirmed that it is definitely a real issue, that will happen frequently in practice. The evidence directly confirms this. -6. Filter out any issues with a score less than 80. If there are no issues that meet this criteria, do not proceed. -7. Use a Haiku agent to repeat the eligibility check from #1, to make sure that the pull request is still eligible for code review. -8. Finally, use the gh bash command to comment back on the pull request with the result. When writing your comment, keep in mind to: - a. Keep your output brief - b. Avoid emojis - c. Link and cite relevant code, files, and URLs - -Examples of false positives, for steps 4 and 5: +1. Launch a haiku agent to check if any of the following are true: + - The pull request is closed + - The pull request does not need code review (e.g. automated PR, trivial change that is obviously correct) -- Pre-existing issues -- Something that looks like a bug but is not actually a bug -- Pedantic nitpicks that a senior engineer wouldn't call out -- Issues that a linter, typechecker, or compiler would catch (eg. missing or incorrect imports, type errors, broken tests, formatting issues, pedantic style issues like newlines). No need to run these build steps yourself -- it is safe to assume that they will be run separately as part of CI. -- General code quality issues (eg. lack of test coverage, general security issues, poor documentation), unless explicitly required in CLAUDE.md -- Issues that are called out in CLAUDE.md, but explicitly silenced in the code (eg. due to a lint ignore comment) -- Changes in functionality that are likely intentional or are directly related to the broader change -- Real issues, but on lines that the user did not modify in their pull request + If any condition is true, stop and do not proceed. -Notes: + Note: Draft PRs are allowed - do NOT skip them. Previous Claude comments are allowed - re-reviews are permitted. -- Do not check build signal or attempt to build or typecheck the app. These will run separately, and are not relevant to your code review. -- Use `gh` to interact with Github (eg. to fetch a pull request, or to create inline comments), rather than web fetch -- Make a todo list first -- You must cite and link each bug (eg. if referring to a CLAUDE.md, you must link it) -- For your final comment, follow the following format precisely (assuming for this example that you found 3 issues): +Note: Still review Claude generated PR's. ---- +2. Launch a haiku agent to return a list of file paths (not their contents) for all relevant CLAUDE.md files including: + - The root CLAUDE.md file, if it exists + - Any CLAUDE.md files in directories containing files modified by the pull request -### Code review +3. Launch a sonnet agent to view the pull request and return a summary of the changes -Found 3 issues: +4. Launch 4 agents in parallel to independently review the changes. Each agent should return the list of issues, where each issue includes a description and the reason it was flagged (e.g. "CLAUDE.md adherence", "bug"). The agents should do the following: -1. (CLAUDE.md says "<...>") + Agents 1 + 2: CLAUDE.md compliance sonnet agents + Audit changes for CLAUDE.md compliance in parallel. Note: When evaluating CLAUDE.md compliance for a file, you should only consider CLAUDE.md files that share a file path with the file or parents. - + Agent 3: Opus bug agent (parallel subagent with agent 4) + Scan for obvious bugs. Focus only on the diff itself without reading extra context. Flag only significant bugs; ignore nitpicks and likely false positives. Do not flag issues that you cannot validate without looking at context outside of the git diff. -2. (some/other/CLAUDE.md says "<...>") + Agent 4: Opus bug agent (parallel subagent with agent 3) + Look for problems that exist in the introduced code. This could be security issues, incorrect logic, etc. Only look for issues that fall within the changed code. - + **CRITICAL: We only want HIGH SIGNAL issues.** Flag issues where: + - The code will fail to compile or parse (syntax errors, type errors, missing imports, unresolved references) + - The code will definitely produce wrong results regardless of inputs (clear logic errors) + - Clear, unambiguous CLAUDE.md violations where you can quote the exact rule being broken -3. (bug due to ) + Do NOT flag: + - Code style or quality concerns + - Potential issues that depend on specific inputs or state + - Subjective suggestions or improvements - + If you are not certain an issue is real, do not flag it. False positives erode trust and waste reviewer time. -πŸ€– Generated with [Claude Code](https://claude.ai/code) + In addition to the above, each subagent should be told the PR title and description. This will help provide context regarding the author's intent. -- If this code review was useful, please react with πŸ‘. Otherwise, react with πŸ‘Ž. +5. For each issue found in the previous step by agents 3 and 4, launch parallel subagents to validate the issue. These subagents should get the PR title and description along with a description of the issue. The agent's job is to review the issue to validate that the stated issue is truly an issue with high confidence. For example, if an issue such as "variable is not defined" was flagged, the subagent's job would be to validate that is actually true in the code. Another example would be CLAUDE.md issues. The agent should validate that the CLAUDE.md rule that was violated is scoped for this file and is actually violated. Use Opus subagents for bugs and logic issues, and sonnet agents for CLAUDE.md violations. ---- +6. Filter out any issues that were not validated in step 5. This step will give us our list of high signal issues for our review. + +7. If issues were found, skip to step 8 to post inline comments directly. + + If NO issues were found, post a summary comment using `gh pr comment` (if `--comment` argument is provided): + "No issues found. Checked for bugs and CLAUDE.md compliance." + +8. Create a list of all comments that you plan on leaving. This is only for you to make sure you are comfortable with the comments. Do not post this list anywhere. -- Or, if you found no issues: +9. Post inline comments for each issue using `mcp__github_inline_comment__create_inline_comment`. For each comment: + - Provide a brief description of the issue + - For small, self-contained fixes, include a committable suggestion block + - For larger fixes (6+ lines, structural changes, or changes spanning multiple locations), describe the issue and suggested fix without a suggestion block + - Never post a committable suggestion UNLESS committing the suggestion fixes the issue entirely. If follow up steps are required, do not leave a committable suggestion. + + **IMPORTANT: Only post ONE comment per unique issue. Do not post duplicate comments.** + +Use this list when evaluating issues in Steps 4 and 5 (these are false positives, do NOT flag): + +- Pre-existing issues +- Something that appears to be a bug but is actually correct +- Pedantic nitpicks that a senior engineer would not flag +- Issues that a linter will catch (do not run the linter to verify) +- General code quality concerns (e.g., lack of test coverage, general security issues) unless explicitly required in CLAUDE.md +- Issues mentioned in CLAUDE.md but explicitly silenced in the code (e.g., via a lint ignore comment) + +Notes: + +- Use gh CLI to interact with GitHub (e.g., fetch pull requests, create comments). Do not use web fetch. +- Create a todo list before starting. +- You must cite and link each issue in inline comments (e.g., if referring to a CLAUDE.md, include a link to it). +- If no issues are found, post a comment with the following format: --- -### Code review +## Code review No issues found. Checked for bugs and CLAUDE.md compliance. -πŸ€– Generated with [Claude Code](https://claude.ai/code) +--- -- When linking to code, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-cli-internal/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15 +- When linking to code in inline comments, follow the following format precisely, otherwise the Markdown preview won't render correctly: https://github.com/anthropics/claude-code/blob/c21d3c10bc8e898b7ac1a2d745bdc9bc4e423afe/package.json#L10-L15 - Requires full git sha - You must provide the full sha. Commands like `https://github.com/owner/repo/blob/$(git rev-parse HEAD)/foo/bar` will not work, since your comment will be directly rendered in Markdown. - Repo name must match the repo you're code reviewing From d38df2d8b735b4ceababc6c6afc48f97a71118a3 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 19 Jan 2026 18:23:23 -0800 Subject: [PATCH 10/62] update code review prompt --- applications/claude/prompts/code-review.md | 34 +++++++--------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/applications/claude/prompts/code-review.md b/applications/claude/prompts/code-review.md index 668c5ac..4fabdab 100644 --- a/applications/claude/prompts/code-review.md +++ b/applications/claude/prompts/code-review.md @@ -5,31 +5,19 @@ description: Code review a pull request Provide a code review for the given pull request. -**Important: Draft PRs ARE allowed and should be reviewed. Do not skip a PR because it is a draft.** - **Agent assumptions (applies to all agents and subagents):** - All tools are functional and will work without error. Do not test tools or make exploratory calls. Make sure this is clear to every subagent that is launched. - Only call a tool if it is required to complete the task. Every tool call should have a clear purpose. To do this, follow these steps precisely: -1. Launch a haiku agent to check if any of the following are true: - - The pull request is closed - - The pull request does not need code review (e.g. automated PR, trivial change that is obviously correct) - - If any condition is true, stop and do not proceed. - - Note: Draft PRs are allowed - do NOT skip them. Previous Claude comments are allowed - re-reviews are permitted. - -Note: Still review Claude generated PR's. - -2. Launch a haiku agent to return a list of file paths (not their contents) for all relevant CLAUDE.md files including: +1. Launch a haiku agent to return a list of file paths (not their contents) for all relevant CLAUDE.md files including: - The root CLAUDE.md file, if it exists - Any CLAUDE.md files in directories containing files modified by the pull request -3. Launch a sonnet agent to view the pull request and return a summary of the changes +2. Launch a sonnet agent to view the pull request and return a summary of the changes -4. Launch 4 agents in parallel to independently review the changes. Each agent should return the list of issues, where each issue includes a description and the reason it was flagged (e.g. "CLAUDE.md adherence", "bug"). The agents should do the following: +3. Launch 4 agents in parallel to independently review the changes. Each agent should return the list of issues, where each issue includes a description and the reason it was flagged (e.g. "CLAUDE.md adherence", "bug"). The agents should do the following: Agents 1 + 2: CLAUDE.md compliance sonnet agents Audit changes for CLAUDE.md compliance in parallel. Note: When evaluating CLAUDE.md compliance for a file, you should only consider CLAUDE.md files that share a file path with the file or parents. @@ -54,18 +42,18 @@ Note: Still review Claude generated PR's. In addition to the above, each subagent should be told the PR title and description. This will help provide context regarding the author's intent. -5. For each issue found in the previous step by agents 3 and 4, launch parallel subagents to validate the issue. These subagents should get the PR title and description along with a description of the issue. The agent's job is to review the issue to validate that the stated issue is truly an issue with high confidence. For example, if an issue such as "variable is not defined" was flagged, the subagent's job would be to validate that is actually true in the code. Another example would be CLAUDE.md issues. The agent should validate that the CLAUDE.md rule that was violated is scoped for this file and is actually violated. Use Opus subagents for bugs and logic issues, and sonnet agents for CLAUDE.md violations. +4. For each issue found in the previous step by agents 3 and 4, launch parallel subagents to validate the issue. These subagents should get the PR title and description along with a description of the issue. The agent's job is to review the issue to validate that the stated issue is truly an issue with high confidence. For example, if an issue such as "variable is not defined" was flagged, the subagent's job would be to validate that is actually true in the code. Another example would be CLAUDE.md issues. The agent should validate that the CLAUDE.md rule that was violated is scoped for this file and is actually violated. Use Opus subagents for bugs and logic issues, and sonnet agents for CLAUDE.md violations. -6. Filter out any issues that were not validated in step 5. This step will give us our list of high signal issues for our review. +5. Filter out any issues that were not validated in step 4. This step will give us our list of high signal issues for our review. -7. If issues were found, skip to step 8 to post inline comments directly. +6. If issues were found, skip to step 7 to post inline comments directly. - If NO issues were found, post a summary comment using `gh pr comment` (if `--comment` argument is provided): + If NO issues were found, output to terminal only: "No issues found. Checked for bugs and CLAUDE.md compliance." -8. Create a list of all comments that you plan on leaving. This is only for you to make sure you are comfortable with the comments. Do not post this list anywhere. +7. Create a list of all comments that you plan on leaving. This is only for you to make sure you are comfortable with the comments. Do not post this list anywhere. -9. Post inline comments for each issue using `mcp__github_inline_comment__create_inline_comment`. For each comment: +8. Post inline comments for each issue using `mcp__github_inline_comment__create_inline_comment`. For each comment: - Provide a brief description of the issue - For small, self-contained fixes, include a committable suggestion block - For larger fixes (6+ lines, structural changes, or changes spanning multiple locations), describe the issue and suggested fix without a suggestion block @@ -73,7 +61,7 @@ Note: Still review Claude generated PR's. **IMPORTANT: Only post ONE comment per unique issue. Do not post duplicate comments.** -Use this list when evaluating issues in Steps 4 and 5 (these are false positives, do NOT flag): +Use this list when evaluating issues in Steps 3 and 4 (these are false positives, do NOT flag): - Pre-existing issues - Something that appears to be a bug but is actually correct @@ -87,7 +75,7 @@ Notes: - Use gh CLI to interact with GitHub (e.g., fetch pull requests, create comments). Do not use web fetch. - Create a todo list before starting. - You must cite and link each issue in inline comments (e.g., if referring to a CLAUDE.md, include a link to it). -- If no issues are found, post a comment with the following format: +- If no issues are found, output the following format to terminal: --- From 61535044842e30dc214c98254c06e4f074ed7b23 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 22 Jan 2026 11:10:56 -0800 Subject: [PATCH 11/62] add osx office apps --- docs/installation/002.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation/002.md b/docs/installation/002.md index f78f452..72bf12a 100644 --- a/docs/installation/002.md +++ b/docs/installation/002.md @@ -14,6 +14,9 @@ * [Skitch](https://evernote.com/products/skitch) * [Harvest](https://www.getharvest.com/) * [Slack](https://slack.com ) + * Numbers + * Pages + * Keynote 6. From the Web install and after each installation open to configure. * [AlfredApp](https://www.alfredapp.com/), and set to open via `cmd + shift + space`. * [Obsidian](https://obsidian.md/) From 6bd593562c1bc2c5c1e9b5203db5a871ceb9130f Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 25 Jan 2026 08:18:34 -0800 Subject: [PATCH 12/62] update gems --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index f4f399a..4183f7f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ PLATFORMS DEPENDENCIES RUBY VERSION - ruby 3.4.3p32 + ruby 4.0.0p0 BUNDLED WITH 2.6.6 From d61556f43bcfba3e3d6537bda80fcc1a7552e573 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 1 Feb 2026 08:20:27 -0800 Subject: [PATCH 13/62] add teamviwer to remote control ai bots --- docs/installation/003.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation/003.md b/docs/installation/003.md index 221c798..96255b6 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -88,6 +88,9 @@ GEM_PATH=.bundle * Configure `sync` via `GitHub`. 20. Configure the [Keyboard Maestro](https://www.keyboardmaestro.com/main/) macro library. * Desktop and Laptops will have very different settings, for example the laptop will have the brightness controls, the desktop may not, but both will have the player controls. +21. Intall [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remove control the desktops including the isolated AI assistant. + * [Lock it Up!](https://www.teamviewer.com/en-us/insights/unattended-access-security/) Immediately via 2FA. + ### Running a Desktop? From 7b997ba9c4ed561dc71761738fce3bd1adcfb4ca Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 1 Feb 2026 15:01:02 -0800 Subject: [PATCH 14/62] add teamviewer --- docs/installation/003.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/003.md b/docs/installation/003.md index 96255b6..1333239 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -90,7 +90,7 @@ GEM_PATH=.bundle * Desktop and Laptops will have very different settings, for example the laptop will have the brightness controls, the desktop may not, but both will have the player controls. 21. Intall [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remove control the desktops including the isolated AI assistant. * [Lock it Up!](https://www.teamviewer.com/en-us/insights/unattended-access-security/) Immediately via 2FA. - + * Setup `allow list` with only my TeamViewer account blocking any external users. ### Running a Desktop? From f75493e48eca89c81a70e56e3913b548c3f33e61 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 2 Feb 2026 14:26:23 -0800 Subject: [PATCH 15/62] add terminal-notifier --- lib/install.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/install.sh b/lib/install.sh index ab4168d..15222a7 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -29,6 +29,7 @@ install_brew_packages() { "gitleaks" "chezmoi" "gh" + "terminal-notifier" ) echo "Running brew update..." From 0e1831ea33a02b8610d748794deae98aa7ab4111 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 2 Feb 2026 14:35:22 -0800 Subject: [PATCH 16/62] extract ai settings --- aitooling/setup.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aitooling/setup.rb b/aitooling/setup.rb index 5d65b34..45fac51 100644 --- a/aitooling/setup.rb +++ b/aitooling/setup.rb @@ -4,11 +4,11 @@ claude: [ { source: "$HOME/.claude/settings.json", - destination: "$HOME/.myconfigurations/aitooling/claude/settings.json" + destination: "$HOME/.myconfigurations.ai/claude/settings.json" }, { source: "$HOME/.claude/CLAUDE.md", - destination: "$HOME/.myconfigurations/aitooling/claude/CLAUDE.md" + destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md" } ] } From 6552985ef74bb646597d0180b6b1b4f5c4909bcc Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 3 Feb 2026 06:06:33 -0800 Subject: [PATCH 17/62] remove teamviewer --- docs/installation/003.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/installation/003.md b/docs/installation/003.md index 1333239..221c798 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -88,9 +88,6 @@ GEM_PATH=.bundle * Configure `sync` via `GitHub`. 20. Configure the [Keyboard Maestro](https://www.keyboardmaestro.com/main/) macro library. * Desktop and Laptops will have very different settings, for example the laptop will have the brightness controls, the desktop may not, but both will have the player controls. -21. Intall [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remove control the desktops including the isolated AI assistant. - * [Lock it Up!](https://www.teamviewer.com/en-us/insights/unattended-access-security/) Immediately via 2FA. - * Setup `allow list` with only my TeamViewer account blocking any external users. ### Running a Desktop? From 139c2a30f7c8d21d602dffd570e07099a3505cf3 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 3 Feb 2026 06:54:18 -0800 Subject: [PATCH 18/62] add teamviewer --- docs/installation/003.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation/003.md b/docs/installation/003.md index 221c798..1333239 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -88,6 +88,9 @@ GEM_PATH=.bundle * Configure `sync` via `GitHub`. 20. Configure the [Keyboard Maestro](https://www.keyboardmaestro.com/main/) macro library. * Desktop and Laptops will have very different settings, for example the laptop will have the brightness controls, the desktop may not, but both will have the player controls. +21. Intall [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remove control the desktops including the isolated AI assistant. + * [Lock it Up!](https://www.teamviewer.com/en-us/insights/unattended-access-security/) Immediately via 2FA. + * Setup `allow list` with only my TeamViewer account blocking any external users. ### Running a Desktop? From 61db6b099d8ea77cfa08387d4eff32ff1d533e80 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 5 Feb 2026 12:59:23 -0800 Subject: [PATCH 19/62] porting ai tooling settings to new repo --- aitooling/claude/CLAUDE.md | 208 --------------------------------- aitooling/claude/settings.json | 58 --------- 2 files changed, 266 deletions(-) delete mode 100644 aitooling/claude/CLAUDE.md delete mode 100644 aitooling/claude/settings.json diff --git a/aitooling/claude/CLAUDE.md b/aitooling/claude/CLAUDE.md deleted file mode 100644 index 46c5a0b..0000000 --- a/aitooling/claude/CLAUDE.md +++ /dev/null @@ -1,208 +0,0 @@ -# Global Development Guidelines - -These are baseline guidelines for all projects. Project-specific CLAUDE.md files will override or extend these guidelines. - -You are an experienced software engineer with deep expertise in secure, scalable, and maintainable systems. Your role adapts based on project needs - from architectural design to implementation, code review to debugging. When no project-specific instructions exist, default to a Senior Software Architect mindset with 15+ years of experience. - -## Core Responsibilities - -When reviewing code or designs, you will: -- **Call out security issues** (e.g., injection, auth flaws, misconfigured cloud resources) -- **Highlight violations** of clean architecture or SOLID principles -- **Enforce scalability and maintainability** best practices -- **Point out missing tests** or observability gaps -- **Offer precise, actionable improvements** β€” not just vague feedback -- **Operate with real-world pragmatism**, balancing idealism with delivery realities - -## Project Context Detection - -Always analyze the project to determine: -1. **Primary Language**: Check file extensions, package managers, and config files -2. **Framework**: Identify from dependencies, project structure, and conventions -3. **Testing Framework**: Detect from test directories and configuration -4. **Code Style**: Look for linters/formatters (.rubocop.yml, .eslintrc, prettier.config, etc.) -5. **Build Tools**: Identify package managers, build systems, and deployment configs - -## Linter and Code Style Compliance - -**CRITICAL**: Always detect and strictly follow ALL project linters and formatters: -- **Read all linter configs** before writing any code (.rubocop.yml, .eslintrc, .prettierrc, pyproject.toml, etc.) -- **Apply all rules** from detected linters without exception -- **Check subdirectories** for additional linter configs (e.g., config/rubocop/) -- **Run linters** before considering any code complete -- **Never override** project-specific linting rules with general best practices -- **When multiple linters exist**, apply all of them in the appropriate order -- **NEVER automatically disable linter rules** without explicit permission - always ask first before adding ignore comments, disabling rules, or modifying linter configurations - -## Language-Specific Guidelines - -### Ruby/Rails Projects -- **Mental Model**: Think like Sandy Metz (Practical Object-Oriented Design in Ruby) -- **Style**: Apply RuboCop rules from `.rubocop.yml` and `config/rubocop/` -- **Framework Patterns**: Thin controllers, service objects, query objects -- **Testing**: RSpec with FactoryBot, following AAA pattern -- **Database**: PostgreSQL by default, prevent N+1 queries -- **Background Jobs**: Check for Sidekiq, Solid Queue, or ActiveJob - -### JavaScript/TypeScript Projects -- **Modern Patterns**: ES6+, functional programming where appropriate -- **Framework Conventions**: React (hooks, composition), Vue (Composition API), Angular (RxJS) -- **Testing**: Jest/Vitest, React Testing Library, Cypress/Playwright -- **Build Tools**: Webpack, Vite, ESBuild configurations -- **Type Safety**: Enforce strict TypeScript when available -- **State Management**: Context API, Redux Toolkit, Zustand patterns - -### Python Projects -- **Style**: PEP 8, Black formatting, type hints -- **Frameworks**: Django (MTV), FastAPI (async), Flask (minimal) -- **Testing**: pytest with fixtures, unittest for legacy -- **Package Management**: Poetry, pip-tools, or requirements.txt -- **Async Patterns**: asyncio best practices when applicable - -## Universal Best Practices - -### Security First -- **Input Validation**: Always validate and sanitize user input -- **Authentication/Authorization**: Implement proper access controls -- **File Permissions**: **NEVER run `chmod` commands** unless explicitly instructed by the user - - File permissions are security-critical and changes can expose sensitive data - - Always ask for explicit permission before modifying file permissions - - If permission issues arise, report them and ask the user how to proceed -- **Secrets Management**: Never hardcode credentials, use environment variables - - **NEVER read `.env` files** - they contain sensitive credentials, API keys, and secrets - - Use `.env.example` as reference for required environment variables - - For environment configuration questions, refer to `.env.example` only - - **NEVER read Rails secrets files** (`config/secrets.yml`, `config/credentials.yml.enc`, `config/master.key`) - they contain encrypted credentials and sensitive application secrets - - If asked about Rails secrets, only provide commands to view them (e.g., `rails credentials:edit`, `rails credentials:show`) - NEVER read the files directly -- **Dependencies**: Regular updates, vulnerability scanning -- **OWASP Top 10**: Always consider common vulnerabilities - -### Architecture & Design -- **SOLID Principles**: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion -- **Clean Architecture**: Separation of concerns, dependency rule -- **Design Patterns**: Apply appropriately, avoid over-engineering -- **API Design**: RESTful principles, GraphQL best practices, gRPC patterns -- **Microservices**: When beneficial, with proper service boundaries - -### Code Quality -- **Readability**: Clear naming, self-documenting code -- **Testing**: Unit, integration, E2E with appropriate coverage - - **NEVER mark tests as skipped** (skip, xit, it.skip, @skip, etc.) - fix them or ask for guidance - - **When working with specs**: Never skip as a solution, always fix the underlying issue - - **Maintain all existing tests** in working condition - - **Fix failing tests** rather than disabling them - - **Test Data Creation**: - - **Always use factories** (FactoryBot, factory_girl, etc.) if they exist in the project - - **NEVER create test data directly** in specs - use the project's factory pattern - - **Check for existing factories** before creating new test data - - **Follow project conventions** for traits, sequences, and associations -- **Documentation**: Meaningful comments, API docs, architecture decisions -- **Error Handling**: Graceful degradation, proper logging -- **Performance**: Profiling before optimizing, caching strategies - -### DevOps & Operations -- **CI/CD**: Automated testing, deployment pipelines -- **Containerization**: Docker best practices, multi-stage builds -- **Orchestration**: Kubernetes patterns, Helm charts -- **Monitoring**: Metrics, logs, traces (OpenTelemetry) -- **Infrastructure as Code**: Terraform, CloudFormation, Pulumi - -### Cloud & Scalability -- **Heroku (Primary)**: - - Leverage buildpacks and add-ons ecosystem - - Use Heroku Postgres, Redis, and managed services - - Configure dynos for appropriate scaling - - Implement Heroku CI/CD pipelines - - Monitor with Heroku metrics and logging -- **AWS (Secondary)**: - - S3 for object storage and static assets - - CloudFront for CDN distribution - - RDS for managed databases when needed - - ElastiCache for Redis clustering - - Lambda for serverless functions -- **Horizontal Scaling**: Stateless design, load balancing -- **Caching**: Redis (Heroku Redis or ElastiCache), CDNs, application-level caching -- **Message Queues**: RabbitMQ, Kafka, AWS SQS/SNS -- **Database Scaling**: Read replicas, connection pooling, follower databases - -### Database Performance & Optimization - -#### PostgreSQL Optimization -- **Query Performance**: - - Always use `EXPLAIN ANALYZE` for slow queries - - Create appropriate indexes (B-tree, GIN, GiST, BRIN) - - Use partial indexes for filtered queries - - Implement covering indexes to avoid heap lookups - - Monitor and eliminate N+1 queries -- **Configuration Tuning**: - - Adjust `shared_buffers` (typically 25% of RAM) - - Configure `work_mem` for complex queries - - Set appropriate `max_connections` with connection pooling - - Enable `pg_stat_statements` for query analysis - - Configure autovacuum for optimal performance -- **Schema Design**: - - Use appropriate data types (avoid overusing TEXT) - - Implement table partitioning for large datasets - - Consider JSONB for semi-structured data - - Use materialized views for expensive aggregations - - Implement proper foreign key constraints with indexes -- **Connection Management**: - - Use PgBouncer or similar for connection pooling - - Configure appropriate pool sizes - - Implement prepared statements wisely - - Monitor connection states and idle transactions - -#### Redis Optimization -- **Data Structure Selection**: - - Use appropriate data types (strings, hashes, lists, sets, sorted sets) - - Implement HyperLogLog for cardinality estimation - - Use bitmaps for boolean tracking - - Consider Redis Streams for event sourcing -- **Memory Management**: - - Configure `maxmemory` and eviction policies (LRU, LFU, TTL) - - Use key expiration strategically - - Monitor memory fragmentation - - Implement key naming conventions for organization -- **Performance Patterns**: - - Use pipelining for bulk operations - - Implement Lua scripts for atomic operations - - Use Redis transactions (MULTI/EXEC) appropriately - - Consider Redis Cluster for horizontal scaling -- **Persistence Strategy**: - - Choose between RDB snapshots and AOF logs - - Configure appropriate save intervals - - Monitor replication lag in master-slave setups - - Implement proper backup strategies - -#### General Database Performance -- **Monitoring & Metrics**: - - Track query execution times and frequency - - Monitor connection pool utilization - - Watch for lock contention and deadlocks - - Set up alerts for slow queries and high load -- **Caching Strategy**: - - Implement multi-tier caching (application, Redis, CDN) - - Use cache-aside pattern for read-heavy workloads - - Implement write-through for consistency - - Consider time-based and event-based invalidation -- **Data Access Patterns**: - - Batch operations when possible - - Use read replicas for analytics queries - - Implement database sharding for massive scale - - Consider CQRS for complex domains - -## Working Principles - -1. **Context Awareness**: Always check project-specific configurations and conventions -2. **Linter Compliance First**: Project linters override any general style preferences -3. **Pragmatic Approach**: Balance perfect architecture with delivery timelines -4. **Continuous Learning**: Stay updated with evolving best practices -5. **Clear Communication**: Explain the "why" behind recommendations -6. **Risk Assessment**: Prioritize critical issues over minor improvements - -## File Organization -- **Temporary Files**: Always use project's `./tmp` or `.tmp` directory -- **Project Structure**: Follow language/framework conventions -- **Configuration**: Respect .gitignore and tool-specific exclusions - -> **Important**: Always analyze the specific project context first. Check for configuration files, dependencies, and existing patterns. Project-specific conventions always take precedence over these global guidelines. Local CLAUDE.md files in project directories override these defaults. When uncertain, ask for clarification rather than making assumptions. diff --git a/aitooling/claude/settings.json b/aitooling/claude/settings.json deleted file mode 100644 index 82b1548..0000000 --- a/aitooling/claude/settings.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-settings.json", - "enabledPlugins": { - "frontend-design@claude-plugins-official": true, - "playwright@claude-plugins-official": true, - "code-simplifier@claude-plugins-official": true, - "code-review@claude-code-plugins": true - }, - "feedbackSurveyState": { - "lastShownTime": 1754090209216 - }, - "sandboxing": { - "Read": { - "deny": [ - "**/.env", - "**/.env.*", - "**/config/secrets.yml", - "**/config/credentials.yml.enc", - "**/config/master.key", - "**/.myconfigurations.private", - "**/*.pem", - "**/*.key", - "**/*.p12", - "**/*.pfx", - "**/*.secret", - "**/*.secrets", - "**/*credentials*", - "~/.ssh/**", - "~/.gnupg/**", - "**/.aws/credentials", - "**/.aws/config", - "**/secrets/**", - "**/*.keystore", - "**/*.jks", - ".myconfigurations.private" - ] - }, - "Bash": { - "deny": [ - "**/chmod **/.env*", - "**/chmod **/config/secrets.yml", - "**/chmod **/config/credentials.yml.enc", - "**/chmod **/config/master.key", - "**/chmod **/.myconfigurations.private", - "**/cat **/.env*", - "**/cat **/config/secrets.yml", - "**/cat **/config/credentials.yml.enc", - "**/head **/.env*", - "**/tail **/.env*", - "**/less **/.env*", - "**/more **/.env*", - "**/vim **/.env*", - "**/nano **/.env*", - ".myconfigurations.private" - ] - } - } -} \ No newline at end of file From 0ae0a86a609298d675732a76b9d35d74f0b43c1a Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 10 Feb 2026 09:29:25 -0800 Subject: [PATCH 20/62] update plugins --- docs/installation/004.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index c81168b..73600b9 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -14,6 +14,6 @@ #### Favorite Skills in Claude * @anthropics/claude-plugins-official/playwright -* @anthropics/claude-plugins-official/playwrigcode-simplifierht +* @anthropics/claude-plugins-official/code-simplifier * @anthropics/claude-plugins-official/frontend-design * @anthropics/claude-plugins-official/code-review From fb1f456b4817c5fab09c1bd852b8aed007ce68d0 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 10 Feb 2026 13:30:57 -0800 Subject: [PATCH 21/62] update AI tooling --- aitooling/setup.rb | 24 ------------------------ applications/claude/.keep | 0 applications/claude/setup.rb | 27 +++++++++++++++++++++++++++ applications/setup.rb | 10 ++++++++++ lib/setup.rb | 1 - 5 files changed, 37 insertions(+), 25 deletions(-) delete mode 100644 aitooling/setup.rb delete mode 100644 applications/claude/.keep create mode 100644 applications/claude/setup.rb diff --git a/aitooling/setup.rb b/aitooling/setup.rb deleted file mode 100644 index 45fac51..0000000 --- a/aitooling/setup.rb +++ /dev/null @@ -1,24 +0,0 @@ -PATHS ||= [] -PATHS.concat([ - { - claude: [ - { - source: "$HOME/.claude/settings.json", - destination: "$HOME/.myconfigurations.ai/claude/settings.json" - }, - { - source: "$HOME/.claude/CLAUDE.md", - destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md" - } - ] - } -]) - - - - - - - - - diff --git a/applications/claude/.keep b/applications/claude/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb new file mode 100644 index 0000000..f58aec1 --- /dev/null +++ b/applications/claude/setup.rb @@ -0,0 +1,27 @@ +#!/usr/bin/env ruby +# ruby "$HOME/.myconfigurations/applications/claude/setup.rb" + +require_relative 'helpers/setup_helper' + +PATHS ||= [] +PATHS.concat([ + { + claude: [ + { + source: ".claude/settings.local.json", + destination: "$HOME/.myconfigurations.ai/claude/local/settings.json" + } + ] + } +]) + +# Process all collected paths +SetupHelper.process_paths(PATHS) + + + + + + + + diff --git a/applications/setup.rb b/applications/setup.rb index 3743f85..5e25595 100644 --- a/applications/setup.rb +++ b/applications/setup.rb @@ -7,6 +7,16 @@ destination: "$HOME/.myconfigurations/applications/tmux/conf" } ], + claude: [ + { + source: "$HOME/.claude/settings.json", + destination: "$HOME/.myconfigurations.ai/claude/global/settings.json" + }, + { + source: "$HOME/.claude/CLAUDE.md", + destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md" + } + ], vscode: [ { source: "$HOME/Library/Application Support/Code/User/settings.json", diff --git a/lib/setup.rb b/lib/setup.rb index 5f912c0..1b6e045 100644 --- a/lib/setup.rb +++ b/lib/setup.rb @@ -7,7 +7,6 @@ PATHS = [] # Load all setup files - they will append to PATHS -require_relative '../aitooling/setup' require_relative '../dotfiles/setup' require_relative '../applications/setup' From d4cccce325450dea42f3f591b11ad8a98caaf73b Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 10 Feb 2026 22:15:44 -0800 Subject: [PATCH 22/62] add specs to cover the ruby files --- .rspec | 3 + Gemfile | 4 + Gemfile.lock | 15 ++ spec/helpers/path_helper_spec.rb | 134 +++++++++++ spec/helpers/setup_helper_spec.rb | 372 ++++++++++++++++++++++++++++++ spec/spec_helper.rb | 20 ++ 6 files changed, 548 insertions(+) create mode 100644 .rspec create mode 100644 spec/helpers/path_helper_spec.rb create mode 100644 spec/helpers/setup_helper_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..7a2cc1a --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--format documentation +--color diff --git a/Gemfile b/Gemfile index 75cf79c..87fd054 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,7 @@ source "https://rubygems.org" git_source( :github ) { |repo| "https://github.com/#{repo}.git" } ruby "4.0.0" + +group :test do + gem "rspec", "~> 3.13" +end diff --git a/Gemfile.lock b/Gemfile.lock index 4183f7f..55a96c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,27 @@ GEM remote: https://rubygems.org/ specs: + diff-lcs (1.6.2) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.7) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) PLATFORMS ruby x86_64-darwin-24 DEPENDENCIES + rspec (~> 3.13) RUBY VERSION ruby 4.0.0p0 diff --git a/spec/helpers/path_helper_spec.rb b/spec/helpers/path_helper_spec.rb new file mode 100644 index 0000000..3b50874 --- /dev/null +++ b/spec/helpers/path_helper_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'helpers/path_helper' + +# PathHelper.expand_shell_path takes a path string that may contain shell-style +# variables ($HOME, $USER, $PWD) and returns a fully expanded absolute path. +# It performs simple string substitution β€” not a full shell eval β€” then delegates +# to File.expand_path for final resolution. +RSpec.describe PathHelper do + describe '.expand_shell_path' do + # -------------------------------------------------------------------------- + # $HOME expansion + # The method replaces the literal string "$HOME" with ENV['HOME']. + # This is the most common variable used in the project's setup configs + # (e.g., "$HOME/.tmux.conf" β†’ "/Users/christopherhough/.tmux.conf"). + # -------------------------------------------------------------------------- + context 'when the path contains $HOME' do + it 'replaces $HOME with the HOME environment variable' do + path = '$HOME/.config/settings.json' + result = described_class.expand_shell_path(path) + + expect(result).to eq(File.join(ENV['HOME'], '.config/settings.json')) + end + + # Paths from applications/setup.rb use $HOME in both source and destination, + # e.g., "$HOME/.claude/settings.json". Verify multiple $HOME tokens in a + # single string are all replaced. + it 'replaces multiple occurrences of $HOME' do + path = '$HOME/from/$HOME/to' + result = described_class.expand_shell_path(path) + + expected = File.join(ENV['HOME'], 'from', ENV['HOME'], 'to') + expect(result).to eq(expected) + end + end + + # -------------------------------------------------------------------------- + # $USER expansion + # The method replaces "$USER" with ENV['USER'], but only when that + # environment variable is set. This guards against nil substitution. + # -------------------------------------------------------------------------- + context 'when the path contains $USER' do + it 'replaces $USER with the USER environment variable' do + path = '/home/$USER/projects' + result = described_class.expand_shell_path(path) + + expect(result).to eq("/home/#{ENV['USER']}/projects") + end + end + + # When ENV['USER'] is nil (e.g., inside some containerized environments), + # the $USER token must be left untouched so it doesn't blow up. + context 'when ENV["USER"] is nil' do + around do |example| + original_user = ENV['USER'] + ENV.delete('USER') + example.run + ensure + ENV['USER'] = original_user + end + + it 'leaves $USER unexpanded in the path' do + path = '/home/$USER/projects' + result = described_class.expand_shell_path(path) + + # File.expand_path won't touch the literal "$USER" text, so it stays. + expect(result).to include('$USER') + end + end + + # -------------------------------------------------------------------------- + # $PWD expansion + # Replaces "$PWD" with Dir.pwd β€” useful when paths are relative to the + # directory the setup script is invoked from. + # -------------------------------------------------------------------------- + context 'when the path contains $PWD' do + it 'replaces $PWD with the current working directory' do + path = '$PWD/lib/helpers' + result = described_class.expand_shell_path(path) + + expect(result).to eq(File.join(Dir.pwd, 'lib/helpers')) + end + end + + # -------------------------------------------------------------------------- + # Combined variables + # The project's setup files can theoretically mix variables. Ensure all + # three are expanded in a single pass. + # -------------------------------------------------------------------------- + context 'when the path contains multiple different variables' do + it 'expands all recognized variables' do + path = '$HOME/$USER/$PWD' + result = described_class.expand_shell_path(path) + + # After substitution the path is processed by File.expand_path, which + # resolves it to an absolute path. We check that none of the variable + # tokens remain in the result. + expect(result).not_to include('$HOME') + expect(result).not_to include('$USER') + expect(result).not_to include('$PWD') + expect(result).to include(ENV['HOME']) + end + end + + # -------------------------------------------------------------------------- + # No variables β€” passthrough + # A plain absolute path should be returned unchanged (after expand_path + # normalization). + # -------------------------------------------------------------------------- + context 'when the path contains no variables' do + it 'returns the path resolved by File.expand_path' do + path = '/usr/local/bin/tool' + result = described_class.expand_shell_path(path) + + expect(result).to eq('/usr/local/bin/tool') + end + end + + # -------------------------------------------------------------------------- + # Relative path resolution + # File.expand_path converts relative paths to absolute ones using the + # current working directory as the base. Verify this final step works. + # -------------------------------------------------------------------------- + context 'when the path is relative after substitution' do + it 'resolves it to an absolute path' do + path = 'relative/path/file.txt' + result = described_class.expand_shell_path(path) + + expect(result).to eq(File.expand_path('relative/path/file.txt')) + expect(Pathname.new(result)).to be_absolute + end + end + end +end diff --git a/spec/helpers/setup_helper_spec.rb b/spec/helpers/setup_helper_spec.rb new file mode 100644 index 0000000..517a8f4 --- /dev/null +++ b/spec/helpers/setup_helper_spec.rb @@ -0,0 +1,372 @@ +# frozen_string_literal: true + +require 'tmpdir' +require 'fileutils' +require 'helpers/setup_helper' + +# SetupHelper.process_paths receives a nested data structure (Array of Hashes) +# that mirrors the format used in applications/setup.rb and dotfiles/setup.rb. +# +# Example input (from applications/setup.rb): +# +# [ +# { +# tmux: [ +# { source: "$HOME/.tmux.conf", +# destination: "$HOME/.myconfigurations/applications/tmux/conf" } +# ], +# claude: [ +# { source: "$HOME/.claude/settings.json", +# destination: "$HOME/.myconfigurations.ai/claude/global/settings.json" }, +# { source: "$HOME/.claude/CLAUDE.md", +# destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md" } +# ] +# } +# ] +# +# For each entry the method creates a symlink at `source` pointing to +# `destination`, creating parent directories as needed. It handles several +# edge cases: existing correct symlinks (skip), incorrect symlinks (replace), +# and regular files at the source location (skip with warning). +# +# All specs use a temporary directory so the real filesystem is never touched. +RSpec.describe SetupHelper do + describe '.process_paths' do + # Create a fresh temp directory for every example and clean it up after. + # We override $HOME so that PathHelper.expand_shell_path resolves "$HOME" + # to our sandbox instead of the real home directory. + let(:tmpdir) { Dir.mktmpdir('setup_helper_spec') } + + before do + @original_home = ENV['HOME'] + ENV['HOME'] = tmpdir + end + + after do + ENV['HOME'] = @original_home + FileUtils.rm_rf(tmpdir) + end + + # Helper to suppress puts output during tests so spec output stays clean. + around do |example| + original_stdout = $stdout + $stdout = StringIO.new + example.run + ensure + $stdout = original_stdout + end + + # -------------------------------------------------------------------------- + # Basic symlink creation + # The most common path: source does not exist yet, destination file exists, + # and we expect a symlink to be created at source pointing to destination. + # -------------------------------------------------------------------------- + context 'when the source does not exist' do + it 'creates a symlink from source to destination' do + # Simulate a destination config file that already exists in the repo. + dest_file = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + FileUtils.mkdir_p(File.dirname(dest_file)) + File.write(dest_file, 'tmux config content') + + # This mirrors the tmux entry from applications/setup.rb. + paths = [ + { + tmux: [ + { + source: '$HOME/.tmux.conf', + destination: '$HOME/.myconfigurations/applications/tmux/conf' + } + ] + } + ] + + described_class.process_paths(paths) + + source_path = File.join(tmpdir, '.tmux.conf') + + # The source should now be a symlink pointing to the destination. + expect(File.symlink?(source_path)).to be true + expect(File.readlink(source_path)).to eq(dest_file) + end + end + + # -------------------------------------------------------------------------- + # Correct symlink already exists β€” skip + # When re-running setup, existing correct symlinks should not be recreated. + # The method prints "Symlink already correct" and calls `next`. + # -------------------------------------------------------------------------- + context 'when a correct symlink already exists at source' do + it 'leaves the symlink unchanged' do + source_path = File.join(tmpdir, '.tmux.conf') + dest_file = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + FileUtils.mkdir_p(File.dirname(dest_file)) + File.write(dest_file, 'tmux config') + + # Pre-create the correct symlink. + File.symlink(dest_file, source_path) + + paths = [ + { + tmux: [ + { + source: '$HOME/.tmux.conf', + destination: '$HOME/.myconfigurations/applications/tmux/conf' + } + ] + } + ] + + described_class.process_paths(paths) + + # Symlink should still exist and still point to the same destination. + expect(File.symlink?(source_path)).to be true + expect(File.readlink(source_path)).to eq(dest_file) + end + end + + # -------------------------------------------------------------------------- + # Incorrect symlink β€” replace + # If a symlink exists but points to the wrong target (e.g., after moving + # config files), the method deletes the old symlink and creates a new one. + # -------------------------------------------------------------------------- + context 'when an incorrect symlink exists at source' do + it 'removes the old symlink and creates a correct one' do + source_path = File.join(tmpdir, '.tmux.conf') + wrong_dest = File.join(tmpdir, 'old/wrong/path') + correct_dest = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + + FileUtils.mkdir_p(File.dirname(wrong_dest)) + FileUtils.mkdir_p(File.dirname(correct_dest)) + File.write(correct_dest, 'correct config') + + # Pre-create a symlink pointing to the wrong location. + File.symlink(wrong_dest, source_path) + + paths = [ + { + tmux: [ + { + source: '$HOME/.tmux.conf', + destination: '$HOME/.myconfigurations/applications/tmux/conf' + } + ] + } + ] + + described_class.process_paths(paths) + + # The symlink should now point to the correct destination. + expect(File.symlink?(source_path)).to be true + expect(File.readlink(source_path)).to eq(correct_dest) + end + end + + # -------------------------------------------------------------------------- + # Regular file exists at source β€” skip + # If a real (non-symlink) file exists at the source location, the method + # refuses to overwrite it. This protects user data that hasn't been moved + # into the configuration repo yet. + # -------------------------------------------------------------------------- + context 'when a regular file exists at source' do + it 'does not create a symlink and leaves the file intact' do + source_path = File.join(tmpdir, '.tmux.conf') + dest_file = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + FileUtils.mkdir_p(File.dirname(dest_file)) + File.write(dest_file, 'repo config') + + # Create a real file (not a symlink) at the source path. + File.write(source_path, 'user local config') + + paths = [ + { + tmux: [ + { + source: '$HOME/.tmux.conf', + destination: '$HOME/.myconfigurations/applications/tmux/conf' + } + ] + } + ] + + described_class.process_paths(paths) + + # The regular file should remain untouched β€” no symlink created. + expect(File.symlink?(source_path)).to be false + expect(File.read(source_path)).to eq('user local config') + end + end + + # -------------------------------------------------------------------------- + # Parent directory creation + # Both source and destination directories are created automatically with + # FileUtils.mkdir_p if they don't exist. This is important for first-time + # setup on a fresh machine where directories like ~/.claude/ may not exist. + # -------------------------------------------------------------------------- + context 'when parent directories do not exist' do + it 'creates source and destination parent directories' do + # Use a deeply nested path that definitely doesn't exist yet. + paths = [ + { + newtool: [ + { + source: '$HOME/.config/newtool/deep/settings.json', + destination: '$HOME/.myconfigurations/apps/newtool/deep/settings.json' + } + ] + } + ] + + described_class.process_paths(paths) + + source_dir = File.join(tmpdir, '.config/newtool/deep') + dest_dir = File.join(tmpdir, '.myconfigurations/apps/newtool/deep') + + # Both directory trees should have been created. + expect(Dir.exist?(source_dir)).to be true + expect(Dir.exist?(dest_dir)).to be true + end + end + + # -------------------------------------------------------------------------- + # Multiple tools in a single path group + # The applications/setup.rb file defines tmux, claude, and vscode in a + # single hash. Verify that all tools in the group are processed. + # -------------------------------------------------------------------------- + context 'when processing multiple tools in one path group' do + it 'creates symlinks for every tool entry' do + # Set up destination files for two tools. + tmux_dest = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + claude_dest = File.join(tmpdir, '.myconfigurations.ai/claude/global/settings.json') + + FileUtils.mkdir_p(File.dirname(tmux_dest)) + FileUtils.mkdir_p(File.dirname(claude_dest)) + File.write(tmux_dest, 'tmux') + File.write(claude_dest, 'claude') + + # This mirrors the structure from applications/setup.rb with two tools. + paths = [ + { + tmux: [ + { + source: '$HOME/.tmux.conf', + destination: '$HOME/.myconfigurations/applications/tmux/conf' + } + ], + claude: [ + { + source: '$HOME/.claude/settings.json', + destination: '$HOME/.myconfigurations.ai/claude/global/settings.json' + } + ] + } + ] + + described_class.process_paths(paths) + + tmux_source = File.join(tmpdir, '.tmux.conf') + claude_source = File.join(tmpdir, '.claude/settings.json') + + # Both tools should have their symlinks created. + expect(File.symlink?(tmux_source)).to be true + expect(File.readlink(tmux_source)).to eq(tmux_dest) + + expect(File.symlink?(claude_source)).to be true + expect(File.readlink(claude_source)).to eq(claude_dest) + end + end + + # -------------------------------------------------------------------------- + # Multiple path entries for a single tool + # Claude has two path entries (settings.json and CLAUDE.md). + # VSCode has three (settings, keybindings, snippets). + # Verify all entries under one tool key are processed. + # -------------------------------------------------------------------------- + context 'when a tool has multiple path entries' do + it 'creates a symlink for each path entry' do + settings_dest = File.join(tmpdir, '.myconfigurations.ai/claude/global/settings.json') + claude_md_dest = File.join(tmpdir, '.myconfigurations.ai/claude/brains/global/CLAUDE.md') + + FileUtils.mkdir_p(File.dirname(settings_dest)) + FileUtils.mkdir_p(File.dirname(claude_md_dest)) + File.write(settings_dest, '{}') + File.write(claude_md_dest, '# CLAUDE') + + # Mirrors the claude entry from applications/setup.rb exactly. + paths = [ + { + claude: [ + { + source: '$HOME/.claude/settings.json', + destination: '$HOME/.myconfigurations.ai/claude/global/settings.json' + }, + { + source: '$HOME/.claude/CLAUDE.md', + destination: '$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md' + } + ] + } + ] + + described_class.process_paths(paths) + + settings_source = File.join(tmpdir, '.claude/settings.json') + claude_md_source = File.join(tmpdir, '.claude/CLAUDE.md') + + expect(File.symlink?(settings_source)).to be true + expect(File.readlink(settings_source)).to eq(settings_dest) + + expect(File.symlink?(claude_md_source)).to be true + expect(File.readlink(claude_md_source)).to eq(claude_md_dest) + end + end + + # -------------------------------------------------------------------------- + # Multiple path groups (separate array elements) + # The main lib/setup.rb loads both dotfiles/setup.rb and + # applications/setup.rb, each appending a separate hash to the PATHS array. + # Verify that multiple array elements are all processed. + # -------------------------------------------------------------------------- + context 'when processing multiple path groups' do + it 'processes each group independently' do + dotfile_dest = File.join(tmpdir, '.myconfigurations/dotfiles/.zshrc') + app_dest = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + + FileUtils.mkdir_p(File.dirname(dotfile_dest)) + FileUtils.mkdir_p(File.dirname(app_dest)) + File.write(dotfile_dest, 'zsh config') + File.write(app_dest, 'tmux config') + + # Two separate path groups, as if loaded from two different setup files. + paths = [ + { + dotfiles: [ + { + source: '$HOME/.zshrc', + destination: '$HOME/.myconfigurations/dotfiles/.zshrc' + } + ] + }, + { + tmux: [ + { + source: '$HOME/.tmux.conf', + destination: '$HOME/.myconfigurations/applications/tmux/conf' + } + ] + } + ] + + described_class.process_paths(paths) + + zshrc_source = File.join(tmpdir, '.zshrc') + tmux_source = File.join(tmpdir, '.tmux.conf') + + expect(File.symlink?(zshrc_source)).to be true + expect(File.readlink(zshrc_source)).to eq(dotfile_dest) + + expect(File.symlink?(tmux_source)).to be true + expect(File.readlink(tmux_source)).to eq(app_dest) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..83f572a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Add the project's lib directory to the load path so we can require helpers directly. +$LOAD_PATH.unshift File.expand_path('../lib', __dir__) + +RSpec.configure do |config| + # Use expect syntax exclusively (no `should`). + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # Prevent monkey-patching of objects with `should`. + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + # Run specs in random order to surface order-dependent bugs. + config.order = :random + Kernel.srand config.seed +end From 1818351a109e1cc1a9fd9c410010135e9be2dc73 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 10 Feb 2026 22:37:55 -0800 Subject: [PATCH 23/62] add rubocop and fix cops --- .rubocop.yml | 28 +++ Gemfile | 8 +- Gemfile.lock | 40 +++++ applications/claude/setup.rb | 26 ++- applications/setup.rb | 33 ++-- config/rubocop/layout.yml | 42 +++++ config/rubocop/metrics.yml | 20 +++ config/rubocop/rspec.yml | 47 +++++ config/rubocop/style.yml | 46 +++++ dotfiles/.pryrc | 18 +- dotfiles/setup.rb | 37 ++-- lib/helpers/path_helper.rb | 14 +- lib/helpers/setup_helper.rb | 50 +++--- lib/setup.rb | 12 +- spec/helpers/path_helper_spec.rb | 104 +++++------ spec/helpers/setup_helper_spec.rb | 286 +++++++++++++++--------------- spec/spec_helper.rb | 2 +- 17 files changed, 511 insertions(+), 302 deletions(-) create mode 100644 .rubocop.yml mode change 100644 => 100755 applications/claude/setup.rb create mode 100644 config/rubocop/layout.yml create mode 100644 config/rubocop/metrics.yml create mode 100644 config/rubocop/rspec.yml create mode 100644 config/rubocop/style.yml mode change 100644 => 100755 lib/setup.rb diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..586622c --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,28 @@ +plugins: + - rubocop-rspec + - rubocop-performance + +AllCops: + TargetRubyVersion: 4.0 + EnabledByDefault: true + DisplayCopNames: true + Exclude: + - tmp/**/* + - vendor/**/* + - .ruby-lsp/**/* + +Bundler/GemComment: + Enabled: false +Bundler/GemVersion: + Enabled: false +Bundler/OrderedGems: + Enabled: false + +Lint/ConstantResolution: + Enabled: false + +inherit_from: + - config/rubocop/metrics.yml + - config/rubocop/style.yml + - config/rubocop/layout.yml + - config/rubocop/rspec.yml diff --git a/Gemfile b/Gemfile index 87fd054..cfec110 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,16 @@ # frozen_string_literal: true -# rubocop:disable Layout/EmptyLines - source "https://rubygems.org" git_source( :github ) { |repo| "https://github.com/#{repo}.git" } ruby "4.0.0" +group :development, :test do + gem "rubocop", require: false + gem "rubocop-performance", require: false + gem "rubocop-rspec", require: false +end + group :test do gem "rspec", "~> 3.13" end diff --git a/Gemfile.lock b/Gemfile.lock index 55a96c7..d6cf51f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,19 @@ GEM remote: https://rubygems.org/ specs: + ast (2.4.3) diff-lcs (1.6.2) + json (2.18.1) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + parallel (1.27.0) + parser (3.3.10.1) + ast (~> 2.4.1) + racc + prism (1.9.0) + racc (1.8.1) + rainbow (3.1.1) + regexp_parser (2.11.3) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -15,6 +27,31 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.7) + rubocop (1.84.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.0) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-performance (1.26.1) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.47.1, < 2.0) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + ruby-progressbar (1.13.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) PLATFORMS ruby @@ -22,6 +59,9 @@ PLATFORMS DEPENDENCIES rspec (~> 3.13) + rubocop + rubocop-performance + rubocop-rspec RUBY VERSION ruby 4.0.0p0 diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb old mode 100644 new mode 100755 index f58aec1..87e810b --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -1,27 +1,21 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + # ruby "$HOME/.myconfigurations/applications/claude/setup.rb" -require_relative 'helpers/setup_helper' +require_relative "helpers/setup_helper" -PATHS ||= [] -PATHS.concat([ +PATHS = [].freeze +PATHS.push( { claude: [ { source: ".claude/settings.local.json", - destination: "$HOME/.myconfigurations.ai/claude/local/settings.json" + destination: "$HOME/.myconfigurations.ai/claude/local/settings.json", } - ] - } -]) + ], + }, +) # Process all collected paths -SetupHelper.process_paths(PATHS) - - - - - - - - +SetupHelper.process_paths( PATHS ) diff --git a/applications/setup.rb b/applications/setup.rb index 5e25595..a4b7787 100644 --- a/applications/setup.rb +++ b/applications/setup.rb @@ -1,44 +1,37 @@ -PATHS ||= [] -PATHS.concat([ +# frozen_string_literal: true + +PATHS = [].freeze +PATHS.push( { tmux: [ { source: "$HOME/.tmux.conf", - destination: "$HOME/.myconfigurations/applications/tmux/conf" + destination: "$HOME/.myconfigurations/applications/tmux/conf", } ], claude: [ { source: "$HOME/.claude/settings.json", - destination: "$HOME/.myconfigurations.ai/claude/global/settings.json" + destination: "$HOME/.myconfigurations.ai/claude/global/settings.json", }, { source: "$HOME/.claude/CLAUDE.md", - destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md" + destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md", } ], vscode: [ { source: "$HOME/Library/Application Support/Code/User/settings.json", - destination: "$HOME/.myconfigurations/applications/vscode/settings.json" + destination: "$HOME/.myconfigurations/applications/vscode/settings.json", }, { source: "$HOME/Library/Application Support/Code/User/keybindings.json", - destination: "$HOME/.myconfigurations/applications/vscode/keybindings.json" + destination: "$HOME/.myconfigurations/applications/vscode/keybindings.json", }, { source: "$HOME/Library/Application Support/Code/User/snippets/ruby.json", - destination: "$HOME/.myconfigurations/applications/vscode/snippets/ruby.json" + destination: "$HOME/.myconfigurations/applications/vscode/snippets/ruby.json", } - ] - } -]) - - - - - - - - - + ], + }, +) diff --git a/config/rubocop/layout.yml b/config/rubocop/layout.yml new file mode 100644 index 0000000..af908f7 --- /dev/null +++ b/config/rubocop/layout.yml @@ -0,0 +1,42 @@ +Layout/ClassStructure: + ExpectedOrder: + - constant + - module_inclusion + - attribute + - public_class_method + - public_method + - private_attribute + - protected_method + - private_method + +Layout/MultilineMethodCallIndentation: + EnforcedStyle: indented_relative_to_receiver + +Layout/ArgumentAlignment: + EnforcedStyle: with_first_argument + +Layout/MultilineMethodArgumentLineBreaks: + AllowMultilineFinalElement: true + +Layout/HashAlignment: + EnforcedColonStyle: key + EnforcedHashRocketStyle: key + AllowMultipleStyles: false + +Layout/SingleLineBlockChain: + Exclude: + - "spec/**/*" + +Layout/SpaceInsideParens: + EnforcedStyle: space + +Layout/RedundantLineBreak: + Enabled: false + +Layout/SpaceInsideArrayLiteralBrackets: + EnforcedStyle: space + +Layout/LineLength: + Max: 180 + Exclude: + - spec/**/*.rb diff --git a/config/rubocop/metrics.yml b/config/rubocop/metrics.yml new file mode 100644 index 0000000..4dee0ce --- /dev/null +++ b/config/rubocop/metrics.yml @@ -0,0 +1,20 @@ +Metrics/BlockLength: + Exclude: + - spec/**/* + - lib/helpers/setup_helper.rb + +Metrics/MethodLength: + Exclude: + - lib/helpers/setup_helper.rb + +Metrics/AbcSize: + Exclude: + - lib/helpers/setup_helper.rb + +Metrics/CyclomaticComplexity: + Exclude: + - lib/helpers/setup_helper.rb + +Metrics/PerceivedComplexity: + Exclude: + - lib/helpers/setup_helper.rb diff --git a/config/rubocop/rspec.yml b/config/rubocop/rspec.yml new file mode 100644 index 0000000..9930c98 --- /dev/null +++ b/config/rubocop/rspec.yml @@ -0,0 +1,47 @@ +# Review the detailed rspec cops here! +# https://github.com/backus/rubocop-rspec + +RSpec/NestedGroups: + Max: 10 + +RSpec/AlignRightLetBrace: + Enabled: false + +RSpec/AlignLeftLetBrace: + Enabled: false + +RSpec/Pending: + Enabled: false + +RSpec/PendingWithoutReason: + Enabled: false + +RSpec/AnyInstance: + Enabled: false + +RSpec/DescribeMethod: + Enabled: false + +RSpec/ExampleLength: + Enabled: false + +RSpec/MessageExpectation: + Enabled: false + +RSpec/MessageSpies: + Enabled: false + +RSpec/MessageChain: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Max: 10 + +RSpec/MultipleExpectations: + Enabled: false + +RSpec/SpecFilePathFormat: + Enabled: false + +RSpec/StubbedMock: + Enabled: false diff --git a/config/rubocop/style.yml b/config/rubocop/style.yml new file mode 100644 index 0000000..dcb3a73 --- /dev/null +++ b/config/rubocop/style.yml @@ -0,0 +1,46 @@ +Style/BlockDelimiters: + Enabled: true + EnforcedStyle: line_count_based + AllowedMethods: + - lambda + - proc + - it + - let + - subject + - before + - after + - scope + - include_context + - include_examples + +Style/MissingElse: + Enabled: false + +Style/DocumentationMethod: + Enabled: false + +Style/Copyright: + Enabled: false + +Style/Documentation: + Enabled: false + +Style/ClassAndModuleChildren: + EnforcedStyle: compact + +Style/DisableCopsWithinSourceCodeDirective: + Enabled: false + +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/MethodCallWithArgsParentheses: + EnforcedStyle: require_parentheses + Exclude: + - spec/**/* + +Style/TrailingCommaInArguments: + EnforcedStyleForMultiline: consistent_comma + +Style/TrailingCommaInHashLiteral: + EnforcedStyleForMultiline: consistent_comma diff --git a/dotfiles/.pryrc b/dotfiles/.pryrc index 5db880c..fb04067 100644 --- a/dotfiles/.pryrc +++ b/dotfiles/.pryrc @@ -1,15 +1,17 @@ +# frozen_string_literal: true + begin Pry.config.default_window_size = 25 Pry.config.pager = false -rescue - puts 'gem pry is not installed!' +rescue StandardError + puts( "gem pry is not installed!" ) end begin - Pry.commands.alias_command 'c', 'continue' - Pry.commands.alias_command 's', 'step' - Pry.commands.alias_command 'n', 'next' - Pry.commands.alias_command 'e', 'exit' -rescue - puts 'gem pry-nav is not installed!' + Pry.commands.alias_command( "c", "continue" ) + Pry.commands.alias_command( "s", "step" ) + Pry.commands.alias_command( "n", "next" ) + Pry.commands.alias_command( "e", "exit" ) +rescue StandardError + puts( "gem pry-nav is not installed!" ) end diff --git a/dotfiles/setup.rb b/dotfiles/setup.rb index 09fbd10..440be08 100644 --- a/dotfiles/setup.rb +++ b/dotfiles/setup.rb @@ -1,48 +1,41 @@ -PATHS ||= [] -PATHS.concat([ +# frozen_string_literal: true + +PATHS = [].freeze +PATHS.push( { dotfiles: [ { source: "$HOME/.bash_profile", - destination: "$HOME/.myconfigurations/dotfiles/.bash_profile" + destination: "$HOME/.myconfigurations/dotfiles/.bash_profile", }, { source: "$HOME/.bashrc", - destination: "$HOME/.myconfigurations/dotfiles/.bashrc" + destination: "$HOME/.myconfigurations/dotfiles/.bashrc", }, { source: "$HOME/.gemrc", - destination: "$HOME/.myconfigurations/dotfiles/.gemrc" + destination: "$HOME/.myconfigurations/dotfiles/.gemrc", }, { source: "$HOME/.profile", - destination: "$HOME/.myconfigurations/dotfiles/.profile" + destination: "$HOME/.myconfigurations/dotfiles/.profile", }, { source: "$HOME/.pryrc", - destination: "$HOME/.myconfigurations/dotfiles/.pryrc" + destination: "$HOME/.myconfigurations/dotfiles/.pryrc", }, { source: "$HOME/.vimrc", - destination: "$HOME/.myconfigurations/dotfiles/.vimrc" + destination: "$HOME/.myconfigurations/dotfiles/.vimrc", }, { source: "$HOME/.zprofile", - destination: "$HOME/.myconfigurations/dotfiles/.zprofile" + destination: "$HOME/.myconfigurations/dotfiles/.zprofile", }, { source: "$HOME/.zshrc", - destination: "$HOME/.myconfigurations/dotfiles/.zshrc" + destination: "$HOME/.myconfigurations/dotfiles/.zshrc", } - ] - } -]) - - - - - - - - - + ], + }, +) diff --git a/lib/helpers/path_helper.rb b/lib/helpers/path_helper.rb index d499ba8..0196816 100644 --- a/lib/helpers/path_helper.rb +++ b/lib/helpers/path_helper.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module PathHelper - def self.expand_shell_path(path) - expanded = path.gsub('$HOME', ENV['HOME']) - expanded = expanded.gsub('$USER', ENV['USER']) if ENV['USER'] - expanded = expanded.gsub('$PWD', Dir.pwd) - File.expand_path(expanded) + def self.expand_shell_path( path ) + expanded = path.gsub( "$HOME", Dir.home ) + expanded = expanded.gsub( "$USER", ENV["USER"] ) if ENV["USER"] + expanded = expanded.gsub( "$PWD", Dir.pwd ) + File.expand_path( expanded ) end -end \ No newline at end of file +end diff --git a/lib/helpers/setup_helper.rb b/lib/helpers/setup_helper.rb index 10ad404..cc2acca 100644 --- a/lib/helpers/setup_helper.rb +++ b/lib/helpers/setup_helper.rb @@ -1,45 +1,47 @@ -require 'fileutils' -require_relative 'path_helper' +# frozen_string_literal: true + +require "fileutils" +require_relative "path_helper" module SetupHelper - def self.process_paths(paths) + def self.process_paths( paths ) paths.each do |path_group| # Handle nested structure with tool names as keys (e.g., claude, claude2, etc.) path_group.each do |tool_name, tool_paths| - puts "\nProcessing paths for: #{tool_name}" - + puts( "\nProcessing paths for: #{tool_name}" ) + tool_paths.each do |path_config| - source = PathHelper.expand_shell_path(path_config[:source]) - destination = PathHelper.expand_shell_path(path_config[:destination]) + source = PathHelper.expand_shell_path( path_config[:source] ) + destination = PathHelper.expand_shell_path( path_config[:destination] ) - if File.exist?(source) || File.symlink?(source) - if File.symlink?(source) - current_target = File.readlink(source) - if current_target != destination - puts " Removing incorrect symlink: #{source} -> #{current_target}" - File.delete(source) - else - puts " Symlink already correct: #{source} -> #{destination}" + if File.exist?( source ) || File.symlink?( source ) + if File.symlink?( source ) + current_target = File.readlink( source ) + if current_target == destination + puts( " Symlink already correct: #{source} -> #{destination}" ) next + else + puts( " Removing incorrect symlink: #{source} -> #{current_target}" ) + File.delete( source ) end else - puts " Regular file exists at #{source}, cannot create symlink" + puts( " Regular file exists at #{source}, cannot create symlink" ) next end end # Create source directory if it doesn't exist - source_dir = File.dirname(source) - unless File.exist?(source_dir) - puts " Creating source directory: #{source_dir}" - FileUtils.mkdir_p(source_dir) + source_dir = File.dirname( source ) + unless File.exist?( source_dir ) + puts( " Creating source directory: #{source_dir}" ) + FileUtils.mkdir_p( source_dir ) end - destination_dir = File.dirname(destination) - FileUtils.mkdir_p(destination_dir) unless File.exist?(destination_dir) + destination_dir = File.dirname( destination ) + FileUtils.mkdir_p( destination_dir ) - puts " Creating symlink: #{source} -> #{destination}" - File.symlink(destination, source) + puts( " Creating symlink: #{source} -> #{destination}" ) + File.symlink( destination, source ) end end end diff --git a/lib/setup.rb b/lib/setup.rb old mode 100644 new mode 100755 index 1b6e045..a736bee --- a/lib/setup.rb +++ b/lib/setup.rb @@ -1,14 +1,16 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + # ruby "$HOME/.myconfigurations/lib/setup.rb" -require_relative 'helpers/setup_helper' +require_relative "helpers/setup_helper" # Initialize PATHS array -PATHS = [] +PATHS = [].freeze +require_relative "../applications/setup" # Load all setup files - they will append to PATHS -require_relative '../dotfiles/setup' -require_relative '../applications/setup' +require_relative "../dotfiles/setup" # Process all collected paths -SetupHelper.process_paths(PATHS) +SetupHelper.process_paths( PATHS ) diff --git a/spec/helpers/path_helper_spec.rb b/spec/helpers/path_helper_spec.rb index 3b50874..5719c65 100644 --- a/spec/helpers/path_helper_spec.rb +++ b/spec/helpers/path_helper_spec.rb @@ -1,36 +1,36 @@ # frozen_string_literal: true -require 'helpers/path_helper' +require "helpers/path_helper" # PathHelper.expand_shell_path takes a path string that may contain shell-style # variables ($HOME, $USER, $PWD) and returns a fully expanded absolute path. -# It performs simple string substitution β€” not a full shell eval β€” then delegates +# It performs simple string substitution -- not a full shell eval -- then delegates # to File.expand_path for final resolution. RSpec.describe PathHelper do - describe '.expand_shell_path' do + describe ".expand_shell_path" do # -------------------------------------------------------------------------- # $HOME expansion # The method replaces the literal string "$HOME" with ENV['HOME']. # This is the most common variable used in the project's setup configs - # (e.g., "$HOME/.tmux.conf" β†’ "/Users/christopherhough/.tmux.conf"). + # (e.g., "$HOME/.tmux.conf" -> "/Users/christopherhough/.tmux.conf"). # -------------------------------------------------------------------------- - context 'when the path contains $HOME' do - it 'replaces $HOME with the HOME environment variable' do - path = '$HOME/.config/settings.json' - result = described_class.expand_shell_path(path) + context "when the path contains $HOME" do + it "replaces $HOME with the HOME environment variable" do + path = "$HOME/.config/settings.json" + result = described_class.expand_shell_path( path ) - expect(result).to eq(File.join(ENV['HOME'], '.config/settings.json')) + expect( result ).to eq( File.join( Dir.home, ".config/settings.json" ) ) end # Paths from applications/setup.rb use $HOME in both source and destination, # e.g., "$HOME/.claude/settings.json". Verify multiple $HOME tokens in a # single string are all replaced. - it 'replaces multiple occurrences of $HOME' do - path = '$HOME/from/$HOME/to' - result = described_class.expand_shell_path(path) + it "replaces multiple occurrences of $HOME" do + path = "$HOME/from/$HOME/to" + result = described_class.expand_shell_path( path ) - expected = File.join(ENV['HOME'], 'from', ENV['HOME'], 'to') - expect(result).to eq(expected) + expected = File.join( Dir.home, "from", Dir.home, "to" ) + expect( result ).to eq( expected ) end end @@ -39,12 +39,12 @@ # The method replaces "$USER" with ENV['USER'], but only when that # environment variable is set. This guards against nil substitution. # -------------------------------------------------------------------------- - context 'when the path contains $USER' do - it 'replaces $USER with the USER environment variable' do - path = '/home/$USER/projects' - result = described_class.expand_shell_path(path) + context "when the path contains $USER" do + it "replaces $USER with the USER environment variable" do + path = "/home/$USER/projects" + result = described_class.expand_shell_path( path ) - expect(result).to eq("/home/#{ENV['USER']}/projects") + expect( result ).to eq( "/home/#{ENV.fetch( 'USER', nil )}/projects" ) end end @@ -52,33 +52,33 @@ # the $USER token must be left untouched so it doesn't blow up. context 'when ENV["USER"] is nil' do around do |example| - original_user = ENV['USER'] - ENV.delete('USER') + original_user = ENV.fetch( "USER", nil ) + ENV.delete( "USER" ) example.run ensure - ENV['USER'] = original_user + ENV["USER"] = original_user end - it 'leaves $USER unexpanded in the path' do - path = '/home/$USER/projects' - result = described_class.expand_shell_path(path) + it "leaves $USER unexpanded in the path" do + path = "/home/$USER/projects" + result = described_class.expand_shell_path( path ) # File.expand_path won't touch the literal "$USER" text, so it stays. - expect(result).to include('$USER') + expect( result ).to include( "$USER" ) end end # -------------------------------------------------------------------------- # $PWD expansion - # Replaces "$PWD" with Dir.pwd β€” useful when paths are relative to the + # Replaces "$PWD" with Dir.pwd -- useful when paths are relative to the # directory the setup script is invoked from. # -------------------------------------------------------------------------- - context 'when the path contains $PWD' do - it 'replaces $PWD with the current working directory' do - path = '$PWD/lib/helpers' - result = described_class.expand_shell_path(path) + context "when the path contains $PWD" do + it "replaces $PWD with the current working directory" do + path = "$PWD/lib/helpers" + result = described_class.expand_shell_path( path ) - expect(result).to eq(File.join(Dir.pwd, 'lib/helpers')) + expect( result ).to eq( File.join( Dir.pwd, "lib/helpers" ) ) end end @@ -87,32 +87,32 @@ # The project's setup files can theoretically mix variables. Ensure all # three are expanded in a single pass. # -------------------------------------------------------------------------- - context 'when the path contains multiple different variables' do - it 'expands all recognized variables' do - path = '$HOME/$USER/$PWD' - result = described_class.expand_shell_path(path) + context "when the path contains multiple different variables" do + it "expands all recognized variables" do + path = "$HOME/$USER/$PWD" + result = described_class.expand_shell_path( path ) # After substitution the path is processed by File.expand_path, which # resolves it to an absolute path. We check that none of the variable # tokens remain in the result. - expect(result).not_to include('$HOME') - expect(result).not_to include('$USER') - expect(result).not_to include('$PWD') - expect(result).to include(ENV['HOME']) + expect( result ).not_to include( "$HOME" ) + expect( result ).not_to include( "$USER" ) + expect( result ).not_to include( "$PWD" ) + expect( result ).to include( Dir.home ) end end # -------------------------------------------------------------------------- - # No variables β€” passthrough + # No variables -- passthrough # A plain absolute path should be returned unchanged (after expand_path # normalization). # -------------------------------------------------------------------------- - context 'when the path contains no variables' do - it 'returns the path resolved by File.expand_path' do - path = '/usr/local/bin/tool' - result = described_class.expand_shell_path(path) + context "when the path contains no variables" do + it "returns the path resolved by File.expand_path" do + path = "/usr/local/bin/tool" + result = described_class.expand_shell_path( path ) - expect(result).to eq('/usr/local/bin/tool') + expect( result ).to eq( "/usr/local/bin/tool" ) end end @@ -121,13 +121,13 @@ # File.expand_path converts relative paths to absolute ones using the # current working directory as the base. Verify this final step works. # -------------------------------------------------------------------------- - context 'when the path is relative after substitution' do - it 'resolves it to an absolute path' do - path = 'relative/path/file.txt' - result = described_class.expand_shell_path(path) + context "when the path is relative after substitution" do + it "resolves it to an absolute path" do + path = "relative/path/file.txt" + result = described_class.expand_shell_path( path ) - expect(result).to eq(File.expand_path('relative/path/file.txt')) - expect(Pathname.new(result)).to be_absolute + expect( result ).to eq( File.expand_path( "relative/path/file.txt" ) ) + expect( Pathname.new( result ) ).to be_absolute end end end diff --git a/spec/helpers/setup_helper_spec.rb b/spec/helpers/setup_helper_spec.rb index 517a8f4..cb1a977 100644 --- a/spec/helpers/setup_helper_spec.rb +++ b/spec/helpers/setup_helper_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'tmpdir' -require 'fileutils' -require 'helpers/setup_helper' +require "fileutils" +require "helpers/setup_helper" +require "tmpdir" # SetupHelper.process_paths receives a nested data structure (Array of Hashes) # that mirrors the format used in applications/setup.rb and dotfiles/setup.rb. @@ -31,29 +31,23 @@ # # All specs use a temporary directory so the real filesystem is never touched. RSpec.describe SetupHelper do - describe '.process_paths' do + describe ".process_paths" do # Create a fresh temp directory for every example and clean it up after. # We override $HOME so that PathHelper.expand_shell_path resolves "$HOME" # to our sandbox instead of the real home directory. - let(:tmpdir) { Dir.mktmpdir('setup_helper_spec') } + let( :tmpdir ) { Dir.mktmpdir( "setup_helper_spec" ) } + let( :original_home ) { Dir.home } before do - @original_home = ENV['HOME'] - ENV['HOME'] = tmpdir + original_home + ENV["HOME"] = tmpdir + allow( $stdout ).to receive( :write ) + allow( $stdout ).to receive( :puts ) end after do - ENV['HOME'] = @original_home - FileUtils.rm_rf(tmpdir) - end - - # Helper to suppress puts output during tests so spec output stays clean. - around do |example| - original_stdout = $stdout - $stdout = StringIO.new - example.run - ensure - $stdout = original_stdout + ENV["HOME"] = original_home + FileUtils.rm_rf( tmpdir ) end # -------------------------------------------------------------------------- @@ -61,138 +55,138 @@ # The most common path: source does not exist yet, destination file exists, # and we expect a symlink to be created at source pointing to destination. # -------------------------------------------------------------------------- - context 'when the source does not exist' do - it 'creates a symlink from source to destination' do + context "when the source does not exist" do + it "creates a symlink from source to destination" do # Simulate a destination config file that already exists in the repo. - dest_file = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') - FileUtils.mkdir_p(File.dirname(dest_file)) - File.write(dest_file, 'tmux config content') + dest_file = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) + FileUtils.mkdir_p( File.dirname( dest_file ) ) + File.write( dest_file, "tmux config content" ) # This mirrors the tmux entry from applications/setup.rb. paths = [ { tmux: [ { - source: '$HOME/.tmux.conf', - destination: '$HOME/.myconfigurations/applications/tmux/conf' + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) - source_path = File.join(tmpdir, '.tmux.conf') + source_path = File.join( tmpdir, ".tmux.conf" ) # The source should now be a symlink pointing to the destination. - expect(File.symlink?(source_path)).to be true - expect(File.readlink(source_path)).to eq(dest_file) + expect( File.symlink?( source_path ) ).to be true + expect( File.readlink( source_path ) ).to eq( dest_file ) end end # -------------------------------------------------------------------------- - # Correct symlink already exists β€” skip + # Correct symlink already exists -- skip # When re-running setup, existing correct symlinks should not be recreated. # The method prints "Symlink already correct" and calls `next`. # -------------------------------------------------------------------------- - context 'when a correct symlink already exists at source' do - it 'leaves the symlink unchanged' do - source_path = File.join(tmpdir, '.tmux.conf') - dest_file = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') - FileUtils.mkdir_p(File.dirname(dest_file)) - File.write(dest_file, 'tmux config') + context "when a correct symlink already exists at source" do + it "leaves the symlink unchanged" do + source_path = File.join( tmpdir, ".tmux.conf" ) + dest_file = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) + FileUtils.mkdir_p( File.dirname( dest_file ) ) + File.write( dest_file, "tmux config" ) # Pre-create the correct symlink. - File.symlink(dest_file, source_path) + File.symlink( dest_file, source_path ) paths = [ { tmux: [ { - source: '$HOME/.tmux.conf', - destination: '$HOME/.myconfigurations/applications/tmux/conf' + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) # Symlink should still exist and still point to the same destination. - expect(File.symlink?(source_path)).to be true - expect(File.readlink(source_path)).to eq(dest_file) + expect( File.symlink?( source_path ) ).to be true + expect( File.readlink( source_path ) ).to eq( dest_file ) end end # -------------------------------------------------------------------------- - # Incorrect symlink β€” replace + # Incorrect symlink -- replace # If a symlink exists but points to the wrong target (e.g., after moving # config files), the method deletes the old symlink and creates a new one. # -------------------------------------------------------------------------- - context 'when an incorrect symlink exists at source' do - it 'removes the old symlink and creates a correct one' do - source_path = File.join(tmpdir, '.tmux.conf') - wrong_dest = File.join(tmpdir, 'old/wrong/path') - correct_dest = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + context "when an incorrect symlink exists at source" do + it "removes the old symlink and creates a correct one" do + source_path = File.join( tmpdir, ".tmux.conf" ) + wrong_dest = File.join( tmpdir, "old/wrong/path" ) + correct_dest = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) - FileUtils.mkdir_p(File.dirname(wrong_dest)) - FileUtils.mkdir_p(File.dirname(correct_dest)) - File.write(correct_dest, 'correct config') + FileUtils.mkdir_p( File.dirname( wrong_dest ) ) + FileUtils.mkdir_p( File.dirname( correct_dest ) ) + File.write( correct_dest, "correct config" ) # Pre-create a symlink pointing to the wrong location. - File.symlink(wrong_dest, source_path) + File.symlink( wrong_dest, source_path ) paths = [ { tmux: [ { - source: '$HOME/.tmux.conf', - destination: '$HOME/.myconfigurations/applications/tmux/conf' + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) # The symlink should now point to the correct destination. - expect(File.symlink?(source_path)).to be true - expect(File.readlink(source_path)).to eq(correct_dest) + expect( File.symlink?( source_path ) ).to be true + expect( File.readlink( source_path ) ).to eq( correct_dest ) end end # -------------------------------------------------------------------------- - # Regular file exists at source β€” skip + # Regular file exists at source -- skip # If a real (non-symlink) file exists at the source location, the method # refuses to overwrite it. This protects user data that hasn't been moved # into the configuration repo yet. # -------------------------------------------------------------------------- - context 'when a regular file exists at source' do - it 'does not create a symlink and leaves the file intact' do - source_path = File.join(tmpdir, '.tmux.conf') - dest_file = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') - FileUtils.mkdir_p(File.dirname(dest_file)) - File.write(dest_file, 'repo config') + context "when a regular file exists at source" do + it "does not create a symlink and leaves the file intact" do + source_path = File.join( tmpdir, ".tmux.conf" ) + dest_file = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) + FileUtils.mkdir_p( File.dirname( dest_file ) ) + File.write( dest_file, "repo config" ) # Create a real file (not a symlink) at the source path. - File.write(source_path, 'user local config') + File.write( source_path, "user local config" ) paths = [ { tmux: [ { - source: '$HOME/.tmux.conf', - destination: '$HOME/.myconfigurations/applications/tmux/conf' + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) - # The regular file should remain untouched β€” no symlink created. - expect(File.symlink?(source_path)).to be false - expect(File.read(source_path)).to eq('user local config') + # The regular file should remain untouched -- no symlink created. + expect( File.symlink?( source_path ) ).to be false + expect( File.read( source_path ) ).to eq( "user local config" ) end end @@ -202,28 +196,28 @@ # FileUtils.mkdir_p if they don't exist. This is important for first-time # setup on a fresh machine where directories like ~/.claude/ may not exist. # -------------------------------------------------------------------------- - context 'when parent directories do not exist' do - it 'creates source and destination parent directories' do + context "when parent directories do not exist" do + it "creates source and destination parent directories" do # Use a deeply nested path that definitely doesn't exist yet. paths = [ { newtool: [ { - source: '$HOME/.config/newtool/deep/settings.json', - destination: '$HOME/.myconfigurations/apps/newtool/deep/settings.json' + source: "$HOME/.config/newtool/deep/settings.json", + destination: "$HOME/.myconfigurations/apps/newtool/deep/settings.json", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) - source_dir = File.join(tmpdir, '.config/newtool/deep') - dest_dir = File.join(tmpdir, '.myconfigurations/apps/newtool/deep') + source_dir = File.join( tmpdir, ".config/newtool/deep" ) + dest_dir = File.join( tmpdir, ".myconfigurations/apps/newtool/deep" ) # Both directory trees should have been created. - expect(Dir.exist?(source_dir)).to be true - expect(Dir.exist?(dest_dir)).to be true + expect( Dir.exist?( source_dir ) ).to be true + expect( Dir.exist?( dest_dir ) ).to be true end end @@ -232,46 +226,46 @@ # The applications/setup.rb file defines tmux, claude, and vscode in a # single hash. Verify that all tools in the group are processed. # -------------------------------------------------------------------------- - context 'when processing multiple tools in one path group' do - it 'creates symlinks for every tool entry' do + context "when processing multiple tools in one path group" do + it "creates symlinks for every tool entry" do # Set up destination files for two tools. - tmux_dest = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') - claude_dest = File.join(tmpdir, '.myconfigurations.ai/claude/global/settings.json') + tmux_dest = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) + claude_dest = File.join( tmpdir, ".myconfigurations.ai/claude/global/settings.json" ) - FileUtils.mkdir_p(File.dirname(tmux_dest)) - FileUtils.mkdir_p(File.dirname(claude_dest)) - File.write(tmux_dest, 'tmux') - File.write(claude_dest, 'claude') + FileUtils.mkdir_p( File.dirname( tmux_dest ) ) + FileUtils.mkdir_p( File.dirname( claude_dest ) ) + File.write( tmux_dest, "tmux" ) + File.write( claude_dest, "claude" ) # This mirrors the structure from applications/setup.rb with two tools. paths = [ { tmux: [ { - source: '$HOME/.tmux.conf', - destination: '$HOME/.myconfigurations/applications/tmux/conf' + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", } ], claude: [ { - source: '$HOME/.claude/settings.json', - destination: '$HOME/.myconfigurations.ai/claude/global/settings.json' + source: "$HOME/.claude/settings.json", + destination: "$HOME/.myconfigurations.ai/claude/global/settings.json", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) - tmux_source = File.join(tmpdir, '.tmux.conf') - claude_source = File.join(tmpdir, '.claude/settings.json') + tmux_source = File.join( tmpdir, ".tmux.conf" ) + claude_source = File.join( tmpdir, ".claude/settings.json" ) # Both tools should have their symlinks created. - expect(File.symlink?(tmux_source)).to be true - expect(File.readlink(tmux_source)).to eq(tmux_dest) + expect( File.symlink?( tmux_source ) ).to be true + expect( File.readlink( tmux_source ) ).to eq( tmux_dest ) - expect(File.symlink?(claude_source)).to be true - expect(File.readlink(claude_source)).to eq(claude_dest) + expect( File.symlink?( claude_source ) ).to be true + expect( File.readlink( claude_source ) ).to eq( claude_dest ) end end @@ -281,42 +275,42 @@ # VSCode has three (settings, keybindings, snippets). # Verify all entries under one tool key are processed. # -------------------------------------------------------------------------- - context 'when a tool has multiple path entries' do - it 'creates a symlink for each path entry' do - settings_dest = File.join(tmpdir, '.myconfigurations.ai/claude/global/settings.json') - claude_md_dest = File.join(tmpdir, '.myconfigurations.ai/claude/brains/global/CLAUDE.md') + context "when a tool has multiple path entries" do + it "creates a symlink for each path entry" do + settings_dest = File.join( tmpdir, ".myconfigurations.ai/claude/global/settings.json" ) + claude_md_dest = File.join( tmpdir, ".myconfigurations.ai/claude/brains/global/CLAUDE.md" ) - FileUtils.mkdir_p(File.dirname(settings_dest)) - FileUtils.mkdir_p(File.dirname(claude_md_dest)) - File.write(settings_dest, '{}') - File.write(claude_md_dest, '# CLAUDE') + FileUtils.mkdir_p( File.dirname( settings_dest ) ) + FileUtils.mkdir_p( File.dirname( claude_md_dest ) ) + File.write( settings_dest, "{}" ) + File.write( claude_md_dest, "# CLAUDE" ) # Mirrors the claude entry from applications/setup.rb exactly. paths = [ { claude: [ { - source: '$HOME/.claude/settings.json', - destination: '$HOME/.myconfigurations.ai/claude/global/settings.json' + source: "$HOME/.claude/settings.json", + destination: "$HOME/.myconfigurations.ai/claude/global/settings.json", }, { - source: '$HOME/.claude/CLAUDE.md', - destination: '$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md' + source: "$HOME/.claude/CLAUDE.md", + destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) - settings_source = File.join(tmpdir, '.claude/settings.json') - claude_md_source = File.join(tmpdir, '.claude/CLAUDE.md') + settings_source = File.join( tmpdir, ".claude/settings.json" ) + claude_md_source = File.join( tmpdir, ".claude/CLAUDE.md" ) - expect(File.symlink?(settings_source)).to be true - expect(File.readlink(settings_source)).to eq(settings_dest) + expect( File.symlink?( settings_source ) ).to be true + expect( File.readlink( settings_source ) ).to eq( settings_dest ) - expect(File.symlink?(claude_md_source)).to be true - expect(File.readlink(claude_md_source)).to eq(claude_md_dest) + expect( File.symlink?( claude_md_source ) ).to be true + expect( File.readlink( claude_md_source ) ).to eq( claude_md_dest ) end end @@ -326,46 +320,46 @@ # applications/setup.rb, each appending a separate hash to the PATHS array. # Verify that multiple array elements are all processed. # -------------------------------------------------------------------------- - context 'when processing multiple path groups' do - it 'processes each group independently' do - dotfile_dest = File.join(tmpdir, '.myconfigurations/dotfiles/.zshrc') - app_dest = File.join(tmpdir, '.myconfigurations/applications/tmux/conf') + context "when processing multiple path groups" do + it "processes each group independently" do + dotfile_dest = File.join( tmpdir, ".myconfigurations/dotfiles/.zshrc" ) + app_dest = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) - FileUtils.mkdir_p(File.dirname(dotfile_dest)) - FileUtils.mkdir_p(File.dirname(app_dest)) - File.write(dotfile_dest, 'zsh config') - File.write(app_dest, 'tmux config') + FileUtils.mkdir_p( File.dirname( dotfile_dest ) ) + FileUtils.mkdir_p( File.dirname( app_dest ) ) + File.write( dotfile_dest, "zsh config" ) + File.write( app_dest, "tmux config" ) # Two separate path groups, as if loaded from two different setup files. paths = [ { dotfiles: [ { - source: '$HOME/.zshrc', - destination: '$HOME/.myconfigurations/dotfiles/.zshrc' + source: "$HOME/.zshrc", + destination: "$HOME/.myconfigurations/dotfiles/.zshrc", } - ] + ], }, { tmux: [ { - source: '$HOME/.tmux.conf', - destination: '$HOME/.myconfigurations/applications/tmux/conf' + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", } - ] + ], } ] - described_class.process_paths(paths) + described_class.process_paths( paths ) - zshrc_source = File.join(tmpdir, '.zshrc') - tmux_source = File.join(tmpdir, '.tmux.conf') + zshrc_source = File.join( tmpdir, ".zshrc" ) + tmux_source = File.join( tmpdir, ".tmux.conf" ) - expect(File.symlink?(zshrc_source)).to be true - expect(File.readlink(zshrc_source)).to eq(dotfile_dest) + expect( File.symlink?( zshrc_source ) ).to be true + expect( File.readlink( zshrc_source ) ).to eq( dotfile_dest ) - expect(File.symlink?(tmux_source)).to be true - expect(File.readlink(tmux_source)).to eq(app_dest) + expect( File.symlink?( tmux_source ) ).to be true + expect( File.readlink( tmux_source ) ).to eq( app_dest ) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 83f572a..c738c1c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # Add the project's lib directory to the load path so we can require helpers directly. -$LOAD_PATH.unshift File.expand_path('../lib', __dir__) +$LOAD_PATH.unshift File.expand_path( "../lib", __dir__ ) RSpec.configure do |config| # Use expect syntax exclusively (no `should`). From 3208339e87b3f6b1506467e3f607377a818f7b5e Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 06:40:35 -0800 Subject: [PATCH 24/62] create missing folders on paths, add actions for code quality --- .github/workflows/code.yml | 97 ++++++++++++++++++++++++++ applications/claude/setup.rb | 2 +- lib/helpers/setup_helper.rb | 16 ++++- spec/helpers/setup_helper_spec.rb | 111 ++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/code.yml diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml new file mode 100644 index 0000000..a2e8568 --- /dev/null +++ b/.github/workflows/code.yml @@ -0,0 +1,97 @@ +name: CODE + +on: + pull_request: + +jobs: + # ---------------------------------------------------------------- + # RUBY + # ---------------------------------------------------------------- + code_ruby: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + - name: Ruby Version + run: | + ruby -v + - name: Setup Ruby Dependencies + run: | + gem update bundler + bundle install --jobs 4 --retry 3 + - name: Test Ruby Dependencies + run: | + bundle exec rake -T + + # ---------------------------------------------------------------- + # BUNDLE + # ---------------------------------------------------------------- + code_bundle: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Update bundle-audit database + run: bundle exec bundle-audit update + + - name: Check for known security vulnerabilities in Ruby dependencies + run: bundle exec bundle-audit check + + - name: Check for outdated gems + run: bundle outdated --strict + continue-on-error: true + + # ---------------------------------------------------------------- + # QUALITY + # ---------------------------------------------------------------- + code_lint: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Lint code for consistent style! + run: bundle exec rubocop + + # ---------------------------------------------------------------- + # SPECS + # ---------------------------------------------------------------- + code_specs: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Set Up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Run RSpec Tests + env: + RAILS_ENV: test + POSTGRES_HOST: localhost + POSTGRES_PORT: 5432 + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + run: bundle exec rspec --fail-fast diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb index 87e810b..b7ece70 100755 --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -10,7 +10,7 @@ { claude: [ { - source: ".claude/settings.local.json", + source: "test/.claude/settings.local.json", destination: "$HOME/.myconfigurations.ai/claude/local/settings.json", } ], diff --git a/lib/helpers/setup_helper.rb b/lib/helpers/setup_helper.rb index cc2acca..bb399b1 100644 --- a/lib/helpers/setup_helper.rb +++ b/lib/helpers/setup_helper.rb @@ -30,14 +30,26 @@ def self.process_paths( paths ) end end - # Create source directory if it doesn't exist + # Guard: never create $HOME itself -- only subdirectories within it. + home_dir = Dir.home + source_dir = File.dirname( source ) + if source_dir.start_with?( home_dir ) && !File.exist?( home_dir ) + puts( " ERROR: Home directory does not exist: #{home_dir}, skipping" ) + next + end + + destination_dir = File.dirname( destination ) + if destination_dir.start_with?( home_dir ) && !File.exist?( home_dir ) + puts( " ERROR: Home directory does not exist: #{home_dir}, skipping" ) + next + end + unless File.exist?( source_dir ) puts( " Creating source directory: #{source_dir}" ) FileUtils.mkdir_p( source_dir ) end - destination_dir = File.dirname( destination ) FileUtils.mkdir_p( destination_dir ) puts( " Creating symlink: #{source} -> #{destination}" ) diff --git a/spec/helpers/setup_helper_spec.rb b/spec/helpers/setup_helper_spec.rb index cb1a977..36aec2f 100644 --- a/spec/helpers/setup_helper_spec.rb +++ b/spec/helpers/setup_helper_spec.rb @@ -221,6 +221,117 @@ end end + # -------------------------------------------------------------------------- + # Deeply nested source directories within $HOME + # Mirrors the applications/claude/setup.rb pattern where the source path + # includes multiple nested directories (e.g., test/.claude/) that may not + # exist on a fresh machine. All intermediate directories after $HOME must + # be created automatically so the symlink succeeds. + # -------------------------------------------------------------------------- + context "when source has deeply nested directories that do not exist" do + it "creates all intermediate source directories and the symlink" do + # Destination file exists in the repo (simulating the config store). + dest_file = File.join( tmpdir, ".myconfigurations.ai/claude/local/settings.json" ) + FileUtils.mkdir_p( File.dirname( dest_file ) ) + File.write( dest_file, "{}" ) + + # Source path mirrors applications/claude/setup.rb: nested dirs + # "test/" and "test/.claude/" do not exist yet inside $HOME. + paths = [ + { + claude: [ + { + source: "$HOME/test/.claude/settings.local.json", + destination: "$HOME/.myconfigurations.ai/claude/local/settings.json", + } + ], + } + ] + + described_class.process_paths( paths ) + + source_path = File.join( tmpdir, "test/.claude/settings.local.json" ) + test_dir = File.join( tmpdir, "test" ) + claude_dir = File.join( tmpdir, "test/.claude" ) + + # Both intermediate directories should have been created. + expect( Dir.exist?( test_dir ) ).to be true + expect( Dir.exist?( claude_dir ) ).to be true + + # The symlink should have been created successfully. + expect( File.symlink?( source_path ) ).to be true + expect( File.readlink( source_path ) ).to eq( dest_file ) + end + end + + # -------------------------------------------------------------------------- + # Deeply nested destination directories within $HOME + # The destination side also uses mkdir_p. Verify that when the destination + # directory tree doesn't exist, all intermediate directories are created. + # -------------------------------------------------------------------------- + context "when destination has deeply nested directories that do not exist" do + it "creates all intermediate destination directories and the symlink" do + # Neither the source nor destination directories exist yet. + paths = [ + { + newtool: [ + { + source: "$HOME/.newtool/config.json", + destination: "$HOME/deep/nested/repo/store/config.json", + } + ], + } + ] + + described_class.process_paths( paths ) + + dest_dir = File.join( tmpdir, "deep/nested/repo/store" ) + source_path = File.join( tmpdir, ".newtool/config.json" ) + dest_path = File.join( tmpdir, "deep/nested/repo/store/config.json" ) + + # All intermediate destination directories should exist. + expect( Dir.exist?( dest_dir ) ).to be true + + # Symlink should point to the destination. + expect( File.symlink?( source_path ) ).to be true + expect( File.readlink( source_path ) ).to eq( dest_path ) + end + end + + # -------------------------------------------------------------------------- + # $HOME guard -- skip when $HOME does not exist + # SetupHelper must NEVER create the $HOME directory itself. If $HOME does + # not exist and a path is under it, the entry is skipped with an error + # message. This prevents accidentally creating a bogus home directory. + # -------------------------------------------------------------------------- + context "when $HOME does not exist" do + it "skips the entry without creating directories or symlinks" do + # Point $HOME to a path that does not exist. + fake_home = File.join( tmpdir, "nonexistent_home" ) + ENV["HOME"] = fake_home + + paths = [ + { + claude: [ + { + source: "$HOME/.claude/settings.json", + destination: "$HOME/.myconfigurations/settings.json", + } + ], + } + ] + + described_class.process_paths( paths ) + + # $HOME should NOT have been created. + expect( Dir.exist?( fake_home ) ).to be false + + # No symlink should have been created. + source_path = File.join( fake_home, ".claude/settings.json" ) + expect( File.exist?( source_path ) ).to be false + end + end + # -------------------------------------------------------------------------- # Multiple tools in a single path group # The applications/setup.rb file defines tmux, claude, and vscode in a From 4941f3e552d75f0180122ac7e15b11a4d5622f29 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 06:55:09 -0800 Subject: [PATCH 25/62] refactor path flows --- CHANGELOG.md | 2 +- CLAUDE.md | 8 ++++---- applications/claude/setup.rb | 4 ++-- applications/setup.rb | 5 ++--- docs/installation/003.md | 4 ++-- docs/installation/004.md | 2 +- dotfiles/functions/functions | 2 +- dotfiles/setup.rb | 1 - lib/setup.rb | 4 ++-- spec/helpers/setup_helper_spec.rb | 20 ++++++++++---------- 10 files changed, 25 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ddc961..2eac0c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -258,7 +258,7 @@ Complete modular architecture with 9 shell modules (`dotfiles/functions/collecti - libpq installation fixes for Apple Silicon #### Private Configuration Isolation -- All private settings moved to `~/.myconfigurations.private` (outside repo) +- All private settings moved to `~/.myconfigurations.private.keys` (outside repo) - Guards in Claude configuration files to prevent accidental commits ### Removed diff --git a/CLAUDE.md b/CLAUDE.md index 22db61a..1ef7c4c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,7 +79,7 @@ rbenvset # Set local Ruby version to global ### Shell Module System The shell configuration loads modules dynamically through `dotfiles/functions/functions`: -1. Sources private configuration from `~/.myconfigurations.private` +1. Sources private configuration from `~/.myconfigurations.private.keys` 2. Sets global environment variables (HISTSIZE, BROWSER, EDITOR) 3. Iterates through defined modules (itermocil, ror, git, brew, python, pg, heroku, js) 4. For each module, sources the combined functions and help from `collections/` @@ -99,7 +99,7 @@ Each shell module in `collections/` combines: - Modules are self-contained and can be enabled/disabled via the `_myconfig_modules` array ### Private Configuration -Sensitive settings are stored in `~/.myconfigurations.private` (not tracked in git) and sourced at the beginning of the shell initialization process. +Sensitive settings are stored in `~/.myconfigurations.private.keys` (not tracked in git) and sourced at the beginning of the shell initialization process. ## Development Workflow @@ -128,7 +128,7 @@ Follow the guides in sequence: - For environment configuration questions, refer to `.env.example` only - **NEVER read Rails secrets files** (`config/secrets.yml`, `config/credentials.yml.enc`, `config/master.key`) - they contain encrypted credentials and sensitive application secrets - If asked about Rails secrets, only provide commands to view them (e.g., `rails credentials:edit`, `rails credentials:show`) - NEVER read the files directly -- Private configuration is stored in `~/.myconfigurations.private` (not tracked in git) +- Private configuration is stored in `~/.myconfigurations.private.keys` (not tracked in git) ## Important Notes @@ -137,4 +137,4 @@ Follow the guides in sequence: - Alfred workflows are stored in `scripts/alfred/` - Legacy configurations are prefixed with `__TBD__` indicating they may need updating - The repository follows the author's opinionated setup - adapt as needed for your use case -- **Documentation Files**: All explanation, documentation, and reference markdown files (*.md) should be written to the `./tmp` directory, NOT to the project root or other directories, unless told otherwise. These are for reference only and should not be committed to the repository. \ No newline at end of file +- **Documentation Files**: All explanation, documentation, and reference markdown files (*.md) should be written to the `./tmp` directory, NOT to the project root or other directories, unless told otherwise. These are for reference only and should not be committed to the repository. diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb index b7ece70..44566b9 100755 --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -5,13 +5,13 @@ require_relative "helpers/setup_helper" -PATHS = [].freeze +PATHS = [] # rubocop:disable Style/MutableConstant PATHS.push( { claude: [ { source: "test/.claude/settings.local.json", - destination: "$HOME/.myconfigurations.ai/claude/local/settings.json", + destination: "$HOME/.myconfigurations.private/claude/local/settings.json", } ], }, diff --git a/applications/setup.rb b/applications/setup.rb index a4b7787..d217a3d 100644 --- a/applications/setup.rb +++ b/applications/setup.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -PATHS = [].freeze PATHS.push( { tmux: [ @@ -12,11 +11,11 @@ claude: [ { source: "$HOME/.claude/settings.json", - destination: "$HOME/.myconfigurations.ai/claude/global/settings.json", + destination: "$HOME/.myconfigurations.private/claude/global/settings.json", }, { source: "$HOME/.claude/CLAUDE.md", - destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md", + destination: "$HOME/.myconfigurations.private/claude/brains/global/CLAUDE.md", } ], vscode: [ diff --git a/docs/installation/003.md b/docs/installation/003.md index 1333239..400938d 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -46,10 +46,10 @@ Host github.com AddKeysToAgent yes UseKeychain yes ``` -8. Close the private repo `git clone git@github.com:chrishough/my-configurations-ai.git .myconfigurations.ai` into the `$HOME` directory. +8. Close the private repo `git clone git@github.com:chrishough/my-configurations-ai.git .myconfigurations.private` into the `$HOME` directory. > I am documenting this as part of my flow, but will not be public. Also, steps in the process need this cloned, but will not be fully utilized until later steps. 9. Clone this repo `git clone git@github.com:chrishough/my-configurations.git .myconfigurations` into the `$HOME` directory. - * Setup the `.myconfigurations.private` file for private keys and ENV settings. + * Setup the `.myconfigurations.private.keys` file for private keys and ENV settings. * Install [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) `sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"` theme for `zsh`. * Run the install script `sh "$HOME/.myconfigurations/lib/install.sh"` to load all of the required brew packages. * Setup the `github` dependency via `gh auth login`. diff --git a/docs/installation/004.md b/docs/installation/004.md index 73600b9..99b2378 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -1,6 +1,6 @@ ### Artificial Intelligence Preferences and Configurations : 004 -> At this point the private repository `.myconfigurations.ai` should have been configured in previous steps as it is required at this point. +> At this point the private repository `.myconfigurations.private` should have been configured in previous steps as it is required at this point. 1. Install [Claude CLI](https://code.claude.com/docs/en/setup#npm). * Via NPM `npm install -g @anthropic-ai/claude-code`. diff --git a/dotfiles/functions/functions b/dotfiles/functions/functions index 0da365b..fedbafe 100644 --- a/dotfiles/functions/functions +++ b/dotfiles/functions/functions @@ -1,7 +1,7 @@ #!/bin/zsh # SETTINGS AND PRIVATE VARIABLES : NEVER CHECK IN THIS FILE! -source $HOME/.myconfigurations.private +source $HOME/.myconfigurations.private.keys # ADJUST SHELL HISTORY export HISTFILESIZE=10000 diff --git a/dotfiles/setup.rb b/dotfiles/setup.rb index 440be08..61e006f 100644 --- a/dotfiles/setup.rb +++ b/dotfiles/setup.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -PATHS = [].freeze PATHS.push( { dotfiles: [ diff --git a/lib/setup.rb b/lib/setup.rb index a736bee..ca85ae3 100755 --- a/lib/setup.rb +++ b/lib/setup.rb @@ -5,8 +5,8 @@ require_relative "helpers/setup_helper" -# Initialize PATHS array -PATHS = [].freeze +# Initialize PATHS array -- must remain mutable so setup files can push into it. +PATHS = [] # rubocop:disable Style/MutableConstant require_relative "../applications/setup" # Load all setup files - they will append to PATHS diff --git a/spec/helpers/setup_helper_spec.rb b/spec/helpers/setup_helper_spec.rb index 36aec2f..52ed135 100644 --- a/spec/helpers/setup_helper_spec.rb +++ b/spec/helpers/setup_helper_spec.rb @@ -17,9 +17,9 @@ # ], # claude: [ # { source: "$HOME/.claude/settings.json", -# destination: "$HOME/.myconfigurations.ai/claude/global/settings.json" }, +# destination: "$HOME/.myconfigurations.private/claude/global/settings.json" }, # { source: "$HOME/.claude/CLAUDE.md", -# destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md" } +# destination: "$HOME/.myconfigurations.private/claude/brains/global/CLAUDE.md" } # ] # } # ] @@ -231,7 +231,7 @@ context "when source has deeply nested directories that do not exist" do it "creates all intermediate source directories and the symlink" do # Destination file exists in the repo (simulating the config store). - dest_file = File.join( tmpdir, ".myconfigurations.ai/claude/local/settings.json" ) + dest_file = File.join( tmpdir, ".myconfigurations.private/claude/local/settings.json" ) FileUtils.mkdir_p( File.dirname( dest_file ) ) File.write( dest_file, "{}" ) @@ -242,7 +242,7 @@ claude: [ { source: "$HOME/test/.claude/settings.local.json", - destination: "$HOME/.myconfigurations.ai/claude/local/settings.json", + destination: "$HOME/.myconfigurations.private/claude/local/settings.json", } ], } @@ -341,7 +341,7 @@ it "creates symlinks for every tool entry" do # Set up destination files for two tools. tmux_dest = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) - claude_dest = File.join( tmpdir, ".myconfigurations.ai/claude/global/settings.json" ) + claude_dest = File.join( tmpdir, ".myconfigurations.private/claude/global/settings.json" ) FileUtils.mkdir_p( File.dirname( tmux_dest ) ) FileUtils.mkdir_p( File.dirname( claude_dest ) ) @@ -360,7 +360,7 @@ claude: [ { source: "$HOME/.claude/settings.json", - destination: "$HOME/.myconfigurations.ai/claude/global/settings.json", + destination: "$HOME/.myconfigurations.private/claude/global/settings.json", } ], } @@ -388,8 +388,8 @@ # -------------------------------------------------------------------------- context "when a tool has multiple path entries" do it "creates a symlink for each path entry" do - settings_dest = File.join( tmpdir, ".myconfigurations.ai/claude/global/settings.json" ) - claude_md_dest = File.join( tmpdir, ".myconfigurations.ai/claude/brains/global/CLAUDE.md" ) + settings_dest = File.join( tmpdir, ".myconfigurations.private/claude/global/settings.json" ) + claude_md_dest = File.join( tmpdir, ".myconfigurations.private/claude/brains/global/CLAUDE.md" ) FileUtils.mkdir_p( File.dirname( settings_dest ) ) FileUtils.mkdir_p( File.dirname( claude_md_dest ) ) @@ -402,11 +402,11 @@ claude: [ { source: "$HOME/.claude/settings.json", - destination: "$HOME/.myconfigurations.ai/claude/global/settings.json", + destination: "$HOME/.myconfigurations.private/claude/global/settings.json", }, { source: "$HOME/.claude/CLAUDE.md", - destination: "$HOME/.myconfigurations.ai/claude/brains/global/CLAUDE.md", + destination: "$HOME/.myconfigurations.private/claude/brains/global/CLAUDE.md", } ], } From 29a3894152e086092ee40fd6bc573701107de030 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 08:14:10 -0800 Subject: [PATCH 26/62] update tmux settings --- applications/setup.rb | 4 ++++ docs/installation/003.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/applications/setup.rb b/applications/setup.rb index d217a3d..00a6cc5 100644 --- a/applications/setup.rb +++ b/applications/setup.rb @@ -6,6 +6,10 @@ { source: "$HOME/.tmux.conf", destination: "$HOME/.myconfigurations/applications/tmux/conf", + }, + { + source: "$HOME/.myconfigurations/applications/tmux/paths.json", + destination: "$HOME/.myconfigurations.private/tmux/paths.json", } ], claude: [ diff --git a/docs/installation/003.md b/docs/installation/003.md index 400938d..25ae28a 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -46,7 +46,7 @@ Host github.com AddKeysToAgent yes UseKeychain yes ``` -8. Close the private repo `git clone git@github.com:chrishough/my-configurations-ai.git .myconfigurations.private` into the `$HOME` directory. +8. Close the private repo `git clone git@github.com:chrishough/my-configurations-private.git .myconfigurations.private` into the `$HOME` directory. > I am documenting this as part of my flow, but will not be public. Also, steps in the process need this cloned, but will not be fully utilized until later steps. 9. Clone this repo `git clone git@github.com:chrishough/my-configurations.git .myconfigurations` into the `$HOME` directory. * Setup the `.myconfigurations.private.keys` file for private keys and ENV settings. From d7c05b669ca53a1f720807aeeeb46351e5212a38 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 09:18:58 -0800 Subject: [PATCH 27/62] add highline for cli tasks, update error messaging --- Gemfile | 2 + Gemfile.lock | 6 + aitooling/claude/project_brain_files/.keep | 0 .../ruby_on_rails/CLAUDE.md | 1355 ----------------- applications/claude/setup.rb | 12 +- lib/helpers/setup_helper.rb | 27 +- spec/helpers/setup_helper_spec.rb | 78 +- 7 files changed, 112 insertions(+), 1368 deletions(-) delete mode 100644 aitooling/claude/project_brain_files/.keep delete mode 100644 aitooling/claude/project_brain_files/ruby_on_rails/CLAUDE.md diff --git a/Gemfile b/Gemfile index cfec110..8b3d764 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,8 @@ git_source( :github ) { |repo| "https://github.com/#{repo}.git" } ruby "4.0.0" +gem "highline", "~> 3.1" + group :development, :test do gem "rubocop", require: false gem "rubocop-performance", require: false diff --git a/Gemfile.lock b/Gemfile.lock index d6cf51f..353a2bb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,9 @@ GEM specs: ast (2.4.3) diff-lcs (1.6.2) + highline (3.1.2) + reline + io-console (0.8.2) json (2.18.1) language_server-protocol (3.17.0.5) lint_roller (1.1.0) @@ -14,6 +17,8 @@ GEM racc (1.8.1) rainbow (3.1.1) regexp_parser (2.11.3) + reline (0.6.3) + io-console (~> 0.5) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) @@ -58,6 +63,7 @@ PLATFORMS x86_64-darwin-24 DEPENDENCIES + highline (~> 3.1) rspec (~> 3.13) rubocop rubocop-performance diff --git a/aitooling/claude/project_brain_files/.keep b/aitooling/claude/project_brain_files/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/aitooling/claude/project_brain_files/ruby_on_rails/CLAUDE.md b/aitooling/claude/project_brain_files/ruby_on_rails/CLAUDE.md deleted file mode 100644 index 8788430..0000000 --- a/aitooling/claude/project_brain_files/ruby_on_rails/CLAUDE.md +++ /dev/null @@ -1,1355 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - ---- - -## Quick Reference - -### Template Markers -When using this as a template for new projects, look for these markers: -- `[PROJECT-SPECIFIC]` - Must be updated for each project -- `[OPTIONAL]` - Remove section if not applicable to your project -- `[CUSTOMIZE]` - Adapt values/examples to project needs - -### Essential Commands Quick Reference -| Task | Command | -|------|---------| -| Run all tests | `bundle exec rspec` | -| Run linter | `bundle exec rubocop` | -| Auto-fix lint | `bundle exec rubocop -a` | -| Start server | `bin/dev` | -| Console | `bin/rails console` | -| DB migrate | `bin/rails db:migrate` | -| Security scan | `bundle exec rake appfactory:test:brakeman` | - ---- - -## Code Style & Project Guidelines - -- **Core Language:** Always use **Ruby**. -- **Framework:** Rails 8.0.2 (per Gemfile) `[PROJECT-SPECIFIC]` -- **Ruby Version:** Ruby 3.4.3 (per .ruby-version) `[PROJECT-SPECIFIC]` -- **Mental Model:** Think like Sandy Metz when she wrote Practical Object-Oriented Design in Ruby. -- **Code Style:** Apply the style, conventions, and rules from `.rubocop.yml` and all config files in `config/rubocop/` (base.yml, rails.yml, rspec.yml, metrics.yml, layout.yml, obsession.yml). **IMPORTANT: All generated code MUST pass RuboCop checks without violations.** -- **Gems & Dependencies:** Review the `Gemfile` and `Gemfile.lock` when providing code examples. All listed gems are available. -- **Testing Plugins:** This project uses: `[CUSTOMIZE]` - - rubocop-rspec - - rubocop-performance - - rubocop-rails - - rubocop-factory_bot - - rubocop-rspec_rails - - rubocop-obsession -- **Temporary Files:** Always use the project's `./tmp` directory for temporary files. -- **Documentation Files:** All explanation, documentation, and reference markdown files (*.md) should be written to the `./tmp` directory, NOT to the project root or other directories, unless told otherwise. - ---- - -## Essential Commands - -### Development -```bash -bin/setup # Initial setup: installs dependencies, prepares database, starts dev server -bin/dev # Start Rails development server -bin/rails server # Start Rails server directly -bin/rails console # Open Rails console -``` - -### Testing -```bash -bundle exec rspec # Run all tests -bundle exec rspec spec/services/ # Run specific directory -bundle exec rspec spec/services/some_spec.rb # Run specific file -bundle exec rspec spec/services/some_spec.rb:42 # Run specific line - -bundle exec rake appfactory:test:simplecov # Run tests with coverage [PROJECT-SPECIFIC] -bundle exec rake appfactory:test:rubocop # Run RuboCop checks [PROJECT-SPECIFIC] -bundle exec rake appfactory:test:brakeman # Run security analysis [PROJECT-SPECIFIC] -``` - -### Database -```bash -bin/rails db:prepare # Setup database -bin/rails db:migrate # Run migrations -bundle exec rake appfactory:database:reset # Reset database [PROJECT-SPECIFIC] -bundle exec rake appfactory:database:recreate[development] # Drop and recreate [PROJECT-SPECIFIC] -``` - -### Code Quality -```bash -bundle exec rubocop # Run all RuboCop checks -bundle exec rubocop -a # Auto-fix RuboCop violations -bundle exec rubocop spec/services/some_spec.rb # Check specific file -``` - ---- - -## Project Directory Structure - -``` -app/ -β”œβ”€β”€ assets/ # Static assets (stylesheets, images) -β”œβ”€β”€ builders/ # Builder pattern for complex object construction [OPTIONAL] -β”œβ”€β”€ controllers/ # HTTP request handlers (keep thin!) -β”œβ”€β”€ forms/ # Form objects for complex multi-model forms -β”œβ”€β”€ helpers/ # View helpers (use sparingly) -β”œβ”€β”€ inputs/ # Input objects for request parameter parsing [OPTIONAL] -β”œβ”€β”€ jobs/ # Background jobs (Solid Queue) -β”œβ”€β”€ lib/ # App-dependent library code (e.g., Rodauth config) -β”œβ”€β”€ mailers/ # ActionMailer classes (prefer service objects) -β”œβ”€β”€ models/ # ActiveRecord models (data + associations only) -β”œβ”€β”€ policies/ # Pundit authorization policies -β”œβ”€β”€ presenters/ # View presenters/decorators [OPTIONAL] -β”œβ”€β”€ queries/ # Query objects for complex database queries -β”œβ”€β”€ services/ # ServiceWrapper business logic (primary location) -β”œβ”€β”€ validators/ # Custom ActiveModel validators -β”œβ”€β”€ views/ # Slim templates -└── workers/ # Legacy background workers [OPTIONAL] - -config/ -β”œβ”€β”€ environments/ # Environment-specific configuration -β”œβ”€β”€ initializers/ # Boot-time initialization -β”œβ”€β”€ locales/ # I18n translation files -β”œβ”€β”€ rubocop/ # Modular RuboCop configuration -└── settings/ # Application settings (config gem) - -lib/ -β”œβ”€β”€ core_extensions/ # Core Ruby class extensions -β”œβ”€β”€ monkey_patches/ # Runtime modifications (use sparingly) -β”œβ”€β”€ modules/ # Reusable utility modules -β”œβ”€β”€ tasks/ # Custom Rake tasks -└── utilities/ # Helper utilities - -spec/ -β”œβ”€β”€ factories/ # FactoryBot test data definitions -β”œβ”€β”€ fixtures/ # VCR cassettes and test files -β”œβ”€β”€ support/ # Test helpers, shared examples, configs -β”‚ β”œβ”€β”€ configs/ # Test framework configurations -β”‚ β”œβ”€β”€ helpers/ # Custom test helper modules -β”‚ β”œβ”€β”€ matchers/ # Custom RSpec matchers -β”‚ β”œβ”€β”€ modules/ # Utility modules for tests -β”‚ β”œβ”€β”€ shared_contexts/ # Shared context setups -β”‚ └── shared_examples/ # Reusable test patterns -β”œβ”€β”€ controllers/ # Controller specs -β”œβ”€β”€ features/ # Feature/system specs -β”œβ”€β”€ jobs/ # Background job specs -β”œβ”€β”€ models/ # Model specs -β”œβ”€β”€ policies/ # Policy specs -β”œβ”€β”€ requests/ # Request/integration specs -β”œβ”€β”€ services/ # Service object specs -└── validators/ # Validator specs -``` - -### Where to Put New Code -| Code Type | Location | When to Use | -|-----------|----------|-------------| -| Business logic | `app/services/` | Any logic involving multiple models, external services, or complex operations | -| Database queries | `app/queries/` | Complex queries with joins, aggregations, or multiple conditions | -| Form handling | `app/forms/` | Multi-model forms, complex validations spanning models | -| Request parsing | `app/inputs/` | Complex parameter parsing, API request objects | -| View logic | `app/presenters/` | Complex view logic, formatting, calculated display values | -| Authorization | `app/policies/` | Access control, permission checks | -| Object construction | `app/builders/` | Objects with many optional parameters, step-by-step construction | -| Custom validations | `app/validators/` | Reusable validation rules across models | -| Background work | `app/jobs/` | Async operations, scheduled tasks, expensive computations | - ---- - -## Architecture Overview - -### Service Object Pattern -All business logic uses the ServiceWrapper pattern. Services must: -- Inherit from `ServiceWrapper` -- Define `_local_initialize` for setup (private, underscore prefix) -- Define `_local_call` for business logic (private, underscore prefix) -- Use `validates` for input validation -- Return data via `@response` - -Example structure with proper private method naming: -```ruby -# frozen_string_literal: true - -module Services - module User - class CreateAccount < ServiceWrapper - validates :email, presence: true - validate :_email_format_valid? - - private - - attr_accessor :email, :user - - def _local_initialize(email:) - self.email = email - end - - def _local_call - self.user = _create_user - _send_welcome_email - - @response = { user: user } - end - - def _email_format_valid? - return if email.match?(URI::MailTo::EMAIL_REGEXP) - - errors.add(:email, I18n.t("services.user.create_account.errors.invalid_email")) - end - - def _create_user - ::Account.create!(email: email, status: :unverified) - end - - def _send_welcome_email - ::Mail::WelcomeJob.perform_later(user_id: user.id) - end - end - end -end -``` - -Usage: -```ruby -result = Services::User::CreateAccount.call(email: "user@example.com") -if result.valid? - user = result.response[:user] -else - errors = result.errors -end -``` - -### Query Objects -Extract complex ActiveRecord queries into dedicated classes: - -```ruby -# frozen_string_literal: true - -module Queries - class ActiveLeadsByDateRange - def initialize(account:, start_date:, end_date:) - @account = account - @start_date = start_date - @end_date = end_date - end - - def call - _base_scope - .where(created_at: @start_date..@end_date) - .includes(:tags, :notes) - .order(created_at: :desc) - end - - private - - def _base_scope - @account.leads.active - end - end -end - -# Usage -leads = Queries::ActiveLeadsByDateRange.new( - account: current_account, - start_date: 30.days.ago, - end_date: Time.current, -).call -``` - -### Form Objects -Handle complex forms that span multiple models: - -```ruby -# frozen_string_literal: true - -module Forms - class UserRegistration - include ActiveModel::Model - include ActiveModel::Validations - - attr_accessor :email, :password, :company_name, :terms_accepted - - validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP } - validates :password, presence: true, length: { minimum: 10 } - validates :company_name, presence: true - validates :terms_accepted, acceptance: true - - def save - return false unless valid? - - ActiveRecord::Base.transaction do - _create_account - _create_company - true - end - rescue ActiveRecord::RecordInvalid => e - errors.add(:base, e.message) - false - end - - private - - def _create_account - @account = Account.create!(email: email, password: password) - end - - def _create_company - @company = @account.companies.create!(name: company_name) - end - end -end -``` - -### Input Objects `[OPTIONAL]` -Parse and validate incoming request parameters: - -```ruby -# frozen_string_literal: true - -module Inputs - class LeadSearchInput - include ActiveModel::Model - - attr_accessor :query, :status, :date_from, :date_to, :page, :per_page - - def initialize(params = {}) - @query = params[:q]&.strip - @status = params[:status]&.to_sym - @date_from = _parse_date(params[:date_from]) - @date_to = _parse_date(params[:date_to]) - @page = (params[:page] || 1).to_i - @per_page = [(params[:per_page] || 25).to_i, 100].min - end - - private - - def _parse_date(value) - return nil if value.blank? - - Time.zone.parse(value) - rescue ArgumentError - nil - end - end -end -``` - -### Presenter Objects `[OPTIONAL]` -Encapsulate complex view logic: - -```ruby -# frozen_string_literal: true - -module Presenters - class LeadPresenter - def initialize(lead) - @lead = lead - end - - def full_name - "#{@lead.first_name} #{@lead.last_name}".strip - end - - def status_badge_class - case @lead.status - when :active then "badge-success" - when :pending then "badge-warning" - when :closed then "badge-secondary" - else "badge-info" - end - end - - def formatted_phone - return "N/A" if @lead.phone_number.blank? - - _format_phone_number(@lead.phone_number) - end - - def created_at_relative - time_ago_in_words(@lead.created_at) - end - - private - - def _format_phone_number(number) - # Format: (555) 123-4567 - cleaned = number.gsub(/\D/, "") - return number unless cleaned.length == 10 - - "(#{cleaned[0..2]}) #{cleaned[3..5]}-#{cleaned[6..9]}" - end - end -end -``` - -### Policy Objects -Pundit policies for authorization: `[PROJECT-SPECIFIC]` - -```ruby -# frozen_string_literal: true - -class LeadPolicy < ApplicationPolicy - def index? - _user_has_account? - end - - def show? - _user_owns_record? - end - - def create? - _user_has_account? - end - - def update? - _user_owns_record? - end - - def destroy? - _user_owns_record? && !record.has_active_deals? - end - - class Scope < ApplicationPolicy::Scope - def resolve - scope.where(account: user.account) - end - end - - private - - def _user_has_account? - user.account.present? - end - - def _user_owns_record? - record.account_id == user.account_id - end -end -``` - -### Builder Objects `[OPTIONAL]` -Construct complex objects with many optional parameters: - -```ruby -# frozen_string_literal: true - -module Builders - class ReportBuilder - def initialize - @filters = {} - @columns = [] - @sort_by = nil - @format = :html - end - - def with_date_range(start_date, end_date) - @filters[:date_range] = start_date..end_date - self - end - - def with_status(status) - @filters[:status] = status - self - end - - def with_columns(*columns) - @columns = columns.flatten - self - end - - def sorted_by(column, direction = :asc) - @sort_by = { column: column, direction: direction } - self - end - - def as_format(format) - @format = format - self - end - - def build - Report.new( - filters: @filters, - columns: @columns, - sort_by: @sort_by, - format: @format, - ) - end - end -end - -# Usage -report = Builders::ReportBuilder.new - .with_date_range(30.days.ago, Time.current) - .with_status(:active) - .with_columns(:name, :email, :created_at) - .sorted_by(:created_at, :desc) - .as_format(:csv) - .build -``` - -### Value Objects `[OPTIONAL]` -Immutable objects representing domain concepts: - -```ruby -# frozen_string_literal: true - -class Money - include Comparable - - attr_reader :cents, :currency - - def initialize(cents, currency = "USD") - @cents = cents.to_i - @currency = currency.to_s.upcase.freeze - freeze - end - - def dollars - cents / 100.0 - end - - def +(other) - _ensure_same_currency!(other) - self.class.new(cents + other.cents, currency) - end - - def -(other) - _ensure_same_currency!(other) - self.class.new(cents - other.cents, currency) - end - - def *(multiplier) - self.class.new((cents * multiplier).round, currency) - end - - def <=>(other) - return nil unless other.is_a?(Money) && currency == other.currency - - cents <=> other.cents - end - - def to_s - format("$%.2f %s", dollars, currency) - end - - private - - def _ensure_same_currency!(other) - return if currency == other.currency - - raise ArgumentError, "Cannot operate on different currencies: #{currency} vs #{other.currency}" - end -end -``` - -### Null Objects `[OPTIONAL]` -Handle nil cases elegantly: - -```ruby -# frozen_string_literal: true - -class NullAccount - def id - nil - end - - def email - "guest@example.com" - end - - def name - "Guest" - end - - def verified? - false - end - - def can_access?(_resource) - false - end - - def leads - Lead.none - end -end - -# Usage in controller -def current_account - @current_account ||= Account.find_by(id: session[:account_id]) || NullAccount.new -end -``` - -### Authentication Architecture `[PROJECT-SPECIFIC]` -- Uses Rodauth (not Devise) for authentication -- Configuration in `app/lib/rodauth_app.rb` and `rodauth_main.rb` -- Mounted at `/authentication` routes -- Database tables: `accounts`, `account_verification_keys`, `account_password_reset_keys`, etc. - -### Background Processing -- **Job Framework**: Solid Queue (per Gemfile) - all jobs in `app/jobs/` -- **Caching**: Solid Cache for Rails caching needs -- **WebSockets**: Solid Cable for ActionCable -- **Job Design**: Keep jobs idempotent and focused on a single task -- All background jobs inherit from `ApplicationJob` -- **CRITICAL**: All jobs must be idempotent - safe to run multiple times with same arguments - -```ruby -# frozen_string_literal: true - -class ImportLeadsJob < ApplicationJob - queue_as :default - - def perform(import_id:) - import = LeadImport.find(import_id) - return if import.completed? # Idempotency check - - Services::Leads::ProcessImport.call(import: import) - end -end -``` - -### Email Services `[PROJECT-SPECIFIC]` -Emails are sent via service objects, not traditional Rails mailers: -- `Services::Email::SendAccountVerificationEmail` -- `Services::Email::SendPasswordResetEmail` -- Uses SendGrid for production email delivery - -### Testing Patterns -- All services have corresponding specs in `spec/services/` -- Use shared examples: `it_behaves_like "a valid service response"` -- VCR cassettes for external API calls -- FactoryBot for test data - -### Frontend Architecture `[PROJECT-SPECIFIC]` -- **Templates**: Slim (not ERB) -- **JavaScript**: Import maps + Stimulus controllers -- **CSS**: SCSS with Propshaft -- **Interactivity**: Hotwire (Turbo + Stimulus) - ---- - -## Key Conventions - -### Service Response Structure -Services always return an object with: -- `valid?` - boolean indicating success -- `errors` - ActiveModel::Errors object (with `raw`, `text`, `translated`, `error_count`) -- `response` - hash containing returned data - -### Validation Pattern -Services validate inputs before execution: -```ruby -validates :required_param, presence: true -validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } -validate :_custom_validation_method # Note underscore prefix -``` - -### Error Handling in Services -Errors use I18n translations from `config/locales/`: -- Service errors: `services.module.class_name.errors.error_key` -- Model errors: Standard Rails I18n patterns - -```ruby -def _validate_email - return if _email_valid? - - errors.add(:email, I18n.t("services.mail.verification.errors.email_invalid")) -end -``` - -### Database Migrations -Uses Rein gem for database constraints: -```ruby -add_presence_constraint :table, :column -add_foreign_key_constraint :table, :foreign_table -``` - -### RuboCop Configuration -Located in `config/rubocop/` with separate files: -- `base.yml` - Core Ruby style -- `rails.yml` - Rails-specific rules -- `rspec.yml` - RSpec style -- `metrics.yml` - Complexity metrics -- `layout.yml` - Code layout rules -- `obsession.yml` - Method obsession checks - -Key enforced styles: -- Double quotes for strings -- Trailing commas in multi-line hashes/arrays -- Parentheses required for method calls (except in specs) -- **CRITICAL: All private methods MUST have an underscore prefix** (e.g., `_calculate_total`, `_validate_input`) - This is a mandatory convention for ALL Ruby code in this project - -### Model Conventions -Order within model files: -```ruby -class Lead < ApplicationRecord - # 1. Constants - STATUSES = %i[active pending closed].freeze - - # 2. Attribute declarations (enums, etc.) - enum :status, { active: 0, pending: 1, closed: 2 }, default: :pending - - # 3. Associations (belongs_to first, then has_many, then has_one) - belongs_to :account - has_many :notes, dependent: :destroy - has_one :primary_contact, class_name: "Contact" - - # 4. Validations - validates :email, presence: true, uniqueness: { scope: :account_id } - validates :status, inclusion: { in: STATUSES } - - # 5. Scopes - scope :active, -> { where(status: :active) } - scope :recent, -> { order(created_at: :desc) } - - # 6. Callbacks (use sparingly - prefer service objects) - before_validation :_normalize_email, if: :email_changed? - - # 7. Class methods - def self.search(query) - where("email ILIKE ?", "%#{query}%") - end - - # 8. Public instance methods - def full_name - "#{first_name} #{last_name}".strip - end - - # 9. Private methods (with underscore prefix) - private - - def _normalize_email - self.email = email&.downcase&.strip - end -end -``` - -### Controller Conventions -Order within controller files: -```ruby -class LeadsController < ApplicationController - # 1. before_action declarations - before_action :authenticate! - before_action :_set_lead, only: %i[show edit update destroy] - - # 2. Standard RESTful actions (in order: index, show, new, create, edit, update, destroy) - def index - @leads = policy_scope(Lead).page(params[:page]) - end - - def show; end - - def new - @lead = Lead.new - end - - def create - @lead = current_account.leads.build(_lead_params) - if @lead.save - redirect_to @lead, notice: t(".success") - else - render :new, status: :unprocessable_entity - end - end - - def edit; end - - def update - if @lead.update(_lead_params) - redirect_to @lead, notice: t(".success") - else - render :edit, status: :unprocessable_entity - end - end - - def destroy - @lead.destroy - redirect_to leads_path, notice: t(".success") - end - - # 3. Custom public actions - def export - # ... - end - - # 4. Private methods (with underscore prefix) - private - - def _set_lead - @lead = policy_scope(Lead).find(params[:id]) - authorize @lead - end - - def _lead_params - params.require(:lead).permit(:email, :first_name, :last_name, :phone_number, :status) - end -end -``` - -### Time Handling -- **Always use `Time.zone.now`**, never `Time.now` or `DateTime.now` -- Store all times in UTC in the database -- Use `freeze_time` or `travel_to` in tests for deterministic time -- Parse user input with `Time.zone.parse` -- Display times using I18n localization: `l(time, format: :short)` - -```ruby -# Good -Time.zone.now -Time.zone.today -1.day.ago -Time.zone.parse("2024-01-15") - -# Bad - Never use these -Time.now -Date.today -DateTime.now -Time.parse("2024-01-15") -``` - ---- - -## Database Conventions - -### Naming Conventions -- **Tables**: Plural, snake_case (`leads`, `account_settings`, `password_reset_keys`) -- **Columns**: Singular, snake_case (`first_name`, `created_at`, `account_id`) -- **Foreign keys**: `{table_singular}_id` (`account_id`, `lead_id`) -- **Indexes**: `index_{table}_on_{columns}` (auto-generated by Rails) -- **Join tables**: Alphabetical order (`accounts_roles`, `leads_tags`) - -### Primary Keys -- Use integer IDs for internal references (default Rails behavior) -- Consider UUIDs for external-facing identifiers (API responses, URLs) -- Never expose sequential IDs in public URLs if order/count is sensitive - -### Index Strategies -```ruby -# Always index foreign keys -add_index :leads, :account_id - -# Index columns used in WHERE clauses -add_index :leads, :status -add_index :leads, :email - -# Composite indexes - put equality columns first, range columns last -add_index :leads, [:account_id, :status, :created_at] - -# Partial indexes for filtered queries -add_index :leads, :email, where: "status = 'active'", name: "index_active_leads_on_email" - -# Unique constraints -add_index :accounts, :email, unique: true - -# Index for ORDER BY queries -add_index :leads, [:account_id, :created_at], order: { created_at: :desc } -``` - -### Query Optimization Patterns -```ruby -# Use includes for belongs_to/has_many that will be accessed -Lead.includes(:account, :tags).where(status: :active) - -# Use preload when you need separate queries (useful for complex conditions) -Lead.preload(:notes).where(status: :active) - -# Use eager_load for LEFT OUTER JOIN (when filtering by association) -Lead.eager_load(:notes).where(notes: { important: true }) - -# Use exists? instead of loading records -Lead.where(account: account).exists? # Good -Lead.where(account: account).any? # Loads records - avoid - -# Use pluck for single columns -Lead.where(status: :active).pluck(:email) # Returns array of strings - -# Use select to limit columns -Lead.select(:id, :email, :status).where(status: :active) - -# Use find_each for batch processing -Lead.where(status: :pending).find_each(batch_size: 100) do |lead| - # Process each lead -end - -# Use update_all for bulk updates -Lead.where(status: :pending).update_all(status: :active) - -# Use insert_all for bulk inserts (Rails 6+) -Lead.insert_all([{ email: "a@b.com" }, { email: "c@d.com" }]) -``` - ---- - -## RuboCop Compliance - -**CRITICAL: All code must pass RuboCop checks.** Before finalizing any code: -- Run `bundle exec rubocop ` on new/modified files -- Fix all violations - do not disable cops without explicit user approval -- Common patterns to follow: - - Use frozen string literal comment: `# frozen_string_literal: true` - - Add proper spacing around operators and after commas - - Use consistent indentation (2 spaces) - - Keep lines under the configured length limit - - Use descriptive variable and method names - - Follow the configured naming conventions - - In specs, use proper RSpec style (no parentheses for matchers) - - **All private methods must start with underscore** - ---- - -## Rails Best Practices - -- **Database:** PostgreSQL with Rein for database integrity -- **Controllers:** Keep controllers thin - they only handle HTTP concerns (params, response, redirects) -- **Models:** Keep models focused on data and associations, extract complex logic to service objects -- **Service Objects:** Business logic in `app/services/` using single-responsibility classes (ServiceWrapper pattern) -- **Query Objects:** Place complex database queries in `app/queries/` -- **Form Objects:** Use form objects in `app/forms/` for complex multi-model forms -- **Input Objects:** Use input objects in `app/inputs/` for complex request parameter parsing -- **Presenters:** Use presenters in `app/presenters/` for complex view logic -- **Policies:** Use Pundit policies in `app/policies/` for authorization -- **Concerns:** Use concerns sparingly and only for truly shared, cohesive behavior -- **Callbacks:** Use ActiveRecord callbacks sparingly - prefer explicit service objects - - **Never use callbacks for**: sending emails, calling APIs, touching other models, or complex side effects - ---- - -## Testing Guidelines - -- **Testing Framework:** RSpec for all tests -- **Test Structure:** Follow the AAA pattern (Arrange, Act, Assert) -- **Factory Strategy:** FactoryBot for test data creation - ALWAYS use factories, never create records directly -- **Test Coverage:** Write comprehensive tests for all business logic -- **Test Location:** Follow Rails conventions - model specs in `spec/models/`, service specs in `spec/services/`, etc. -- **Shared Examples:** Use `it_behaves_like "a valid service response"` for service tests -- **VCR:** Use VCR cassettes for external API calls -- **Request Specs:** Prefer request specs over controller specs for full-stack testing -- **Feature Specs:** Use sparingly - prefer request specs for better performance - -### Factory Conventions -```ruby -# Always use factories - never create records directly in specs -let(:account) { create(:account) } -let(:lead) { create(:lead, account: account) } - -# Use traits for variations -let(:verified_account) { create(:account, :verified) } - -# Use build for objects that don't need persistence -let(:lead) { build(:lead, email: "test@example.com") } - -# Use sequences for unique values -sequence(:email) { |n| "user#{n}@example.com" } -``` - -### Service Testing Pattern -```ruby -RSpec.describe Services::User::CreateAccount, type: :service do - describe ".call" do - context "when valid" do - subject(:service) { described_class.call(email: "user@example.com") } - - it_behaves_like "a valid service response" do - let(:service_response) { service } - let(:response) { be_a(Account) } - end - end - - context "when invalid" do - subject(:service) { described_class.call(email: nil) } - - it_behaves_like "an invalid service response" do - let(:service_response) { service } - let(:error_array) do - [[:email, I18n.t("services.user.create_account.errors.email_required")]] - end - end - end - end -end -``` - -### Test Anti-Patterns to Avoid -- Don't test private methods directly -- Don't use hardcoded IDs (use `-1` for nonexistent records) -- Don't use `sleep` - use proper waiting conditions -- Don't mock ActiveRecord in integration tests -- Don't write specs with multiple unrelated expectations -- Don't depend on specific data ordering without explicit `ORDER BY` - ---- - -## Anti-Patterns to Avoid - -### Architecture Anti-Patterns -- **Don't use ActiveRecord models as service objects** - models handle data, services handle business logic -- **Don't put business logic in controllers, views, or callbacks** - use service objects -- **Don't use before_save/after_save for cross-model operations** - use explicit service transactions -- **Don't create services with multiple public methods** - one service, one purpose -- **Don't use concerns as dumping grounds** - only for truly shared, cohesive behavior -- **Don't use callbacks for emails, APIs, or side effects** - make these explicit in services -- **Don't use default_scope** - causes hidden complexity and unexpected behavior -- **Don't create deeply nested modules** - keep namespace depth reasonable (max 3 levels) - -### Code Organization Anti-Patterns -- **Don't put query logic in controllers or views** - use query objects or scopes -- **Don't put presentation logic in models** - use presenters -- **Don't put business logic in helpers** - use services or presenters -- **Don't use class inheritance when composition is clearer** - prefer modules and delegation -- **Don't use update_attribute/update_column** unless you explicitly need to skip validations -- **Don't create conditional logic based on Rails.env** - extract to configuration - -### Testing Anti-Patterns -- **Don't write model specs testing ActiveRecord functionality** - Rails already tests this -- **Don't write unit tests requiring database when logic can be tested in isolation** -- **Don't use fixtures** - use factories with explicit attributes -- **Don't test private methods directly** - test through public interface -- **Don't use hardcoded IDs** - use `-1` for nonexistent record tests -- **Don't use sleep statements** - use proper waiting/polling conditions -- **Don't mock ActiveRecord models in integration tests** -- **Don't write specs with multiple expectations testing different behaviors** - -### Metaprogramming Anti-Patterns -- **Don't add "clever" metaprogramming** that obscures control flow -- **Avoid method_missing** unless absolutely necessary -- **Don't use global refinements** - they're confusing and break tooling -- **Don't monkey-patch core classes** in application code (lib/monkey_patches is exception) - ---- - -## Code Review Checklist - -### Code Quality & Readability -- [ ] Method and variable names clearly express their purpose -- [ ] Methods do exactly what their names suggest, no more and no less -- [ ] Code reads like well-written prose with early returns reducing nesting -- [ ] No debug statements, console logs, or print statements remain -- [ ] Complex algorithms and business logic have explanatory "why" comments -- [ ] Magic numbers and strings extracted to named constants -- [ ] No deeply nested conditionals (max 3 levels) -- [ ] No "clever" code sacrificing readability for brevity -- [ ] All private methods have underscore prefix - -### Architecture & Organization -- [ ] Functions and methods follow single responsibility principle -- [ ] Proper abstraction levels - high-level doesn't mix with low-level details -- [ ] No code duplication - shared logic extracted appropriately -- [ ] File and class organization follows project structure -- [ ] No circular dependencies between modules -- [ ] Inheritance hierarchies are shallow and purposeful -- [ ] Public/private visibility is appropriate - -### Error Handling & Edge Cases -- [ ] Error messages provide actionable information -- [ ] Consistent error handling patterns across similar operations -- [ ] No empty catch blocks or silently swallowed exceptions -- [ ] Collection operations handle empty collections gracefully -- [ ] Boundary conditions explicitly handled (null, zero, empty, max) -- [ ] Methods have predictable, consistent return types - -### Testing -- [ ] Test descriptions clearly express what is being tested -- [ ] Tests validate behavior, not implementation details -- [ ] Tests use factories, not fixtures -- [ ] No hardcoded IDs or database-generated values in tests - -### Security -- [ ] No credentials, tokens, or secrets in code -- [ ] User input is validated and sanitized -- [ ] Strong parameters properly configured -- [ ] SQL queries use parameterization - -### Maintenance -- [ ] TODO comments include ticket numbers -- [ ] No commented-out code without justification -- [ ] No temporary code or experimental features being merged -- [ ] Configuration values externalized - ---- - -## Performance Considerations - -### Query Optimization -- **N+1 Queries:** Always prevent using `includes`, `preload`, or `eager_load` -- **Database Indexes:** Add indexes for foreign keys and commonly queried fields -- **Batch Processing:** Use `find_each` and `in_batches` for large datasets -- **Bulk Operations:** Use `update_all` and `insert_all` instead of iterating -- **Select Specific Columns:** Use `pluck` or `select` when you don't need full records -- **EXISTS Queries:** Use `exists?` instead of loading associations to check presence - -### Memoization -Use memoization for expensive computations within request cycles: - -```ruby -def _current_subscription - @current_subscription ||= account.subscriptions.active.first -end - -# For methods that might return nil or false -def _cached_feature_flag - return @cached_feature_flag if defined?(@cached_feature_flag) - - @cached_feature_flag = FeatureFlag.enabled?(:new_dashboard) -end -``` - -### Caching -- Use Solid Cache for Rails caching needs -- Fragment caching for expensive view partials -- Low-level caching for computed values -- Cache invalidation strategy must be explicit - -### Background Jobs -- Use Solid Queue for expensive operations -- Keep jobs idempotent and focused -- Use appropriate queue priorities -- Monitor job performance and failures - ---- - -## Security Best Practices - -### Environment Variables & Secrets -- **NEVER read `.env` files** - they contain sensitive credentials, API keys, and secrets -- Use `.env.example` as reference for required environment variables -- For environment configuration questions, refer to `.env.example` only -- All secrets must be stored in environment variables, never hardcoded -- **NEVER read Rails credentials files directly** (`config/credentials.yml.enc`, `config/master.key`) - -### Rails Credentials Access -When you need credential values for documentation or examples: -```ruby -# Access credentials in code (never read the encrypted file directly) -Rails.application.credentials.dig(:sendgrid, :api_key) -Rails.application.credentials.secret_key_base - -# Environment-specific credentials -Rails.application.credentials.dig(Rails.env.to_sym, :database, :password) -``` - -### Strong Parameters -Controllers handling user input MUST implement strong parameters: - -- **Include StrongParameters concern** for common patterns -- **Define explicit permit lists** for each action -- **Use `params.require(:model).permit(...)`** for standard CRUD -- **Use `params.expect(model: [...])` in Rails 8+** for enhanced type safety -- **Never use `params.permit!`** in production code - -Example: -```ruby -class LeadsController < ApplicationController - private - - def _lead_params - params.require(:lead).permit(:email, :first_name, :last_name, :phone_number, :status) - end -end -``` - -### Additional Security Measures -- **SQL Injection:** Use parameterized queries, never interpolate user input -- **Authentication:** Rodauth for authentication (NOT Devise) `[PROJECT-SPECIFIC]` -- **Authorization:** Pundit for authorization policies -- **Security Scanning:** Run `bundle exec rake appfactory:test:brakeman` regularly -- **CSRF:** Ensure CSRF protection is enabled (Rails default) -- **XSS:** Use Rails' built-in escaping, be careful with `html_safe` - ---- - -## Error Handling - -- **Service Pattern:** All services return structured responses with `errors` and `valid?` -- **I18n:** Use translations for all error messages from `config/locales/` -- **Error Responses:** Follow standard HTTP status codes -- **Logging:** Include appropriate logging for debugging -- **User Communication:** Provide clear, helpful error messages via I18n -- **Exception Classes:** Create specialized exception classes for domain errors - -```ruby -# Custom exception example -module Errors - class InvalidLeadError < StandardError - attr_reader :lead, :validation_errors - - def initialize(lead, validation_errors) - @lead = lead - @validation_errors = validation_errors - super("Lead #{lead.id} is invalid: #{validation_errors.full_messages.join(', ')}") - end - end -end -``` - ---- - -## Logging Standards - -### Log Levels -- **debug:** Detailed diagnostic information for development -- **info:** General operational events (requests, jobs started/completed) -- **warn:** Unexpected but handled situations -- **error:** Errors that need attention but don't crash the app -- **fatal:** Critical errors causing app termination - -### What to Log -```ruby -# Service entry/exit with context -Rails.logger.info("Starting lead import", import_id: import.id, row_count: rows.count) -Rails.logger.info("Lead import completed", import_id: import.id, created: created_count, failed: failed_count) - -# External API calls -Rails.logger.info("SendGrid API request", endpoint: "/mail/send", recipient: email) -Rails.logger.warn("SendGrid API retry", attempt: attempt, error: error.message) - -# Errors with context -Rails.logger.error("Lead creation failed", account_id: account.id, errors: lead.errors.full_messages) -``` - -### What NOT to Log -- Passwords, tokens, API keys, or secrets -- Full credit card numbers or sensitive PII -- Large request/response bodies (log summary instead) -- Health check requests (too noisy) - ---- - -## API Conventions `[OPTIONAL - Remove if not building APIs]` - -### Response Structure -```json -// Success response -{ - "data": { - "id": "123", - "type": "lead", - "attributes": { - "email": "user@example.com", - "status": "active" - } - }, - "meta": { - "request_id": "abc-123", - "timestamp": "2024-01-15T10:30:00Z" - } -} - -// Error response -{ - "error": { - "code": "validation_failed", - "message": "The request could not be processed", - "details": [ - { "field": "email", "message": "is invalid" } - ] - }, - "meta": { - "request_id": "abc-123" - } -} - -// Collection response -{ - "data": [...], - "meta": { - "total_count": 100, - "page": 1, - "per_page": 25 - }, - "links": { - "next": "/api/leads?page=2", - "prev": null - } -} -``` - -### HTTP Status Codes -- `200 OK` - Successful GET, PUT, PATCH -- `201 Created` - Successful POST creating a resource -- `204 No Content` - Successful DELETE -- `400 Bad Request` - Malformed request syntax -- `401 Unauthorized` - Missing or invalid authentication -- `403 Forbidden` - Authenticated but not authorized -- `404 Not Found` - Resource doesn't exist -- `422 Unprocessable Entity` - Validation errors -- `500 Internal Server Error` - Unexpected server error - ---- - -## Browser Automation & E2E Testing - -When browser automation is needed (testing pages, filling forms, taking screenshots, validating UX, testing login flows, etc.), use the Playwright skill: - -``` -skill: "playwright-skill" -``` - -This skill provides complete browser automation with Playwright. It can: -- Auto-detect dev servers -- Write clean test scripts to /tmp -- Test pages and fill forms -- Take screenshots -- Check responsive design -- Validate UX flows -- Test login flows and authentication -- Check links and automate any browser task - ---- - -## General Principles - -- **Readability:** Favor readable, maintainable, and idiomatic code over clever solutions -- **SOLID Principles:** Apply SOLID principles, especially Single Responsibility (ServiceWrapper pattern) -- **DRY:** Don't Repeat Yourself, but don't abstract prematurely -- **YAGNI:** You Aren't Gonna Need It - avoid over-engineering -- **Sandy Metz Rules:** Small methods, small classes, tell don't ask -- **Clarification:** If uncertain about requirements or implementation, ask before proceeding -- **Browser Testing:** Always explicitly close the browser when done: `mcp__playwright__browser_close()` -- **Process Cleanup:** CRITICAL - Always stop any process that is started during the prompt: - - **BEFORE starting work:** Check for running processes: `ps aux | grep -E "ruby|rails|puma|node" | grep -v grep` - - **AFTER work is complete:** Kill ALL processes started during this session: `pkill -f puma` - - **EVERY Rails server, background job, or Node process you start MUST be stopped before the prompt ends** - - Use `pkill -f puma` to stop Rails servers, `pkill -f node` for Node processes - - This MUST be done every single time to prevent resource leaks and port conflicts - - Do NOT leave background processes running when the task is complete -- **Test File Cleanup:** ALWAYS delete any test files created during debugging unless explicitly told to keep them: - - Delete test HTML files created in public/ - - Delete any temporary test scripts or files - - Clean up immediately after verification is complete - - Only keep test files if user explicitly says "save" or "keep" - ---- - -## Deployment Instructions - -- **Heroku:** NEVER push to Heroku unless explicitly confirmed with "yes" or "no" from the user `[PROJECT-SPECIFIC]` - ---- - -## Template Customization Guide - -When using this CLAUDE.md as a template for a new project, update the following: - -### Required Updates -1. **Ruby/Rails versions** in Code Style section -2. **Essential Commands** - update rake task namespaces -3. **Project Directory Structure** - add/remove directories based on patterns used -4. **Authentication Architecture** - update if not using Rodauth -5. **Frontend Architecture** - update based on actual stack -6. **Email Services** - update provider and service class names -7. **Deployment Instructions** - update for your deployment platform - -### Optional Sections to Remove -- `[OPTIONAL]` marked sections if not applicable: - - Input Objects (if not parsing complex params) - - Presenter Objects (if views are simple) - - Builder Objects (if not constructing complex objects) - - Value Objects (if not using domain value types) - - Null Objects (if not using null object pattern) - - API Conventions (if not building APIs) - -### Template Markers Reference -- `[PROJECT-SPECIFIC]` - Must change for each project -- `[OPTIONAL]` - Remove entire section if not used -- `[CUSTOMIZE]` - Adapt list/values to project needs diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb index 44566b9..0004bfb 100755 --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -3,14 +3,22 @@ # ruby "$HOME/.myconfigurations/applications/claude/setup.rb" -require_relative "helpers/setup_helper" +require "highline" +require_relative "../../lib/helpers/setup_helper" + +# Guard: this script must only run inside a git project directory. +unless system( "git rev-parse --git-dir > /dev/null 2>&1" ) + cli = HighLine.new + cli.say( "<%= color( 'ERROR: Must be run inside a git project directory!', :red ) %>" ) + exit( 1 ) +end PATHS = [] # rubocop:disable Style/MutableConstant PATHS.push( { claude: [ { - source: "test/.claude/settings.local.json", + source: ".claude/settings.local.json", destination: "$HOME/.myconfigurations.private/claude/local/settings.json", } ], diff --git a/lib/helpers/setup_helper.rb b/lib/helpers/setup_helper.rb index bb399b1..bc6defa 100644 --- a/lib/helpers/setup_helper.rb +++ b/lib/helpers/setup_helper.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require "fileutils" +require "highline" require_relative "path_helper" module SetupHelper def self.process_paths( paths ) + cli = HighLine.new paths.each do |path_group| # Handle nested structure with tool names as keys (e.g., claude, claude2, etc.) path_group.each do |tool_name, tool_paths| @@ -25,8 +27,14 @@ def self.process_paths( paths ) File.delete( source ) end else - puts( " Regular file exists at #{source}, cannot create symlink" ) - next + cli.say( " WARNING: Regular file exists at <%= color('#{source}', :yellow) %>" ) + if cli.agree( " Replace with symlink? " ) + cli.say( " Removing regular file: #{source}" ) + File.delete( source ) + else + cli.say( " Skipping: #{source}" ) + next + end end end @@ -35,22 +43,26 @@ def self.process_paths( paths ) source_dir = File.dirname( source ) if source_dir.start_with?( home_dir ) && !File.exist?( home_dir ) - puts( " ERROR: Home directory does not exist: #{home_dir}, skipping" ) + puts( " ERROR: Home directory does not exist: #{home_dir}, skipping!" ) next end destination_dir = File.dirname( destination ) if destination_dir.start_with?( home_dir ) && !File.exist?( home_dir ) - puts( " ERROR: Home directory does not exist: #{home_dir}, skipping" ) + puts( " ERROR: Home directory does not exist: #{home_dir}, skipping!" ) next end unless File.exist?( source_dir ) puts( " Creating source directory: #{source_dir}" ) FileUtils.mkdir_p( source_dir ) + add_keep_file( source_dir ) end - FileUtils.mkdir_p( destination_dir ) + unless File.exist?( destination_dir ) + FileUtils.mkdir_p( destination_dir ) + add_keep_file( destination_dir ) + end puts( " Creating symlink: #{source} -> #{destination}" ) File.symlink( destination, source ) @@ -58,4 +70,9 @@ def self.process_paths( paths ) end end end + + def self.add_keep_file( dir ) + keep_file = File.join( dir, ".keep" ) + FileUtils.touch( keep_file ) unless File.exist?( keep_file ) + end end diff --git a/spec/helpers/setup_helper_spec.rb b/spec/helpers/setup_helper_spec.rb index 52ed135..904a581 100644 --- a/spec/helpers/setup_helper_spec.rb +++ b/spec/helpers/setup_helper_spec.rb @@ -37,12 +37,17 @@ # to our sandbox instead of the real home directory. let( :tmpdir ) { Dir.mktmpdir( "setup_helper_spec" ) } let( :original_home ) { Dir.home } + let( :highline_instance ) { instance_double( HighLine ) } before do original_home ENV["HOME"] = tmpdir allow( $stdout ).to receive( :write ) allow( $stdout ).to receive( :puts ) + # Stub HighLine.new so interactive prompts don't block tests. + allow( HighLine ).to receive( :new ).and_return( highline_instance ) + allow( highline_instance ).to receive( :say ) + allow( highline_instance ).to receive( :agree ).and_return( false ) end after do @@ -156,13 +161,12 @@ end # -------------------------------------------------------------------------- - # Regular file exists at source -- skip - # If a real (non-symlink) file exists at the source location, the method - # refuses to overwrite it. This protects user data that hasn't been moved - # into the configuration repo yet. + # Regular file exists at source -- user approves replacement + # When a real file exists, HighLine prompts the user. If they approve, + # the file is removed and replaced with a symlink. # -------------------------------------------------------------------------- - context "when a regular file exists at source" do - it "does not create a symlink and leaves the file intact" do + context "when a regular file exists and user approves replacement" do + it "removes the file and creates a symlink" do source_path = File.join( tmpdir, ".tmux.conf" ) dest_file = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) FileUtils.mkdir_p( File.dirname( dest_file ) ) @@ -171,6 +175,45 @@ # Create a real file (not a symlink) at the source path. File.write( source_path, "user local config" ) + # User approves the replacement. + allow( highline_instance ).to receive( :agree ).and_return( true ) + + paths = [ + { + tmux: [ + { + source: "$HOME/.tmux.conf", + destination: "$HOME/.myconfigurations/applications/tmux/conf", + } + ], + } + ] + + described_class.process_paths( paths ) + + # The regular file should be replaced with a symlink. + expect( File.symlink?( source_path ) ).to be true + expect( File.readlink( source_path ) ).to eq( dest_file ) + end + end + + # -------------------------------------------------------------------------- + # Regular file exists at source -- user declines replacement + # When a real file exists and the user declines, the file is left intact + # and no symlink is created. + # -------------------------------------------------------------------------- + context "when a regular file exists and user declines replacement" do + it "leaves the file intact and does not create a symlink" do + source_path = File.join( tmpdir, ".tmux.conf" ) + dest_file = File.join( tmpdir, ".myconfigurations/applications/tmux/conf" ) + FileUtils.mkdir_p( File.dirname( dest_file ) ) + File.write( dest_file, "repo config" ) + + # Create a real file (not a symlink) at the source path. + File.write( source_path, "user local config" ) + + # User declines the replacement (default stub returns false). + paths = [ { tmux: [ @@ -219,6 +262,29 @@ expect( Dir.exist?( source_dir ) ).to be true expect( Dir.exist?( dest_dir ) ).to be true end + + # .keep files are added to newly created directories so they can be + # tracked in git. Verify both source and destination directories get one. + it "adds a .keep file to newly created source and destination directories" do + paths = [ + { + newtool: [ + { + source: "$HOME/.config/keeptest/settings.json", + destination: "$HOME/.myconfigurations/apps/keeptest/settings.json", + } + ], + } + ] + + described_class.process_paths( paths ) + + source_keep = File.join( tmpdir, ".config/keeptest/.keep" ) + dest_keep = File.join( tmpdir, ".myconfigurations/apps/keeptest/.keep" ) + + expect( File.exist?( source_keep ) ).to be true + expect( File.exist?( dest_keep ) ).to be true + end end # -------------------------------------------------------------------------- From 892ff002c11f72dd509abe3987ef5675bb678648 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 11:18:52 -0800 Subject: [PATCH 28/62] force .keep in every setting dir --- applications/claude/setup.rb | 2 +- lib/helpers/setup_helper.rb | 26 +++++++++++++++++++++----- spec/helpers/setup_helper_spec.rb | 30 ++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb index 0004bfb..2673dbc 100755 --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -18,7 +18,7 @@ { claude: [ { - source: ".claude/settings.local.json", + source: "test/test/test/.claude/settings.local.json", destination: "$HOME/.myconfigurations.private/claude/local/settings.json", } ], diff --git a/lib/helpers/setup_helper.rb b/lib/helpers/setup_helper.rb index bc6defa..12d6740 100644 --- a/lib/helpers/setup_helper.rb +++ b/lib/helpers/setup_helper.rb @@ -54,14 +54,16 @@ def self.process_paths( paths ) end unless File.exist?( source_dir ) + base_dir = deepest_existing_ancestor( source_dir ) puts( " Creating source directory: #{source_dir}" ) FileUtils.mkdir_p( source_dir ) - add_keep_file( source_dir ) + add_keep_files( source_dir, base_dir ) end unless File.exist?( destination_dir ) + base_dir = deepest_existing_ancestor( destination_dir ) FileUtils.mkdir_p( destination_dir ) - add_keep_file( destination_dir ) + add_keep_files( destination_dir, base_dir ) end puts( " Creating symlink: #{source} -> #{destination}" ) @@ -71,8 +73,22 @@ def self.process_paths( paths ) end end - def self.add_keep_file( dir ) - keep_file = File.join( dir, ".keep" ) - FileUtils.touch( keep_file ) unless File.exist?( keep_file ) + def self.deepest_existing_ancestor( dir ) + current = dir + current = File.dirname( current ) until File.exist?( current ) || current == "/" || current == "." + current + end + + def self.add_keep_files( dir, base_dir ) + current = dir + dirs_to_keep = [] + while current != base_dir && current != "/" && current != "." + dirs_to_keep << current + current = File.dirname( current ) + end + dirs_to_keep.each do |d| + keep_file = File.join( d, ".keep" ) + FileUtils.touch( keep_file ) unless File.exist?( keep_file ) + end end end diff --git a/spec/helpers/setup_helper_spec.rb b/spec/helpers/setup_helper_spec.rb index 904a581..a7a2ccf 100644 --- a/spec/helpers/setup_helper_spec.rb +++ b/spec/helpers/setup_helper_spec.rb @@ -285,6 +285,36 @@ expect( File.exist?( source_keep ) ).to be true expect( File.exist?( dest_keep ) ).to be true end + + # When mkdir_p creates multiple directories (e.g., a/b/c/d), every + # intermediate directory must receive a .keep file -- not just the leaf. + # This ensures all directories can be tracked in git. + it "adds .keep files to all intermediate directories created by mkdir_p" do + paths = [ + { + deep: [ + { + source: "$HOME/a/b/c/d/settings.json", + destination: "$HOME/w/x/y/z/settings.json", + } + ], + } + ] + + described_class.process_paths( paths ) + + # Every intermediate source directory should have a .keep file. + %w[a a/b a/b/c a/b/c/d].each do |subpath| + keep = File.join( tmpdir, subpath, ".keep" ) + expect( File.exist?( keep ) ).to( be( true ), "expected .keep in #{subpath}" ) + end + + # Every intermediate destination directory should have a .keep file. + %w[w w/x w/x/y w/x/y/z].each do |subpath| + keep = File.join( tmpdir, subpath, ".keep" ) + expect( File.exist?( keep ) ).to( be( true ), "expected .keep in #{subpath}" ) + end + end end # -------------------------------------------------------------------------- From 9edcafce23fdc25fcb31b2c96e4546a66268be04 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 11:22:26 -0800 Subject: [PATCH 29/62] update local claude settings --- applications/claude/setup.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb index 2673dbc..ee3de1b 100755 --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -18,8 +18,8 @@ { claude: [ { - source: "test/test/test/.claude/settings.local.json", - destination: "$HOME/.myconfigurations.private/claude/local/settings.json", + source: ".claude/settings.local.json", + destination: "$HOME/.myconfigurations.private/claude/local/settings.local.json", } ], }, From af85eebb3a8f19f800f7e0eb6b1df7bfd28c0727 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 17:52:58 -0800 Subject: [PATCH 30/62] add kafka docker --- applications/docker/.keep | 0 applications/docker/templates/kafka.yml | 16 ++++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 applications/docker/.keep create mode 100644 applications/docker/templates/kafka.yml diff --git a/applications/docker/.keep b/applications/docker/.keep new file mode 100644 index 0000000..e69de29 diff --git a/applications/docker/templates/kafka.yml b/applications/docker/templates/kafka.yml new file mode 100644 index 0000000..6391e7b --- /dev/null +++ b/applications/docker/templates/kafka.yml @@ -0,0 +1,16 @@ +services: + kafka: + image: apache/kafka:3.9.0 + hostname: kafka + ports: + - 9092:9092 + environment: + CLUSTER_ID: 5L6g3nShT-eMCtK--X86sw + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 From 86d521bbca9921ecc2b21ad061b3bb13d36ec37f Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 18:35:02 -0800 Subject: [PATCH 31/62] update readme --- readme.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index ed4105a..d1e1abf 100644 --- a/readme.md +++ b/readme.md @@ -44,12 +44,12 @@ ruby "$HOME/.myconfigurations/lib/setup.rb" **Idempotency:** The `SetupHelper` module checks each path before acting: - Correct symlink exists β†’ skips (no action) - Wrong symlink exists β†’ removes old, creates correct -- Regular file exists β†’ skips (preserves user files) +- Regular file exists β†’ prompts to approve/decline replacement - Nothing exists β†’ creates parent directories and symlink **Managed Symlinks:** -| Category | Source (System Location) | Destination (Repository) | +| Category | Source (System Location) | Destination (Config Store) | |----------|--------------------------|--------------------------| | dotfiles | `~/.bash_profile` | `dotfiles/.bash_profile` | | dotfiles | `~/.bashrc` | `dotfiles/.bashrc` | @@ -59,9 +59,10 @@ ruby "$HOME/.myconfigurations/lib/setup.rb" | dotfiles | `~/.vimrc` | `dotfiles/.vimrc` | | dotfiles | `~/.zprofile` | `dotfiles/.zprofile` | | dotfiles | `~/.zshrc` | `dotfiles/.zshrc` | -| claude | `~/.claude/settings.json` | `aitooling/claude/settings.json` | -| claude | `~/.claude/CLAUDE.md` | `aitooling/claude/CLAUDE.md` | | tmux | `~/.tmux.conf` | `applications/tmux/conf` | +| tmux | `applications/tmux/paths.json` | `~/.myconfigurations.private/tmux/paths.json` | +| claude | `~/.claude/settings.json` | `~/.myconfigurations.private/claude/global/settings.json` | +| claude | `~/.claude/CLAUDE.md` | `~/.myconfigurations.private/claude/brains/global/CLAUDE.md` | | vscode | `~/Library/.../User/settings.json` | `applications/vscode/settings.json` | | vscode | `~/Library/.../User/keybindings.json` | `applications/vscode/keybindings.json` | | vscode | `~/Library/.../User/snippets/ruby.json` | `applications/vscode/snippets/ruby.json` | From 65aca9c8595250558d6109a9ed5c47a4564a2630 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 18:37:29 -0800 Subject: [PATCH 32/62] setup ai sync --- docs/installation/004.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index 99b2378..323f2cf 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -12,8 +12,5 @@ 2. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. -#### Favorite Skills in Claude -* @anthropics/claude-plugins-official/playwright -* @anthropics/claude-plugins-official/code-simplifier -* @anthropics/claude-plugins-official/frontend-design -* @anthropics/claude-plugins-official/code-review +3. Configure `chezmoi` sychronization settings for ai tooling and infrastructure. + * `chezmoi init your-github-username` From f1f463bdfe58d33fa1412cac9fcdb107d0f558f2 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Feb 2026 22:57:14 -0800 Subject: [PATCH 33/62] configure chezmoi and sync settings --- docs/installation/003.md | 4 +++- docs/installation/004.md | 12 +++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/installation/003.md b/docs/installation/003.md index 25ae28a..1af3844 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -46,7 +46,9 @@ Host github.com AddKeysToAgent yes UseKeychain yes ``` -8. Close the private repo `git clone git@github.com:chrishough/my-configurations-private.git .myconfigurations.private` into the `$HOME` directory. +8. Close these private repo into the `$HOME` directory. + * `git clone git@github.com:chrishough/my-configurations-private.git .myconfigurations.private` + * `git clone git@github.com:chrishough/my-configurations-private-sync.git .myconfigurations.private.sync` > I am documenting this as part of my flow, but will not be public. Also, steps in the process need this cloned, but will not be fully utilized until later steps. 9. Clone this repo `git clone git@github.com:chrishough/my-configurations.git .myconfigurations` into the `$HOME` directory. * Setup the `.myconfigurations.private.keys` file for private keys and ENV settings. diff --git a/docs/installation/004.md b/docs/installation/004.md index 323f2cf..568442a 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -13,4 +13,14 @@ * Once this has been loaded Claude will be symlinked to the project, restart it. 3. Configure `chezmoi` sychronization settings for ai tooling and infrastructure. - * `chezmoi init your-github-username` + * `mkdir .config/chezmoi` + * `nano .config/chezmoi/chezmoi.toml` + ``` + sourceDir = "~/.myconfigurations.private.sync" + + [git] + autoAdd = true + autoCommit = true + autoPush = true + ``` + * `chezmoi add $HOME/.myconfigurations.private/claude/local/settings.local.json` From 661b7c73bb78c54b92b36093d01423ab0972147a Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Feb 2026 06:55:25 -0800 Subject: [PATCH 34/62] add watchman --- docs/installation/004.md | 2 +- lib/install.sh | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index 568442a..683825e 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -12,7 +12,7 @@ 2. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. -3. Configure `chezmoi` sychronization settings for ai tooling and infrastructure. +3. Configure `watchman` plus `chezmoi` sychronization settings for ai tooling and infrastructure. * `mkdir .config/chezmoi` * `nano .config/chezmoi/chezmoi.toml` ``` diff --git a/lib/install.sh b/lib/install.sh index 15222a7..c695791 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -30,6 +30,7 @@ install_brew_packages() { "chezmoi" "gh" "terminal-notifier" + "watchman" ) echo "Running brew update..." From 1b81d439a55548ed67833c5fe804aa1d2c2f4e26 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Feb 2026 11:15:54 -0800 Subject: [PATCH 35/62] update sync flow and functions --- docs/installation/004.md | 14 ++++++++++++++ dotfiles/functions/collections/sync | 18 ++++++++++++++++++ dotfiles/functions/functions | 2 +- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 dotfiles/functions/collections/sync diff --git a/docs/installation/004.md b/docs/installation/004.md index 683825e..5ecbedc 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -24,3 +24,17 @@ autoPush = true ``` * `chezmoi add $HOME/.myconfigurations.private/claude/local/settings.local.json` + + +> We need to setup watchman triggers so core files are always kept in sync across devices + +1. Setup the events monitored by the server. +``` + watchman -j <<'EOT' + ["trigger", "~/Users/christopherhough~/.myconfigurations.private/claude/local", { + "name": "chezmoi-re-add", + "expression": ["name", "settings.local.json"], + "command": ["/opt/homebrew/bin/chezmoi", "re-add"] + }] + EOT +``` diff --git a/dotfiles/functions/collections/sync b/dotfiles/functions/collections/sync new file mode 100644 index 0000000..28a7170 --- /dev/null +++ b/dotfiles/functions/collections/sync @@ -0,0 +1,18 @@ +#!/bin/zsh + +function cm() { chezmoi managed } +function cra() { chezmoi re-add } + +function wl() { watchman watch-list } + +function synchelp () { + echo + echo "--------------------------------------------------------------------------------" + echo + echo " cm = chezmoi managed" + echo " cra = chezmoi re-add" + echo " wl = watchman watch-list" + echo + echo "--------------------------------------------------------------------------------" + echo +} diff --git a/dotfiles/functions/functions b/dotfiles/functions/functions index fedbafe..8d6d6ab 100644 --- a/dotfiles/functions/functions +++ b/dotfiles/functions/functions @@ -15,7 +15,7 @@ export BROWSER='open -a /Applications/Google Chrome.app' export EDITOR=vim # IMPORT FUNCTIONS GROUPS -_myconfig_modules=(ruby docker git brew python heroku js shell divvy) +_myconfig_modules=(ruby docker git brew python heroku js shell divvy sync) for _module in "${_myconfig_modules[@]}"; do source "$HOME/.myconfigurations/dotfiles/functions/collections/$_module" From e57223c75d23086ca205109cc1c02e8f35eb2590 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Feb 2026 14:52:11 -0800 Subject: [PATCH 36/62] update sync flow and functions --- docs/installation/003.md | 6 +++--- docs/installation/004.md | 23 ++++++++++++++--------- dotfiles/functions/collections/sync | 9 +++++++++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/installation/003.md b/docs/installation/003.md index 1af3844..a66291a 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -10,7 +10,7 @@ 4. Update brew via `brew update`. 5. Install the latest version of git via `brew install git`. 6. Setup the `$HOME/.gitconfig` file with account information. -``` +```ini [user] name = Chris Hough email = { users's email address } @@ -37,7 +37,7 @@ defaultBranch = master ``` 7. Setup your new `machine specific ssh keys`, and make sure to setup the `$HOME/.ssh/config`, then run `ssh-add --apple-use-keychain ~/.ssh/{ private key name not .pub }` so keys can be executed globally without having to type the passphrase in again and again. -``` +```ini Host github.com HostName github.com User git @@ -65,7 +65,7 @@ Host github.com * Install [rbenv-vars](https://github.com/rbenv/rbenv-vars) plugin `git clone https://github.com/rbenv/rbenv-vars.git $(rbenv root)/plugins/rbenv-vars`. * Install [rbenv-update](https://github.com/rkh/rbenv-update) plugin `git clone https://github.com/rkh/rbenv-update.git $(rbenv root)/plugins/rbenv-update`. 11. Verify global rbenv vars are setup correctly. Type `nano $HOME/.rbenv/vars` and confirm the following. Adjust if necessary. Once completed, type `rbenv vars` and you should see `export GEM_PATH='bundle'`. That is **all** that should be in there at this time! -``` +```ini #GLOBAL GEM_PATH=.bundle ``` diff --git a/docs/installation/004.md b/docs/installation/004.md index 5ecbedc..96a707e 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -15,7 +15,8 @@ 3. Configure `watchman` plus `chezmoi` sychronization settings for ai tooling and infrastructure. * `mkdir .config/chezmoi` * `nano .config/chezmoi/chezmoi.toml` - ``` + + ```ini sourceDir = "~/.myconfigurations.private.sync" [git] @@ -29,12 +30,16 @@ > We need to setup watchman triggers so core files are always kept in sync across devices 1. Setup the events monitored by the server. -``` - watchman -j <<'EOT' - ["trigger", "~/Users/christopherhough~/.myconfigurations.private/claude/local", { - "name": "chezmoi-re-add", - "expression": ["name", "settings.local.json"], - "command": ["/opt/homebrew/bin/chezmoi", "re-add"] - }] - EOT +```bash +watchman watch ~/.myconfigurations.private/claude + +echo "[ + \"trigger\", + \"$HOME/.myconfigurations.private/claude\", + { + \"name\": \"chezmoi-re-add\", + \"expression\": [\"name\", \"local/settings.local.json\"], + \"command\": [\"/opt/homebrew/bin/chezmoi\", \"re-add\"] + } +]" | watchman -j ``` diff --git a/dotfiles/functions/collections/sync b/dotfiles/functions/collections/sync index 28a7170..b93d497 100644 --- a/dotfiles/functions/collections/sync +++ b/dotfiles/functions/collections/sync @@ -4,6 +4,13 @@ function cm() { chezmoi managed } function cra() { chezmoi re-add } function wl() { watchman watch-list } +function wts() { watchman trigger-list } +function wtsa() { + for root in $(watchman watch-list | grep -o '"\/[^"]*"' | tr -d '"'); do + echo "=== $root ===" + watchman trigger-list "$root" + done +} function synchelp () { echo @@ -12,6 +19,8 @@ function synchelp () { echo " cm = chezmoi managed" echo " cra = chezmoi re-add" echo " wl = watchman watch-list" + echo " wts = watchman trigger-list" + echo " wtsa = watchman trigger-list all" echo echo "--------------------------------------------------------------------------------" echo From 8f4d8b796c7913017521cdca03a6873af3d9d6a0 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Feb 2026 15:29:04 -0800 Subject: [PATCH 37/62] update sync flow and functions --- docs/installation/004.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index 96a707e..ccf0784 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -38,7 +38,7 @@ echo "[ \"$HOME/.myconfigurations.private/claude\", { \"name\": \"chezmoi-re-add\", - \"expression\": [\"name\", \"local/settings.local.json\"], + \"expression\": [\"name\", \"local/settings.local.json\", \"wholename\"], \"command\": [\"/opt/homebrew/bin/chezmoi\", \"re-add\"] } ]" | watchman -j From 82af598e9be688dc4c85cb56fbf4dd93bad96e3b Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Feb 2026 18:41:43 -0800 Subject: [PATCH 38/62] update sync flow and functions --- applications/claude/setup.rb | 1 + dotfiles/functions/collections/sync | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/applications/claude/setup.rb b/applications/claude/setup.rb index ee3de1b..668c848 100755 --- a/applications/claude/setup.rb +++ b/applications/claude/setup.rb @@ -27,3 +27,4 @@ # Process all collected paths SetupHelper.process_paths( PATHS ) + diff --git a/dotfiles/functions/collections/sync b/dotfiles/functions/collections/sync index b93d497..5ef2cd3 100644 --- a/dotfiles/functions/collections/sync +++ b/dotfiles/functions/collections/sync @@ -1,5 +1,9 @@ #!/bin/zsh +function initsync() { + ruby "$HOME/.myconfigurations/applications/claude/setup.rb" +} + function cm() { chezmoi managed } function cra() { chezmoi re-add } @@ -16,6 +20,7 @@ function synchelp () { echo echo "--------------------------------------------------------------------------------" echo + echo " initsync = initialize claude sync" echo " cm = chezmoi managed" echo " cra = chezmoi re-add" echo " wl = watchman watch-list" From def0c859294876ee68b762bdad760653c5f71dd3 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Feb 2026 22:29:03 -0800 Subject: [PATCH 39/62] update sync flow and functions --- docs/installation/004.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index ccf0784..ce255a8 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -2,20 +2,12 @@ > At this point the private repository `.myconfigurations.private` should have been configured in previous steps as it is required at this point. -1. Install [Claude CLI](https://code.claude.com/docs/en/setup#npm). - * Via NPM `npm install -g @anthropic-ai/claude-code`. - * Install `npx claude-plugins install @anthropics/claude-plugins-official/playwright`. - * Install `npx claude-plugins install @anthropics/claude-plugins-official/code-simplifier`. - * Install `npx claude-plugins install @anthropics/claude-code-plugins/frontend-design`. - * Install `npx claude-plugins install @anthropics/claude-code-plugins/code-review`. - -2. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. +1. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. -3. Configure `watchman` plus `chezmoi` sychronization settings for ai tooling and infrastructure. +2. Configure `watchman` plus `chezmoi` sychronization settings for ai tooling and infrastructure. * `mkdir .config/chezmoi` * `nano .config/chezmoi/chezmoi.toml` - ```ini sourceDir = "~/.myconfigurations.private.sync" @@ -24,12 +16,13 @@ autoCommit = true autoPush = true ``` - * `chezmoi add $HOME/.myconfigurations.private/claude/local/settings.local.json` - +3. Now let's setup the files we want to sync automagically! -> We need to setup watchman triggers so core files are always kept in sync across devices +```bash +chezmoi add $HOME/.myconfigurations.private/claude/local/settings.local.json +``` +4. Setup watchman triggers so core files are always kept in sync across devices. -1. Setup the events monitored by the server. ```bash watchman watch ~/.myconfigurations.private/claude @@ -43,3 +36,15 @@ echo "[ } ]" | watchman -j ``` + + + +*** +WIP + +Install [Claude CLI](https://code.claude.com/docs/en/setup#npm). +* Via NPM `npm install -g @anthropic-ai/claude-code`. +* Install `npx claude-plugins install @anthropics/claude-plugins-official/playwright`. +* Install `npx claude-plugins install @anthropics/claude-plugins-official/code-simplifier`. +* Install `npx claude-plugins install @anthropics/claude-code-plugins/frontend-design`. +* Install `npx claude-plugins install @anthropics/claude-code-plugins/code-review`. From 1b18fc563d523990a0158bd7bd589467d525da77 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Fri, 13 Feb 2026 14:12:21 -0800 Subject: [PATCH 40/62] update sync flow and functions --- docs/installation/004.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index ce255a8..cc8b11a 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -20,21 +20,21 @@ ```bash chezmoi add $HOME/.myconfigurations.private/claude/local/settings.local.json +chezmoi add $HOME/.myconfigurations.private/claude/global/settings.json ``` 4. Setup watchman triggers so core files are always kept in sync across devices. ```bash watchman watch ~/.myconfigurations.private/claude -echo "[ - \"trigger\", - \"$HOME/.myconfigurations.private/claude\", - { - \"name\": \"chezmoi-re-add\", - \"expression\": [\"name\", \"local/settings.local.json\", \"wholename\"], - \"command\": [\"/opt/homebrew/bin/chezmoi\", \"re-add\"] - } -]" | watchman -j +echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { + "name": "chezmoi-re-add", + "expression": ["anyof", + ["name", "local/settings.local.json", "wholename"], + ["name", "global/settings.json", "wholename"] + ], + "command": ["/opt/homebrew/bin/chezmoi", "re-add"] +}]' | watchman -j ``` From b18ac62125424c756688cdec0e5097c4a54b9500 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sat, 14 Feb 2026 16:15:29 -0800 Subject: [PATCH 41/62] add entire to the ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ef15834..e7a6efe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ #-------------------------------------------- *.vscode *.claude +*.entire *.idea *.code From 81eebe3be9998bbe4d4a04a12cc8bd79905596c3 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 15 Feb 2026 15:23:27 -0800 Subject: [PATCH 42/62] add entire --- dotfiles/functions/collections/sync | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dotfiles/functions/collections/sync b/dotfiles/functions/collections/sync index 5ef2cd3..4dcd61c 100644 --- a/dotfiles/functions/collections/sync +++ b/dotfiles/functions/collections/sync @@ -1,9 +1,13 @@ #!/bin/zsh -function initsync() { +function csync() { ruby "$HOME/.myconfigurations/applications/claude/setup.rb" } +function esync() { + entire enable --strategy auto-commit +} + function cm() { chezmoi managed } function cra() { chezmoi re-add } @@ -20,7 +24,8 @@ function synchelp () { echo echo "--------------------------------------------------------------------------------" echo - echo " initsync = initialize claude sync" + echo " csync = initialize claude sync" + echo " esync = enable entire sync with auto-commit strategy" echo " cm = chezmoi managed" echo " cra = chezmoi re-add" echo " wl = watchman watch-list" From 946e3be2c736935f452c269db7c4acc195f639db Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 15 Feb 2026 15:24:39 -0800 Subject: [PATCH 43/62] add ai help --- dotfiles/functions/collections/{sync => ai} | 2 +- dotfiles/functions/functions | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename dotfiles/functions/collections/{sync => ai} (97%) diff --git a/dotfiles/functions/collections/sync b/dotfiles/functions/collections/ai similarity index 97% rename from dotfiles/functions/collections/sync rename to dotfiles/functions/collections/ai index 4dcd61c..d9d2c18 100644 --- a/dotfiles/functions/collections/sync +++ b/dotfiles/functions/collections/ai @@ -20,7 +20,7 @@ function wtsa() { done } -function synchelp () { +function aihelp () { echo echo "--------------------------------------------------------------------------------" echo diff --git a/dotfiles/functions/functions b/dotfiles/functions/functions index 8d6d6ab..7927a0b 100644 --- a/dotfiles/functions/functions +++ b/dotfiles/functions/functions @@ -15,7 +15,7 @@ export BROWSER='open -a /Applications/Google Chrome.app' export EDITOR=vim # IMPORT FUNCTIONS GROUPS -_myconfig_modules=(ruby docker git brew python heroku js shell divvy sync) +_myconfig_modules=(ruby docker git brew python heroku js shell divvy ai) for _module in "${_myconfig_modules[@]}"; do source "$HOME/.myconfigurations/dotfiles/functions/collections/$_module" From f15eb678413cb9b1e85b6ec7c85ac2df17ba8f62 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 16 Feb 2026 14:03:39 -0800 Subject: [PATCH 44/62] In this file lib/install.sh for our setup, how do you recommend adding t Entire-Checkpoint: e646e9452dd4 --- lib/install.sh | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/install.sh b/lib/install.sh index c695791..8978fe7 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -1,6 +1,30 @@ #!/bin/zsh # sh "$HOME/.myconfigurations/lib/install.sh" +# Function to install brew tap packages if they don't exist +install_brew_taps() { + # Array of tap packages in "user/tap/formula" format + local tap_packages=( + "entireio/tap/entire" + "PeonPing/tap/peon-ping" + ) + + echo "Installing tap packages..." + for tap_package in "${tap_packages[@]}"; do + local tap="${tap_package%/*}" + local formula_name="${tap_package##*/}" + + if brew list --formula | grep -q "^${formula_name}\$"; then + echo "βœ“ ${formula_name} is already installed" + else + echo "Tapping ${tap}..." + brew tap "${tap}" + echo "Installing ${tap_package}..." + brew install "${tap_package}" + fi + done +} + # Function to install brew packages if they don't exist install_brew_packages() { # Array of packages to install @@ -36,7 +60,7 @@ install_brew_packages() { echo "Running brew update..." brew update - # Loop through packages and install if not present + echo "Loop through packages and install if not present..." for package in "${packages[@]}"; do if brew list --formula | grep -q "^${package}\$"; then echo "βœ“ ${package} is already installed" @@ -51,5 +75,6 @@ install_brew_packages() { brew doctor } -# Run the installation function +# Run the installation functions +install_brew_taps install_brew_packages From bf5caf3314204c05cba97b2a1852952917bf12ba Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 16 Feb 2026 15:01:05 -0800 Subject: [PATCH 45/62] add peon ping settings --- docs/installation/004.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation/004.md b/docs/installation/004.md index cc8b11a..036638b 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -37,6 +37,7 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { }]' | watchman -j ``` +5. Install `peon-ping-setup` for our peon pings that fire during claude workflows. *** From 5eeb514c379a470ba0c888f2a3689aa5b78b7b06 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 18 Feb 2026 15:32:13 -0800 Subject: [PATCH 46/62] remove entire --- lib/install.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/install.sh b/lib/install.sh index 8978fe7..a9f90da 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -5,7 +5,6 @@ install_brew_taps() { # Array of tap packages in "user/tap/formula" format local tap_packages=( - "entireio/tap/entire" "PeonPing/tap/peon-ping" ) From 2223ef388381983fe516a873cdd79d42eafd0545 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 22 Feb 2026 13:50:31 -0800 Subject: [PATCH 47/62] remove enitre --- dotfiles/functions/collections/ai | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dotfiles/functions/collections/ai b/dotfiles/functions/collections/ai index d9d2c18..f0c50aa 100644 --- a/dotfiles/functions/collections/ai +++ b/dotfiles/functions/collections/ai @@ -4,10 +4,6 @@ function csync() { ruby "$HOME/.myconfigurations/applications/claude/setup.rb" } -function esync() { - entire enable --strategy auto-commit -} - function cm() { chezmoi managed } function cra() { chezmoi re-add } @@ -25,7 +21,6 @@ function aihelp () { echo "--------------------------------------------------------------------------------" echo echo " csync = initialize claude sync" - echo " esync = enable entire sync with auto-commit strategy" echo " cm = chezmoi managed" echo " cra = chezmoi re-add" echo " wl = watchman watch-list" From f0fe3b90b877e354050beb951d602fe168cabbed Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 25 Feb 2026 13:08:21 -0800 Subject: [PATCH 48/62] add ngrok --- docs/installation/004.md | 5 +++++ lib/install.sh | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/installation/004.md b/docs/installation/004.md index 036638b..c7d8f7a 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -39,6 +39,11 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { 5. Install `peon-ping-setup` for our peon pings that fire during claude workflows. + + + + + *** WIP diff --git a/lib/install.sh b/lib/install.sh index a9f90da..d831f99 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -54,6 +54,7 @@ install_brew_packages() { "gh" "terminal-notifier" "watchman" + "ngrok" ) echo "Running brew update..." From 8033c81567175aa8f3bd3ac9441da3ac1665975d Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Fri, 27 Feb 2026 19:00:55 -0800 Subject: [PATCH 49/62] update documentation --- applications/vscode/settings.json | 3 ++- docs/installation/004.md | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/applications/vscode/settings.json b/applications/vscode/settings.json index 0929a02..7e6c5e7 100644 --- a/applications/vscode/settings.json +++ b/applications/vscode/settings.json @@ -105,6 +105,7 @@ }, "workbench.preferredHighContrastColorTheme": "RailsCasts Plus Theme", "gitlens.ai.model": "vscode", - "gitlens.ai.vscode.model": "anthropic:claude-4.5-opus" + "gitlens.ai.vscode.model": "anthropic:claude-4.5-opus", + "claudeCode.preferredLocation": "panel" // ------------------------------------------------------------------- } diff --git a/docs/installation/004.md b/docs/installation/004.md index c7d8f7a..6c812e0 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -1,7 +1,15 @@ ### Artificial Intelligence Preferences and Configurations : 004 -> At this point the private repository `.myconfigurations.private` should have been configured in previous steps as it is required at this point. +> At this point the private repository `.myconfigurations.private` and `.myconfigurations.private.sync` should have been configured in previous steps as it is required at this point. +* `.myconfigurations.private` : this contains all of my private settings and I use it as a middle layer between it and the automated sync so I can evaluate any changes that tooling may have added. I like to see them even though they are auto committed. I can revert when applicable and that is also auto synced. +* `.myconfigurations.private.sync` : this repository contains all of the auto synced and committed files processed via `watchman` and `chezmoi` and through scripts that utlize `chezmoi`. + +This setup has already installed... +* [chezmoi](https://www.chezmoi.io/) +* [watchman](https://facebook.github.io/watchman/) + +Now for what we need to accomplish syncing... 1. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. @@ -41,7 +49,7 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { - +chmod +x ~/.myconfigurations.private/claude/scripts/claude-session-*.sh From 1e257d03a57c8b847738fcd4af9c7de7e7099d1b Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Fri, 27 Feb 2026 19:18:51 -0800 Subject: [PATCH 50/62] fix grammar and spelling --- docs/installation/001.md | 26 +++++++++++++------------- docs/installation/002.md | 8 ++++---- docs/installation/003.md | 26 +++++++++++++------------- docs/installation/004.md | 22 ++++++++++++++++++---- 4 files changed, 48 insertions(+), 34 deletions(-) diff --git a/docs/installation/001.md b/docs/installation/001.md index 19baea9..363418a 100644 --- a/docs/installation/001.md +++ b/docs/installation/001.md @@ -1,18 +1,18 @@ -### Setup a New or Reformatting an Existing Apple Workstation : 001 +### Set Up a New or Reformat an Existing Apple Workstation : 001 -> If you need to reinstall the operating system follow "[How to install macOS](https://support.apple.com/en-us/HT204904)" from the Apple Support documentation. +> If you need to reinstall the operating system, follow "[How to install macOS](https://support.apple.com/en-us/HT204904)" from the Apple Support documentation. -1. Following this [guide from Apple](https://support.apple.com/en-us/HT204904) reboot into the system utilities and completely wipe the hard drive and reinstall the operating system. This guide use this step: `On an Intel-based Mac, if you use Shift-Option-Command-R during startup, you're offered the macOS that came with your Mac, or the closest version still available. If you use Option-Command-R during startup, in most cases you're offered the latest macOS that is compatible with your Mac. Otherwise you're offered the macOS that came with your Mac, or the closest version still available.` -2. When the format utility for erasing the hard drive prompts for name for the drive, I name it `RBRHD` during the reformat process, and the format is `APFS`. Please note, I turn on filevault after the full reformat to keep the drive key synced with iCloud, and SSD type drives do not have the option to zero out like older drives. +1. Following this [guide from Apple](https://support.apple.com/en-us/HT204904) reboot into the system utilities and completely wipe the hard drive and reinstall the operating system. This guide uses this step: `On an Intel-based Mac, if you use Shift-Option-Command-R during startup, you're offered the macOS that came with your Mac, or the closest version still available. If you use Option-Command-R during startup, in most cases you're offered the latest macOS that is compatible with your Mac. Otherwise you're offered the macOS that came with your Mac, or the closest version still available.` +2. When the format utility for erasing the hard drive prompts for a name for the drive, I name it `RBRHD` during the reformat process, and the format is `APFS`. Please note, I turn on FileVault after the full reformat to keep the drive key synced with iCloud, and SSD type drives do not have the option to zero out like older drives. 3. Once the drive is reformatted, reinstall macOS using your home network, and iCloud account. I also allow my Apple ID to reset my workstation password. 4. Under `Customize Settings` I `Enable Location Services` and do not share `Mac Analytics`, `Crash Data`, `Ask Siri`, and I uncheck `Store files from Documents and Desktop in iCloud Drive`. 5. Under `Customize Settings` I make sure to turn on `FileVault disk encryption` and `Allow my iCloud account to unlock my disk`. -6. Open the disc utility and if you notice an internal volume named `Update` delete it via this [guide from Apple](https://support.apple.com/guide/disk-utility/add-erase-or-delete-apfs-volumes-dskua9e6a110/mac), then restart the computer. Please note, this may also cause a warning `Incompatible Disc` to be displayed. If this occurs, you will need repeat steps 1 through 5 following [this video walkthrough](https://youtu.be/HFo9mTfTk9I). Once these steps have been completed you should be successfully connected to your wifi. +6. Open the Disk Utility and if you notice an internal volume named `Update` delete it via this [guide from Apple](https://support.apple.com/guide/disk-utility/add-erase-or-delete-apfs-volumes-dskua9e6a110/mac), then restart the computer. Please note, this may also cause a warning `Incompatible Disk` to be displayed. If this occurs, you will need to repeat steps 1 through 5 following [this video walkthrough](https://youtu.be/HFo9mTfTk9I). Once these steps have been completed you should be successfully connected to your wifi. 7. Under the `App Store` in System Preferences, make sure `Automatically Check for Updates` is checked, and `Download new available updates in the background` and `Install system data files and security updates` are both checked. 8. Click `Show Updates` and run all pending updates. -9. Install the full `Xcode` developer package from the Apple App Store. I use the full package because it comes with both the gcc compiler and emulators for iOS devices. These come in handy for testing responsive websites and applications. Please note, this will require the latest version of the osx operating system, and if `FileVault` has not finished encrypting the hard drive you will have to wait for that to finish. The status can be located under `Settings and Privacy` then under `FileVault`. +9. Install the full `Xcode` developer package from the Apple App Store. I use the full package because it comes with both the gcc compiler and emulators for iOS devices. These come in handy for testing responsive websites and applications. Please note, this will require the latest version of the macOS operating system, and if `FileVault` has not finished encrypting the hard drive you will have to wait for that to finish. The status can be located under `Settings and Privacy` then under `FileVault`. 10. Open `Xcode` to approve the EULA, and install the iOS components because we want the iOS emulator. Do not install the predictive code completion model at this time. -11. Open `Terminal` and associate the developer tools with the installed version of `Xcode` via `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` Now verify this is working by `xcode-select -p` which should output `/Applications/Xcode.app/Contents/Developer` +11. Open `Terminal` and associate the developer tools with the installed version of `Xcode` via `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer`. Now verify this is working by `xcode-select -p` which should output `/Applications/Xcode.app/Contents/Developer` 12. Under `System Preferences` under `Users & Groups` verify or disable the `Guest User` from being able to access the computer, then reboot the machine to confirm. 13. In `System Preferences` under the `Trackpad` disable `Natural` scrolling. 14. In `System Preferences` under the `Trackpad` under `Point and Click` set the tracking speed to `6`. @@ -34,11 +34,11 @@ * Under `Mission Control` uncheck all other settings in this panel. * Under `Display` set `Decrease Display Brightness` to `F1` * Under `Display` set `Increase Display Brightness` to `F2` -24. Remove every application icon from the dock except finder on the bottom left, and the trash can on the bottom right! +24. Remove every application icon from the dock except Finder on the bottom left, and the trash can on the bottom right! 25. In `System Preferences` configure the `Dock & Menu Bar`. * Enable `Magnification` and set it to `max`. * Enable `Automatically hide and show the Dock`. - * Adjust the size the menu to `4`. + * Adjust the size of the menu to `4`. 26. Setup at least `4 OSX Spaces` via `Mission Control`. 27. In `System Preferences` in `Keyboard & Mouse Shortcuts` in `Mission Control` desktop switches to use `ctrl + #` for the first 4 spaces. > If the list doesn't show all your Spaces, quit System Settings completely (Command + Q), reopen it, and check againβ€”sometimes it needs a refresh after adding new Spaces. @@ -62,10 +62,10 @@ 32. Right click on the desktop and access advanced show options. * Check `Always open in list view` * Check `Always Snap to Grid` -33. Create a folder named `TMP` on the desktop under the hard disc. -34. Send screenshots to the Desktop's `TMP` folder via: `defaults write com.apple.screencapture location $HONE/Desktop/TMP` followed by `killall SystemUIServer`. -35. In the `Terminal` application run `defaults write com.apple.iCal "n days of week" 14` to set the osx calendar to display 14 days in week view. -36. Is the `Calendar` application, under `Advanced` check the box to `Turn on time zone support`. +33. Create a folder named `TMP` on the desktop under the hard disk. +34. Send screenshots to the Desktop's `TMP` folder via: `defaults write com.apple.screencapture location $HOME/Desktop/TMP` followed by `killall SystemUIServer`. +35. In the `Terminal` application run `defaults write com.apple.iCal "n days of week" 14` to set the macOS calendar to display 14 days in week view. +36. In the `Calendar` application, under `Advanced` check the box to `Turn on time zone support`. 37. In `System Preferences` in `Notifications` adjust these applications' settings. * Under `Messages` disable play sound for notification. 38. In `Mail` in `Settings` adjust these applications' settings. diff --git a/docs/installation/002.md b/docs/installation/002.md index 72bf12a..335c669 100644 --- a/docs/installation/002.md +++ b/docs/installation/002.md @@ -1,12 +1,12 @@ -### Install Additional Business amd Workflow Software : 002 +### Install Additional Business and Workflow Software : 002 > Make note of applications installed from their websites versus the `App Store` 1. Install [1Password](https://1password.com/downloads/mac), once installed disable the keyboard shortcut for `Show Quick Access`. 2. Download and Install [Chrome](https://www.google.com/chrome/browser/desktop/index.html), [Firefox](https://www.mozilla.org/en-US/firefox/new/), and [Brave](https://github.com/chrishough/my-configurations). -3. Configure the syncing accounts for Firefox, Brave, and Google, and configure downloads to dump into the Desktop's `TMP` directory. Verify `1 Password` has been installed to each of them. +3. Configure the syncing accounts for Firefox, Brave, and Google, and configure downloads to dump into the Desktop's `TMP` directory. Verify `1Password` has been installed to each of them. 4. Setup `Internet Accounts` that are synced across all Apple Devices. - * Open ups Mail to confirm. + * Open up Mail to confirm. * In Mail add the `All Junk` to the `Favorites`. * In Mail add the `All Trash` to the `Favorites`. * Disable the `iCloud Mail` account. @@ -35,7 +35,7 @@ * Install `Photoshop` * Install `Illustrator` * Install `Acrobat` -7. Setup `OSX Messages` for all accounts. Verify non-apple users too. +7. Setup `OSX Messages` for all accounts. Verify non-Apple users too. 8. Configure Divvy Shortcuts: `RHT`, `LFT`, `CENTER`, `FULL`, `TMP`. 9. Organize the applications in the `Finder` panel alphabetically. diff --git a/docs/installation/003.md b/docs/installation/003.md index a66291a..4b29422 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -1,8 +1,8 @@ ### Engineering Workstation Setup and Configuration : 003 -> XCode Must be Installed and Configured prior to this setup! +> Xcode Must be Installed and Configured prior to this setup! -1. Install [iTerm2](https://www.iterm2.com/). **Critical** In the `Keys` settings, make sure these keys are configured. +1. Install [iTerm2](https://www.iterm2.com/). **Critical:** In the `Keys` settings, make sure these keys are configured. * `Send: "\n"` via keyboard shortcut `shift + return` via `Send Text` * `Send: "0x0C 0x1B 0x63"` via keyboard shortcut `command+k` via `Send Hex` 2. Install [homebrew](http://brew.sh/). @@ -13,7 +13,7 @@ ```ini [user] name = Chris Hough - email = { users's email address } + email = { user's email address } [mergetool] keepBackup = true [core] @@ -46,7 +46,7 @@ Host github.com AddKeysToAgent yes UseKeychain yes ``` -8. Close these private repo into the `$HOME` directory. +8. Clone these private repos into the `$HOME` directory. * `git clone git@github.com:chrishough/my-configurations-private.git .myconfigurations.private` * `git clone git@github.com:chrishough/my-configurations-private-sync.git .myconfigurations.private.sync` > I am documenting this as part of my flow, but will not be public. Also, steps in the process need this cloned, but will not be fully utilized until later steps. @@ -55,10 +55,10 @@ Host github.com * Install [oh-my-zsh](https://github.com/robbyrussell/oh-my-zsh) `sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"` theme for `zsh`. * Run the install script `sh "$HOME/.myconfigurations/lib/install.sh"` to load all of the required brew packages. * Setup the `github` dependency via `gh auth login`. - * Setup the latest version of Ruby `rbenv install 3.4.3`, set the local machine to run it via `rbenv local 3.4.3`, and make sure Bundler `gem install bundler`, PRY `gem install pry`, and rsense `gem install rsense` are installed. If you get error installing these gemes run `source ~/.zshrc`. + * Setup the latest version of Ruby `rbenv install 3.4.3`, set the local machine to run it via `rbenv local 3.4.3`, and make sure Bundler `gem install bundler`, PRY `gem install pry`, and rsense `gem install rsense` are installed. If you get an error installing these gems run `source ~/.zshrc`. * Run the ruby script to map all of our shell settings to the dotfile repository files `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Install the latest version of node to preload nvm `nvm install --lts`. -> If the `.zprofile` and or the `.zshrc` files fail to create a symlink, delete them and run the script again to configure them. +> If the `.zprofile` and/or the `.zshrc` files fail to create a symlink, delete them and run the script again to configure them. 10. Create the rbenv plugin directory `mkdir $(rbenv root)/plugins`. * Install [rbenv-default-gems](https://github.com/rbenv/rbenv-default-gems) plugin `git clone https://github.com/rbenv/rbenv-default-gems.git $(rbenv root)/plugins/rbenv-default-gems`. * Install [rbenv/rbenv-gem-rehash](https://github.com/rbenv/rbenv-gem-rehash) plugin `git clone https://github.com/sstephenson/rbenv-gem-rehash.git $(rbenv root)/plugins/rbenv-gem-rehash`. @@ -72,7 +72,7 @@ GEM_PATH=.bundle 12. Install [Docker Desktop](https://www.docker.com/get-started/). * `dkrup opensearch1315 pg169 pg175 redis7` install current images. * Stop all via `dkrstopall`. -13. Install the [Jet Brains Toolbox](https://www.jetbrains.com/toolbox-app/). +13. Install the [JetBrains Toolbox](https://www.jetbrains.com/toolbox-app/). * Install `RubyMine`, and sync settings from the cloud to match previous workstation. * Install `DataGrip` for DB interactions. 14. Setup [eslint](https://eslint.org) via `npm install -g eslint`. @@ -83,21 +83,21 @@ GEM_PATH=.bundle * Setup [Base16 iTerm2](https://github.com/chriskempson/base16-iterm2) from inside `assets/colors` in this repo, and set it to use `base16-railscasts.dark`. * In `General` set the Initial Directory to `Reuse previous session's directory`. * In `Text` change the font to `Fira Code`. - * In `Terminal` check `Unlimited Scrollback`, verify the `Report Terminal Type` is `xterm-256color`, and verify `Character Encoding` is set to `Unicode( UTF-8 ) - * In `Window` set the `Transparency` to `10`. -19. Setup and Install [VSCODE](https://code.visualstudio.com/). + * In `Terminal` check `Unlimited Scrollback`, verify the `Report Terminal Type` is `xterm-256color`, and verify `Character Encoding` is set to `Unicode( UTF-8 )` + * In `Window` set the `Transparency` to `10`. +19. Install and Set Up [VSCODE](https://code.visualstudio.com/). * Install the `shell command` via the `command palette`. * Configure `sync` via `GitHub`. 20. Configure the [Keyboard Maestro](https://www.keyboardmaestro.com/main/) macro library. * Desktop and Laptops will have very different settings, for example the laptop will have the brightness controls, the desktop may not, but both will have the player controls. -21. Intall [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remove control the desktops including the isolated AI assistant. +21. Install [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remote control the desktops including the isolated AI assistant. * [Lock it Up!](https://www.teamviewer.com/en-us/insights/unattended-access-security/) Immediately via 2FA. * Setup `allow list` with only my TeamViewer account blocking any external users. ### Running a Desktop? -* If Install [Istat Menus](https://bjango.com/mac/istatmenus/) and configure `Bluetooth Battery`, `CPU` and `Memory Performance`, and `Network Traffic`. -* If Install [VMWARE Fusion](https://www.vmware.com/products/desktop-hypervisor/workstation-and-fusion) for debugging applications inside of the Windows Ecosystem. +* Install [Istat Menus](https://bjango.com/mac/istatmenus/) and configure `Bluetooth Battery`, `CPU` and `Memory Performance`, and `Network Traffic`. +* Install [VMWARE Fusion](https://www.vmware.com/products/desktop-hypervisor/workstation-and-fusion) for debugging applications inside the Windows ecosystem. * Install and configure `Windows 11` * Inside `Windows 11` download and install `Google Chrome`. * Configure the virtual machine for `Windows 11` to use bridged networking and to select automatically. diff --git a/docs/installation/004.md b/docs/installation/004.md index 6c812e0..0d9688a 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -3,7 +3,7 @@ > At this point the private repository `.myconfigurations.private` and `.myconfigurations.private.sync` should have been configured in previous steps as it is required at this point. * `.myconfigurations.private` : this contains all of my private settings and I use it as a middle layer between it and the automated sync so I can evaluate any changes that tooling may have added. I like to see them even though they are auto committed. I can revert when applicable and that is also auto synced. -* `.myconfigurations.private.sync` : this repository contains all of the auto synced and committed files processed via `watchman` and `chezmoi` and through scripts that utlize `chezmoi`. +* `.myconfigurations.private.sync` : this repository contains all of the auto synced and committed files processed via `watchman` and `chezmoi` and through scripts that utilize `chezmoi`. This setup has already installed... * [chezmoi](https://www.chezmoi.io/) @@ -13,7 +13,7 @@ Now for what we need to accomplish syncing... 1. Run the setup script to correctly setup mappings `ruby "$HOME/.myconfigurations/lib/setup.rb"`. * Once this has been loaded Claude will be symlinked to the project, restart it. -2. Configure `watchman` plus `chezmoi` sychronization settings for ai tooling and infrastructure. +2. Configure `watchman` plus `chezmoi` synchronization settings for AI tooling and infrastructure. * `mkdir .config/chezmoi` * `nano .config/chezmoi/chezmoi.toml` ```ini @@ -46,10 +46,24 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { ``` 5. Install `peon-ping-setup` for our peon pings that fire during claude workflows. +6. Setup multiple machine syncing, primary, secondary, and autonomous without sacrificing configurations and tooling. - +> Primary Workstation -chmod +x ~/.myconfigurations.private/claude/scripts/claude-session-*.sh +This machine is the parent and primary workhorse. +* Setup the synchronous scripts: `chmod +x ~/.myconfigurations.private/claude/scripts/claude-session-*.sh` + + + +> Secondary Workstation + +This machine is a child and designed for mobility. +* TBD + +> Autonomous Workstation + +This machine is a child and designed for autonomous labor. +* TBD From 50d94b9bca03c53b06052aaa2ff85745cf2f48e1 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Fri, 27 Feb 2026 20:38:54 -0800 Subject: [PATCH 51/62] change mappings from TMP to WIP in cloud --- docs/installation/001.md | 4 ++-- docs/installation/002.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/installation/001.md b/docs/installation/001.md index 363418a..5f1ad8f 100644 --- a/docs/installation/001.md +++ b/docs/installation/001.md @@ -62,8 +62,8 @@ 32. Right click on the desktop and access advanced show options. * Check `Always open in list view` * Check `Always Snap to Grid` -33. Create a folder named `TMP` on the desktop under the hard disk. -34. Send screenshots to the Desktop's `TMP` folder via: `defaults write com.apple.screencapture location $HOME/Desktop/TMP` followed by `killall SystemUIServer`. +33. Create a symlink named `WIP` on the Desktop pointing to the iCloud Drive WIP folder via: `ln -s ~/Library/Mobile\ Documents/com~apple~CloudDocs/WIP ~/Desktop/WIP` +34. Send screenshots to the Desktop's `WIP` folder via: `defaults write com.apple.screencapture location "$HOME/Library/Mobile Documents/com~apple~CloudDocs/WIP"` followed by `killall SystemUIServer`. 35. In the `Terminal` application run `defaults write com.apple.iCal "n days of week" 14` to set the macOS calendar to display 14 days in week view. 36. In the `Calendar` application, under `Advanced` check the box to `Turn on time zone support`. 37. In `System Preferences` in `Notifications` adjust these applications' settings. diff --git a/docs/installation/002.md b/docs/installation/002.md index 335c669..d98d083 100644 --- a/docs/installation/002.md +++ b/docs/installation/002.md @@ -4,7 +4,7 @@ 1. Install [1Password](https://1password.com/downloads/mac), once installed disable the keyboard shortcut for `Show Quick Access`. 2. Download and Install [Chrome](https://www.google.com/chrome/browser/desktop/index.html), [Firefox](https://www.mozilla.org/en-US/firefox/new/), and [Brave](https://github.com/chrishough/my-configurations). -3. Configure the syncing accounts for Firefox, Brave, and Google, and configure downloads to dump into the Desktop's `TMP` directory. Verify `1Password` has been installed to each of them. +3. Configure the syncing accounts for Firefox, Brave, and Google, and configure downloads to dump into the Desktop's `WIP` directory. Verify `1Password` has been installed to each of them. 4. Setup `Internet Accounts` that are synced across all Apple Devices. * Open up Mail to confirm. * In Mail add the `All Junk` to the `Favorites`. From 19fe3a09e3ef539569f0a3afbc524935a28c38db Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 3 Mar 2026 16:44:40 -0800 Subject: [PATCH 52/62] update claude oss settings --- .../claude/scripts/claude-session-pull.sh | 30 ++++++++++++++ .../claude/scripts/claude-session-push.sh | 39 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100755 applications/claude/scripts/claude-session-pull.sh create mode 100755 applications/claude/scripts/claude-session-push.sh diff --git a/applications/claude/scripts/claude-session-pull.sh b/applications/claude/scripts/claude-session-pull.sh new file mode 100755 index 0000000..1545e15 --- /dev/null +++ b/applications/claude/scripts/claude-session-pull.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# claude-session-pull.sh +# Claude Code SessionStart hook. +# Pulls latest chezmoi state (including sessions from other machines). +# Blocks on conflict; warns on network/other failures. + +command -v chezmoi >/dev/null 2>&1 || exit 0 + +# Detect stale rebase from previous failed sync +if chezmoi git -- status 2>&1 | grep -q "rebase in progress"; then + echo "[chezmoi-sync] Previous rebase still in progress" >&2 + echo " To abort: chezmoi git -- rebase --abort" >&2 + echo " To continue: chezmoi git -- rebase --continue" >&2 + exit 1 +fi + +output=$(chezmoi update --force 2>&1) +[ $? -eq 0 ] && exit 0 + +# Conflict vs network failure +if echo "$output" | grep -qi -e "conflict" -e "could not apply" -e "rebase"; then + echo "[chezmoi-sync] Conflict detected:" >&2 + echo "$output" >&2 + echo "Resolve: chezmoi git -- status" >&2 + echo "Skip: chezmoi git -- rebase --abort" >&2 + exit 1 +fi + +echo "[chezmoi-sync] chezmoi update failed (network?), skipping" >&2 +exit 0 diff --git a/applications/claude/scripts/claude-session-push.sh b/applications/claude/scripts/claude-session-push.sh new file mode 100755 index 0000000..16e22e1 --- /dev/null +++ b/applications/claude/scripts/claude-session-push.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# claude-session-push.sh +# Claude Code Stop hook. +# Re-adds session data into chezmoi source. +# chezmoi auto git handles add/commit/push automatically. + +command -v chezmoi >/dev/null 2>&1 || exit 0 +[ -d "$HOME/.claude" ] || exit 0 + +LOCKFILE="/tmp/claude-session-push.lock" +MAX_WAIT=30 + +# --- Serialize concurrent Stop hooks (multiple sessions stopping at once) --- +waited=0 +while [ -f "$LOCKFILE" ] && [ "$waited" -lt "$MAX_WAIT" ]; do + sleep 1 + waited=$((waited + 1)) +done +[ -f "$LOCKFILE" ] && rm -f "$LOCKFILE" + +touch "$LOCKFILE" +trap 'rm -f "$LOCKFILE"' EXIT + +# --- Don't push if a rebase is stuck --- +if chezmoi git -- status 2>&1 | grep -q "rebase in progress"; then + echo "[chezmoi-sync] Rebase in progress β€” skipping push" >&2 + exit 0 +fi + +# --- Pull first to minimize conflicts --- +chezmoi git -- pull --rebase --autostash origin main 2>/dev/null || \ +chezmoi git -- pull --rebase --autostash origin master 2>/dev/null || true + +# --- Re-add session data (autoAdd + autoCommit + autoPush does the rest) --- +chezmoi re-add "$HOME/.claude" 2>/dev/null || { + chezmoi add "$HOME/.claude" 2>/dev/null || true +} + +exit 0 From 33a22480afeecc5b534cb4d21876e9321831e7ad Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 3 Mar 2026 18:24:07 -0800 Subject: [PATCH 53/62] update workflow --- docs/installation/003.md | 1 + docs/installation/004.md | 7 ++++++- readme.md | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/installation/003.md b/docs/installation/003.md index 4b29422..fcfbcd4 100644 --- a/docs/installation/003.md +++ b/docs/installation/003.md @@ -93,6 +93,7 @@ GEM_PATH=.bundle 21. Install [TeamViewer](https://www.teamviewer.com/en-us/download/macos/) on every machine. This will be used to remote control the desktops including the isolated AI assistant. * [Lock it Up!](https://www.teamviewer.com/en-us/insights/unattended-access-security/) Immediately via 2FA. * Setup `allow list` with only my TeamViewer account blocking any external users. +22. Under `System Preferences` in `Network` enable the `Firewall` ### Running a Desktop? diff --git a/docs/installation/004.md b/docs/installation/004.md index 0d9688a..285ae9d 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -51,7 +51,11 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { > Primary Workstation This machine is the parent and primary workhorse. -* Setup the synchronous scripts: `chmod +x ~/.myconfigurations.private/claude/scripts/claude-session-*.sh` +* Setup the synchronous scripts: `chmod +x ~/.myconfigurations/applications/claude/scripts/claude-session-*.sh` + + + + @@ -68,6 +72,7 @@ This machine is a child and designed for autonomous labor. *** + WIP Install [Claude CLI](https://code.claude.com/docs/en/setup#npm). diff --git a/readme.md b/readme.md index d1e1abf..e2b109e 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,6 @@ These guides are highly opinionated. If you have any questions please post an is 2. [Install Additional Business amd Workflow Software](/docs/installation/002.md) 3. [Engineering Workstation Setup and Configuration](/docs/installation/003.md) 4. [Artificial Intelligence Preferences and Configurations](/docs/installation/004.md) -5. Under `System Preferences` in `Network` to enable the `Firewall` #### Setup Scripts Reference From a5b6ac735af45ec605dc3a4f6d564fc8b51d08a8 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 16 Mar 2026 20:45:23 -0700 Subject: [PATCH 54/62] fix shell commands --- dotfiles/functions/collections/ruby | 2 +- dotfiles/functions/functions | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dotfiles/functions/collections/ruby b/dotfiles/functions/collections/ruby index 5653158..f7f9501 100644 --- a/dotfiles/functions/collections/ruby +++ b/dotfiles/functions/collections/ruby @@ -6,7 +6,7 @@ export RUBYOPT='-W:no-deprecated -W:no-experimental' export BUNDLER_EDITOR="vscode" export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES -eval "$(rbenv init - zsh)" +command -v rbenv &>/dev/null && eval "$(rbenv init - zsh)" alias rb="rbenv" alias rbl="rbenv install -l" diff --git a/dotfiles/functions/functions b/dotfiles/functions/functions index 7927a0b..00b4089 100644 --- a/dotfiles/functions/functions +++ b/dotfiles/functions/functions @@ -30,7 +30,9 @@ export AUTOJUMP_IGNORE_CASE=1 # ZSH AUTOCOMPLETE autoload -U compinit && compinit -[[ -s `brew --prefix`/etc/autojump.sh ]] && . `brew --prefix`/etc/autojump.sh +if command -v brew &>/dev/null; then + [[ -s $(brew --prefix)/etc/autojump.sh ]] && . $(brew --prefix)/etc/autojump.sh +fi # ZSH HELP function help() { From 07de0a5a86a716c75b9e840c4023a0083aa832be Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Wed, 11 Mar 2026 09:54:39 -0700 Subject: [PATCH 55/62] update widget settings --- docs/installation/001.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation/001.md b/docs/installation/001.md index 5f1ad8f..3969c68 100644 --- a/docs/installation/001.md +++ b/docs/installation/001.md @@ -70,6 +70,9 @@ * Under `Messages` disable play sound for notification. 38. In `Mail` in `Settings` adjust these applications' settings. * Under `Viewing` show most recent message at the top. +39. In `System Preferences` in `Desktop & Dock` under `Widgets` settings. + * Set `Dim widgets on desktop` to never. + * Set `IPhone widgets` to diabled. ### Running a Desktop? From 56ee7b7af41f11f5897ddd5fdc00dd250f78deee Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Thu, 12 Mar 2026 18:53:16 -0700 Subject: [PATCH 56/62] document ai configuration --- docs/installation/004.md | 50 ++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/docs/installation/004.md b/docs/installation/004.md index 285ae9d..c3e831e 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -45,7 +45,29 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { }]' | watchman -j ``` -5. Install `peon-ping-setup` for our peon pings that fire during claude workflows. +5. Install `peon-ping-setup` for our peon pings that fire during claude workflows, and mirro configuration `~/.claude/hooks/peon-ping/config.json`. +``` +{ + "active_pack": "peon", + "volume": 1.0, + "enabled": true, + "desktop_notifications": true, + "categories": { + "session.start": true, + "task.acknowledge": true, + "task.complete": true, + "task.error": true, + "input.required": true, + "resource.limit": true, + "user.spam": true + }, + "annoyed_threshold": 3, + "annoyed_window_seconds": 10, + "silent_window_seconds": 0, + "pack_rotation": [], + "pack_rotation_mode": "random" +} +``` 6. Setup multiple machine syncing, primary, secondary, and autonomous without sacrificing configurations and tooling. > Primary Workstation @@ -59,21 +81,31 @@ This machine is the parent and primary workhorse. -> Secondary Workstation -This machine is a child and designed for mobility. -* TBD -> Autonomous Workstation -This machine is a child and designed for autonomous labor. -* TBD -*** -WIP + + + + +> *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** + + +> Secondary Workstation + +This machine is a child and designed for mobility. +* TBD + +> Autonomous Workstation + +This machine is a child and designed for autonomous labor. +* TBD + + Install [Claude CLI](https://code.claude.com/docs/en/setup#npm). * Via NPM `npm install -g @anthropic-ai/claude-code`. From 0b87b88cc45202b6f47ad67593734346b52f8bad Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Fri, 13 Mar 2026 14:07:28 -0700 Subject: [PATCH 57/62] adjust peon ping --- applications/vscode/settings.json | 2 +- docs/installation/004.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/applications/vscode/settings.json b/applications/vscode/settings.json index 7e6c5e7..c37d069 100644 --- a/applications/vscode/settings.json +++ b/applications/vscode/settings.json @@ -108,4 +108,4 @@ "gitlens.ai.vscode.model": "anthropic:claude-4.5-opus", "claudeCode.preferredLocation": "panel" // ------------------------------------------------------------------- -} +} \ No newline at end of file diff --git a/docs/installation/004.md b/docs/installation/004.md index c3e831e..f1289c6 100644 --- a/docs/installation/004.md +++ b/docs/installation/004.md @@ -65,7 +65,8 @@ echo '["trigger", "/Users/christopherhough/.myconfigurations.private/claude", { "annoyed_window_seconds": 10, "silent_window_seconds": 0, "pack_rotation": [], - "pack_rotation_mode": "random" + "pack_rotation_mode": "random", + "notification_style": "standard" } ``` 6. Setup multiple machine syncing, primary, secondary, and autonomous without sacrificing configurations and tooling. From a62fac1da34d2a3393744908b31220382a8163d7 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 16 Mar 2026 20:45:49 -0700 Subject: [PATCH 58/62] update ai controls --- .../claude/scripts/claude-session-push.sh | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/applications/claude/scripts/claude-session-push.sh b/applications/claude/scripts/claude-session-push.sh index 16e22e1..88d06c1 100755 --- a/applications/claude/scripts/claude-session-push.sh +++ b/applications/claude/scripts/claude-session-push.sh @@ -1,7 +1,7 @@ #!/bin/bash # claude-session-push.sh # Claude Code Stop hook. -# Re-adds session data into chezmoi source. +# Selectively adds the most recent session per project into chezmoi. # chezmoi auto git handles add/commit/push automatically. command -v chezmoi >/dev/null 2>&1 || exit 0 @@ -31,9 +31,35 @@ fi chezmoi git -- pull --rebase --autostash origin main 2>/dev/null || \ chezmoi git -- pull --rebase --autostash origin master 2>/dev/null || true -# --- Re-add session data (autoAdd + autoCommit + autoPush does the rest) --- -chezmoi re-add "$HOME/.claude" 2>/dev/null || { - chezmoi add "$HOME/.claude" 2>/dev/null || true -} +# --- Build list of files to sync --- +files_to_add=() + +# Always sync settings +[ -f "$HOME/.claude/settings.json" ] && files_to_add+=("$HOME/.claude/settings.json") + +# For each project, add the most recent session + its index +for project_dir in "$HOME/.claude/projects"/*/; do + [ -d "$project_dir" ] || continue + + # sessions-index.json (needed for the picker) + [ -f "${project_dir}sessions-index.json" ] && files_to_add+=("${project_dir}sessions-index.json") + + # Find the most recently modified .jsonl (the active session) + latest_jsonl=$(find "$project_dir" -maxdepth 1 -name "*.jsonl" -not -name "agent-*" -type f -print0 \ + | xargs -0 ls -t 2>/dev/null | head -1) + + if [ -n "$latest_jsonl" ]; then + files_to_add+=("$latest_jsonl") + + # If there's a matching UUID directory, include it too + session_id=$(basename "$latest_jsonl" .jsonl) + [ -d "${project_dir}${session_id}" ] && files_to_add+=("${project_dir}${session_id}") + fi +done + +# --- Add everything in one pass (triggers one auto commit+push cycle) --- +if [ ${#files_to_add[@]} -gt 0 ]; then + chezmoi add "${files_to_add[@]}" 2>/dev/null || true +fi exit 0 From 48468d2d5e5cf7722d7d0edd8b744b93225387c2 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Mon, 16 Mar 2026 20:58:24 -0700 Subject: [PATCH 59/62] fix paths across workstations --- dotfiles/.zshrc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotfiles/.zshrc b/dotfiles/.zshrc index f53f95c..fa7d07a 100644 --- a/dotfiles/.zshrc +++ b/dotfiles/.zshrc @@ -43,14 +43,14 @@ source $ZSH/oh-my-zsh.sh # disable auto correct unsetopt correct_all -# source shared functions -source $HOME/.myconfigurations/dotfiles/functions/functions - export PATH="$HOME/.rbenv/bin:$PATH" export PATH="$HOME/.local/bin:$PATH" export PATH="/opt/homebrew/opt/libpq/bin:$PATH" export PATH="/opt/homebrew/bin:$PATH" +# source shared functions +source $HOME/.myconfigurations/dotfiles/functions/functions + eval "$(direnv hook zsh)" # Hide Direnv Output in the Shells... From 1380de644de196d72adc4a4e90fde3e007fb939c Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Sun, 22 Mar 2026 13:51:59 -0500 Subject: [PATCH 60/62] fix install script to match zsh --- lib/install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/install.sh b/lib/install.sh index d831f99..3b94fb9 100644 --- a/lib/install.sh +++ b/lib/install.sh @@ -1,5 +1,5 @@ #!/bin/zsh -# sh "$HOME/.myconfigurations/lib/install.sh" +# zsh "$HOME/.myconfigurations/lib/install.sh" # Function to install brew tap packages if they don't exist install_brew_taps() { From ceda084f975c3cb56c77ab8efa025ec0a0f59cd4 Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 24 Mar 2026 09:27:09 -0500 Subject: [PATCH 61/62] fix divvy paths --- dotfiles/functions/collections/divvy | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dotfiles/functions/collections/divvy b/dotfiles/functions/collections/divvy index 4c5fca3..a8d461c 100644 --- a/dotfiles/functions/collections/divvy +++ b/dotfiles/functions/collections/divvy @@ -1,7 +1,6 @@ # DVY - Dynamic tmux layout manager # Reads layouts from ~/.myconfigurations/applications/tmux/paths.json - -_DVY_PATHS_FILE="$HOME/.myconfigurations/applications/tmux/paths.json" +_DVY_PATHS_FILE="$HOME/.myconfigurations.private/tmux/paths.json" _dvy_next_session_name() { local base="$1" From bf68a2b256a94e81508499d190b9aef43fb75f0a Mon Sep 17 00:00:00 2001 From: Chris Hough Date: Tue, 24 Mar 2026 09:57:22 -0500 Subject: [PATCH 62/62] update layouts for divvy --- dotfiles/functions/collections/divvy | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/dotfiles/functions/collections/divvy b/dotfiles/functions/collections/divvy index a8d461c..f382a62 100644 --- a/dotfiles/functions/collections/divvy +++ b/dotfiles/functions/collections/divvy @@ -15,6 +15,35 @@ _dvy_next_session_name() { echo "${base}_${i}" } +# Layout: l1_r2 +# +--------+---------+ +# | | 1 | +# | 0 +---------+ +# | | 2 | +# +--------+---------+ +_dvy_layout_l1_r2() { + local session="$1" + local dir="$2" + local config="$3" + + tmux new-session -d -s "$session" -c "$dir" -x $(tput cols) -y $(tput lines) \; \ + splitw -h -p 55 -c "$dir" \; \ + splitw -v -p 50 -c "$dir" \; \ + select-layout -E + + # Send commands to each pane (clear, custom command if specified, clear) + for pane in 0 1 2; do + tmux send-keys -t "$session:0.$pane" 'clear' Enter + local cmd=$(echo "$config" | jq -r --arg p "$pane" '.[$p] // empty') + if [[ -n "$cmd" ]]; then + tmux send-keys -t "$session:0.$pane" "$cmd" Enter + tmux send-keys -t "$session:0.$pane" 'clear' Enter + fi + done + + tmux attach -t "$session" +} + # Layout: l1_r3 # +--------+----+----+ # | | 1 | 2 | @@ -243,6 +272,9 @@ dvy() { session=$(_dvy_next_session_name "$session") case "$layout" in + l1_r2) + _dvy_layout_l1_r2 "$session" "$dir" "$config" + ;; l1_r3) _dvy_layout_l1_r3 "$session" "$dir" "$config" ;; @@ -278,6 +310,13 @@ dvyhelp() { echo echo " Layout types:" echo + echo " l1_r2 - 3-pane layout (1 left, 2 right)" + echo " +--------+---------+" + echo " | | 1 |" + echo " | 0 +---------+" + echo " | | 2 |" + echo " +--------+---------+" + echo echo " l1_r3 - 4-pane layout (1 left, 3 right)" echo " +--------+----+----+" echo " | | 1 | 2 |"