diff --git a/.gitmodules b/.gitmodules index fd5037d..c3ede33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "talon/talon.dir/user"] path = talon/talon.dir/user url = https://github.com/alexrudy/talon.git +[submodule "claude/private"] + path = claude/private + url = https://github.com/alexrudy/claude-private.git diff --git a/claude/CLAUDE.md b/claude/CLAUDE.md new file mode 100644 index 0000000..a752d16 --- /dev/null +++ b/claude/CLAUDE.md @@ -0,0 +1,31 @@ + + +# Personal global instructions + +## Writing pull request bodies + +A PR body is a short narrative about a decision, not an inventory of the diff. +The diff already shows *what* changed; the body explains *why*, and flags what a +reviewer or a future reader needs to know. Write it like a note to a colleague. + +- **Lead with the why.** Open with the problem or motivation and the decision you + made — not a list of changed files. +- **Prose over scaffolding.** Default to one to three short paragraphs. Don't + reach for `## Summary` / `## Changes` / `## Test plan` headers unless the PR is + genuinely large. First person is good ("I'm leaving the single-update path in + place because…"). +- **Don't re-render the diff.** Don't enumerate every file, function, or symbol, + and don't wrap every identifier in backticks. Name only the pieces needed to + follow the reasoning; trust the diff for the rest. +- **Keep the honest hedges.** Say what you're unsure about, what to watch after + it ships, what you deliberately left out, and any follow-ups. These caveats are + the most valuable part of a body and the easiest to drop. +- **Scale to the change.** A one-line change gets a one-sentence body. Reserve + design-doc depth — sections, tables, enumerated trade-offs — for PRs that are + genuinely a design decision. +- **Respect the repo.** If a project has its own PR template or conventions, + follow those over this guidance. diff --git a/claude/bin/install-claude.sh b/claude/bin/install-claude.sh new file mode 100755 index 0000000..ccf5e5f --- /dev/null +++ b/claude/bin/install-claude.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -eu + +# shellcheck source=installers/prelude.sh +. "${DOTFILES}/installers/prelude.sh" + +# Link personal Claude Code config into ~/.claude. +# +# The repo-wide *.symlink / *.dir conventions flatten to ~/., so they +# can't target nested paths like ~/.claude/CLAUDE.md or ~/.claude/skills/. +# And ~/.claude holds Claude Code runtime state (credentials, history, +# sessions), so we link individual items in rather than the directory itself — +# which also lets machine-local skills/commands coexist with the shared ones. +# +# Public, non-sensitive config lives under claude/. Anything personal (skills or +# commands that embed identities, channel IDs, colleague names, etc.) lives in +# the private submodule at claude/private/ so it never lands in this public repo. +# Both trees link into the same ~/.claude/{skills,commands}. + +# Symlink $1 (a file or dir in this repo) to $2 (an absolute path under +# ~/.claude). Idempotent: skips if already linked correctly; replaces a stale +# symlink; backs up a real file/dir to .backup before linking. +link_claude_path() { + local src target shortname + src="$1" + target="$2" + shortname="${target#"${HOME}/"}" + + if test -L "$target" && test "$(_realpath "$target")" = "$(_realpath "$src")"; then + _debug "✅ ${shortname} already linked" + return 0 + fi + + mkdir -p "$(dirname "$target")" + + if test -L "$target"; then + rm -f "$target" # stale symlink — nothing to preserve + elif test -e "$target"; then + mv "$target" "${target}.backup" # real file/dir — keep a backup + _message "⚠️ backed up existing ${shortname} to ${shortname}.backup" + fi + + ln -s "$(_realpath "$src")" "$target" + _message "✅ linked ${shortname} → ${src#"${HOME}/"}" +} + +# Link every child of claude/<$1> into ~/.claude/<$2>, one symlink per entry (so +# public, private, and machine-local entries coexist). No-op when the source dir +# is absent — e.g. the private submodule isn't checked out on this machine. +link_claude_children() { + local srcsub dstsub entry name + srcsub="$1" + dstsub="$2" + test -d "${DOTFILES}/claude/${srcsub}" || return 0 + for entry in "${DOTFILES}/claude/${srcsub}"/*; do + test -e "$entry" || continue + name="$(basename "$entry")" + link_claude_path "$entry" "${HOME}/.claude/${dstsub}/${name}" + done +} + +_process "🤖 linking Claude Code config" +link_claude_path "${DOTFILES}/claude/CLAUDE.md" "${HOME}/.claude/CLAUDE.md" +link_claude_children "skills" "skills" # public, non-sensitive +link_claude_children "commands" "commands" # public, non-sensitive +link_claude_children "private/skills" "skills" # private submodule +link_claude_children "private/commands" "commands" # private submodule +_finished "✅ Claude Code config linked" diff --git a/claude/private b/claude/private new file mode 160000 index 0000000..4230a76 --- /dev/null +++ b/claude/private @@ -0,0 +1 @@ +Subproject commit 4230a76a774e04c08c19a6819a7c3a0dfe8d41dd diff --git a/install.sh b/install.sh index f5d10c8..b789240 100755 --- a/install.sh +++ b/install.sh @@ -313,6 +313,21 @@ apt_run() { return 1 fi } + +# Best-effort, per-submodule init/update. Runs each submodule on its own so one +# that needs credentials (e.g. a private submodule on a fresh machine) can +# neither abort the caller nor block sibling public submodules. +# GIT_TERMINAL_PROMPT=0 makes a credential-less submodule fail fast instead of +# hanging on an interactive prompt. Anything that consumes a missing submodule +# should no-op until it is fetched — re-run update.sh once credentials exist. +update_submodules_best_effort() { + git -C "${DOTFILES}" submodule status 2>/dev/null | while read -r _ _sm _; do + if ! GIT_TERMINAL_PROMPT=0 git -C "${DOTFILES}" \ + submodule update --init --recursive "$_sm" > /dev/null 2>&1; then + _message "⚠️ submodule ${_sm} not fetched (private repos need git auth); skipping" + fi + done +} # END included from installers/functions.sh # BEGIN included from installers/versions.sh @@ -356,12 +371,15 @@ install_dotfiles() { if test -d "${DOTFILES}" ; then if command_exists git; then _message "🐙 Pull latest dotfiles from github" - if git -C "$DOTFILES" pull --quiet --recurse-submodules > /dev/null 2>&1 ; then + if GIT_TERMINAL_PROMPT=0 git -C "$DOTFILES" pull --quiet --recurse-submodules > /dev/null 2>&1 ; then _message "🐙 Updated dotfiles git repo" else # Not a hard failure _message "⚠️ Failed to update git repo" fi + # Fetch/init any submodule not yet checked out — e.g. a private one + # skipped on a credential-less first install. Best-effort, never fatal. + update_submodules_best_effort fi else exit 1 @@ -382,7 +400,10 @@ install_dotfiles() { download_git_clone() { if command_exists git; then _process "🐙 cloning ${GITHUB_REPO} from github" - git clone --recursive "https://github.com/${GITHUB_REPO}.git" "${DOTFILES}" + git clone "https://github.com/${GITHUB_REPO}.git" "${DOTFILES}" + # Fetch submodules separately and best-effort so a private one (e.g. + # claude/private) that needs credentials can't abort the whole install. + update_submodules_best_effort else exit 1 fi diff --git a/installers/downloaders/download-git-clone.sh b/installers/downloaders/download-git-clone.sh index 508b4b1..2ad97fb 100644 --- a/installers/downloaders/download-git-clone.sh +++ b/installers/downloaders/download-git-clone.sh @@ -7,7 +7,10 @@ set -eu download_git_clone() { if command_exists git; then _process "🐙 cloning ${GITHUB_REPO} from github" - git clone --recursive "https://github.com/${GITHUB_REPO}.git" "${DOTFILES}" + git clone "https://github.com/${GITHUB_REPO}.git" "${DOTFILES}" + # Fetch submodules separately and best-effort so a private one (e.g. + # claude/private) that needs credentials can't abort the whole install. + update_submodules_best_effort else exit 1 fi diff --git a/installers/downloaders/download-git-pull.sh b/installers/downloaders/download-git-pull.sh index 707cb48..98829e1 100644 --- a/installers/downloaders/download-git-pull.sh +++ b/installers/downloaders/download-git-pull.sh @@ -8,12 +8,15 @@ download_git_pull() { if test -d "${DOTFILES}" ; then if command_exists git; then _message "🐙 Pull latest dotfiles from github" - if git -C "$DOTFILES" pull --quiet --recurse-submodules > /dev/null 2>&1 ; then + if GIT_TERMINAL_PROMPT=0 git -C "$DOTFILES" pull --quiet --recurse-submodules > /dev/null 2>&1 ; then _message "🐙 Updated dotfiles git repo" else # Not a hard failure _message "⚠️ Failed to update git repo" fi + # Fetch/init any submodule not yet checked out — e.g. a private one + # skipped on a credential-less first install. Best-effort, never fatal. + update_submodules_best_effort fi else exit 1 diff --git a/installers/functions.sh b/installers/functions.sh index 2d21208..5ee83eb 100644 --- a/installers/functions.sh +++ b/installers/functions.sh @@ -237,3 +237,18 @@ apt_run() { return 1 fi } + +# Best-effort, per-submodule init/update. Runs each submodule on its own so one +# that needs credentials (e.g. a private submodule on a fresh machine) can +# neither abort the caller nor block sibling public submodules. +# GIT_TERMINAL_PROMPT=0 makes a credential-less submodule fail fast instead of +# hanging on an interactive prompt. Anything that consumes a missing submodule +# should no-op until it is fetched — re-run update.sh once credentials exist. +update_submodules_best_effort() { + git -C "${DOTFILES}" submodule status 2>/dev/null | while read -r _ _sm _; do + if ! GIT_TERMINAL_PROMPT=0 git -C "${DOTFILES}" \ + submodule update --init --recursive "$_sm" > /dev/null 2>&1; then + _message "⚠️ submodule ${_sm} not fetched (private repos need git auth); skipping" + fi + done +} diff --git a/update.sh b/update.sh index a38cf70..26c48f1 100755 --- a/update.sh +++ b/update.sh @@ -310,6 +310,21 @@ apt_run() { return 1 fi } + +# Best-effort, per-submodule init/update. Runs each submodule on its own so one +# that needs credentials (e.g. a private submodule on a fresh machine) can +# neither abort the caller nor block sibling public submodules. +# GIT_TERMINAL_PROMPT=0 makes a credential-less submodule fail fast instead of +# hanging on an interactive prompt. Anything that consumes a missing submodule +# should no-op until it is fetched — re-run update.sh once credentials exist. +update_submodules_best_effort() { + git -C "${DOTFILES}" submodule status 2>/dev/null | while read -r _ _sm _; do + if ! GIT_TERMINAL_PROMPT=0 git -C "${DOTFILES}" \ + submodule update --init --recursive "$_sm" > /dev/null 2>&1; then + _message "⚠️ submodule ${_sm} not fetched (private repos need git auth); skipping" + fi + done +} # END included from installers/functions.sh # BEGIN included from installers/versions.sh @@ -384,12 +399,15 @@ update () { if test -d "${DOTFILES}" ; then if command_exists git; then _message "🐙 Pull latest dotfiles from github" - if git -C "$DOTFILES" pull --quiet --recurse-submodules > /dev/null 2>&1 ; then + if GIT_TERMINAL_PROMPT=0 git -C "$DOTFILES" pull --quiet --recurse-submodules > /dev/null 2>&1 ; then _message "🐙 Updated dotfiles git repo" else # Not a hard failure _message "⚠️ Failed to update git repo" fi + # Fetch/init any submodule not yet checked out — e.g. a private one + # skipped on a credential-less first install. Best-effort, never fatal. + update_submodules_best_effort fi else exit 1