From e21b517a6da1ffce712cfa1f34fdc201c0ff8967 Mon Sep 17 00:00:00 2001 From: mainframev Date: Sat, 24 Oct 2026 23:28:51 +0200 Subject: [PATCH 1/5] feat(skills): track the skills CLI lockfile and restore missing skills Agent skills installed with `npx skills` were not reproducible: the lockfile at ~/.agents/.skill-lock.json was ignored and install.sh never reinstalled anything, so a new machine or Codespace lost every skill. Link the lockfile through dotstate and add install_agent_skills, which runs one `skills add --global --agent claude-code` per source repository for the skills missing from ~/.agents/skills. It skips in CI and rejects lockfile names that are not plain directory names. --- .dotstate-profiles.toml | 1 + .gitignore | 4 +- README.md | 5 +- common/.agents/.skill-lock.json | 303 ++++++++++++++++++++++++++++++++ install.sh | 76 ++++++++ test.sh | 134 +++++++++++++- 6 files changed, 515 insertions(+), 8 deletions(-) create mode 100644 common/.agents/.skill-lock.json mode change 100755 => 100644 test.sh diff --git a/.dotstate-profiles.toml b/.dotstate-profiles.toml index 719453a..cd984e8 100644 --- a/.dotstate-profiles.toml +++ b/.dotstate-profiles.toml @@ -2,6 +2,7 @@ version = 2 [common] synced_files = [ + ".agents/.skill-lock.json", ".agents/skills/paranoid-review", ".config/bat", ".config/brew", diff --git a/.gitignore b/.gitignore index e1513a4..d77b25e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,11 @@ !/AGENTS.md !/README.md -# Shared skills: only tracked skills, not ones the skills CLI installs +# Shared skills: only tracked skills and the skills CLI lockfile, not the +# skills it installs /common/.agents/* !/common/.agents/skills/ +!/common/.agents/.skill-lock.json # pi: only the settings file, never auth or session state /common/.pi/* diff --git a/README.md b/README.md index 088239d..b000460 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ cd ~/dotfiles plugins, and dotstate (from its Homebrew tap on macOS, the pinned release on Linux). On first run it writes `~/.config/dotstate/config.toml` for this checkout in Local mode with the `macos` or `linux` profile, then links -everything with dotstate. Files in the way are moved to +everything with dotstate and reinstalls any agent skill recorded in +`~/.agents/.skill-lock.json` that is missing from `~/.agents/skills`, using +`npx skills add --global --agent claude-code`. Files in the way are moved to `~/.dotstate-backups//` with their paths kept. Re-running is safe and links paths added to the manifest since. Run `./test.sh` to validate configs and installer behavior. @@ -61,6 +63,7 @@ What I currently use: - [Neovim](https://neovim.io/) - Hyperextensible Vim-based text editor - [OpenCode](https://opencode.ai/) - AI coding assistant - [pi](https://github.com/earendil-works/pi) - Coding agent; only `~/.pi/agent/settings.json` is managed, and pi installs missing packages from it on startup +- [skills](https://github.com/vercel-labs/skills) - Agent skills CLI; its lockfile `~/.agents/.skill-lock.json` is managed, the installed skills are not - [Starship](https://starship.rs/) - Cross-shell prompt - [Taskwarrior](https://taskwarrior.org/) - Command line task manager - [Tmux](https://github.com/tmux/tmux/wiki) - Terminal multiplexer diff --git a/common/.agents/.skill-lock.json b/common/.agents/.skill-lock.json new file mode 100644 index 0000000..1056985 --- /dev/null +++ b/common/.agents/.skill-lock.json @@ -0,0 +1,303 @@ +{ + "version": 3, + "skills": { + "agent-browser": { + "source": "vercel-labs/agent-browser", + "sourceType": "github", + "sourceUrl": "https://github.com/vercel-labs/agent-browser.git", + "skillPath": "skills/agent-browser/SKILL.md", + "skillFolderHash": "82def67855a0d2aad7794207951820fa5422fdb0", + "installedAt": "2026-09-29T20:28:53.399Z", + "updatedAt": "2026-09-29T20:28:53.399Z" + }, + "find-skills": { + "source": "vercel-labs/skills", + "sourceType": "github", + "sourceUrl": "https://github.com/vercel-labs/skills.git", + "skillPath": "skills/find-skills/SKILL.md", + "skillFolderHash": "76a98a285cb0434f3d39e1a873823556330e398b", + "installedAt": "2026-09-29T20:28:56.653Z", + "updatedAt": "2026-09-29T20:28:56.653Z" + }, + "cavecrew": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/cavecrew/SKILL.md", + "skillFolderHash": "58b9a0bdb00d97953bfc840cb4dc3b38faf03759", + "installedAt": "2026-09-29T20:37:02.354Z", + "updatedAt": "2026-09-29T20:38:17.532Z" + }, + "caveman": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman/SKILL.md", + "skillFolderHash": "2d9a159619205a175577d6540b1ed64fd73d83b1", + "installedAt": "2026-09-29T20:37:02.356Z", + "updatedAt": "2026-09-29T20:38:17.534Z" + }, + "caveman-commit": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-commit/SKILL.md", + "skillFolderHash": "7b600195c1d3cdd277dc608c56384c86afb7ec11", + "installedAt": "2026-09-29T20:37:02.356Z", + "updatedAt": "2026-09-29T20:38:17.534Z" + }, + "caveman-compress": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-compress/SKILL.md", + "skillFolderHash": "21aa5ecc4fdf9667604102c6ec723030ec4f4b9c", + "installedAt": "2026-09-29T20:37:02.356Z", + "updatedAt": "2026-09-29T20:38:17.535Z" + }, + "caveman-discover": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-discover/SKILL.md", + "skillFolderHash": "d43d2ff7268e8df5db2198977b79b765b723b9d9", + "installedAt": "2026-09-29T20:37:02.357Z", + "updatedAt": "2026-09-29T20:38:17.535Z" + }, + "caveman-evidence-review": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-evidence-review/SKILL.md", + "skillFolderHash": "50875bf7d4a1ce7a64d92cc4127b3c63c2e0701a", + "installedAt": "2026-09-29T20:37:02.357Z", + "updatedAt": "2026-09-29T20:38:17.535Z" + }, + "caveman-explore": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-explore/SKILL.md", + "skillFolderHash": "f26926e617ca8108e7ab77a9f6e100c228888ffe", + "installedAt": "2026-09-29T20:37:02.357Z", + "updatedAt": "2026-09-29T20:38:17.536Z" + }, + "caveman-help": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-help/SKILL.md", + "skillFolderHash": "7f39fe20663ec4d859cfed3883927a79a4ed2eff", + "installedAt": "2026-09-29T20:37:02.358Z", + "updatedAt": "2026-09-29T20:38:17.536Z" + }, + "caveman-learn": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-learn/SKILL.md", + "skillFolderHash": "908fae7ced871b3505fecc7c7d294d5f9dd39ca8", + "installedAt": "2026-09-29T20:37:02.359Z", + "updatedAt": "2026-09-29T20:38:17.537Z" + }, + "caveman-manage": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-manage/SKILL.md", + "skillFolderHash": "0a94a5cc3a4fd218b07d8853e4065e9a513fe51f", + "installedAt": "2026-09-29T20:37:02.359Z", + "updatedAt": "2026-09-29T20:38:17.537Z" + }, + "caveman-optimize": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-optimize/SKILL.md", + "skillFolderHash": "3311c453a59a8a27ad0555949e24fea81daf77fa", + "installedAt": "2026-09-29T20:37:02.359Z", + "updatedAt": "2026-09-29T20:38:17.538Z" + }, + "caveman-review": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-review/SKILL.md", + "skillFolderHash": "515d07fbd383fb9a15ad94647d133f681dc56197", + "installedAt": "2026-09-29T20:37:02.360Z", + "updatedAt": "2026-09-29T20:38:17.538Z" + }, + "caveman-setup": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-setup/SKILL.md", + "skillFolderHash": "be2dfccebf403aa38e16bb39e367a3f045e24b07", + "installedAt": "2026-09-29T20:37:02.360Z", + "updatedAt": "2026-09-29T20:38:17.539Z" + }, + "caveman-stats": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/caveman-stats/SKILL.md", + "skillFolderHash": "a2e477b9de0ebf214b86f11dffdd2a0122e1fc1f", + "installedAt": "2026-09-29T20:37:02.361Z", + "updatedAt": "2026-09-29T20:38:17.539Z" + }, + "investigate-first": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/investigate-first/SKILL.md", + "skillFolderHash": "c20174862b4748fc26ce55192a8431e9ef662947", + "installedAt": "2026-09-29T20:37:02.361Z", + "updatedAt": "2026-09-29T20:38:17.539Z" + }, + "lean-build": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/lean-build/SKILL.md", + "skillFolderHash": "47bc09c3509d7f58f0f890836ab7f382987fd487", + "installedAt": "2026-09-29T20:37:02.362Z", + "updatedAt": "2026-09-29T20:38:17.540Z" + }, + "migration": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/migration/SKILL.md", + "skillFolderHash": "b1066e51b4e7cb9a943554a98c3810653ccff059", + "installedAt": "2026-09-29T20:37:02.362Z", + "updatedAt": "2026-09-29T20:38:17.540Z" + }, + "safe-refactor": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/safe-refactor/SKILL.md", + "skillFolderHash": "49fd11fcfa4fd63ab175380ed8f80b992090e5e4", + "installedAt": "2026-09-29T20:37:02.362Z", + "updatedAt": "2026-09-29T20:38:17.540Z" + }, + "surgical-patch": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/surgical-patch/SKILL.md", + "skillFolderHash": "f7316a9431eff84811b6fd100af70e6984751c91", + "installedAt": "2026-09-29T20:37:02.362Z", + "updatedAt": "2026-09-29T20:38:17.541Z" + }, + "verify-and-stop": { + "source": "JuliusBrussee/caveman", + "sourceType": "github", + "sourceUrl": "https://github.com/JuliusBrussee/caveman.git", + "skillPath": "skills/verify-and-stop/SKILL.md", + "skillFolderHash": "daaaceac55b83384dc3685c3b946699b9bc29331", + "installedAt": "2026-09-29T20:37:02.363Z", + "updatedAt": "2026-09-29T20:38:17.541Z" + }, + "unslop": { + "source": "theclaymethod/unslop", + "sourceType": "github", + "sourceUrl": "https://github.com/theclaymethod/unslop.git", + "skillPath": "SKILL.md", + "skillFolderHash": "17ed39c9d0b522f44190ff0c6233867eadee192a", + "installedAt": "2026-09-30T20:53:41.316Z", + "updatedAt": "2026-09-30T20:53:41.316Z" + }, + "fastapi-expert": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/fastapi-expert/SKILL.md", + "skillFolderHash": "a61d5c47c9abf702f60d33c9d381141e690ea931", + "installedAt": "2026-10-16T01:31:36.746Z", + "updatedAt": "2026-10-16T01:31:36.746Z" + }, + "architecture-designer": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/architecture-designer/SKILL.md", + "skillFolderHash": "d9baa48932919d8db398cfc0953b3ec07b08356e", + "installedAt": "2026-10-16T01:31:36.748Z", + "updatedAt": "2026-10-16T01:31:36.748Z" + }, + "react-expert": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/react-expert/SKILL.md", + "skillFolderHash": "be511b42f21056772c6437dd6be3c51a54dfd30e", + "installedAt": "2026-10-16T01:31:36.750Z", + "updatedAt": "2026-10-16T01:31:36.750Z" + }, + "nextjs-developer": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/nextjs-developer/SKILL.md", + "skillFolderHash": "c92ec0f7f9e29d87be7322134c2c20e5fe9e44f6", + "installedAt": "2026-10-16T01:31:36.751Z", + "updatedAt": "2026-10-16T01:31:36.751Z" + }, + "code-reviewer": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/code-reviewer/SKILL.md", + "skillFolderHash": "7a96d8709f89d08fb37686736412be3c1bed6d99", + "installedAt": "2026-10-16T01:31:36.751Z", + "updatedAt": "2026-10-16T01:31:36.751Z" + }, + "code-documenter": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/code-documenter/SKILL.md", + "skillFolderHash": "1d2532889c15d70eb6d6230bd0d99657905a4b4e", + "installedAt": "2026-10-16T01:31:36.752Z", + "updatedAt": "2026-10-16T01:31:36.752Z" + }, + "cli-developer": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/cli-developer/SKILL.md", + "skillFolderHash": "d48ca3288f613f09b0efccab6f2b2092b4e58a69", + "installedAt": "2026-10-16T01:31:36.753Z", + "updatedAt": "2026-10-16T01:31:36.753Z" + }, + "rust-engineer": { + "source": "jeffallan/claude-skills", + "sourceType": "github", + "sourceUrl": "https://github.com/jeffallan/claude-skills.git", + "skillPath": "skills/rust-engineer/SKILL.md", + "skillFolderHash": "ac751682f4202d7cc0fe5c82ec9a815b83ca9f70", + "installedAt": "2026-10-16T01:31:36.754Z", + "updatedAt": "2026-10-16T01:31:36.754Z" + } + }, + "dismissed": { + "findSkillsPrompt": true + }, + "lastSelectedAgents": [ + "amp", + "cline", + "codex", + "cursor", + "droid", + "gemini-cli", + "github-copilot", + "kilo", + "kimi-code-cli", + "opencode", + "pi", + "warp", + "zed", + "claude-code" + ] +} \ No newline at end of file diff --git a/install.sh b/install.sh index f1bc519..d2c5c9c 100755 --- a/install.sh +++ b/install.sh @@ -798,6 +798,81 @@ install_herdr_plugins() { fi } +# Reinstall agent skills recorded in the skills CLI lockfile (linked from +# common/.agents/.skill-lock.json) that are missing from ~/.agents/skills, +# with one `skills add` per source repository, linked for Claude Code +install_agent_skills() { + if [ -n "${CI:-}" ]; then + print_info "CI environment detected, skipping agent skill installation" + return + fi + + local lock="$HOME/.agents/.skill-lock.json" + if [ ! -f "$lock" ]; then + print_warning "No skills lockfile at ~/.agents/.skill-lock.json, skipping agent skills" + return + fi + + if ! command_exists npx; then + print_warning "npx not installed, skipping agent skill installation" + return + fi + + if ! command_exists jq; then + print_error "jq is required to read the skills lockfile but is not installed" + return 1 + fi + + print_header "Installing agent skills..." + + local sources + if ! sources="$(jq -er '.skills | [.[].sourceUrl] | unique[]' "$lock" 2>/dev/null)"; then + print_error "Failed to read sources from $lock" + return 1 + fi + + local errors=0 + local source + local name + local missing + + while IFS= read -r source; do + missing=() + while IFS= read -r name; do + # Skill names become paths under ~/.agents/skills + case "$name" in + ""|.*|*/*) + print_error "Skipping invalid skill name in $lock: $name" + errors=$((errors + 1)) + ;; + *) + if [ -d "$HOME/.agents/skills/$name" ]; then + print_success "Agent skill already installed: $name" + else + missing+=("$name") + fi + ;; + esac + done < <(jq -r --arg src "$source" \ + '.skills | to_entries[] | select(.value.sourceUrl == $src) | .key' "$lock") + + [ ${#missing[@]} -gt 0 ] || continue + + # --skill takes every following argument, so it goes last + print_info "Installing agent skills from $source: ${missing[*]}" + if npx -y skills add "$source" --global --agent claude-code --yes --skill "${missing[@]}" "$SKILLS_TEST_DIR/bin/npx" <<'STUB' +#!/usr/bin/env bash +echo "$*" >> "$SKILLS_STUB_LOG" +case "$*" in + *"${SKILLS_STUB_FAIL:-}"*) exit 1 ;; +esac +STUB + chmod +x "$SKILLS_TEST_DIR/bin/npx" + + # $1=lockfile JSON, $2=source to fail (or ""), $3=CI value + run_skills_scenario() { + local home="$SKILLS_TEST_DIR/home" + rm -rf "$home" + mkdir -p "$home/.agents/skills/installed" + printf '%s' "$1" > "$home/.agents/.skill-lock.json" + : > "$SKILLS_TEST_DIR/log" + local ci=(-u CI) + [ -z "${3:-}" ] || ci=("CI=$3") + # Only the npx stub, jq and the base system tools are reachable + env "${ci[@]}" HOME="$home" PATH="$SKILLS_TEST_DIR/bin:$SKILLS_JQ_DIR:/usr/bin:/bin" \ + SKILLS_STUB_LOG="$SKILLS_TEST_DIR/log" SKILLS_STUB_FAIL="$2" \ + bash -c 'source ./install.sh; install_agent_skills' >/dev/null 2>&1 + } + + SKILLS_LOCK='{"version":3,"skills":{ + "installed":{"sourceUrl":"https://github.com/a/one.git"}, + "alpha":{"sourceUrl":"https://github.com/a/one.git"}, + "beta":{"sourceUrl":"https://github.com/a/one.git"}, + "gamma":{"sourceUrl":"https://github.com/b/two.git"}}}' + + if run_skills_scenario "$SKILLS_LOCK" "" "" && + [ "$(wc -l < "$SKILLS_TEST_DIR/log")" -eq 2 ] && + grep -qx -- '-y skills add https://github.com/a/one.git --global --agent claude-code --yes --skill alpha beta' "$SKILLS_TEST_DIR/log" && + grep -qx -- '-y skills add https://github.com/b/two.git --global --agent claude-code --yes --skill gamma' "$SKILLS_TEST_DIR/log"; then + echo -e "${GREEN}✓ missing skills install once per source and installed ones are skipped${NC}" + else + echo -e "${RED}✗ install_agent_skills must run one skills add per source for missing skills only${NC}" + cat "$SKILLS_TEST_DIR/log" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + if ! run_skills_scenario "$SKILLS_LOCK" "b/two" "" && + grep -q 'a/one' "$SKILLS_TEST_DIR/log"; then + echo -e "${GREEN}✓ a failed source fails the step without stopping other sources${NC}" + else + echo -e "${RED}✗ install_agent_skills must try every source and fail when one does${NC}" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + if ! run_skills_scenario '{"version":3,"skills":{"../escape":{"sourceUrl":"https://github.com/a/one.git"}}}' "" "" && + [ ! -s "$SKILLS_TEST_DIR/log" ]; then + echo -e "${GREEN}✓ skill names that are not plain directory names are rejected${NC}" + else + echo -e "${RED}✗ install_agent_skills must reject skill names containing / or starting with .${NC}" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + if ! run_skills_scenario 'not json' "" "" && [ ! -s "$SKILLS_TEST_DIR/log" ]; then + echo -e "${GREEN}✓ a malformed lockfile fails without installing anything${NC}" + else + echo -e "${RED}✗ install_agent_skills must fail on a malformed lockfile${NC}" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + if run_skills_scenario "$SKILLS_LOCK" "" "true" && [ ! -s "$SKILLS_TEST_DIR/log" ]; then + echo -e "${GREEN}✓ agent skill installation is skipped in CI${NC}" + else + echo -e "${RED}✗ install_agent_skills must skip in CI${NC}" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + skills_test14_cleanup + eval "${skills_test14_prev_exit_trap:-trap - EXIT}" + eval "${skills_test14_prev_int_trap:-trap - INT}" + eval "${skills_test14_prev_term_trap:-trap - TERM}" + unset -f skills_test14_cleanup run_skills_scenario +else + echo -e "${YELLOW}⚠ jq not installed, skipping agent skill installation check${NC}" + WARNINGS=$((WARNINGS + 1)) +fi + +if [ $SKILLS_ERRORS -gt 0 ]; then + ERRORS=$((ERRORS + SKILLS_ERRORS)) +fi +echo + # Summary echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then From e2d8f34994766f2d9be9bcfb0e655703ab4a3785 Mon Sep 17 00:00:00 2001 From: mainframev Date: Sat, 24 Oct 2026 23:28:58 +0200 Subject: [PATCH 2/5] feat(claude): track portable Claude Code settings and themes ~/.claude was unmanaged, so plugins, marketplaces, the status line and the Rose Pine theme had to be set up again on every machine. Link ~/.claude/settings.json and ~/.claude/themes through dotstate. The tracked settings hold only portable keys: the fluentui autoMode context is dropped and its permission rule now lives in that repository's local settings, since this repository is public. A test rejects autoMode, permissions, env and apiKeyHelper in the tracked file. --- .dotstate-profiles.toml | 2 ++ .gitignore | 5 +++ README.md | 1 + common/.claude/settings.json | 49 +++++++++++++++++++++++++ common/.claude/themes/rose-pine.json | 53 ++++++++++++++++++++++++++++ test.sh | 46 +++++++++++++++++++++--- 6 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 common/.claude/settings.json create mode 100644 common/.claude/themes/rose-pine.json mode change 100644 => 100755 test.sh diff --git a/.dotstate-profiles.toml b/.dotstate-profiles.toml index cd984e8..6776633 100644 --- a/.dotstate-profiles.toml +++ b/.dotstate-profiles.toml @@ -4,6 +4,8 @@ version = 2 synced_files = [ ".agents/.skill-lock.json", ".agents/skills/paranoid-review", + ".claude/settings.json", + ".claude/themes", ".config/bat", ".config/brew", ".config/btop", diff --git a/.gitignore b/.gitignore index d77b25e..3f4fa9c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,11 @@ !/common/.agents/skills/ !/common/.agents/.skill-lock.json +# Claude Code: only portable settings and themes, never sessions or caches +/common/.claude/* +!/common/.claude/settings.json +!/common/.claude/themes/ + # pi: only the settings file, never auth or session state /common/.pi/* !/common/.pi/agent/ diff --git a/README.md b/README.md index b000460..909db11 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ What I currently use: - [Bat](https://github.com/sharkdp/bat) - Cat clone with syntax highlighting - [btop](https://github.com/aristocratos/btop) - Resource monitor - [ccstatusline](https://github.com/sirmalloc/ccstatusline) - Claude Code status line +- [Claude Code](https://code.claude.com/) - AI coding agent; only `~/.claude/settings.json` (statusline, plugins, marketplaces, theme) and `~/.claude/themes` are managed. Keep project-specific `autoMode` and permissions out of the tracked file, since this repository is public - [Git](https://git-scm.com/) with [Delta](https://github.com/dandavison/delta) - Version control with Delta configured as the pager - [GitHub CLI](https://cli.github.com/) - GitHub command line tool - [gh-dash](https://github.com/dlvhdr/gh-dash) - GitHub CLI dashboard diff --git a/common/.claude/settings.json b/common/.claude/settings.json new file mode 100644 index 0000000..dbb048c --- /dev/null +++ b/common/.claude/settings.json @@ -0,0 +1,49 @@ +{ + "statusLine": { + "type": "command", + "command": "ccstatusline", + "padding": 0, + "refreshInterval": 10 + }, + "enabledPlugins": { + "frontend-design@claude-plugins-official": true, + "mattpocock-skills@claude-plugins-official": true, + "code-simplifier@anthropic-plugin-directory": true, + "code-review@anthropic-plugin-directory": true, + "lua-lsp@anthropic-plugin-directory": true, + "commit-commands@anthropic-plugin-directory": true, + "remember@claude-plugins-official": true, + "excalidraw-plugin@anthropic-plugin-directory": true, + "web-dev@awesome-claude-code-plugins": true, + "backend-architect@awesome-claude-code-plugins": true, + "code-architect@awesome-claude-code-plugins": true + }, + "extraKnownMarketplaces": { + "ponytail": { + "source": { + "source": "github", + "repo": "DietrichGebert/ponytail" + } + }, + "obsidian-skills": { + "source": { + "source": "github", + "repo": "kepano/obsidian-skills" + } + }, + "plannotator": { + "source": { + "source": "github", + "repo": "backnotprop/plannotator" + } + }, + "awesome-claude-code-plugins": { + "source": { + "source": "github", + "repo": "ccplugins/awesome-claude-code-plugins" + } + } + }, + "theme": "custom:rose-pine", + "skipDangerousModePermissionPrompt": true +} diff --git a/common/.claude/themes/rose-pine.json b/common/.claude/themes/rose-pine.json new file mode 100644 index 0000000..2a1ff20 --- /dev/null +++ b/common/.claude/themes/rose-pine.json @@ -0,0 +1,53 @@ +{ + "name": "Rosé Pine", + "base": "dark", + "overrides": { + "claude": "#ebbcba", + "claudeShimmer": "#f5d7d5", + "text": "#e0def4", + "inverseText": "#191724", + "inactive": "#908caa", + "inactiveShimmer": "#b5b1cc", + "subtle": "#524f67", + "suggestion": "#c4a7e7", + "permission": "#c4a7e7", + "permissionShimmer": "#dccaf0", + "remember": "#c4a7e7", + "success": "#9ccfd8", + "error": "#eb6f92", + "warning": "#f6c177", + "warningShimmer": "#fad8a6", + "merged": "#c4a7e7", + "promptBorder": "#524f67", + "promptBorderShimmer": "#6e6a86", + "planMode": "#31748f", + "autoAccept": "#c4a7e7", + "bashBorder": "#eb6f92", + "ide": "#9ccfd8", + "fastMode": "#f6c177", + "fastModeShimmer": "#fad8a6", + "diffAdded": "#1f2e3f", + "diffRemoved": "#4d2d3f", + "diffAddedDimmed": "#1c2231", + "diffRemovedDimmed": "#322231", + "diffAddedWord": "#244154", + "diffRemovedWord": "#783f56", + "userMessageBackground": "#1f1d2e", + "userMessageBackgroundHover": "#26233a", + "bashMessageBackgroundColor": "#1f1d2e", + "memoryBackgroundColor": "#1f1d2e", + "selectionBg": "#403d52", + "rate_limit_fill": "#c4a7e7", + "rate_limit_empty": "#403d52", + "briefLabelYou": "#9ccfd8", + "briefLabelClaude": "#ebbcba", + "red_FOR_SUBAGENTS_ONLY": "#eb6f92", + "blue_FOR_SUBAGENTS_ONLY": "#31748f", + "green_FOR_SUBAGENTS_ONLY": "#9ccfd8", + "yellow_FOR_SUBAGENTS_ONLY": "#f6c177", + "purple_FOR_SUBAGENTS_ONLY": "#c4a7e7", + "orange_FOR_SUBAGENTS_ONLY": "#ea9a97", + "pink_FOR_SUBAGENTS_ONLY": "#ebbcba", + "cyan_FOR_SUBAGENTS_ONLY": "#9ccfd8" + } +} diff --git a/test.sh b/test.sh old mode 100644 new mode 100755 index 99b3cd0..e9ca9bb --- a/test.sh +++ b/test.sh @@ -861,6 +861,35 @@ else MANIFEST_ERRORS=$((MANIFEST_ERRORS + 1)) fi +if git check-ignore -q --no-index common/.claude/settings.json || + git check-ignore -q --no-index common/.claude/themes/rose-pine.json; then + echo -e "${RED}✗ common/.claude/settings.json and themes/ must remain trackable${NC}" + MANIFEST_ERRORS=$((MANIFEST_ERRORS + 1)) +else + echo -e "${GREEN}✓ Claude Code settings and themes are trackable${NC}" +fi + +if git check-ignore -q --no-index common/.claude/projects/x/session.jsonl && + git check-ignore -q --no-index common/.claude/settings.local.json && + git check-ignore -q --no-index common/.claude/.credentials.json; then + echo -e "${GREEN}✓ Claude Code sessions, local settings and credentials are ignored${NC}" +else + echo -e "${RED}✗ only settings.json and themes/ may be tracked under common/.claude${NC}" + MANIFEST_ERRORS=$((MANIFEST_ERRORS + 1)) +fi + +if command_exists jq && [ -f common/.claude/settings.json ] && + jq -e 'has("autoMode") or has("permissions") or has("env") or has("apiKeyHelper") | not' \ + common/.claude/settings.json >/dev/null 2>&1; then + echo -e "${GREEN}✓ tracked Claude Code settings hold no project, permission or credential keys${NC}" +elif ! command_exists jq; then + echo -e "${YELLOW}⚠ jq not installed, skipping Claude Code settings key check${NC}" + WARNINGS=$((WARNINGS + 1)) +else + echo -e "${RED}✗ common/.claude/settings.json must be valid JSON without autoMode, permissions, env or apiKeyHelper${NC}" + MANIFEST_ERRORS=$((MANIFEST_ERRORS + 1)) +fi + if [ -L "common/.config/opencode/skills/paranoid-review" ] && [ -f "common/.config/opencode/skills/paranoid-review/SKILL.md" ]; then echo -e "${GREEN}✓ opencode paranoid-review skill links to the shared skill${NC}" else @@ -899,12 +928,14 @@ if command_exists dotstate; then # Existing machine: unmanaged files at linked paths next to app state LINK_HOME="$LINK_TEST_ROOT/macos" - mkdir -p "$LINK_HOME/.pi/agent" "$LINK_HOME/.agents/skills/other-skill" + mkdir -p "$LINK_HOME/.pi/agent" "$LINK_HOME/.agents/skills/other-skill" "$LINK_HOME/.claude/projects" printf '%s\n' 'unmanaged zshrc' > "$LINK_HOME/.zshrc" echo '{"unmanaged":true}' > "$LINK_HOME/.pi/agent/settings.json" echo '{"secret":"state"}' > "$LINK_HOME/.pi/agent/auth.json" echo '{"version":3}' > "$LINK_HOME/.agents/.skill-lock.json" echo 'existing' > "$LINK_HOME/.agents/skills/other-skill/SKILL.md" + echo '{"unmanaged":"claude"}' > "$LINK_HOME/.claude/settings.json" + echo 'session' > "$LINK_HOME/.claude/projects/session.jsonl" LINK_RESULT=0 link_run "$LINK_HOME" macos run1 || LINK_RESULT=$? @@ -917,7 +948,8 @@ if command_exists dotstate; then [ -n "$LINK_BACKUP" ] && grep -qx 'unmanaged zshrc' "$LINK_BACKUP/.zshrc" && grep -q '"unmanaged"' "$LINK_BACKUP/.pi/agent/settings.json" && - grep -q '"version":3' "$LINK_BACKUP/.agents/.skill-lock.json"; then + grep -q '"version":3' "$LINK_BACKUP/.agents/.skill-lock.json" && + grep -q '"claude"' "$LINK_BACKUP/.claude/settings.json"; then echo -e "${GREEN}✓ macos profile links common and macOS configs and backs up conflicting files${NC}" else echo -e "${RED}✗ link_dotfiles must link the macos profile and back up conflicting files (exit $LINK_RESULT)${NC}" @@ -931,10 +963,14 @@ if command_exists dotstate; then [ -d "$LINK_HOME/.agents/skills" ] && [ ! -L "$LINK_HOME/.agents/skills" ] && [ -L "$LINK_HOME/.agents/skills/paranoid-review" ] && [ "$(cat "$LINK_HOME/.agents/skills/other-skill/SKILL.md")" = "existing" ] && - [ "$(readlink "$LINK_HOME/.agents/.skill-lock.json")" = "$LINK_REPO/common/.agents/.skill-lock.json" ]; then - echo -e "${GREEN}✓ ~/.pi/agent and ~/.agents stay real directories with their state untouched${NC}" + [ "$(readlink "$LINK_HOME/.agents/.skill-lock.json")" = "$LINK_REPO/common/.agents/.skill-lock.json" ] && + [ -d "$LINK_HOME/.claude" ] && [ ! -L "$LINK_HOME/.claude" ] && + [ "$(readlink "$LINK_HOME/.claude/settings.json")" = "$LINK_REPO/common/.claude/settings.json" ] && + [ "$(readlink "$LINK_HOME/.claude/themes")" = "$LINK_REPO/common/.claude/themes" ] && + [ "$(cat "$LINK_HOME/.claude/projects/session.jsonl")" = "session" ]; then + echo -e "${GREEN}✓ ~/.pi/agent, ~/.agents and ~/.claude stay real directories with their state untouched${NC}" else - echo -e "${RED}✗ only pi settings.json, the skills lockfile and paranoid-review may be linked inside ~/.pi/agent and ~/.agents${NC}" + echo -e "${RED}✗ only tracked settings, themes, the skills lockfile and paranoid-review may be linked inside ~/.pi/agent, ~/.agents and ~/.claude${NC}" LINK_ERRORS=$((LINK_ERRORS + 1)) fi From e60e72b380d1ef071d0a4fdd462d135a2b71168d Mon Sep 17 00:00:00 2001 From: mainframev Date: Sat, 24 Oct 2026 23:31:15 +0200 Subject: [PATCH 3/5] docs: drop Claude Code and skills entries from the tools list --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 909db11..a0a3dc3 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,6 @@ What I currently use: - [Bat](https://github.com/sharkdp/bat) - Cat clone with syntax highlighting - [btop](https://github.com/aristocratos/btop) - Resource monitor - [ccstatusline](https://github.com/sirmalloc/ccstatusline) - Claude Code status line -- [Claude Code](https://code.claude.com/) - AI coding agent; only `~/.claude/settings.json` (statusline, plugins, marketplaces, theme) and `~/.claude/themes` are managed. Keep project-specific `autoMode` and permissions out of the tracked file, since this repository is public - [Git](https://git-scm.com/) with [Delta](https://github.com/dandavison/delta) - Version control with Delta configured as the pager - [GitHub CLI](https://cli.github.com/) - GitHub command line tool - [gh-dash](https://github.com/dlvhdr/gh-dash) - GitHub CLI dashboard @@ -64,7 +63,6 @@ What I currently use: - [Neovim](https://neovim.io/) - Hyperextensible Vim-based text editor - [OpenCode](https://opencode.ai/) - AI coding assistant - [pi](https://github.com/earendil-works/pi) - Coding agent; only `~/.pi/agent/settings.json` is managed, and pi installs missing packages from it on startup -- [skills](https://github.com/vercel-labs/skills) - Agent skills CLI; its lockfile `~/.agents/.skill-lock.json` is managed, the installed skills are not - [Starship](https://starship.rs/) - Cross-shell prompt - [Taskwarrior](https://taskwarrior.org/) - Command line task manager - [Tmux](https://github.com/tmux/tmux/wiki) - Terminal multiplexer From 36124a40b8780803811c5a1864f413132982139e Mon Sep 17 00:00:00 2001 From: mainframev Date: Sun, 25 Oct 2026 00:32:32 +0200 Subject: [PATCH 4/5] fix(skills): protect real Claude skill directories and accept empty locks The skills CLI removes whatever sits at ~/.claude/skills/ before linking, so a locked skill missing from ~/.agents/skills could delete a hand-made Claude skill of the same name. Skip such skills with a warning instead, honoring CLAUDE_CONFIG_DIR like the CLI does. `jq -e` exited 4 on a lockfile with no skills, aborting install.sh under set -e. Read sources without -e and validate the shape explicitly, so an empty inventory succeeds while malformed JSON, a missing skills object or a non-string sourceUrl still fail. Also restore the executable bit on test.sh lost in e21b517. --- README.md | 11 ++++++----- install.sh | 16 +++++++++++++++- test.sh | 34 +++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index a0a3dc3..ab657c2 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,12 @@ plugins, and dotstate (from its Homebrew tap on macOS, the pinned release on Linux). On first run it writes `~/.config/dotstate/config.toml` for this checkout in Local mode with the `macos` or `linux` profile, then links everything with dotstate and reinstalls any agent skill recorded in -`~/.agents/.skill-lock.json` that is missing from `~/.agents/skills`, using -`npx skills add --global --agent claude-code`. Files in the way are moved to -`~/.dotstate-backups//` with their paths kept. Re-running is safe and -links paths added to the manifest since. Run `./test.sh` to validate configs and -installer behavior. +`~/.agents/.skill-lock.json` that is missing from `~/.agents/skills`, using `npx +skills add --global --agent claude-code`, leaving any skill whose +`~/.claude/skills/` is a real directory untouched. Files in the way are +moved to `~/.dotstate-backups//` with their paths kept. Re-running is +safe and links paths added to the manifest since. Run `./test.sh` to validate +configs and installer behavior. Coming from the old Stow layout, `install.sh` moves untracked state left at the old paths (for example `.config/zsh/secrets.zsh`, herdr sessions, tmux plugins) diff --git a/install.sh b/install.sh index d2c5c9c..574ae9b 100755 --- a/install.sh +++ b/install.sh @@ -825,18 +825,30 @@ install_agent_skills() { print_header "Installing agent skills..." + # An empty inventory yields no sources; anything but an object of + # entries with a non-empty sourceUrl is an error local sources - if ! sources="$(jq -er '.skills | [.[].sourceUrl] | unique[]' "$lock" 2>/dev/null)"; then + if ! sources="$(jq -r ' + .skills + | if type == "object" and all(.[]; (.sourceUrl | type) == "string" and .sourceUrl != "") + then [.[].sourceUrl] | unique[] + else error("invalid skills") + end + ' "$lock" 2>/dev/null)"; then print_error "Failed to read sources from $lock" return 1 fi + # The skills CLI replaces whatever is at the Claude Code link path, so + # never let it delete a real directory there + local claude_skills="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/skills" local errors=0 local source local name local missing while IFS= read -r source; do + [ -n "$source" ] || continue missing=() while IFS= read -r name; do # Skill names become paths under ~/.agents/skills @@ -848,6 +860,8 @@ install_agent_skills() { *) if [ -d "$HOME/.agents/skills/$name" ]; then print_success "Agent skill already installed: $name" + elif [ -e "$claude_skills/$name" ] && [ ! -L "$claude_skills/$name" ]; then + print_warning "Not installing agent skill $name: $claude_skills/$name exists and is not a link; move it away to install" else missing+=("$name") fi diff --git a/test.sh b/test.sh index e9ca9bb..5205a73 100755 --- a/test.sh +++ b/test.sh @@ -1095,17 +1095,21 @@ esac STUB chmod +x "$SKILLS_TEST_DIR/bin/npx" - # $1=lockfile JSON, $2=source to fail (or ""), $3=CI value + # $1=lockfile JSON, $2=source to fail (or ""), $3=CI value, $4=skill to + # leave as a real directory in ~/.claude/skills, $5=skill to leave as a + # dangling link there run_skills_scenario() { local home="$SKILLS_TEST_DIR/home" rm -rf "$home" - mkdir -p "$home/.agents/skills/installed" + mkdir -p "$home/.agents/skills/installed" "$home/.claude/skills" printf '%s' "$1" > "$home/.agents/.skill-lock.json" + [ -z "${4:-}" ] || { mkdir -p "$home/.claude/skills/$4"; echo 'local' > "$home/.claude/skills/$4/SKILL.md"; } + [ -z "${5:-}" ] || ln -s "../../.agents/skills/$5" "$home/.claude/skills/$5" : > "$SKILLS_TEST_DIR/log" local ci=(-u CI) [ -z "${3:-}" ] || ci=("CI=$3") # Only the npx stub, jq and the base system tools are reachable - env "${ci[@]}" HOME="$home" PATH="$SKILLS_TEST_DIR/bin:$SKILLS_JQ_DIR:/usr/bin:/bin" \ + env -u CLAUDE_CONFIG_DIR "${ci[@]}" HOME="$home" PATH="$SKILLS_TEST_DIR/bin:$SKILLS_JQ_DIR:/usr/bin:/bin" \ SKILLS_STUB_LOG="$SKILLS_TEST_DIR/log" SKILLS_STUB_FAIL="$2" \ bash -c 'source ./install.sh; install_agent_skills' >/dev/null 2>&1 } @@ -1143,10 +1147,30 @@ STUB SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) fi - if ! run_skills_scenario 'not json' "" "" && [ ! -s "$SKILLS_TEST_DIR/log" ]; then + if run_skills_scenario "$SKILLS_LOCK" "" "" "alpha" "gamma" && + grep -qx -- '-y skills add https://github.com/a/one.git --global --agent claude-code --yes --skill beta' "$SKILLS_TEST_DIR/log" && + grep -q -- '--skill gamma$' "$SKILLS_TEST_DIR/log" && + [ "$(cat "$SKILLS_TEST_DIR/home/.claude/skills/alpha/SKILL.md")" = "local" ]; then + echo -e "${GREEN}✓ real directories in ~/.claude/skills are never handed to the skills CLI${NC}" + else + echo -e "${RED}✗ install_agent_skills must skip skills whose ~/.claude/skills path is a real directory${NC}" + cat "$SKILLS_TEST_DIR/log" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + if ! run_skills_scenario 'not json' "" "" && [ ! -s "$SKILLS_TEST_DIR/log" ] && + ! run_skills_scenario '{"version":3,"skills":{"alpha":{"sourceUrl":1}}}' "" "" && [ ! -s "$SKILLS_TEST_DIR/log" ] && + ! run_skills_scenario '{"version":3}' "" "" && [ ! -s "$SKILLS_TEST_DIR/log" ]; then echo -e "${GREEN}✓ a malformed lockfile fails without installing anything${NC}" else - echo -e "${RED}✗ install_agent_skills must fail on a malformed lockfile${NC}" + echo -e "${RED}✗ install_agent_skills must fail on malformed JSON, a missing skills object or a non-string sourceUrl${NC}" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) + fi + + if run_skills_scenario '{"version":3,"skills":{}}' "" "" && [ ! -s "$SKILLS_TEST_DIR/log" ]; then + echo -e "${GREEN}✓ an empty skill inventory succeeds without installing anything${NC}" + else + echo -e "${RED}✗ install_agent_skills must accept a lockfile with no skills${NC}" SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) fi From daba4c5bf2aa1f3c47d7d7e11f8c00fd86046137 Mon Sep 17 00:00:00 2001 From: mainframev Date: Sun, 25 Oct 2026 00:49:04 +0200 Subject: [PATCH 5/5] fix(claude): install ccstatusline for the tracked status line The tracked settings run ccstatusline as the status line, but nothing in the bootstrap installed it and it has no Homebrew formula, so a fresh machine showed no status line. Install it with npm from the Brewfile's node when missing; a pinned binary avoids an npx registry lookup on every 10s refresh. --- README.md | 20 ++++++++++---------- install.sh | 23 +++++++++++++++++++++++ test.sh | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ab657c2..1916330 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,16 @@ cd ~/dotfiles ``` `install.sh` installs Homebrew and the Brewfile, oh-my-zsh and its custom -plugins, and dotstate (from its Homebrew tap on macOS, the pinned release on -Linux). On first run it writes `~/.config/dotstate/config.toml` for this -checkout in Local mode with the `macos` or `linux` profile, then links -everything with dotstate and reinstalls any agent skill recorded in -`~/.agents/.skill-lock.json` that is missing from `~/.agents/skills`, using `npx -skills add --global --agent claude-code`, leaving any skill whose -`~/.claude/skills/` is a real directory untouched. Files in the way are -moved to `~/.dotstate-backups//` with their paths kept. Re-running is -safe and links paths added to the manifest since. Run `./test.sh` to validate -configs and installer behavior. +plugins, ccstatusline (with npm), and dotstate (from its Homebrew tap on macOS, +the pinned release on Linux). On first run it writes +`~/.config/dotstate/config.toml` for this checkout in Local mode with the +`macos` or `linux` profile, then links everything with dotstate and reinstalls +any agent skill recorded in `~/.agents/.skill-lock.json` that is missing from +`~/.agents/skills`, using `npx skills add --global --agent claude-code`, leaving +any skill whose `~/.claude/skills/` is a real directory untouched. Files +in the way are moved to `~/.dotstate-backups//` with their paths +kept. Re-running is safe and links paths added to the manifest since. Run +`./test.sh` to validate configs and installer behavior. Coming from the old Stow layout, `install.sh` moves untracked state left at the old paths (for example `.config/zsh/secrets.zsh`, herdr sessions, tmux plugins) diff --git a/install.sh b/install.sh index 574ae9b..b09eccb 100755 --- a/install.sh +++ b/install.sh @@ -504,6 +504,28 @@ install_opencode() { fi } +# Install ccstatusline, the Claude Code status line command set in +# common/.claude/settings.json. It has no Homebrew formula, so use npm from +# the Brewfile's node. +install_ccstatusline() { + if command_exists ccstatusline; then + print_success "ccstatusline already installed" + return + fi + + if ! command_exists npm; then + print_warning "npm not installed, skipping ccstatusline installation" + return + fi + + print_header "Installing ccstatusline..." + if npm install --global ccstatusline "$CCSTATUS_TEST_DIR/bin/npm" <<'STUB' +#!/usr/bin/env bash +echo "$*" >> "$CCSTATUS_STUB_LOG" +STUB +chmod +x "$CCSTATUS_TEST_DIR/bin/npm" + +# $1=extra stub to put on PATH (ccstatusline or "") +run_ccstatus_scenario() { + : > "$CCSTATUS_TEST_DIR/log" + rm -f "$CCSTATUS_TEST_DIR/bin/ccstatusline" + [ -z "$1" ] || { printf '#!/bin/sh\n' > "$CCSTATUS_TEST_DIR/bin/$1"; chmod +x "$CCSTATUS_TEST_DIR/bin/$1"; } + env PATH="$CCSTATUS_TEST_DIR/bin:/usr/bin:/bin" CCSTATUS_STUB_LOG="$CCSTATUS_TEST_DIR/log" \ + bash -c 'source ./install.sh; install_ccstatusline' >/dev/null 2>&1 +} + +if run_ccstatus_scenario "" && grep -qx 'install --global ccstatusline' "$CCSTATUS_TEST_DIR/log" && + run_ccstatus_scenario ccstatusline && [ ! -s "$CCSTATUS_TEST_DIR/log" ]; then + echo -e "${GREEN}✓ ccstatusline installs with npm only when missing${NC}" +else + echo -e "${RED}✗ install_ccstatusline must run npm install --global ccstatusline only when it is missing${NC}" + CCSTATUS_ERRORS=$((CCSTATUS_ERRORS + 1)) +fi + +INSTALL_LINE=$(grep -n '^ install_brew_packages$' install.sh | cut -d: -f1) +CCSTATUS_LINE=$(grep -n '^ install_ccstatusline$' install.sh | cut -d: -f1) +if [ -n "$INSTALL_LINE" ] && [ -n "$CCSTATUS_LINE" ] && [ "$INSTALL_LINE" -lt "$CCSTATUS_LINE" ]; then + echo -e "${GREEN}✓ ccstatusline installs after the Brewfile provides node${NC}" +else + echo -e "${RED}✗ install_ccstatusline must run after install_brew_packages in main${NC}" + CCSTATUS_ERRORS=$((CCSTATUS_ERRORS + 1)) +fi + +rm -rf "$CCSTATUS_TEST_DIR" +unset -f run_ccstatus_scenario +if [ $CCSTATUS_ERRORS -gt 0 ]; then + ERRORS=$((ERRORS + CCSTATUS_ERRORS)) +fi +echo + # Summary echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" if [ $ERRORS -eq 0 ] && [ $WARNINGS -eq 0 ]; then