diff --git a/.dotstate-profiles.toml b/.dotstate-profiles.toml index 719453a..6776633 100644 --- a/.dotstate-profiles.toml +++ b/.dotstate-profiles.toml @@ -2,7 +2,10 @@ version = 2 [common] 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 e1513a4..3f4fa9c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,16 @@ !/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 + +# 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/* diff --git a/README.md b/README.md index 088239d..1916330 100644 --- a/README.md +++ b/README.md @@ -30,13 +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. 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/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/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/install.sh b/install.sh index f1bc519..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 /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 + 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" + 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 + ;; + 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[@]}" /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 @@ -892,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=$? @@ -909,7 +947,9 @@ if command_exists dotstate; then [ "$(readlink "$LINK_HOME/.config/kitty")" = "$LINK_REPO/macos/.config/kitty" ] && [ -n "$LINK_BACKUP" ] && grep -qx 'unmanaged zshrc' "$LINK_BACKUP/.zshrc" && - grep -q '"unmanaged"' "$LINK_BACKUP/.pi/agent/settings.json"; then + grep -q '"unmanaged"' "$LINK_BACKUP/.pi/agent/settings.json" && + 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}" @@ -923,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" ] && - [ "$(cat "$LINK_HOME/.agents/.skill-lock.json")" = '{"version":3}' ]; then - echo -e "${GREEN}✓ ~/.pi/agent and ~/.agents/skills 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 and paranoid-review may be linked inside ~/.pi/agent and ~/.agents/skills${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 @@ -1014,6 +1058,188 @@ if [ $LINK_ERRORS -gt 0 ]; then fi echo +# Test 14: install_agent_skills restores missing skills from the lockfile +echo -e "${BLUE}→ Checking agent skill installation...${NC}" +SKILLS_ERRORS=0 + +LINK_LINE=$(grep -n '^ link_dotfiles$' install.sh | cut -d: -f1) +SKILLS_INSTALL_LINE=$(grep -n '^ install_agent_skills$' install.sh | cut -d: -f1) +if [ -n "$LINK_LINE" ] && [ -n "$SKILLS_INSTALL_LINE" ] && [ "$LINK_LINE" -lt "$SKILLS_INSTALL_LINE" ]; then + echo -e "${GREEN}✓ agent skills install after dotfiles are linked${NC}" +else + echo -e "${RED}✗ install_agent_skills must run after link_dotfiles in main${NC}" + SKILLS_ERRORS=$((SKILLS_ERRORS + 1)) +fi + +if command_exists jq; then + SKILLS_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/agent-skills-test.XXXXXX")" + SKILLS_JQ_DIR="$(dirname "$(command -v jq)")" + + skills_test14_prev_exit_trap="$(trap -p EXIT)" + skills_test14_prev_int_trap="$(trap -p INT)" + skills_test14_prev_term_trap="$(trap -p TERM)" + skills_test14_cleanup() { + rm -rf "$SKILLS_TEST_DIR" + } + trap skills_test14_cleanup EXIT INT TERM + + # Stub npx: logs every invocation ("$*") and fails for one scripted source, + # so tests never reach the network or the real skills directory + mkdir -p "$SKILLS_TEST_DIR/bin" + cat > "$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, $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" "$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 -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 + } + + 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 "$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 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 + + 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 + +# Test 15: install_ccstatusline installs the status line command once +echo -e "${BLUE}→ Checking ccstatusline installation...${NC}" +CCSTATUS_ERRORS=0 +CCSTATUS_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ccstatusline-test.XXXXXX")" +mkdir -p "$CCSTATUS_TEST_DIR/bin" +cat > "$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