Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ import petriplot as pp # For Petri-specific plotting helpers
- **Secrets are per-project**: API keys require `setup-envrc` in each project. Running `npm postinstall` or `pip install` in a project without `.envrc` cannot access secrets (this is intentional — supply chain defense). Legacy `.secrets` / `.env` files may still exist locally but are no longer the intended runtime path.
- **min-release-age quarantine**: All package managers have a 7-day delay on new releases. Packages published <7 days ago will fail to install. This is intentional. See `claude/rules/supply-chain-security.md` for override syntax
- **Pueue + systemd slices**: `j*` aliases require pueue + systemd user session. `systemd --user` doesn't work inside Claude Code sandbox (bubblewrap blocks D-Bus) — test from normal shell. Cgroup delegation may need one-time `sudo systemctl set-property user-$(id -u).slice Delegate=yes`. Config in `config/resources.conf` (edit when scaling machine).
- **CLI tool package strategy**: macOS uses Homebrew (ecosystem, GUI apps, libraries). Linux uses apt for baseline + mise `github:` backend for modern versions of fast-moving CLI tools (fzf, bat, eza, fd, ripgrep, delta, dust, zoxide, jless, just, sd, duf, gum, vivid). apt packages are often years behind upstream; mise downloads release binaries from GitHub with version tracking (`mise upgrade --all`). Homebrew on Linux was rejected (too heavy, installs own gcc/glibc). See `PACKAGES_CORE` (apt/brew), `PACKAGES_MACOS` (brew), `PACKAGES_LINUX_MISE` (mise) in `config.sh`. **Node.js is the exception** — it's a global runtime (NodeSource `setup_lts.x` on Linux, brew on macOS), installed via `install_node` in `scripts/shared/helpers.sh`, NOT mise. Reason: tools shebang against `node` (e.g. obsidian-headless's `ob` → `#!/usr/bin/env node`) and systemd/cron can't see mise's shell-activated shims. Tracks the **current LTS line** (never an odd "Current" release); the skip-guard floor is the live latest-LTS major fetched from nodejs.org (never EOL — older node is converged up), so re-running `install.sh` after an LTS rollover force-upgrades the major and native modules must be rebuilt. `install_node` deliberately does NOT gate the `apt-get install` on the NodeSource script's exit code (it can exit non-zero after writing the repo, which once left a box on stock Ubuntu node 18). bun stays the package manager/runtime for JS/TS; it is NOT a Node version manager and can't run direct-V8 native modules like better-sqlite3
- **CLI tool package strategy**: macOS uses Homebrew (ecosystem, GUI apps, libraries). Linux uses apt for baseline + mise `github:` backend for modern versions of fast-moving CLI tools (fzf, bat, eza, fd, ripgrep, delta, dust, zoxide, jless, just, sd, duf, gum, vivid). apt packages are often years behind upstream; mise downloads release binaries from GitHub with version tracking (`mise upgrade --all`). Homebrew on Linux was rejected (too heavy, installs own gcc/glibc). See `PACKAGES_CORE` (apt/brew), `PACKAGES_MACOS` (brew), `PACKAGES_LINUX_MISE` (mise) in `config.sh`. **Node.js is the exception** — it's a global runtime (NodeSource `setup_lts.x` on Linux, brew on macOS), installed via `install_node` in `scripts/shared/helpers.sh`, NOT mise. Reason: tools shebang against `node` (e.g. obsidian-headless's `ob` → `#!/usr/bin/env node`) and systemd/cron can't see mise's shell-activated shims. Tracks the **current LTS line** (never an odd "Current" release); the skip-guard floor is the live latest-LTS major fetched from nodejs.org (never EOL — older node is converged up), so re-running `install.sh` after an LTS rollover force-upgrades the major and native modules must be rebuilt. `install_node` deliberately does NOT gate the `apt-get install` on the NodeSource script's exit code (it can exit non-zero after writing the repo, which once left a box on stock Ubuntu node 18). Its skip-guard checks the **system node** (`/usr/bin/node`, brew paths) via `system_node_path()`, never `node` from PATH — a mise-installed node shadows a stale apt node in interactive shells and would mask the guard forever; `install_node` also evicts any mise-managed node (`evict_mise_node`) once the system node is healthy. bun stays the package manager/runtime for JS/TS; it is NOT a Node version manager and can't run direct-V8 native modules like better-sqlite3. **One home per JS-ecosystem CLI on Linux: bun global (`~/.bun/bin`)** — `update-ai-tools` updates via `bun add -g` and removes `~/.npm-global/bin` duplicates that shadow the bun copy (`~/.npm-global` remains only as the sudo-free `NPM_CONFIG_PREFIX` home for socket-cli); root-owned strays in `/usr/local/bin` are flagged for manual removal
- **Rust + bash dual implementations**: Some tools have a Rust version (for speed) and a bash fallback. Keep both in sync. Rust source lives in `tools/claude-tools/src/`, bash in `claude/`. Recompile with `cd tools/claude-tools && cargo build --release` then `cp target/release/claude-tools ../../custom_bins/`. Current dual-impl tools: statusline (`statusline.rs` + `claude/statusline.sh`), usage (`usage.rs` + inline in `statusline.sh`)
- **`mas 7.0.0` requires sudo for every install** (`mas install`, `mas get`, `mas purchase`). `mas` self-escalates (calls `sudo` internally). `install.sh --apps` pre-warms sudo with `sudo -v` (interactive TTY only) and keeps it alive with a background heartbeat for the duration of `brew bundle`, so a single password entry covers all mas apps. `mas account` was removed in 7.0.0 — no CLI way to confirm the signed-in store account (App Store UI only). iCloud account and Media & Purchases (store) account can differ; `mas` only cares about the store account.

Expand All @@ -409,3 +409,4 @@ import petriplot as pp # For Petri-specific plotting helpers
- rust-skills plugin removed (2026-05-26). UserPromptSubmit matcher was hyper-broad ("error", "async", "API", "implement", "explain", "how to" — injected ~100 lines on most prompts). Neuter-via-SessionStart-hook didn't hold (still fired same session) and mutates a tracked file in the marketplace clone, blocking future `git pull`. Re-add if Rust work picks up
- mas `install` vs `get`: `brew bundle` drives App Store installs via `mas install` (re-download only — fails "Redownload Unavailable" on a new machine even for owned apps). `mas get` (= `mas purchase`) is acquire+install and actually works. Fixed by `custom_bins/mas-get`, called before `brew bundle` in install.sh. Ref: github.com/Homebrew/brew/issues/21559 (2026-06-20)
- Hourly checks: `usage-ping` warms the subscription 5-hour window (must run with `ANTHROPIC_API_KEY` unset so it uses OAuth, not the metered API — tested and confirmed working 2026-06-21); `tmux-resume` auto-resumes rate-limited tmux Claude/Codex panes. Parser bug caught: `IFS='|' read` splits on `|` inside ERE patterns, garbling the send sequence — fixed to use ` | ` (space-pipe-space) as field delimiter. Also: deploy `--only=usage-ping,tmux-resume` may race on Linux crontab since both run in parallel; install each setup script sequentially for guaranteed cron entries. (2026-06-21)
- node/JS-CLI drift audit on the Linux box: `npm` resolved 4 ways (socket alias → mise node 24 → `/usr/bin/npm` 9.2 distro deb → usr-merge dup) and `codex` had 4 installs (`~/.npm-global` 0.144.5 shadowing the bun copy 0.144.1 the daily updater maintained, plus native standalone and a stale root-owned `/usr/local/bin` one). Root causes: `install_node`'s guard read `node -v` from PATH so mise's node 24 masked the stock Ubuntu node 18 (NodeSource repo configured, candidate 24.18.0 never installed), and `update-ai-tools` updated the bun copy without checking PATH shadowing. Fixed: guard now uses `system_node_path()`, `evict_mise_node` converges mise, `dedupe_bun_shadows` removes npm-global duplicates. bun's `bun add -g X@latest` also lags ~7 days behind npm by design (bunfig `minimumReleaseAge` quarantine) — a version gap between bun and npm copies is not a bug (2026-07-19)
31 changes: 30 additions & 1 deletion custom_bins/update-ai-tools
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,31 @@ trap release_lock EXIT

# ─── Core Update Logic ──────────────────────────────────────────────────────

# A bun-managed CLI can be shadowed by an npm -g copy: ~/.npm-global/bin
# precedes ~/.bun/bin on the interactive PATH, so `bun add -g` updates a binary
# the shell never resolves and the daily update silently has no effect (found
# 2026-07-19: codex 0.144.5 in npm-global shadowing the bun copy the updater
# maintained). One home per JS CLI: bun global. Remove user-owned npm
# duplicates; root-owned ones only get flagged (need sudo).
dedupe_bun_shadows() {
local tool_name="$1" package_name="$2"
local npm_copy="$HOME/.npm-global/bin/$tool_name"
if [[ -e "$npm_copy" ]]; then
if [[ "$DRY_RUN" == "true" ]]; then
log "[DRY RUN] Would remove npm-global duplicate: $npm_copy"
elif command -v npm &>/dev/null; then
log "Removing npm-global duplicate of $tool_name (it shadows the bun-managed copy)"
NPM_CONFIG_PREFIX="$HOME/.npm-global" npm uninstall -g "$package_name" 2>&1 \
|| log_err "failed to remove npm-global duplicate at $npm_copy"
else
log_err "npm-global duplicate at $npm_copy shadows the bun copy — remove it manually"
fi
fi
if [[ -e "/usr/local/bin/$tool_name" ]]; then
log_err "root-owned duplicate at /usr/local/bin/$tool_name — remove manually: sudo rm /usr/local/bin/$tool_name"
fi
}

# Update a tool using platform-appropriate manager (brew on macOS, bun on Linux)
# Usage: update_tool <command_name> <brew_package> <package_name>
update_tool() {
Expand All @@ -106,7 +131,10 @@ update_tool() {
if [[ "$DRY_RUN" == "true" ]]; then
case "$method" in
brew) log "[DRY RUN] Would run: brew upgrade $brew_name" ;;
bun) log "[DRY RUN] Would run: bun add -g ${package_name}@latest" ;;
bun)
log "[DRY RUN] Would run: bun add -g ${package_name}@latest"
dedupe_bun_shadows "$tool_name" "$package_name"
;;
*) log "[DRY RUN] $tool_name: no known update method" ;;
esac
return 0
Expand All @@ -122,6 +150,7 @@ update_tool() {
bun)
bun add -g "${package_name}@latest" 2>&1 \
|| log_err "$tool_name bun update failed"
dedupe_bun_shadows "$tool_name" "$package_name"
;;
*)
log_err "$tool_name installed but no known update method (expected brew or bun)"
Expand Down
36 changes: 35 additions & 1 deletion scripts/shared/helpers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -893,19 +893,52 @@ install_gh_from_release() {
# fails the guard and gets converged up to current LTS). Consequence: after an
# LTS rollover, re-running install.sh force-upgrades the major (e.g. 24→26) — so
# native modules (better-sqlite3) must be rebuilt against the new ABI afterward.
# Path of the SYSTEM node — the one cron/systemd/shebang contexts resolve.
# Deliberately ignores PATH: a mise-installed node shadows a stale apt node in
# interactive shells and would make version guards pass forever (found
# 2026-07-19: mise node 24 masking Ubuntu's stock node 18, with the NodeSource
# repo configured but its candidate never installed).
system_node_path() {
local cand
for cand in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
[[ -x "$cand" ]] && { echo "$cand"; return 0; }
done
return 1
}

# Node must never be mise-managed: mise's shims are invisible to cron/systemd,
# and its node shadows the system node (see system_node_path). Evict only once
# a healthy system node exists so we never delete the box's only modern node.
evict_mise_node() {
local want="$1"
cmd_exists mise || return 0
mise ls node 2>/dev/null | grep -q . || return 0
local sys_node
if ! sys_node=$(system_node_path) || (( $("$sys_node" -v | cut -d. -f1 | tr -d 'v') < want )); then
log_warning "mise-managed node present but system node is missing/stale — keeping mise node for now; re-run install.sh after the system node is fixed"
return 0
fi
log_warning "Evicting node from mise (node is a global runtime: NodeSource/brew, never mise)"
mise unuse -g node 2>/dev/null || true
mise uninstall --all node 2>/dev/null || true
}

install_node() {
# Current LTS major from nodejs.org; dist index is newest-first and r['lts']
# is the codename (truthy) for LTS releases, false otherwise.
local want
want=$(curl -fsSL https://nodejs.org/dist/index.json 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(next(r['version'] for r in d if r['lts'])[1:].split('.')[0])" 2>/dev/null)
[[ "$want" =~ ^[0-9]+$ ]] || want=24
if is_installed node && (( $(node -v | cut -d. -f1 | tr -d 'v') >= want )); then
local sys_node
if sys_node=$(system_node_path) && (( $("$sys_node" -v | cut -d. -f1 | tr -d 'v') >= want )); then
evict_mise_node "$want"
return 0
fi
log_info "Installing Node ${want} LTS..."
if is_macos; then
brew_install node
evict_mise_node "$want"
return 0
fi
# Linux: NodeSource setup_lts.x adds the repo + runs apt update. Its script
Expand All @@ -917,6 +950,7 @@ install_node() {
curl -fsSL https://deb.nodesource.com/setup_lts.x | $SUDO -E bash - \
|| log_warning "NodeSource setup script exited non-zero (repo may still be configured) — continuing"
$SUDO apt-get install -y nodejs || log_warning "Node install via apt failed — install Node LTS manually"
evict_mise_node "$want"
}

# ─── Mise (Universal Version Manager) ─────────────────────────────────────────
Expand Down