diff --git a/CLAUDE.md b/CLAUDE.md index 69878308..22b59e62 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,7 @@ If you're an AI agent (Claude Code, Codex, etc.) working in this repo, read this | Add a new alias | `config/aliases.sh` (or `aliases_.sh` for env-specific) | | Add a deploy component | Create `deploy_X()` in `deploy.sh` — see [Adding New Features](#adding-new-features) | | Add a custom binary | Drop it in `custom_bins/` (already on PATH); `chmod +x` | +| Install/manage Mac apps | Add a line to `config/apps.conf` → run `app-picker` (gum TUI) → `brew bundle --file=config/Brewfile`. Official casks + `mas` only, **no third-party taps**. Then `scripts/setup/auth-setup` | | Add an encrypted secret | `secrets-edit` (interactive dotenv editor) | | Run an experiment with resource caps | `jexp uv run python -m ...` (Linux: needs pueue + systemd user session) | | Commit / commit + push + PR | `/commit` skill or `/commit-push-sync` | diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index dc293dc8..00000000 --- a/GEMINI.md +++ /dev/null @@ -1,6 +0,0 @@ -# GEMINI.md - -This project uses [CLAUDE.md](CLAUDE.md) as the single source of truth for project-specific guidelines, architecture, and workflows. -Please refer to that file for all development patterns, conventions, and command usages. - -For global Gemini agent guidelines and AI safety protocols, refer to [gemini/GEMINI.md](gemini/GEMINI.md). diff --git a/README.md b/README.md index 4bc990e4..bbde43fd 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ For cloud environments (RunPod, Hetzner, Lambda Labs, etc): - [AI Assistants](#ai-assistants) - [Claude Code](#claude-code-primary-ai-assistant) - [Codex CLI](#codex-cli-openai) - - [Gemini CLI](#gemini-cli-google) + - [Antigravity CLI + OpenCode](#antigravity-cli-google--opencode) - [Terminal & Shell](#terminal--shell) - [Ghostty](#ghostty-terminal-emulator) - [Powerlevel10k Prompt](#powerlevel10k-prompt) @@ -273,22 +273,22 @@ claude-tools context --list # Show active plugins and available prof The configuration follows the same research discipline as Claude Code but adapted for Codex's execution model. -### Gemini CLI (Google) +### Antigravity CLI (Google) + OpenCode -[Gemini CLI](https://github.com/google-gemini/gemini-cli) can sync with Claude Code configurations: +Gemini CLI was retired by Google on **2026-06-18**; [Antigravity CLI](https://antigravity.google/docs/cli-features) (`agy`) is its official successor. [OpenCode](https://opencode.ai) is installed alongside as a model-agnostic OSS option. Both are installed by the `ai-tools` component. + +Antigravity CLI can sync with Claude Code skills: ```bash -./scripts/sync_claude_to_gemini.sh # Syncs skills/agents/permissions +./scripts/sync_claude_to_antigravity.sh # Symlinks Claude skills into agy ``` **What it does:** -- Symlinks Claude Code skills to `~/.gemini/skills/` -- Converts Claude agents to Gemini skill format -- Syncs permissions from `.claude/settings.json` to Gemini policies -- Creates `GEMINI.md` pointer to CLAUDE.md +- Symlinks Claude Code skills to `~/.gemini/antigravity-cli/skills/` +- Project instructions come from `AGENTS.md` (Antigravity reads it natively) -**Note:** Gemini CLI uses a different skills format. The sync script adapts Claude's configuration but some features may not translate directly. +**Note:** Antigravity CLI is closed-source and brand-new; its skills/permissions schema differs from Claude's. The skills sync is adapted but untested end-to-end — permission sync is not yet ported (see the script header). ## Terminal & Shell diff --git a/claude/hooks/auto_commit_worker.sh b/claude/hooks/auto_commit_worker.sh index eaae5c28..de3ae9b8 100755 --- a/claude/hooks/auto_commit_worker.sh +++ b/claude/hooks/auto_commit_worker.sh @@ -63,7 +63,7 @@ fi : "${AUTO_AGENT_APPROVAL_FILE:=$HOME/.claude/flags/auto-agent-approved-until}" : "${AUTO_AGENT_STATE_DIR:=$HOME/.claude/state}" : "${AUTO_AGENT_LOG_DIR:=$HOME/.claude/logs/auto-commit}" -: "${AUTO_COMMIT_BACKEND_ORDER:=codex,gemini}" +: "${AUTO_COMMIT_BACKEND_ORDER:=codex,opencode}" : "${AUTO_COMMIT_ENABLE_CLAUDE_FALLBACK:=0}" : "${AUTO_COMMIT_DRY_RUN:=0}" : "${AUTO_AGENT_EXCLUDE_REGEX:=^\\.claude/worktrees/}" @@ -246,10 +246,9 @@ run_backend() { command -v codex >/dev/null 2>&1 || return 1 run_with_timeout 240 codex -a never -s workspace-write exec --cd "$REPO_ROOT" --skip-git-repo-check "$prompt" >> "$log_file" 2>&1 ;; - gemini) - command -v gemini >/dev/null 2>&1 || return 1 - run_with_timeout 240 gemini \ - -p "$prompt" --approval-mode yolo --output-format text >> "$log_file" 2>&1 + opencode) + command -v opencode >/dev/null 2>&1 || return 1 + run_with_timeout 240 opencode run "$prompt" >> "$log_file" 2>&1 ;; claude) [[ "${AUTO_COMMIT_ENABLE_CLAUDE_FALLBACK:-0}" == "1" ]] || return 1 diff --git a/claude/rules/supply-chain-security.md b/claude/rules/supply-chain-security.md index 2378d320..587ca02e 100644 --- a/claude/rules/supply-chain-security.md +++ b/claude/rules/supply-chain-security.md @@ -50,6 +50,50 @@ All package managers are configured with a **7-day quarantine** (`min-release-ag - Skip hash verification for production Python dependencies - Bypass min-release-age quarantine without explicit user approval +## Tool Selection: Security Floor, then Adoption (two-gate) + +When choosing between tools, apply two gates in order: + +1. **Hard security floor (non-negotiable):** official core formula / cask / Mac App Store + only; **no third-party taps** without approval; notarization + quarantine on for casks; + `min-release-age` for language packages. A tool that fails the floor is out regardless + of popularity. +2. **Among options that clear the floor, prefer the more *modern / adopted* one** — GitHub + stars, monthly actives, release cadence, and HN/Reddit consensus. Higher adoption is + *also* a security positive (more eyes → faster CVE discovery), so this complements the + floor rather than fighting it. Don't default to a stale "boring" tool when a + well-adopted modern one clears the same floor. + +**Residual-risk case:** a tool that is *young AND single-maintainer AND not-yet-widely- +adopted* (e.g. FineTune). High stars only partially offset bus-factor risk — such tools may +be *added* but ship **default-OFF** (conscious opt-in), never auto-on. + +## GUI Apps & Brewfile (macOS) + +Apps live in `config/apps.conf` (registry) → `config/Brewfile` (generated by `app-picker`). + +- **Homebrew official casks + Mac App Store (`mas`) ONLY.** NEVER add a third-party tap + to `apps.conf`, install.sh, or a Brewfile without explicit user approval. +- **Prefer `mas`** (sandboxed, Apple-reviewed) when an app ships full-featured on the + App Store — highest trust tier ("MAS-first"). Use a cask when MAS is crippled/absent. +- Before adding any app: run `brew info ` / `mas info `, verify vendor + homepage. +- **Never `--no-quarantine`.** Gatekeeper + notarization must stay enabled; that's the + defense against malicious casks (brew also verifies a pinned sha256 on download). +- Tier in `apps.conf`: 1 = official vendor auto-approve · 2 = mature OSS (review) · + 3 = explicit approval (ships `default=false`). + +## curl|bash Installers + +Official-page `curl … | sh` gives **authenticity** (HTTPS proves the domain) but NOT +**integrity** (runs whatever's live, unpinned, unreviewed). Prefer, best→worst: +1. Official Homebrew **formula** if one exists (`uv`, `rustup-init`, `bun`) — vendor's + artifact + sha pin + reviewed PR + reproducible. +2. No formula → `curl -o` a versioned URL and **verify the vendor checksum/signature**. +3. Blind `curl … | sh` only as last resort, HTTPS-to-official-domain only. + +Eyeballing the script ("glance at it") is a smell test for gross tampering, NOT an +integrity control — don't treat it as a safeguard. + ## Secrets Awareness - API keys are scoped per-project via direnv `.envrc`, NOT globally exported diff --git a/config.sh b/config.sh index 1de19cc0..96ead572 100644 --- a/config.sh +++ b/config.sh @@ -33,12 +33,12 @@ INSTALL_REGISTRY=( "core|Core packages, CLI tools, gh, SOPS/age, uv|all|true" "zsh|ZSH + oh-my-zsh + powerlevel10k theme|all|true" "tmux|Terminal multiplexer|all|true" - "ai-tools|Claude Code, Gemini CLI, Codex CLI|all|true" + "ai-tools|Claude Code, Codex CLI, OpenCode, Antigravity CLI|all|true" "extras|hyperfine, gitui, code2prompt, terminal-notifier|all|true" "cleanup|Automatic cleanup (macOS only)|all|true" - "experimental|ty type checker, zerobrew|all|true" + "experimental|ty type checker, zotero MCP|all|true" "macos-settings|macOS system defaults (Dock, Finder, keyboard)|macos|true" - "finicky|Finicky browser routing|macos|true" + "apps|GUI + App Store apps via Brewfile (picker TUI)|macos|true" "docker|Docker engine + compose|linux|true" "pueue|Pueue job scheduler + pueued daemon|linux|true" "create-user|Create non-root dev user|linux|true" @@ -64,7 +64,7 @@ DEPLOY_REGISTRY=( "dep-audit|Weekly dependency audit (supply chain defense)|all|true" "cleanup|Auto-cleanup Downloads/Screenshots (macOS)|all|true" "claude-cleanup|Remove idle Claude sessions after 24h|all|true" - "ai-update|Daily auto-update: Claude, Gemini, Codex|all|true" + "ai-update|Daily auto-update: Claude, Codex, OpenCode|all|true" "mcp-sync|Daily shared MCP sync for Claude and Codex|all|true" "brew-update|Weekly package upgrade + cleanup|all|true" "claude-tools|Build claude-tools Rust binary|all|true" @@ -229,7 +229,7 @@ apply_profile() { INSTALL_DOCKER=false INSTALL_EXTRAS=false INSTALL_MACOS_SETTINGS=false - INSTALL_FINICKY=false + INSTALL_APPS=false DEPLOY_EDITOR=false DEPLOY_SERENA=false DEPLOY_GHOSTTY=false diff --git a/config/Brewfile b/config/Brewfile new file mode 100644 index 00000000..a7c7af79 --- /dev/null +++ b/config/Brewfile @@ -0,0 +1,43 @@ +# Brewfile — GENERATED by app-picker from config/apps.conf. Do not edit by hand. +# Regenerate: app-picker (or app-picker --defaults) +# Install: brew bundle --file=config/Brewfile +# Policy: official casks + Mac App Store only; never --no-quarantine. + +# mas-cli drives Mac App Store installs (must be signed into the App Store). +brew "mas" + +# ── Casks (official Homebrew casks) ── +cask "aldente" +cask "alfred" +cask "antigravity" +cask "appcleaner" +cask "beardedspice" +cask "chatgpt" +cask "claude" +cask "cleanshot" +cask "cursor" +cask "dropbox" +cask "finicky" +cask "ghostty" +cask "granola" +cask "keyboardcleantool" +cask "mouseless" +cask "nordvpn" +cask "notion" +cask "popclip" +cask "readdle-spark" +cask "slack" +cask "spotify" +cask "stats" +cask "super-productivity" +cask "tailscale-app" +cask "voiceink" +cask "zed" + +# ── Mac App Store (sandboxed, Apple-reviewed) ── +mas "2FAS Auth Browser Extension", id: 6443941139 +mas "Bear", id: 1091189122 +mas "Bitwarden", id: 1352778147 +mas "Things 3", id: 904280696 +mas "Userscripts", id: 1463298887 +mas "uBlock Origin Lite", id: 6745342698 diff --git a/config/ai_automation.sh b/config/ai_automation.sh index cfcbc4df..26178266 100755 --- a/config/ai_automation.sh +++ b/config/ai_automation.sh @@ -35,7 +35,7 @@ # Auto-commit policy # Keep Claude fallback opt-in because it is usually the most expensive backend. -: "${AUTO_COMMIT_BACKEND_ORDER:=codex,gemini}" +: "${AUTO_COMMIT_BACKEND_ORDER:=codex,opencode}" : "${AUTO_COMMIT_ENABLE_CLAUDE_FALLBACK:=0}" : "${AUTO_COMMIT_DRY_RUN:=0}" : "${AUTO_COMMIT_USE_ASYNC:=1}" diff --git a/config/aliases.sh b/config/aliases.sh index 29f8ee91..f72a19f5 100644 --- a/config/aliases.sh +++ b/config/aliases.sh @@ -1215,7 +1215,7 @@ fi # AI CLI Tools # ------------------------------------------------------------------- # Health check for all AI CLI tools -alias ai-check='echo "Checking AI CLI tools..." && claude --version 2>/dev/null && gemini --version 2>/dev/null && codex --version 2>/dev/null' +alias ai-check='echo "Checking AI CLI tools..." && claude --version 2>/dev/null && codex --version 2>/dev/null && opencode --version 2>/dev/null' # Log sandbox denials for a command (macOS/Linux) codex-denials() { @@ -1322,21 +1322,6 @@ alias ai-update='update-ai-tools' # Detects: brew (macOS), apt/dnf/pacman (Linux) alias pkg-update='update-packages' -# zerobrew: faster Homebrew client (use zb for interactive installs, brew for scripts) -# `zb install` falls back to `brew install` on failure (zerobrew doesn't handle casks) -if command -v zb &>/dev/null; then - zb() { - if [[ "$1" == "install" ]]; then - shift - command zb install "$@" || { echo "→ zb failed, falling back to brew install" >&2; brew install "$@"; } - else - command zb "$@" - fi - } - alias zbi='zb install' - alias zbu='zb uninstall' -fi - # Auto-agent guard controls alias auto-guard='auto-agent-guardctl status' alias auto-approve='auto-agent-guardctl approve' diff --git a/config/apps.conf b/config/apps.conf new file mode 100644 index 00000000..3d29784b --- /dev/null +++ b/config/apps.conf @@ -0,0 +1,115 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# App Registry — single source of truth for GUI apps + App Store apps +# ═══════════════════════════════════════════════════════════════════════════════ +# Consumed by: custom_bins/app-picker (gum toggle TUI → generates config/Brewfile) +# Installed by: brew bundle --file=config/Brewfile (install.sh --apps) +# +# Format (pipe-delimited, one app per line): +# method | id | category | tier | default | name | description | auth +# +# method brew | cask | mas +# id formula name (brew) / cask token (cask) / numeric App Store id (mas) +# category text tasks editor llm cli meetings cloud search messaging +# productivity time voice audio vpn auth safari music misc antivirus security +# tier 1 = official-vendor auto-approve · 2 = mature OSS (review) · 3 = explicit approval +# (tier-3 ships default=false; toggle on deliberately) +# default true | false — initial toggle state in the picker +# name display name; for `mas` this is the exact App Store title in the Brewfile +# auth post-install setup the auth-setup checklist surfaces: +# none login apikey pair-phone safari-ext license alfred things-cloud +# +# SECURITY POLICY (see claude/rules/supply-chain-security.md): +# - Homebrew official casks + Mac App Store only. NEVER add a third-party tap here. +# - Prefer `mas` (sandboxed, Apple-reviewed) when an app ships full-featured on the +# App Store — that's the highest trust tier ("MAS-first"). +# - Run `brew info ` / `mas info ` and verify vendor+homepage before adding. +# - Never use --no-quarantine; Gatekeeper/notarization must stay on. +# ═══════════════════════════════════════════════════════════════════════════════ + +# ─── text ─────────────────────────────────────────────────────────────────── +mas|1091189122|text|2|true|Bear|Markdown notes (App Store only; bearcli symlinked by deploy)|login +cask|notion|text|1|true|Notion|Notes, docs, and wiki|login + +# ─── task management ───────────────────────────────────────────────────────── +mas|904280696|tasks|2|true|Things 3|GTD task manager (App Store only)|things-cloud + +# ─── coding: editors ───────────────────────────────────────────────────────── +cask|cursor|editor|1|true|Cursor|AI-first code editor (config deployed)|login +cask|antigravity|editor|1|true|Antigravity|Google agentic IDE (config deployed)|login +cask|zed|editor|1|true|Zed|Fast collaborative editor (config deployed)|login + +# ─── coding: LLM desktop apps ──────────────────────────────────────────────── +cask|chatgpt|llm|1|true|ChatGPT|OpenAI desktop app|login +cask|claude|llm|1|true|Claude|Anthropic desktop app|login + +# ─── coding: terminal ──────────────────────────────────────────────────────── +cask|ghostty|cli|1|true|Ghostty|GPU-accelerated terminal (config deployed)|none + +# ─── meetings ──────────────────────────────────────────────────────────────── +cask|granola|meetings|2|true|Granola|AI meeting notes|login + +# ─── cloud storage ─────────────────────────────────────────────────────────── +cask|dropbox|cloud|1|true|Dropbox|File sync and storage|login +cask|google-drive|cloud|1|false|Google Drive|Google cloud storage (optional)|login + +# ─── search / launcher ─────────────────────────────────────────────────────── +cask|alfred|search|2|true|Alfred|Launcher + workflows (prefs sync via Dropbox)|alfred + +# ─── messaging ─────────────────────────────────────────────────────────────── +cask|slack|messaging|1|true|Slack|Team messaging|login +cask|readdle-spark|messaging|2|true|Spark|Email client (MAS build also full-featured)|login + +# ─── productivity ──────────────────────────────────────────────────────────── +cask|mouseless|productivity|2|true|Mouseless|Keyboard-driven mouse control (config deployed)|none +cask|popclip|productivity|2|true|PopClip|Text-selection actions (cask not MAS — MAS abandoned)|license + +# ─── time tracking ─────────────────────────────────────────────────────────── +cask|super-productivity|time|2|true|Super Productivity|Task + time tracker|none +brew|wakatime-cli|time|2|false|WakaTime CLI|Coding time tracker (API key via secrets)|apikey + +# ─── voice ─────────────────────────────────────────────────────────────────── +cask|voiceink|voice|2|true|VoiceInk|Local voice-to-text (downloads model on first run)|none + +# ─── audio device management ───────────────────────────────────────────────── +# FineTune: per-app volume + multi-device output/routing + EQ (free OSS SoundSource alt). +# Default OFF: newer + single-maintainer project — adoption is high but bus-factor +# risk remains, so it's a conscious opt-in (see supply-chain-security.md modernity rule). +cask|finetune|audio|2|false|FineTune|Per-app volume + audio device routing + EQ (OSS)|none +# switchaudio-osx: CLI to switch the SYSTEM default input/output device. Core formula, +# OSS, no audio driver. Pairs with an Alfred keyword: SwitchAudioSource -t output -s "AirPods". +brew|switchaudio-osx|audio|2|false|SwitchAudioSource|CLI default input/output device switcher (Alfred-friendly)|none + +# ─── vpn ───────────────────────────────────────────────────────────────────── +cask|nordvpn|vpn|2|true|NordVPN|VPN client (vpn deploy configures split tunnel)|login +cask|tailscale-app|vpn|1|true|Tailscale|Mesh VPN (vpn deploy configures routing)|login + +# ─── auth / passwords / 2FA ────────────────────────────────────────────────── +# Bitwarden via MAS: the Safari extension ships only in the App Store build, which +# also covers the desktop app — so mas covers both. (cask `bitwarden` = desktop only.) +mas|1352778147|auth|1|true|Bitwarden|Password manager (App Store build incl. Safari ext)|login +mas|6443941139|auth|2|true|2FAS Auth Browser Extension|2FA browser extension (pairs with phone)|pair-phone + +# ─── safari extensions (install via mas; ENABLE manually in Safari settings) ── +mas|6745342698|safari|2|true|uBlock Origin Lite|Content blocker (enable in Safari)|safari-ext +mas|1463298887|safari|2|true|Userscripts|Userscript manager (enable in Safari)|safari-ext + +# ─── music ─────────────────────────────────────────────────────────────────── +cask|spotify|music|1|true|Spotify|Music streaming|login + +# ─── misc utilities ────────────────────────────────────────────────────────── +cask|aldente|misc|2|true|AlDente|Battery charge limiter|none +cask|finicky|misc|2|true|Finicky|Browser routing (config deployed)|none +cask|appcleaner|misc|2|true|AppCleaner|Thorough app uninstaller|none +cask|cleanshot|misc|1|true|CleanShot X|Screenshot + screen recording|license +cask|stats|misc|2|true|Stats|Menu-bar system monitor|none +cask|keyboardcleantool|misc|2|true|KeyboardCleanTool|Disable keyboard for cleaning|none +cask|beardedspice|misc|2|true|BeardedSpice|Media keys for web players|none + +# ─── antivirus ─────────────────────────────────────────────────────────────── +# Optional, default OFF. Trellix (university-managed) is NOT here — installed via uni. +# Do NOT run Malwarebytes real-time protection alongside Trellix (on-demand scan is fine). +cask|malwarebytes|antivirus|2|false|Malwarebytes|On-demand malware scanner (optional)|login + +# ─── security: runtime defense ─────────────────────────────────────────────── +# Optional, default OFF (prompts frequently). Objective-See outbound firewall. +cask|lulu|security|2|false|LuLu|Outbound firewall — catches apps phoning home|none diff --git a/config/experimental.yaml b/config/experimental.yaml index cbb2b97d..a1621291 100644 --- a/config/experimental.yaml +++ b/config/experimental.yaml @@ -28,11 +28,3 @@ settings_keys: [] reason: "Rust-based Python type checker, 10-60x faster than mypy/pyright. Alpha — fall back to pyright if gaps block." status: trial - -- name: zerobrew - added: 2026-04-01 - review_by: 2026-07-01 - installed_via: "curl https://zerobrew.rs/install | bash" - settings_keys: [] - reason: "Fast Rust-based Homebrew client. Same UX, faster." - status: trial diff --git a/config/login_items.conf b/config/login_items.conf new file mode 100644 index 00000000..baf22bb7 --- /dev/null +++ b/config/login_items.conf @@ -0,0 +1,31 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# login_items.conf — apps to register as macOS "Open at Login" items +# ═══════════════════════════════════════════════════════════════════════════════ +# Consumed by: scripts/setup/setup-login-items +# +# Why a separate list (not apps.conf): "launch at login" is orthogonal to install. +# apps.conf decides WHICH apps to install; this decides which menu-bar apps should +# auto-start so they're reliably present in the menu bar. +# +# Format (pipe-delimited, one app per line): +# name [| hidden] +# name exact app name in /Applications (without ".app"); must match the +# name macOS shows in System Settings → Login Items +# hidden true | false — start hidden (no window). Default true, which is +# right for menu-bar utilities. Set false for apps you want to open. +# +# ── How customising works (read this — it answers "do I have to edit dotfiles?") ─ +# NO. Day-to-day, just toggle apps in System Settings → General → Login Items. +# setup-login-items is BOOTSTRAP-ONCE + ADDITIVE: +# • it only ADDS a curated app if it's missing AND it hasn't added it before +# • it NEVER removes anything, and NEVER re-adds something you removed manually +# (it records what it added in ~/.config/dotfiles/login-items.bootstrapped) +# • it does NOT run on every deploy — you invoke it (or it's part of app setup) +# This list exists only to SEED a fresh machine's menu bar. Edit it only to change +# what a brand-new install should auto-add. +# ═══════════════════════════════════════════════════════════════════════════════ + +Stats +FineTune +Tailscale +NordVPN diff --git a/config/macos_settings.sh b/config/macos_settings.sh index e27e35eb..21432890 100755 --- a/config/macos_settings.sh +++ b/config/macos_settings.sh @@ -76,6 +76,15 @@ configure_mouse() { configure_dock() { echo " → Configuring Dock..." + # Auto-hide the Dock + defaults write com.apple.dock autohide -bool true 2>/dev/null || true + + # No delay before the Dock appears on hover (instant show) + defaults write com.apple.dock autohide-delay -float 0 2>/dev/null || true + + # Conservative icon size + defaults write com.apple.dock tilesize -int 48 2>/dev/null || true + # Hide recent apps section defaults write com.apple.dock show-recents -bool false 2>/dev/null || true diff --git a/custom_bins/app-picker b/custom_bins/app-picker new file mode 100755 index 00000000..a66d725b --- /dev/null +++ b/custom_bins/app-picker @@ -0,0 +1,135 @@ +#!/usr/bin/env zsh +# ═══════════════════════════════════════════════════════════════════════════════ +# app-picker — browse the app registry, toggle apps, generate a Brewfile +# ═══════════════════════════════════════════════════════════════════════════════ +# Reads: config/apps.conf (method|id|category|tier|default|name|description|auth) +# Writes: config/Brewfile (brew/cask/mas entries for `brew bundle`) +# +# Usage: +# app-picker # gum TUI: space=toggle, enter=confirm → writes Brewfile +# app-picker --defaults # no TUI; select registry defaults, write Brewfile +# app-picker --dry-run # print the Brewfile to stdout, don't write +# app-picker --conf PATH # registry path (default: /config/apps.conf) +# app-picker --file PATH # Brewfile output path (default: /config/Brewfile) +# +# Trust policy: registry is official casks + Mac App Store only (no third-party taps). +# ═══════════════════════════════════════════════════════════════════════════════ +set -euo pipefail + +# Resolve dotfiles dir (this script lives in custom_bins/) +SCRIPT_DIR="${0:A:h}" +DOT_DIR="${DOT_DIR:-${SCRIPT_DIR:h}}" + +CONF="$DOT_DIR/config/apps.conf" +BREWFILE="$DOT_DIR/config/Brewfile" +MODE="tui" # tui | defaults +DRY_RUN=false + +while [[ $# -gt 0 ]]; do + case "$1" in + --defaults) MODE="defaults"; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --conf) CONF="$2"; shift 2 ;; + --file) BREWFILE="$2"; shift 2 ;; + -h|--help) sed -n '2,18p' "$0"; exit 0 ;; + *) echo "app-picker: unknown arg '$1'" >&2; exit 2 ;; + esac +done + +[[ -f "$CONF" ]] || { echo "app-picker: registry not found: $CONF" >&2; exit 1; } + +# ─── Parse registry ────────────────────────────────────────────────────────── +# Arrays are index-aligned. zsh arrays are 1-based. +typeset -a A_METHOD A_ID A_CAT A_TIER A_DEFAULT A_NAME A_DESC A_AUTH +while IFS='|' read -r method id cat tier def name desc auth; do + [[ -z "${method// }" || "${method[1]}" == "#" ]] && continue + A_METHOD+=("${method// }"); A_ID+=("$id"); A_CAT+=("${cat// }") + A_TIER+=("${tier// }"); A_DEFAULT+=("${def// }") + A_NAME+=("$name"); A_DESC+=("$desc"); A_AUTH+=("${auth// }") +done < "$CONF" + +local n=${#A_METHOD} +(( n > 0 )) || { echo "app-picker: no apps parsed from $CONF" >&2; exit 1; } + +# ─── Build display rows (one per app) + default-selected set ────────────────── +# Row format the TUI shows / we parse back: "\t[cat·Tn] name — desc" +typeset -a rows selected +local i +for (( i=1; i<=n; i++ )); do + local label="${i} [${A_CAT[i]}·T${A_TIER[i]}] ${A_NAME[i]} — ${A_DESC[i]}" + rows+=("$label") + [[ "${A_DEFAULT[i]}" == "true" ]] && selected+=("$label") +done + +# ─── Choose: TUI (gum) or defaults ─────────────────────────────────────────── +# Result is the chosen subset of $rows, newline-separated. +local chosen="" +if [[ "$MODE" == "defaults" ]] || [[ "${NON_INTERACTIVE:-false}" == "true" ]] \ + || ! [[ -t 0 ]] || ! command -v gum &>/dev/null; then + chosen="${(F)selected}" +else + local sel_csv="${(j:,:)selected}" + local height=$(( n + 2 )) + local term_h; term_h=$(tput lines 2>/dev/null || echo 40) + (( height > term_h - 4 )) && height=$(( term_h - 4 )) + chosen=$(gum choose --no-limit --height "$height" \ + --header "Select apps to install (space=toggle, enter=confirm):" \ + --selected "$sel_csv" -- "$rows[@]") || { echo "Cancelled — Brewfile unchanged." >&2; exit 0; } +fi + +# ─── Map chosen rows back to indices ───────────────────────────────────────── +typeset -a pick_idx +local line +while IFS= read -r line; do + [[ -z "$line" ]] && continue + pick_idx+=("${line%% *}") # leading field before the tab = index +done <<< "$chosen" + +(( ${#pick_idx} > 0 )) || { echo "Nothing selected — Brewfile unchanged." >&2; exit 0; } + +# ─── Generate Brewfile ─────────────────────────────────────────────────────── +emit_brewfile() { + local has_mas=false idx + for idx in "${pick_idx[@]}"; do + [[ "${A_METHOD[idx]}" == "mas" ]] && { has_mas=true; break; } + done + + print -r -- "# Brewfile — GENERATED by app-picker from config/apps.conf. Do not edit by hand." + print -r -- "# Regenerate: app-picker (or app-picker --defaults)" + print -r -- "# Install: brew bundle --file=config/Brewfile" + print -r -- "# Policy: official casks + Mac App Store only; never --no-quarantine." + print -r -- "" + + if [[ "$has_mas" == "true" ]]; then + print -r -- "# mas-cli drives Mac App Store installs (must be signed into the App Store)." + print -r -- 'brew "mas"' + print -r -- "" + fi + + # brew formulae + local out + out=$(for idx in "${pick_idx[@]}"; do + if [[ "${A_METHOD[idx]}" == "brew" ]]; then print -r -- "brew \"${A_ID[idx]}\""; fi + done | sort) + [[ -n "$out" ]] && { print -r -- "# ── CLI formulae ──"; print -r -- "$out"; print -r -- ""; } + + # casks + out=$(for idx in "${pick_idx[@]}"; do + if [[ "${A_METHOD[idx]}" == "cask" ]]; then print -r -- "cask \"${A_ID[idx]}\""; fi + done | sort) + [[ -n "$out" ]] && { print -r -- "# ── Casks (official Homebrew casks) ──"; print -r -- "$out"; print -r -- ""; } + + # mas + out=$(for idx in "${pick_idx[@]}"; do + if [[ "${A_METHOD[idx]}" == "mas" ]]; then print -r -- "mas \"${A_NAME[idx]}\", id: ${A_ID[idx]}"; fi + done | sort) + [[ -n "$out" ]] && { print -r -- "# ── Mac App Store (sandboxed, Apple-reviewed) ──"; print -r -- "$out"; } +} + +if [[ "$DRY_RUN" == "true" ]]; then + emit_brewfile +else + emit_brewfile > "$BREWFILE" + echo "✓ Wrote ${#pick_idx} apps to $BREWFILE" + echo " Install with: brew bundle --file=$BREWFILE" +fi diff --git a/custom_bins/update-ai-tools b/custom_bins/update-ai-tools index 840ed30e..65c360c2 100755 --- a/custom_bins/update-ai-tools +++ b/custom_bins/update-ai-tools @@ -2,7 +2,7 @@ # ═══════════════════════════════════════════════════════════════════════════════ # AI CLI Tools Auto-Update # ═══════════════════════════════════════════════════════════════════════════════ -# Updates Claude Code, Gemini CLI, and Codex CLI using the correct method +# Updates Claude Code, Codex CLI, and OpenCode using the correct method # per tool (brew on macOS, bun on Linux). Designed to run from launchd/cron. # # Usage: @@ -37,7 +37,7 @@ while [[ $# -gt 0 ]]; do -h|--help) echo "Usage: update-ai-tools [--dry-run]" echo "" - echo "Updates Claude Code, Gemini CLI, and Codex CLI." + echo "Updates Claude Code, Antigravity CLI, Codex CLI, and OpenCode." echo "Uses brew on macOS and bun on Linux." echo "" echo "Options:" @@ -147,6 +147,34 @@ update_claude() { claude update 2>&1 || log_err "Claude Code update failed" } +# ─── Update Antigravity CLI ───────────────────────────────────────────────── + +# Antigravity CLI (`agy`, Google's Gemini CLI successor) is a macOS cask and a +# manual install on Linux (no bun/npm package) — so it can't use update_tool. +update_antigravity() { + if ! command -v agy &>/dev/null; then + log_skip "agy not installed, skipping" + return 0 + fi + + # No brew (i.e. Linux manual install) → nothing to auto-update. + if ! is_macos || ! command -v brew &>/dev/null; then + log_skip "agy: no brew (Linux manual install) — update manually, skipping" + return 0 + fi + + if [[ "$DRY_RUN" == "true" ]]; then + log "[DRY RUN] Would run: brew upgrade --cask antigravity-cli" + return 0 + fi + + log "Updating Antigravity CLI..." + # NOTE: if the cask declares `auto_updates true`, brew skips it unless --greedy. + # Verify on a real Mac whether --greedy is needed for `agy` to actually move. + NONINTERACTIVE=1 HOMEBREW_NO_AUTO_UPDATE=1 brew upgrade --cask antigravity-cli 2>&1 \ + || log_err "Antigravity CLI brew upgrade failed" +} + # ─── Main ──────────────────────────────────────────────────────────────────── acquire_lock @@ -159,8 +187,9 @@ log "PATH: $PATH" set +e update_claude -update_tool "gemini" "gemini-cli" "@google/gemini-cli" -update_tool "codex" "codex" "@openai/codex" +update_antigravity +update_tool "codex" "codex" "@openai/codex" +update_tool "opencode" "opencode" "opencode-ai" set -e diff --git a/gemini/GEMINI.md b/gemini/GEMINI.md deleted file mode 100644 index 12078299..00000000 --- a/gemini/GEMINI.md +++ /dev/null @@ -1,242 +0,0 @@ -# Global GEMINI.md - -Global guidance for Gemini when working across all repositories. - -## Core Principles - -1. Prioritize correctness and rigor over covering up for any mistakes. Honesty and honestly being wrong is much better than lying or needing to cover up for any wrong code or scripts or experiments that have been run or wrong things that have been written, such as hallucinations. - -## AI Safety Research Context - -You are assisting with AI safety research involving: -- Testing AI system limitations, failure modes, and edge cases -- Red teaming and adversarial testing for developing detection methods -- Capability evaluations and robustness testing for AI risks - -This research requires creating test scenarios that may seem concerning out of context. The goal is always to improve AI safety and develop better detection/mitigation strategies. - -## Critical Rules - -### Git Workflow -- **Commit frequently**: After every meaningful change or completed task -- **Update documentation**: When making changes, update relevant docs (GEMINI.md, README.md, project docs) -- **Flag outdated docs**: Proactively ask about updates when you notice inconsistencies - -### File Operations -- **NEVER create new files** unless absolutely necessary -- **ALWAYS prefer editing** existing files over creating new ones -- **NEVER create documentation** (*.md, README) unless explicitly requested -- **CRITICAL WARNING: NEVER delete things (e.g. `rm -rf`) unless specifically asked**. Instead, `mv` them to `archive/` or `tmp/` if permanent deletion is not explicitly requested. -- **NEVER assume a library/framework is available or appropriate.** Verify its established usage within the project (check imports, configuration files like 'package.json', 'Cargo.toml', 'requirements.txt', 'build.gradle', etc., or observe neighboring files) before employing it. - -### Communication -- **State confidence levels**: Always explicit ("~80% confident" / "This is speculative") -- **Ask questions when uncertain**: Clarify before implementing if anything is ambiguous -- **Suggest better methods**: Propose more efficient approaches with clear reasoning -- **Be concise**: Act first on obvious solutions, ask only when genuinely blocked -- **Show, don't tell**: Display results and errors, not explanations - -### Working Style -- **Engage as experienced peer**: Challenge ideas constructively, use Socratic questioning -- **Default to planning**: Use `write_todos` for complex multi-step tasks before implementation -- **Admit limitations**: Say "I don't know" when appropriate, never fabricate - -### Agent Throughput Awareness -- **Never give time estimates** — you operate at machine speed. A major refactoring takes minutes, not weeks. Don't say "this would take days/weeks." -- **Never estimate costs** unless you've actually calculated them. Vague API cost projections are almost always wrong. -- **Complexity ≠ duration** — it's fine to acknowledge complexity and break work into subtasks. Don't translate complexity into human-scale time estimates. -- **Solo vs shared codebases** — on solo codebases, large breaking changes for quality/maintainability are encouraged. On shared codebases, maintain backwards compatibility. - -## Documentation Lookup Strategy - -**CRITICAL: ALWAYS use internal tools FIRST for documentation lookup. Do NOT use `google_web_search` unless internal tools fail.** - -You have several internal tools configured: -- `codebase_investigator`: For comprehensive understanding, architectural mapping, and system-wide dependencies. -- `search_file_content`: For fast, optimized searches for patterns within files. -- `glob`: For efficiently finding files matching specific glob patterns. -- `read_file`: For reading content of specific files. - -### When to Use Internal Tools vs `google_web_search` - -**✅ ALWAYS USE INTERNAL TOOLS FOR:** -- Understanding the existing codebase, project structure, and dependencies. -- Finding specific code patterns, function definitions, or file paths. -- Reading documentation files within the project (`.md`, `README`, etc.). -- Identifying project conventions, coding styles, and architectural patterns. - -**❌ ONLY USE `google_web_search` WHEN:** -- Internal tools explicitly fail or return "No documentation found" within the project. -- Looking for external library documentation, framework APIs, or general programming concepts not found within the project's `docs/` or `specs/`. -- Searching for news, blog posts, or non-technical content. - -### Internal Tool Usage Examples - -```python -# Example 1: Understanding a complex part of the codebase -codebase_investigator(objective="Understand the data flow in the user authentication module.") - -# Example 2: Finding all occurrences of a specific function call -search_file_content(pattern="validate_user_input", include="src/**/*.py") - -# Example 3: Locating configuration files -glob(pattern="**/*config.py") - -# Example 4: Reading a project's README -read_file(file_path="README.md") -``` - -### Workflow for Documentation Lookup - -1. **Identify what you need**: Project-specific code/docs? External library info? General concept? -2. **Search within project first** using `codebase_investigator`, `search_file_content`, `glob`, `read_file`. Check `docs/` and `specs/` if applicable. -3. **Only fall back to `google_web_search`** if internal tools fail to provide the necessary information from the project context. -4. **Always state which source you used** in your response. - -## File Organization - -### Core Principles -- Never put temporary files in project root → use `tmp/` -- Archive failed/superseded runs to `archive/` -- **Automate logging** - Prefer automatic over manual documentation -- **Single source of truth** - Avoid duplicate documentation across multiple files - -### Automated Logging and Reproducibility - -**Emphasize automated logging for all Gemini's actions and outputs.** This includes: -- **Tool calls**: Every tool call made and its output should be implicitly logged. -- **Internal planning**: The agent's thought process, plans, and subtasks (e.g., from `write_todos`) should be trackable. -- **Code changes**: All file modifications via `write_file` or `replace` are version-controlled via Git. -- **Experiment outputs**: If conducting experiments, ensure outputs are systematically organized and logged (e.g., in timestamped directories). - -**Reproducibility**: Ensure that the steps taken and changes made are reproducible. This means clear tool calls, explicit file paths, and well-defined instructions. - -### Documentation Strategy - -**Automated documentation (preferred):** -- Tool call logs and outputs. -- `write_todos` list reflecting current progress and plans. -- Git commits (code changes and rationale). -- Code comments (inline decisions). - -**Manual (minimal):** -- `specs/` - Project requirements from user. -- `docs/` (optional) - Agent-specific context for this project. - - Project conventions and patterns. - - Debugging procedures specific to this codebase. - - Tool usage patterns (only create files when genuinely useful). -- `NOTES.md` (optional) - Single chronological file for thoughts. - - Free-form, no structure enforcement. - -**Avoid:** -- Separate work_log vs research_log layers. -- Multiple markdown files for narratives (use `NOTES.md` instead or integrate into existing docs). - -### Default Locations (General Guidance) - -- **Temporary files**: `tmp/` (for scratch code and data, delete liberally). -- **Archived items**: `archive/` (for failed/superseded runs, move things here instead of deleting). -- **Agent-specific context**: `docs/` (for project patterns, debugging notes, tool usage). -- **User specifications**: `specs/`. - -## Delegation Strategy (Internal Capabilities) - -**Default: delegate, not do.** Strongly bias towards using specialized tools or "internal capabilities" for non-trivial or parallelisable work. - -### When to Delegate (to Tools/Internal Capabilities) - -| Task | Tool/Capability | When | -|------|-----------------|------| -| Understanding code | `codebase_investigator` | File searches, tracing logic, understanding implementations, architectural analysis. | -| Specific searches | `search_file_content`, `glob` | Quickly finding patterns, file types, or specific declarations. | -| File manipulation | `read_file`, `write_file`, `replace` | All file content operations. | -| Shell commands | `run_shell_command` | Executing system commands, building, testing, linting. | -| Web search | `google_web_search` | External information, general knowledge, library documentation not in project. | -| Task tracking | `write_todos` | Planning and tracking complex, multi-step tasks. | -| Remembering facts | `save_memory` | Storing user-specific preferences or facts for long-term recall. | - -### Principles -- **When in doubt, delegate** - YOU coordinate; TOOLS execute. -- **Prevent context pollution** - Don't read long files; let search/investigation tools summarize or extract. -- **Parallelize** - Spin up multiple tool calls simultaneously when independent. -- **Be specific** - Provide clear, scoped tasks to tools. -- **ASK if unclear** - Don't speculate or fabricate. - -## Research Methodology - -### Before Acting/Writing Code -- **Ask pointed questions**: Have specific research questions, not just "let's see what happens". -- **Document your approach**: Write down prompts, metric definitions, and methodology BEFORE implementing. -- **Predict results**: State expected outcomes before acting (helps catch bugs and understand surprises). -- **Minimize variables**: Change one thing at a time to isolate causes. -- **De-risk first**: Test on smallest viable scope before scaling up. -- **Tight feedback loops**: Optimize for information gain per unit time. - -### Correctness (CRITICAL) -- **Never use mock data** in code (only in unit tests). -- **Never add fallback mechanisms** unless explicitly asked. -- **Avoid try/except** - they mask fatal errors unless specifically used for anticipated and handled exceptions. -- **ASK if you can't find data** - never fabricate. -- **Be skeptical**: If results are surprisingly good/bad, check for bugs, wrong data, or incorrect assumptions. -- Better to fail than to cover up issues. - -### Documentation (Internal & External) - -**Automated documentation:** -- `write_todos` (plans and progress). -- Tool execution logs. -- Git commits. -- Code comments. - -**Critical manual documentation:** -- **User requests/specifications**: Store or refer to `specs/`. -- **Why decisions were made**: Git commits and inline comments. - -**Optional:** -- Use `NOTES.md` for brief, chronological thoughts if helpful. - -### Workflow -1. **Explore**: Read relevant files (via tools), check `specs/`. -2. **Plan**: Design approach, predict results (using `write_todos`). -3. **Start small**: Test on limited scope first. -4. **Implement**: Use appropriate tools for file changes, shell commands. -5. **Verify**: Run tests, linting, type-checking. -6. **Review**: Self-critique against best practices. -7. **Iterate**: Based on verification and review. - -### Common Failure Modes -- Acting without clear questions or understanding. -- Logical misinterpretations. -- Fabricating solutions instead of admitting uncertainty. -- Changing too many variables at once. -- Over-engineering before validating core ideas. - -## Language-Agnostic Guidelines - -- **Match existing code style and conventions**. -- **Preserve exact formatting when editing**. -- **Run validation** (lint/typecheck, e.g., `ruff`/`tsc`) after changes. -- **Keep code readable and maintainable**. Code should be self-documenting. -- **Refactor long functions and files** out when they get unwieldy (e.g., > 50 lines in a function unless it's the main function, > 500 lines in a file). -- **Execution**: Run commands from project root where appropriate. -- **Performance**: Proactively optimize for efficiency (e.g., parallelizing I/O-bound operations, caching) where relevant to the task. - -## Compacting Conversations - -When compressing a conversation, you should: -- Include user instructions mostly in full. -- Clean up instructions to be clearer. -- Note tricky or unexpected conventions. -- Don't make up mock data or specify unknown details. -- Faithfully represent what was given. -- ASK if anything's unclear rather than write with conviction. - -## CLI - -Prefer the following CLI tools when using `run_shell_command`: -- **`ripgrep`** (better `grep`) -- **`fd`** (better `find`) -- **`dust`** (better `du`) -- **`duf`** (better `df`) -- **`bat`** (better `cat` with highlighting and git) -- **`exa`** (better `ls`) diff --git a/install.sh b/install.sh index 817f02a2..30209214 100755 --- a/install.sh +++ b/install.sh @@ -50,12 +50,13 @@ SELECTIVE INSTALLATION: COMPONENTS: --zsh Enable ZSH installation --tmux Enable tmux installation - --ai-tools Enable AI CLI tools (Claude, Gemini, Codex) + --ai-tools Enable AI CLI tools (Claude, Codex, OpenCode, Antigravity) --extras Enable extra CLI tools (hyperfine, gitui, code2prompt) --cleanup Enable automatic cleanup (macOS only) --docker Enable Docker installation (Linux only) --pueue Enable Pueue job scheduler (Linux only) - --experimental Enable experimental features (ty type checker, zerobrew) + --experimental Enable experimental features (ty type checker, zotero MCP) + --apps Install GUI + App Store apps via Brewfile picker (macOS) --create-user Create non-root dev user (Linux only) --no- Disable a component (e.g., --no-ai-tools) --force-reinstall Reinstall tools even if present @@ -155,7 +156,13 @@ fi if ! is_installed uv; then log_info "Installing uv..." - curl -LsSf https://astral.sh/uv/install.sh | sh + # Prefer the official Homebrew formula on macOS: sha-pinned + reviewed, unlike + # the upstream curl|bash installer (authentic over HTTPS but not tamper-evident). + if is_macos && cmd_exists brew; then + brew_install uv + else + curl -LsSf https://astral.sh/uv/install.sh | sh + fi fi fi # INSTALL_CORE @@ -195,11 +202,7 @@ if [[ "$INSTALL_EXTRAS" == "true" ]]; then log_section "INSTALLING EXTRAS" # Rust toolchain (needed for code2prompt) - if ! is_installed cargo; then - log_info "Installing Rust..." - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet - fi - source "$HOME/.cargo/env" 2>/dev/null || true + install_rust_toolchain if is_macos; then install_packages brew "${PACKAGES_EXTRAS_MACOS[@]}" @@ -221,16 +224,12 @@ if [[ "$INSTALL_AI_TOOLS" == "true" ]]; then log_section "INSTALLING AI CLI TOOLS" # Rust toolchain (needed for claude-tools build in deploy.sh) - if ! is_installed cargo; then - log_info "Installing Rust toolchain (user-level, no root needed)..." - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet - fi - source "$HOME/.cargo/env" 2>/dev/null || true + install_rust_toolchain # Pre-set PATH for subshells [[ -d "$HOME/.claude/bin" ]] && export PATH="$HOME/.claude/bin:$PATH" - # Bun must install before Gemini/Codex on Linux (they need `bun add -g`) + # Bun must install before Codex/OpenCode on Linux (they need `bun add -g`) if is_linux && ! cmd_exists bun; then log_info "Installing bun..." curl -fsSL https://bun.sh/install | bash @@ -241,19 +240,15 @@ if [[ "$INSTALL_AI_TOOLS" == "true" ]]; then if is_macos; then # brew has a global lock — sequential install_claude_code - install_gemini_cli install_codex_cli + install_opencode + install_antigravity_cli # official Gemini CLI successor (cask, macOS) else run_parallel "Installing AI CLI tools" \ "claude|install_claude_code" \ - "gemini|install_gemini_cli" \ - "codex|install_codex_cli" - fi - - # Coven (macOS only, lightweight Claude interface) - if is_macos && ! is_installed coven; then - log_info "Installing Coven..." - brew tap Crazytieguy/tap 2>/dev/null && brew_install coven || log_warning "Coven installation failed" + "codex|install_codex_cli" \ + "opencode|install_opencode" \ + "antigravity|install_antigravity_cli" fi # MCP servers (sequential — unclear if concurrent-safe) @@ -385,12 +380,6 @@ if [[ "$INSTALL_EXPERIMENTAL" == "true" ]]; then uv tool install ty 2>/dev/null || log_warning "ty installation failed" fi - # zerobrew: fast Rust-based Homebrew client (installs to /opt/zerobrew, won't touch /opt/homebrew) - if ! is_installed zb; then - log_info "Installing zerobrew (experimental Homebrew alternative)..." - curl --proto '=https' --tlsv1.2 -fsSL https://zerobrew.rs/install | bash 2>/dev/null || log_warning "zerobrew installation failed" - fi - # zotero-mcp-server: Zotero MCP for citation management (see config/experimental.yaml) if ! is_installed zotero-mcp && cmd_exists uv; then log_info "Installing zotero-mcp-server (citation library management)..." @@ -413,11 +402,36 @@ if [[ "$INSTALL_MACOS_SETTINGS" == "true" ]] && is_macos && [[ -f "$DOT_DIR/conf "$DOT_DIR/config/macos_settings.sh" || log_warning "macOS settings had some errors" fi -# ─── Finicky (macOS) ────────────────────────────────────────────────────────── +# ─── GUI + App Store Apps (macOS, via Brewfile) ─────────────────────────────── +# Pick apps in a TUI (app-picker reads config/apps.conf → generates config/Brewfile), +# then install via `brew bundle`. Official casks + Mac App Store only; never +# --no-quarantine (Gatekeeper/notarization must stay on). Finicky now lives here too. + +if [[ "$INSTALL_APPS" == "true" ]] && is_macos; then + log_section "INSTALLING APPS (Brewfile) 📦" -if [[ "$INSTALL_FINICKY" == "true" ]] && is_macos && ! is_cask_installed finicky; then - log_info "Installing Finicky..." - brew_install finicky true + if ! cmd_exists brew; then + log_warning "Homebrew required for apps — skipping" + else + # gum drives the picker; bootstrap it (tiny formula) if missing. + cmd_exists gum || brew_install gum + + brewfile="$DOT_DIR/config/Brewfile" + if [[ "${NON_INTERACTIVE:-false}" == "true" ]] || ! [[ -t 0 ]]; then + log_info "Non-interactive: using committed Brewfile (run 'app-picker' to customise)" + else + # Interactive: let the user toggle apps, regenerating the Brewfile. + "$DOT_DIR/custom_bins/app-picker" || log_warning "app-picker cancelled — using existing Brewfile" + fi + + if [[ -f "$brewfile" ]]; then + log_info "Installing apps from Brewfile (this can take a while)..." + brew bundle --file="$brewfile" || log_warning "Some Brewfile entries failed (mas needs App Store sign-in)" + log_info "Next: run scripts/setup/auth-setup for logins + signature audit" + else + log_warning "No Brewfile at $brewfile — run 'app-picker' first" + fi + fi fi # ─── Done ───────────────────────────────────────────────────────────────────── diff --git a/iterm/onedark.itermcolors b/iterm/onedark.itermcolors deleted file mode 100644 index f848f653..00000000 --- a/iterm/onedark.itermcolors +++ /dev/null @@ -1,303 +0,0 @@ - - - - - Ansi 0 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4392156862745098 - Green Component - 0.38823529411764707 - Red Component - 0.3607843137254902 - - Ansi 1 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4588235294117647 - Green Component - 0.4235294117647059 - Red Component - 0.8784313725490196 - - Ansi 10 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4745098039215686 - Green Component - 0.7647058823529411 - Red Component - 0.596078431372549 - - Ansi 11 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4 - Green Component - 0.6039215686274509 - Red Component - 0.8196078431372549 - - Ansi 12 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.9372549019607843 - Green Component - 0.6862745098039216 - Red Component - 0.3803921568627451 - - Ansi 13 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.8666666666666667 - Green Component - 0.47058823529411764 - Red Component - 0.7764705882352941 - - Ansi 14 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7607843137254902 - Green Component - 0.7137254901960784 - Red Component - 0.33725490196078434 - - Ansi 15 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.3215686274509804 - Green Component - 0.26666666666666666 - Red Component - 0.24313725490196078 - - Ansi 2 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4745098039215686 - Green Component - 0.7647058823529411 - Red Component - 0.596078431372549 - - Ansi 3 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.4823529411764706 - Green Component - 0.7529411764705882 - Red Component - 0.8980392156862745 - - Ansi 4 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.9372549019607843 - Green Component - 0.6862745098039216 - Red Component - 0.3803921568627451 - - Ansi 5 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.8666666666666667 - Green Component - 0.47058823529411764 - Red Component - 0.7764705882352941 - - Ansi 6 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7607843137254902 - Green Component - 0.7137254901960784 - Red Component - 0.33725490196078434 - - Ansi 7 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Ansi 8 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.38823529411764707 - Green Component - 0.3215686274509804 - Red Component - 0.29411764705882354 - - Ansi 9 Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.27450980392156865 - Green Component - 0.3137254901960784 - Red Component - 0.7450980392156863 - - Background Color - - Color Space - sRGB - Blue Component - 0.20392156862745098 - Green Component - 0.17254901960784313 - Red Component - 0.1568627450980392 - - Bold Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Cursor Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Cursor Text Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.20392156862745098 - Green Component - 0.17254901960784313 - Red Component - 0.1568627450980392 - - Foreground Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - Selected Text Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.20392156862745098 - Green Component - 0.17254901960784313 - Red Component - 0.1568627450980392 - - Selection Color - - Alpha Component - 1 - Color Space - sRGB - Blue Component - 0.7490196078431373 - Green Component - 0.6980392156862745 - Red Component - 0.6705882352941176 - - - diff --git a/iterm/onedarker.itermcolors b/iterm/onedarker.itermcolors deleted file mode 100644 index 0f8aee5a..00000000 --- a/iterm/onedarker.itermcolors +++ /dev/null @@ -1,344 +0,0 @@ - - - - - Ansi 0 Color - - Alpha Component - 1 - Blue Component - 0.43921568989753723 - Color Space - sRGB - Green Component - 0.38823530077934265 - Red Component - 0.36078432202339172 - - Ansi 1 Color - - Alpha Component - 1 - Blue Component - 0.45882353186607361 - Color Space - sRGB - Green Component - 0.42352941632270813 - Red Component - 0.87843137979507446 - - Ansi 10 Color - - Alpha Component - 1 - Blue Component - 0.47450980544090271 - Color Space - sRGB - Green Component - 0.76470589637756348 - Red Component - 0.59607845544815063 - - Ansi 11 Color - - Alpha Component - 1 - Blue Component - 0.40000000596046448 - Color Space - sRGB - Green Component - 0.60392159223556519 - Red Component - 0.81960785388946533 - - Ansi 12 Color - - Alpha Component - 1 - Blue Component - 0.93725490570068359 - Color Space - sRGB - Green Component - 0.68627452850341797 - Red Component - 0.3803921639919281 - - Ansi 13 Color - - Alpha Component - 1 - Blue Component - 0.86666667461395264 - Color Space - sRGB - Green Component - 0.47058823704719543 - Red Component - 0.7764706015586853 - - Ansi 14 Color - - Alpha Component - 1 - Blue Component - 0.7607843279838562 - Color Space - sRGB - Green Component - 0.7137255072593689 - Red Component - 0.33725491166114807 - - Ansi 15 Color - - Alpha Component - 1 - Blue Component - 0.15294118225574493 - Color Space - sRGB - Green Component - 0.13333334028720856 - Red Component - 0.12156862765550613 - - Ansi 2 Color - - Alpha Component - 1 - Blue Component - 0.47450980544090271 - Color Space - sRGB - Green Component - 0.76470589637756348 - Red Component - 0.59607845544815063 - - Ansi 3 Color - - Alpha Component - 1 - Blue Component - 0.48235294222831726 - Color Space - sRGB - Green Component - 0.75294119119644165 - Red Component - 0.89803922176361084 - - Ansi 4 Color - - Alpha Component - 1 - Blue Component - 0.93725490570068359 - Color Space - sRGB - Green Component - 0.68627452850341797 - Red Component - 0.3803921639919281 - - Ansi 5 Color - - Alpha Component - 1 - Blue Component - 0.86666667461395264 - Color Space - sRGB - Green Component - 0.47058823704719543 - Red Component - 0.7764706015586853 - - Ansi 6 Color - - Alpha Component - 1 - Blue Component - 0.7607843279838562 - Color Space - sRGB - Green Component - 0.7137255072593689 - Red Component - 0.33725491166114807 - - Ansi 7 Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Ansi 8 Color - - Alpha Component - 1 - Blue Component - 0.38823530077934265 - Color Space - sRGB - Green Component - 0.32156863808631897 - Red Component - 0.29411765933036804 - - Ansi 9 Color - - Alpha Component - 1 - Blue Component - 0.27450981736183167 - Color Space - sRGB - Green Component - 0.31372550129890442 - Red Component - 0.7450980544090271 - - Background Color - - Alpha Component - 1 - Blue Component - 0.15294118225574493 - Color Space - sRGB - Green Component - 0.13333334028720856 - Red Component - 0.12156862765550613 - - Badge Color - - Alpha Component - 0.5 - Blue Component - 0.0 - Color Space - sRGB - Green Component - 0.1491314172744751 - Red Component - 1 - - Bold Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Cursor Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Cursor Guide Color - - Alpha Component - 0.25 - Blue Component - 1 - Color Space - sRGB - Green Component - 0.9268307089805603 - Red Component - 0.70213186740875244 - - Cursor Text Color - - Alpha Component - 1 - Blue Component - 0.15294118225574493 - Color Space - sRGB - Green Component - 0.13333334028720856 - Red Component - 0.12156862765550613 - - Foreground Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - Link Color - - Alpha Component - 1 - Blue Component - 0.73423302173614502 - Color Space - sRGB - Green Component - 0.35916060209274292 - Red Component - 0.0 - - Selected Text Color - - Alpha Component - 1 - Blue Component - 0.20392157137393951 - Color Space - sRGB - Green Component - 0.17254902422428131 - Red Component - 0.15686275064945221 - - Selection Color - - Alpha Component - 1 - Blue Component - 0.74901962280273438 - Color Space - sRGB - Green Component - 0.69803923368453979 - Red Component - 0.67058825492858887 - - - diff --git a/plans/2026-06-16-mac-app-setup-brewfile.md b/plans/2026-06-16-mac-app-setup-brewfile.md new file mode 100644 index 00000000..ec996ef1 --- /dev/null +++ b/plans/2026-06-16-mac-app-setup-brewfile.md @@ -0,0 +1,203 @@ +# Plan: New-Mac App Setup via Brewfile + Toggle TUI + Auth Helpers + +Date: 2026-06-16 +Branch: `claude/quirky-hypatia-wj5t9g` + +## Goal + +Make setting up a fresh Mac a one-command, reviewable experience: +- Install the GUI apps you actually use (casks + Mac App Store). +- Browse each app's description and toggle it on/off before installing. +- Reproducible + re-runnable (Brewfile). +- Encode the ChatGPT trust policy (Homebrew only, official casks, **no new taps**, MAS for App Store vendor apps). +- Prune dotfiles cruft that the security policy says to drop. +- Help with the apps that need auth/manual login. + +Priority order baked into every decision (from your security note): +**Security > Reliability > Reproducibility > Performance > Novelty.** + +--- + +## 1. Architecture + +``` +config/apps.conf # NEW — single source of truth: one line per app + └─ generates → config/Brewfile # brew/cask/mas entries (committed, reproducible) +custom_bins/app-picker # NEW — gum TUI: browse descriptions, toggle, write Brewfile +install.sh --apps # NEW component: bootstrap brew+gum → run picker → brew bundle +scripts/setup/auth-setup # NEW — interactive post-install auth checklist +``` + +Why Brewfile (your choice) + a registry: +- `brew bundle` natively handles `brew "x"`, `cask "x"`, and `mas "App", id: N` — one mechanism covers CLI tools, GUI casks, and App Store apps. +- The committed `config/Brewfile` is the reproducible lock-ish artifact the policy asks for. +- `config/apps.conf` keeps descriptions + category + trust tier + auth notes that a raw Brewfile can't hold; the picker reads it and emits the Brewfile. + +### `config/apps.conf` schema + +``` +# method | id | category | tier | default | name | description | auth +cask | notion | text | 1 | true | Notion | Notes/docs/wiki | login +mas | 904280696 | tasks | 2 | true | Things 3 | GTD task manager (App Store) | things-cloud +brew | wakatime-cli | time | 2 | false | WakaTime CLI | Coding time tracker (API key) | apikey +``` + +- **method**: `brew` (formula) / `cask` / `mas` (App Store). **Selection rule (per your policy: MAS > vendor download > cask):** prefer `mas` when the app is on the App Store AND its sandboxed MAS build isn't feature-crippled (gives sandbox/least-privilege + notarization + no cask supply-chain surface). Use `cask` only when the app needs unsandboxed system access (accessibility, automation, system/network extensions, SMC) or isn't on MAS. Safari extensions are always `mas`. Caveat: `mas` re-installs Apple-ID-owned apps, but first acquisition of paid apps (e.g. Things 3) is a one-time GUI step. +- **tier**: 1 = official vendor auto-approve, 2 = mature OSS review, 3 = needs explicit approval (per your policy). Drives a color tag in the TUI; tier-3 items default OFF. +- **default**: initial toggle state. +- **auth**: token for the auth-setup checklist (`login`, `apikey`, `pair-phone`, `safari-ext`, `license`, `none`). + +### TUI: `gum` (answers your "which needs no install?") + +None of gum/fzf/ratatui preship on a clean Mac, but: +- Homebrew is the *only* hard prereq and `install.sh` already installs it first. +- `gum` is a 1-file brew formula already in your package list and already the engine behind `show_component_menu`. +- ratatui would need a full `cargo build` (heavy, slow on fresh machine). + +So: **bootstrap `gum` immediately after Homebrew**, then reuse the existing menu pattern. The picker shows `name — description` rows grouped by category, with a tier tag, full description visible inline (gum) — space toggles, enter confirms, writes `config/Brewfile`. (If you'd prefer a full-description side panel, fzf `--preview` is a drop-in alternative; gum is the lower-friction default.) + +--- + +## 2. App → install-method mapping (verified) + +Legend: ✅ cask · 🛒 Mac App Store (mas) · ⌘ formula · ⚙️ already has dotfiles *config* (install layer is new) + +| Category | App | Method | Cask/ID | Tier | Notes | +|---|---|---|---|---|---| +| text | Bear | 🛒 | `1091189122` | 2 | App Store only; `bearcli` deploy already symlinks CLI | +| text | Notion | ✅ | `notion` | 1 | | +| tasks | Things 3 | 🛒 | `904280696` | 2 | App Store only | +| coding/editor | Cursor | ✅ | `cursor` | 1 | editor config already deployed | +| coding/editor | Antigravity | ✅ | `antigravity` | 1 | Google; config already deployed | +| coding/editor | Zed | ✅ | `zed` | 1 | config already deployed ⚙️ | +| coding/LLM | ChatGPT | ✅ | `chatgpt` | 1 | OpenAI | +| coding/LLM | Claude | ✅ | `claude` | 1 | Anthropic desktop | +| coding/LLM | Codex CLI | ⌘ | (npm, existing `ai-tools`) | 1 | already installed | +| coding/CLI | Ghostty | ✅ | `ghostty` | 1 | config already deployed ⚙️ | +| meetings | Granola | ✅ | `granola` | 2 | | +| cloud | Dropbox | ✅ | `dropbox` | 1 | | +| cloud | Google Drive | ✅ | `google-drive` | 1 | optional (default OFF) | +| search | Alfred | ✅ | `alfred` | 2 | prefs sync from Dropbox (manual: set sync folder + Powerpack license) | +| messaging | Slack | ✅ | `slack` | 1 | | +| messaging | Spark | 🛒 or ✅ | mas (verify id) / `readdle-spark` cask | 2 | MAS build is full-featured (not crippled) → MAS preferred for sandbox; cask also fine | +| productivity | Mouseless | ✅ | `mouseless` | 2 | config already deployed ⚙️; needs accessibility → cask | +| productivity | PopClip | ✅ | `popclip` | 2 | **Use cask, NOT MAS** — MAS edition abandoned at v2023.9; standalone is sandbox-free + current | +| time | Super Productivity | ✅ | `super-productivity` | 2 | | +| time | WakaTime CLI | ⌘ | `wakatime-cli` | 2 | optional; API key via secrets | +| voice | VoiceInk | ✅ | `voiceink` | 2 | config already deployed ⚙️; downloads model on first run | +| vpn | NordVPN | ✅ | `nordvpn` | 2 | `vpn` deploy already configures split tunnel ⚙️ | +| auth | Bitwarden | ✅ or 🛒 | `bitwarden` cask / mas `1352778147` | 1 | Desktop app **has a cask**. Safari extension ships **only** in the MAS build → use mas if you want the Safari ext (covers both) | +| auth | 2FAS | 🛒 | *verify id* | 2 | mainly phone-paired; Safari ext | +| auth | Tailscale | ✅ | `tailscale-app` | 1 | `vpn` deploy already configures ⚙️ | +| safari-ext | uBlock Origin Lite | 🛒 | `6745342698` | 2 | enable manually in Safari | +| safari-ext | Userscripts | 🛒 | `1463298887` *verify* | 2 | enable manually in Safari | +| safari-ext | 2FAS / Bitwarden | 🛒 | (above) | — | enable manually in Safari | +| music | Spotify | ✅ | `spotify` | 1 | | +| misc | AlDente | ✅ | `aldente` | 2 | | +| misc | Finicky | ✅ | `finicky` | 2 | currently installed inline → fold into Brewfile ⚙️ | +| misc | AppCleaner | ✅ | `appcleaner` | 2 | | +| misc | CleanShot X | ✅ | `cleanshot` | 1 | | +| misc | Stats | ✅ | `stats` | 2 | | +| misc | KeyboardCleanTool | ✅ | `keyboardcleantool` | 2 | | +| misc | BeardedSpice | ✅ | `beardedspice` | 2 | | +| antivirus | Malwarebytes | ✅ | `malwarebytes` | 2 | **optional, default OFF**; lightweight on-demand scanner. **Recommended** AV for personal use | +| antivirus | Trellix | ❌ | — | 3 | Personal install, no cask → checklist manual-install note. **Don't run real-time alongside Malwarebytes.** Recommend skip (heavy enterprise EDR, low value for dev threat model) | + +IDs marked *verify* get a `mas search` / `brew info` check during implementation before committing (policy: `brew info` before install, verify vendor/homepage). Safari extensions can be *installed* but must be *enabled* in Safari manually — the auth checklist will list them. + +### "settings → dotfiles" (menu bar / accessibility / dock) +These aren't apps — they're system defaults. Already handled by `config/macos_settings.sh`. I'll extend it with: +- Dock: which apps are pinned (set from the installed-apps list) + autohide behaviour. +- Menu bar items (where scriptable; Stats/AlDente handle most). +Treated as a follow-up sub-task, not part of the Brewfile. + +--- + +## 3. Auth / manual-setup helper + +New `scripts/setup/auth-setup` (run after `brew bundle`): an interactive gum checklist that, per app needing setup, prints the action and offers to open the app / URL: + +- **git / gh** — already covered (gist sync + `gh auth login`); checklist just verifies. +- **API-key apps (WakaTime)** — wire into existing secrets system (`setup-envrc` / `with-secrets`); no plaintext. +- **GUI logins** (Dropbox, Slack, Spark, Granola, Bitwarden, NordVPN, Tailscale, ChatGPT, Claude, Things Cloud, Spotify) — open app, check off when logged in. Can't be automated (interactive OAuth/passwords) — checklist only. +- **Alfred** — open prefs, point sync folder at Dropbox, apply Powerpack license. +- **Safari extensions** — open Safari → Settings → Extensions, enable uBlock Origin Lite / Bitwarden / 2FAS / Userscripts. +- **VoiceInk** — first-run model download. + +No secrets are stored in plaintext; anything key-based flows through the existing SOPS/BWS path. + +--- + +## 4. Proposed pruning (per-item approval — your choice) + +Driven by your policy ("prefer mature, boring"; "never auto-add taps"; "revisit ZeroBrew in 6–12 months"; "cautious with MCP/random tools"): + +**APPROVED for removal:** + +| # | Remove | Where | Rationale | +|---|---|---|---| +| P1 | **zerobrew** | `install.sh` experimental | Experimental pkg mgr; your note says revisit in 6–12mo. `curl\|bash` install. | +| P3 | **Coven** + `brew tap Crazytieguy/tap` | `install.sh` ai-tools | Third-party tap — violates "never auto-add taps". | + +**KEEP (you declined):** ty type checker, zotero-mcp-server → so the `experimental` component stays. +`OFFICIAL_PLUGINS` audit deferred (can revisit separately). + +--- + +## 5. Policy documentation + +- Extend `claude/rules/supply-chain-security.md` (or add `config/apps.conf` header) with the tier model + "no new taps without approval" + "`brew info` before adding any app". +- Note in `CLAUDE.md` how to add an app (one line in `apps.conf` → re-run picker) and the Brewfile regen flow (`brew bundle dump`-style). +- Optional, not auto-applied: your note prefers `~/code/dotfiles` over `~/.dotfiles`. `DOT_DIR` is auto-detected so nothing breaks either way — I'll mention it but not move the repo. + +--- + +## 6. Malicious apps & executables — install integrity + runtime defense + +Trust tiers gate *what* we install; this section gates *integrity* (is the bytes what the vendor shipped?) and *runtime* (is a trusted-looking app misbehaving?). All additions are official/mature, free unless noted. + +### Already covered +- **macOS**: Gatekeeper + notarization (blocks unsigned/un-notarized on launch), XProtect + XProtect Remediator (Apple malware scanner, auto-updated), App Store sandboxing (MAS apps = highest trust → "MAS-first" rule), TCC permission prompts. +- **Brew casks**: pinned **sha256** verified on download → tampered artifact aborts. +- **Dev deps** (existing): `min-release-age` 7-day quarantine, `ignore-scripts`, weekly `dep-audit`, Socket CLI, gitleaks, pip-audit. + +### Additions (all selected) +1. **Enforce quarantine policy** — never `--no-quarantine` in any cask/Brewfile entry; document that Gatekeeper/notarization must stay enabled. Pure policy, zero cost. → `claude/rules/supply-chain-security.md`. +2. **Signature-verify step in `auth-setup`** — after install, run `spctl --assess --type execute` + `codesign -dv --verbose=4` per app; report any unsigned/un-notarized app before you trust it. Free. +3. **LuLu** (Objective-See, free OSS outbound firewall) — optional cask, **default OFF** (prompts a lot). Fills the runtime/egress gap: catches a signed-but-compromised app phoning home. Document KnockKnock (persistence enumeration) + BlockBlock (persistence alerts) as further optional Objective-See tools. +4. **Harden `curl|bash` installers** — resolves "is official-page curl|bash ok?": + - Official page gives **authenticity** (HTTPS cert proves the domain) but NOT **integrity-over-time** (runs whatever's live, unseen), **pinning** (no agreed sha → tamper passes), or **reproducibility**. + - **Rule, best→worst:** (a) use the official **brew formula** if it exists — `uv`, `rustup-init`, `bun` all do; you get the vendor's artifact + sha pin + reviewed PR + reproducible re-run. (b) No formula → `curl -o` the script to a versioned URL and **verify the vendor's published checksum/signature** if they offer one (this is the actual tamper-evidence). (c) blind `curl … | sh` only as last resort, HTTPS-to-official-domain only. + - **Note on manual inspection:** eyeballing the script ("glance at it") is a low-effort smell test for *gross* tampering (second payloads, surprise `sudo`), NOT an integrity control — a competent attacker defeats it, and you're reading the installer not the binary it fetches. Don't treat it as a safeguard; the safeguards are (a)/(b). + - Migrate existing blind pipes in `install.sh` (uv, rust) to brew formulae / fetch-verify-run. + +--- + +## 7. Files to change + +- **NEW** `config/apps.conf` — app registry (the table above). +- **NEW** `config/Brewfile` — generated, committed. +- **NEW** `custom_bins/app-picker` — gum toggle TUI → writes Brewfile. +- **NEW** `scripts/setup/auth-setup` — auth checklist + `spctl`/`codesign` signature-verify step (§6.2). +- **EDIT** `config.sh` — add `apps` to `INSTALL_REGISTRY`; remove pruned items; add LuLu (optional, OFF) to apps.conf. +- **EDIT** `install.sh` — `--apps` block: bootstrap gum, run picker, `brew bundle --file=config/Brewfile`; remove inline Finicky + pruned experimental/coven blocks; migrate uv/rust `curl|bash` → brew formulae / fetch-verify-run (§6.4). +- **EDIT** `scripts/shared/helpers.sh` — small `brew bundle` + `mas` helpers if needed. +- **EDIT** `claude/rules/supply-chain-security.md`, `CLAUDE.md` — policy + how-to + quarantine/no-`--no-quarantine` rule + curl|bash hardening rule. +- **EDIT** `config/macos_settings.sh` — dock/menu-bar follow-up (optional, can defer). + +## 8. Verification + +- `mas search` / `brew info` each *verify*-flagged id before committing the Brewfile. +- `brew bundle check --file=config/Brewfile` (dry, on a Mac) — can't run here (Linux container); will gate behind a note for you to run, or validate syntax with a parser. +- `app-picker` run with `--dry-run` to confirm it emits a valid Brewfile without installing. +- Shellcheck the new scripts. + +## 9. Resolved decisions + +1. **Prune**: zerobrew + Coven/tap only (P1, P3). ty, zotero-mcp, `experimental` component all stay. +2. **Antivirus**: Trellix = university-managed (checklist note, not Brewfile). Malwarebytes = optional cask, default OFF, conflict note. +3. **Optional apps**: Google Drive + WakaTime default OFF (toggle on in picker). +4. **TUI**: gum (bootstrapped after Homebrew). fzf `--preview` remains a drop-in alt. +5. **Malicious apps/executables** (§6): enforce quarantine policy + `spctl`/`codesign` verify step + LuLu (optional, OFF) + harden `curl|bash` → prefer brew formula, else fetch-verify-run. All four selected. + +All open questions resolved — ready to implement on approval. diff --git a/scripts/cleanup/setup_ai_update.sh b/scripts/cleanup/setup_ai_update.sh index 8ea5f57e..f1f8715b 100755 --- a/scripts/cleanup/setup_ai_update.sh +++ b/scripts/cleanup/setup_ai_update.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Setup daily auto-update for AI CLI tools (Claude Code, Gemini CLI, Codex CLI) +# Setup daily auto-update for AI CLI tools (Claude Code, Antigravity CLI, Codex CLI, OpenCode) # Works on macOS (launchd) and Linux (cron) set -euo pipefail @@ -26,7 +26,7 @@ ensure_bun_for_linux() { return 0 fi - _sched_log_info "bun not found; installing bun for Gemini/Codex updates..." + _sched_log_info "bun not found; installing bun for Codex/OpenCode updates..." if ! command -v curl &>/dev/null; then _sched_log_warn "curl is required to install bun. Skipping AI tools auto-update setup." diff --git a/scripts/cloud/setup.sh b/scripts/cloud/setup.sh index 72def994..bfba3b1e 100755 --- a/scripts/cloud/setup.sh +++ b/scripts/cloud/setup.sh @@ -57,7 +57,7 @@ command -v nvtop &>/dev/null || apt-get install -y nvtop 2>/dev/null || true service cron start 2>/dev/null || true ok "System deps installed" -# ─── Node 20 (for Gemini CLI) ───────────────────────────────────────────────── +# ─── Node 20 (for OpenCode / Node-based AI CLIs) ────────────────────────────── step "Node.js" if ! command -v node &>/dev/null || [[ "$(node -v | cut -d. -f1 | tr -d 'v')" -lt 20 ]]; then log "Installing Node 20..." diff --git a/scripts/setup/auth-setup b/scripts/setup/auth-setup new file mode 100755 index 00000000..a9b22568 --- /dev/null +++ b/scripts/setup/auth-setup @@ -0,0 +1,107 @@ +#!/usr/bin/env zsh +# ═══════════════════════════════════════════════════════════════════════════════ +# auth-setup — post-install login/setup checklist + signature verification +# ═══════════════════════════════════════════════════════════════════════════════ +# Run AFTER `brew bundle`. macOS only. Reads config/apps.conf for per-app auth notes. +# +# Usage: +# auth-setup # interactive checklist (auth steps) + signature verify +# auth-setup --verify # ONLY run the codesign/spctl signature audit +# auth-setup --checklist # ONLY print the auth/login checklist +# +# Nothing here stores secrets. API-key apps are pointed at the existing secrets +# flow (setup-envrc / with-secrets) — never plaintext. +# ═══════════════════════════════════════════════════════════════════════════════ +set -uo pipefail + +SCRIPT_DIR="${0:A:h}" +DOT_DIR="${DOT_DIR:-${SCRIPT_DIR:h:h}}" +CONF="$DOT_DIR/config/apps.conf" + +DO_CHECKLIST=true +DO_VERIFY=true +case "${1:-}" in + --verify) DO_CHECKLIST=false ;; + --checklist) DO_VERIFY=false ;; + -h|--help) sed -n '2,16p' "$0"; exit 0 ;; +esac + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "auth-setup: macOS only (uses spctl/codesign + open)." >&2 + exit 1 +fi +[[ -f "$CONF" ]] || { echo "auth-setup: registry not found: $CONF" >&2; exit 1; } + +hdr() { print -P "%F{cyan}── $* ──%f"; } + +# Human-readable instruction per auth type. +auth_hint() { + case "$1" in + login) echo "Open and sign in." ;; + apikey) echo "API key — add via 'setup-envrc' in the relevant repo (no plaintext)." ;; + pair-phone) echo "Pair with the phone app (scan QR in the desktop/extension)." ;; + safari-ext) echo "Enable in Safari → Settings → Extensions, then grant site access." ;; + license) echo "Enter your license key (paid app)." ;; + alfred) echo "Set sync folder to Dropbox (Prefs → Advanced → Set sync folder), apply Powerpack license." ;; + things-cloud) echo "Sign into Things Cloud to sync across devices." ;; + *) echo "" ;; + esac +} + +# ─── Checklist ─────────────────────────────────────────────────────────────── +print_checklist() { + hdr "Post-install setup checklist" + echo "Apps that need a login or manual step. (GUI logins can't be automated.)" + echo "" + local method id cat tier def name desc auth + while IFS='|' read -r method id cat tier def name desc auth; do + [[ -z "${method// }" || "${method[1]}" == "#" ]] && continue + auth="${auth// }" + [[ -z "$auth" || "$auth" == "none" ]] && continue + printf ' [ ] %-32s %s\n' "$name" "$(auth_hint "$auth")" + done < "$CONF" + + echo "" + hdr "Reminders" + cat <<'EOF' + [ ] App Store — SIGN IN via the App Store GUI first: `mas` can't sign in + (Apple removed the CLI API). A never-acquired free app may + need a one-time "Get" click before `mas install` works. + [ ] git / gh — `gh auth login` (SSH + identity also sync via gist) + [ ] Safari exts — uBlock Origin Lite, Userscripts, Bitwarden, 2FAS: ENABLE in Safari + [ ] Trellix — UNIVERSITY-MANAGED: install/update via your uni, NOT Homebrew. + Do not also run Malwarebytes real-time (on-demand scan is fine). + [ ] Alfred — point sync folder at Dropbox so workflows/prefs follow you + [ ] Login items — run `setup-login-items` to seed menu-bar apps (Stats, FineTune, + Tailscale, NordVPN). Additive/bootstrap-once — won't fight you. +EOF +} + +# ─── Signature / notarization audit (§6.2) ─────────────────────────────────── +# Eyeballing installers is theatre; THIS is the real check — is each app signed +# by a Developer ID and notarized by Apple? +verify_signatures() { + hdr "Signature + notarization audit (/Applications)" + echo "Gatekeeper assessment per installed app. ✓ accepted = signed + notarized." + echo "" + local app appname result + for app in /Applications/*.app; do + [[ -d "$app" ]] || continue + appname="${app:t:r}" + if spctl --assess --type execute "$app" &>/dev/null; then + printf ' %s %s\n' "✓" "$appname" + else + # Not accepted by Gatekeeper — surface the authority so you can judge it. + local authority + authority=$(codesign -dv --verbose=2 "$app" 2>&1 | grep -m1 '^Authority=' | cut -d= -f2-) + printf ' %s %-30s %s\n' "⚠️ " "$appname" "${authority:-unsigned / no Developer ID}" + fi + done + echo "" + echo "⚠️ = not Gatekeeper-accepted: unsigned, ad-hoc, or self-distributed." + echo " Mac App Store + notarized casks should all show ✓. Investigate any ⚠️." +} + +[[ "$DO_CHECKLIST" == "true" ]] && print_checklist +[[ "$DO_CHECKLIST" == "true" && "$DO_VERIFY" == "true" ]] && echo "" +[[ "$DO_VERIFY" == "true" ]] && verify_signatures diff --git a/scripts/setup/setup-login-items b/scripts/setup/setup-login-items new file mode 100755 index 00000000..723f27f5 --- /dev/null +++ b/scripts/setup/setup-login-items @@ -0,0 +1,140 @@ +#!/usr/bin/env zsh +# ═══════════════════════════════════════════════════════════════════════════════ +# setup-login-items — seed macOS "Open at Login" items for menu-bar apps +# ═══════════════════════════════════════════════════════════════════════════════ +# Reads config/login_items.conf and registers each listed app as a login item so +# it's reliably present in the menu bar. macOS only. +# +# DESIGN — bootstrap-once + additive (it will NOT fight your manual changes): +# • Adds a curated app ONLY if it's missing AND we haven't added it before. +# • NEVER removes anything; NEVER re-adds something you removed in System Settings +# (what we've added is recorded in ~/.config/dotfiles/login-items.bootstrapped). +# • Does NOT run on every deploy — invoke it manually (or from the app-setup flow). +# So day-to-day you manage Login Items in System Settings; this only seeds a machine. +# +# Usage: +# setup-login-items # add missing curated apps (respecting prior removals) +# setup-login-items --list # show status of each curated app, change nothing +# setup-login-items --force # re-add even apps you previously removed (ignore state) +# setup-login-items --reset-state # forget what we've bootstrapped (then re-seeds) +# ═══════════════════════════════════════════════════════════════════════════════ +set -uo pipefail +setopt extendedglob # for whitespace-run trimming in the conf-parse loop + +SCRIPT_DIR="${0:A:h}" +DOT_DIR="${DOT_DIR:-${SCRIPT_DIR:h:h}}" +CONF="$DOT_DIR/config/login_items.conf" +STATE="${XDG_CONFIG_HOME:-$HOME/.config}/dotfiles/login-items.bootstrapped" + +MODE="add" +case "${1:-}" in + --list) MODE="list" ;; + --force) MODE="force" ;; + --reset-state) MODE="reset" ;; + -h|--help) sed -n '2,25p' "$0"; exit 0 ;; + "") ;; + *) echo "setup-login-items: unknown option: $1" >&2; exit 1 ;; +esac + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "setup-login-items: macOS only (uses System Events login items)." >&2 + exit 1 +fi +[[ -f "$CONF" ]] || { echo "setup-login-items: list not found: $CONF" >&2; exit 1; } + +hdr() { print -P "%F{cyan}── $* ──%f"; } + +if [[ "$MODE" == "reset" ]]; then + rm -f "$STATE" + echo "Cleared bootstrap state ($STATE). Next run will re-seed all curated apps." + MODE="add" +fi + +mkdir -p "${STATE:h}" +[[ -f "$STATE" ]] || : > "$STATE" + +# Resolve an app name to its bundle path (/Applications or ~/Applications). +app_path_for() { + local name="$1" + for base in "/Applications" "$HOME/Applications"; do + [[ -d "$base/$name.app" ]] && { echo "$base/$name.app"; return 0; } + done + return 1 +} + +# Current login item names (one per line). +# NB: assumes no login-item display name contains a comma (AppleScript joins the +# list with ", "). True for the curated apps; revisit if that ever changes. +current_login_items() { + osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null \ + | tr ',' '\n' | sed 's/^ *//;s/ *$//' +} + +# Add a login item by path; hidden true|false. +add_login_item() { + local path="$1" hidden="$2" + osascript - "$path" "$hidden" <<'EOF' >/dev/null 2>&1 +on run argv + set appPath to item 1 of argv + set isHidden to (item 2 of argv is "true") + tell application "System Events" + make login item at end with properties {path:appPath, hidden:isHidden} + end tell +end run +EOF +} + +EXISTING="$(current_login_items)" +in_existing() { print -r -- "$EXISTING" | grep -qxF "$1"; } +in_state() { grep -qxF "$1" "$STATE" 2>/dev/null; } + +hdr "Login items (curated: $CONF)" + +added=0 skipped=0 missing=0 +while IFS='|' read -r name hidden; do + name="${name##[[:space:]]##}"; name="${name%%[[:space:]]##}" # strip leading/trailing whitespace runs (incl. tabs) + [[ -z "$name" || "${name[1]}" == "#" ]] && continue + hidden="${hidden// }"; [[ -z "$hidden" ]] && hidden="true" + + apath="$(app_path_for "$name" || true)" + + if [[ "$MODE" == "list" ]]; then + # NB: don't name this 'status' — zsh reserves it (read-only alias for $?). + local st + if [[ -z "$apath" ]]; then st="not installed" + elif in_existing "$name"; then st="login item ✓" + elif in_state "$name"; then st="removed by you (respected)" + else st="installed, NOT a login item" + fi + printf ' %-22s %s\n' "$name" "$st" + continue + fi + + if [[ -z "$apath" ]]; then + printf ' %-22s %s\n' "$name" "skip — not installed" + ((missing++)); continue + fi + if in_existing "$name"; then + in_state "$name" || echo "$name" >> "$STATE" # record so we never re-add + printf ' %-22s %s\n' "$name" "already a login item" + ((skipped++)); continue + fi + if [[ "$MODE" != "force" ]] && in_state "$name"; then + printf ' %-22s %s\n' "$name" "skip — you removed it (use --force to re-add)" + ((skipped++)); continue + fi + + if add_login_item "$apath" "$hidden"; then + in_state "$name" || echo "$name" >> "$STATE" + printf ' %-22s %s\n' "$name" "added (hidden=$hidden)" + ((added++)) + else + printf ' %-22s %s\n' "$name" "FAILED to add" + fi +done < "$CONF" + +[[ "$MODE" == "list" ]] && exit 0 + +echo "" +echo "Added $added · skipped $skipped · not-installed $missing" +echo "Manage these any time in System Settings → General → Login Items — changes there stick." diff --git a/scripts/shared/helpers.sh b/scripts/shared/helpers.sh index 38f38c13..3375f2b9 100644 --- a/scripts/shared/helpers.sh +++ b/scripts/shared/helpers.sh @@ -366,6 +366,30 @@ install_direnv() { fi } +# Rust toolchain (cargo) via rustup. macOS: official Homebrew formula (sha-pinned, +# reviewed) provides `rustup-init`; run it non-interactively. Linux: keep the upstream +# rustup installer but pin TLS (--proto '=https' --tlsv1.2) — no brew dependency. +# See claude/rules/supply-chain-security.md § curl|bash Installers. +install_rust_toolchain() { + if is_installed cargo; then + source "$HOME/.cargo/env" 2>/dev/null || true + return 0 + fi + log_info "Installing Rust toolchain (user-level, no root needed)..." + if is_macos && cmd_exists brew; then + # Official formula ships `rustup-init`; install the default stable toolchain. + brew_install rustup + if cmd_exists rustup-init; then + rustup-init -y --quiet 2>/dev/null || log_warning "rustup-init failed" + elif cmd_exists rustup; then + rustup default stable 2>/dev/null || log_warning "rustup default stable failed" + fi + else + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --quiet + fi + source "$HOME/.cargo/env" 2>/dev/null || true +} + install_bws() { if is_installed bws; then return 0; fi log_info "Installing bws (Bitwarden Secrets Manager CLI)..." @@ -416,15 +440,31 @@ install_claude_code() { fi } -install_gemini_cli() { - if is_installed gemini; then return 0; fi - log_info "Installing Gemini CLI..." +install_opencode() { + if is_installed opencode; then return 0; fi + log_info "Installing OpenCode..." + # Official CORE Homebrew formula (NOT the anomalyco/tap) — see supply-chain-security.md if is_macos; then - brew_install gemini-cli + brew_install opencode elif cmd_exists bun; then - bun add -g @google/gemini-cli &>/dev/null || { log_warning "Gemini CLI failed"; return 1; } + bun add -g opencode-ai &>/dev/null || { log_warning "OpenCode failed"; return 1; } + else + log_warning "bun is required to install OpenCode on Linux; skipping" + return 1 + fi +} + +# Antigravity CLI (binary: `agy`) — Google's OFFICIAL successor to Gemini CLI +# (Gemini CLI consumer access ends 2026-06-18). Official cask, no third-party tap. +install_antigravity_cli() { + if is_installed agy --version; then return 0; fi + log_info "Installing Antigravity CLI (agy)..." + if is_macos; then + brew_install antigravity-cli true # official cask else - log_warning "bun is required to install Gemini CLI on Linux; skipping" + # Linux: Google ships a curl installer, but per supply-chain-security.md we + # do NOT blind-pipe an unverified URL. Install manually on Linux. + log_warning "Antigravity CLI on Linux: install manually — https://antigravity.google/docs/cli-features (skipping)" return 1 fi } diff --git a/scripts/sync_claude_to_antigravity.sh b/scripts/sync_claude_to_antigravity.sh new file mode 100755 index 00000000..a165609a --- /dev/null +++ b/scripts/sync_claude_to_antigravity.sh @@ -0,0 +1,285 @@ +#!/bin/bash + +# ============================================================================== +# SYNC CLAUDE CODE TO ANTIGRAVITY CLI +# Purpose: Ports Claude Code agents/skills into Antigravity CLI (`agy`) by symlinking. +# Source: ~/.claude/ +# Target: ~/.gemini/antigravity-cli/skills/ (Antigravity reuses the ~/.gemini dir) +# +# Antigravity CLI is Google's official successor to Gemini CLI (consumer Gemini CLI +# access ended 2026-06-18). Project instructions come from AGENTS.md (already in repo), +# so the old GEMINI.md pointer is no longer generated here. +# +# Permission sync: Antigravity stores settings in +# ~/.gemini/antigravity-cli/settings.json with a "permissions" object holding +# allow/deny/ask arrays of action(target) rule strings (e.g. command(git*), +# read_file(*)). Precedence is Deny > Ask > Allow. This script translates Claude's +# permissions.{allow,deny,ask} into that schema and merges them into the live +# settings.json without clobbering other user settings (marker-tracked, idempotent). +# +# Schema confirmed via https://antigravity.google/docs/cli-permissions (2026-06-16): +# - File: ~/.gemini/antigravity-cli/settings.json +# - permissions.allow / .deny / .ask : arrays of "action(target)" strings +# - action types: command, read_file, write_file, mcp, execute_url, web_* +# - matching: exact by default; "*" is the per-namespace wildcard; glob/regex supported +# +# Only HIGH-CONFIDENCE mappings (command(), read_file()) are written to the live +# permission arrays. Lower-confidence mappings (WebFetch/WebSearch/MCP action names, +# regex match-mode encoding) are emitted as comments in a sidecar file rather than +# guessed into the live config. See the "VERIFY ON A REAL MAC" notes below. +# ============================================================================== + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +HELPER="$SCRIPT_DIR/helpers/enumerate_claude_skills.sh" +SOURCE_DIR="$HOME/.claude" +TARGET_DIR="$HOME/.gemini/antigravity-cli/skills" + +if [ ! -f "$HELPER" ]; then + echo "Error: enumerate_claude_skills.sh not found at $HELPER" >&2 + exit 1 +fi +source "$HELPER" + +mkdir -p "$TARGET_DIR" + +# Clean stale symlinks (broken or from old *__* pattern) +find "$TARGET_DIR" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true +find "$TARGET_DIR" -maxdepth 1 -type l -name '*__*' -delete 2>/dev/null || true + +echo ">>> Syncing Claude Code Skills to Antigravity CLI..." + +enumerate_claude_skills "$SOURCE_DIR" | while IFS=$'\t' read -r type name path; do + case "$type" in + user_skill) + ln -sfn "$path" "$TARGET_DIR/$name" + echo " User Skill: $name" + ;; + standalone_skill) + mkdir -p "$TARGET_DIR/$name" + ln -sfn "$path" "$TARGET_DIR/$name/SKILL.md" + echo " Standalone Skill: $name" + ;; + plugin_skill) + ln -sfn "$path" "$TARGET_DIR/$name" + echo " Plugin Skill: $name" + ;; + agent_skill) + mkdir -p "$TARGET_DIR/$name" + ln -sfn "$path" "$TARGET_DIR/$name/SKILL.md" + echo " Agent Skill: $name" + ;; + esac +done + +TOTAL=$(find "$TARGET_DIR" -maxdepth 1 -mindepth 1 | wc -l | tr -d ' ') +echo " Synced $TOTAL skills to $TARGET_DIR" + +# ---------- Permissions Sync ---------- +# +# Translate Claude Code permissions.{allow,deny,ask} -> Antigravity +# permissions.{allow,deny,ask} (action(target) rule strings) and merge into the +# live settings.json. The merge is marker-tracked so re-runs replace our block +# rather than duplicating, and user-authored rules outside the block survive. + +echo ">>> Syncing Claude Code Permissions to Antigravity CLI..." + +# Prefer the in-repo source of truth; fall back to the deployed symlink target. +CLAUDE_SETTINGS="$DOTFILES_DIR/claude/settings.json" +[ -f "$CLAUDE_SETTINGS" ] || CLAUDE_SETTINGS="$HOME/.claude/settings.json" + +ANTIGRAVITY_SETTINGS="$HOME/.gemini/antigravity-cli/settings.json" +ANTIGRAVITY_SIDECAR="$HOME/.gemini/antigravity-cli/claude_sync_unmapped.txt" + +if [ ! -f "$CLAUDE_SETTINGS" ]; then + echo " Skipping: Claude settings not found at $CLAUDE_SETTINGS" +elif ! command -v python3 >/dev/null 2>&1; then + echo " Skipping: python3 not installed" +else + mkdir -p "$(dirname "$ANTIGRAVITY_SETTINGS")" + python3 - "$CLAUDE_SETTINGS" "$ANTIGRAVITY_SETTINGS" "$ANTIGRAVITY_SIDECAR" <<'PY' +import json +import re +import sys +from pathlib import Path + +claude_path = Path(sys.argv[1]) +ag_path = Path(sys.argv[2]) +sidecar_path = Path(sys.argv[3]) + +# Marker tags wrapping the rules we own inside each permission array, so re-runs +# can replace our contribution while leaving user-authored rules untouched. +BEGIN = "// BEGIN CLAUDE SYNC (auto-generated)" +END = "// END CLAUDE SYNC" + +try: + claude = json.loads(claude_path.read_text()) +except (OSError, json.JSONDecodeError) as exc: + print(f" Skipping: cannot read Claude settings ({exc})") + sys.exit(0) + +perms = claude.get("permissions", {}) + + +def claude_bash_to_command_target(pattern): + """Map a Claude `Bash(...)` permission to an Antigravity command(...) target. + + Claude uses prefix-glob with a trailing ` *` (e.g. `Bash(git *)`), plus some + exact forms (e.g. `Bash(pueue status)`). Antigravity matches `command(...)` + targets exactly by default and treats `*` as a glob wildcard, so: + Bash(git *) -> command(git*) (glob: any git subcommand/args) + Bash(pueue status) -> command(pueue status) (exact) + Returns None for anything we can't confidently express. + """ + m = re.match(r"^Bash\((.*)\)$", pattern, re.DOTALL) + if not m: + return None + inner = m.group(1).strip() + if not inner: + return None + # Trailing " *" is Claude's "this command with any args" idiom -> glob. + if inner.endswith(" *"): + stem = inner[:-2].strip() + if not stem: + return None + return f"command({stem}*)" + # Otherwise treat as an exact command string. If it still contains a glob + # star, Antigravity's glob matcher handles it; pass through verbatim. + return f"command({inner})" + + +# High-confidence: tools whose Antigravity action name + target we can map +# faithfully. Lower-confidence tools go to the sidecar instead of being guessed. +def map_rule(item): + """Return (antigravity_rule, None) if confidently mapped, + else (None, reason) to record as unmapped.""" + if item.startswith("Bash("): + target = claude_bash_to_command_target(item) + if target: + return target, None + return None, "unparsable Bash pattern" + if item == "Read": + return "read_file(*)", None + if item.startswith("Read("): + m = re.match(r"^Read\((.*)\)$", item, re.DOTALL) + if m and m.group(1).strip(): + return f"read_file({m.group(1).strip()})", None + return None, "unparsable Read pattern" + # --- Lower-confidence / unverified action namespaces --- + # The following Claude tools have plausible Antigravity equivalents, but the + # exact action name (web_fetch vs web_search vs web) and the mcp() target + # encoding are NOT confirmed from docs. We deliberately do NOT write these to + # the live config. Best-effort guesses are recorded in the sidecar for a human + # to verify on a real Mac with `agy` (see /permissions output). + if item == "WebFetch": + return None, "WebFetch -> web_fetch(*)? [action name unverified]" + if item.startswith("WebFetch(domain:"): + return None, f"{item} -> web_fetch()? [encoding unverified]" + if item == "WebSearch": + return None, "WebSearch -> web_search(*)? [action name unverified]" + if item.startswith("mcp__"): + return None, f"{item} -> mcp()? [target encoding unverified]" + if item in ("Glob", "Grep", "Search"): + return None, f"{item} (Claude built-in; no Antigravity equivalent)" + return None, f"{item} (no mapping rule)" + + +mapped = {"allow": [], "deny": [], "ask": []} +unmapped = [] +for bucket in ("allow", "deny", "ask"): + for item in perms.get(bucket, []): + rule, reason = map_rule(item) + if rule is not None: + if rule not in mapped[bucket]: + mapped[bucket].append(rule) + else: + unmapped.append(f"[{bucket}] {item} -> {reason}") + +# --- Merge into the existing settings.json, preserving non-permission keys and +# any user-authored rules that live outside our marker block. --- +settings = {} +if ag_path.exists(): + try: + settings = json.loads(ag_path.read_text()) + if not isinstance(settings, dict): + settings = {} + except (OSError, json.JSONDecodeError): + # Don't clobber an unreadable/hand-edited file; bail loudly instead. + print(f" Skipping: {ag_path} exists but is not valid JSON; " + "leaving it untouched.") + sys.exit(0) + +perm_obj = settings.get("permissions") +if not isinstance(perm_obj, dict): + perm_obj = {} + + +def merge_bucket(existing, ours): + """Drop any prior CLAUDE-SYNC block, keep user rules, append fresh block. + + Our block is delimited by BEGIN/END sentinel strings inserted as array + elements. Re-runs strip the old block (between the sentinels) and re-add a + current one, so user-authored entries outside the block are preserved and + our contribution never duplicates. + """ + existing = existing if isinstance(existing, list) else [] + kept = [] + skipping = False + for el in existing: + if el == BEGIN: + skipping = True + continue + if el == END: + skipping = False + continue + if not skipping: + kept.append(el) + if not ours: + return kept + return kept + [BEGIN] + ours + [END] + + +changed_buckets = [] +for bucket in ("allow", "deny", "ask"): + new_list = merge_bucket(perm_obj.get(bucket), mapped[bucket]) + perm_obj[bucket] = new_list + changed_buckets.append(f"{bucket}:{len(mapped[bucket])}") + +settings["permissions"] = perm_obj + +ag_path.parent.mkdir(parents=True, exist_ok=True) +ag_path.write_text(json.dumps(settings, indent=2) + "\n") +print(f" Wrote {ag_path}") +print(f" Synced rules ({', '.join(changed_buckets)})") + +# Record everything we did NOT confidently map, for human verification. +header = [ + "# Claude -> Antigravity permission sync: UNMAPPED / UNVERIFIED entries", + f"# Source: {claude_path}", + "# These Claude permissions were NOT written to settings.json because the", + "# Antigravity action name or target encoding is not confirmed from docs.", + "# Verify against `agy` /permissions on a real Mac, then map by hand.", + "", +] +if unmapped: + sidecar_path.write_text("\n".join(header + sorted(set(unmapped))) + "\n") + print(f" {len(set(unmapped))} unmapped/unverified entries -> {sidecar_path}") +else: + # Nothing unmapped: clear any stale sidecar from a previous run. + if sidecar_path.exists(): + sidecar_path.unlink() +PY +fi + +echo ">>> Done. Antigravity CLI synchronized with Claude Code (skills + permissions)." +echo " (Project instructions: AGENTS.md. Unmapped perms, if any: ~/.gemini/antigravity-cli/claude_sync_unmapped.txt)" +echo "" +echo " # TODO: verify Antigravity permission schema on a real Mac (no \`agy\` in CI/Linux):" +echo " # - Confirm settings.json 'permissions' merges live (run \`agy\` -> /permissions)." +echo " # - Confirm glob target form: does command(git*) auto-approve 'git status'?" +echo " # (docs say default match is EXACT; '*' is the glob wildcard — verify the" +echo " # trailing-star form is read as glob, not a literal '*' character.)" +echo " # - Verify unverified action names in the sidecar: web_fetch / web_search / mcp()." +echo " # - Decide whether regex rules (e.g. 'command(git (status|log).*)' with a" +echo " # match-mode field) are preferable to globs; docs hint at a per-rule match" +echo " # strategy (exact|glob|regex) whose JSON encoding is not yet confirmed." diff --git a/scripts/sync_claude_to_gemini.sh b/scripts/sync_claude_to_gemini.sh deleted file mode 100755 index 51fe677c..00000000 --- a/scripts/sync_claude_to_gemini.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash - -# ============================================================================== -# SYNC CLAUDE CODE TO GEMINI CLI -# Purpose: Ports Claude Code agents and skills into Gemini CLI by symlinking. -# Source: ~/code/dotfiles/claude/ -# Target: ~/.gemini/skills/ -# ============================================================================== - -# --- Check for Stale Script --- -SCRIPT_PATH="${BASH_SOURCE[0]}" -# If running via pipe/eval, SCRIPT_PATH might be empty, use $0 or fallback -if [ -z "$SCRIPT_PATH" ]; then SCRIPT_PATH="$0"; fi - -# Resolve absolute path if possible, or just use it if it exists -if [ -f "$SCRIPT_PATH" ]; then - CURRENT_TIME=$(date +%s) - # Use stat to get modification time (handle macOS vs Linux) - if stat -f %m "$SCRIPT_PATH" >/dev/null 2>&1; then - FILE_TIME=$(stat -f %m "$SCRIPT_PATH") # macOS - else - FILE_TIME=$(stat -c %Y "$SCRIPT_PATH") # Linux - fi - - # 60 days in seconds = 60 * 24 * 3600 = 5184000 - DIFF=$((CURRENT_TIME - FILE_TIME)) - if [ "$DIFF" -gt 5184000 ]; then - echo "================================================================================" - echo "WARNING: This script hasn't been updated in over 60 days." - echo "Please prompt an agent to search for 'Claude Code vs Gemini CLI' to check" - echo "if migration logic needs updates or if new features are available." - echo "================================================================================" - fi -fi - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -DOTFILES_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" -HELPER="$SCRIPT_DIR/helpers/enumerate_claude_skills.sh" -SOURCE_DIR="$HOME/.claude" -TARGET_DIR="$HOME/.gemini/skills" - -if [ ! -f "$HELPER" ]; then - echo "Error: enumerate_claude_skills.sh not found at $HELPER" >&2 - exit 1 -fi -source "$HELPER" - -mkdir -p "$TARGET_DIR" - -# Clean stale symlinks (broken or from old *__* pattern) -find "$TARGET_DIR" -maxdepth 1 -type l ! -exec test -e {} \; -delete 2>/dev/null || true -find "$TARGET_DIR" -maxdepth 1 -type l -name '*__*' -delete 2>/dev/null || true - -echo ">>> Syncing Claude Code Skills to Gemini CLI..." - -enumerate_claude_skills "$SOURCE_DIR" | while IFS=$'\t' read -r type name path; do - case "$type" in - user_skill) - # Directory skill — symlink directly - ln -sfn "$path" "$TARGET_DIR/$name" - echo " User Skill: $name" - ;; - standalone_skill) - # Single .md file — wrap in a directory with SKILL.md symlink - mkdir -p "$TARGET_DIR/$name" - ln -sfn "$path" "$TARGET_DIR/$name/SKILL.md" - echo " Standalone Skill: $name" - ;; - plugin_skill) - # Plugin skill directory — symlink directly - ln -sfn "$path" "$TARGET_DIR/$name" - echo " Plugin Skill: $name" - ;; - agent_skill) - # Agent .md file — wrap in directory with SKILL.md symlink - mkdir -p "$TARGET_DIR/$name" - ln -sfn "$path" "$TARGET_DIR/$name/SKILL.md" - echo " Agent Skill: $name" - ;; - esac -done - -# Count results -TOTAL=$(find "$TARGET_DIR" -maxdepth 1 -mindepth 1 | wc -l | tr -d ' ') -echo " Synced $TOTAL skills to $TARGET_DIR" - -echo ">>> Ensuring GEMINI.md points to CLAUDE.md..." -GEMINI_MD="$DOTFILES_DIR/GEMINI.md" -if [ -f "$DOTFILES_DIR/CLAUDE.md" ]; then - # Create a pointer file if it doesn't exist or if it's a symlink (we want to replace symlink with text) - if [ -L "$GEMINI_MD" ] || [ ! -f "$GEMINI_MD" ]; then - cat > "$GEMINI_MD" <>> Syncing Claude Code Permissions to Gemini CLI Policies..." -CLAUDE_SETTINGS="$DOTFILES_DIR/.claude/settings.json" -POLICY_DIR="$HOME/.gemini/policies" -CONVERT_SCRIPT="$DOTFILES_DIR/scripts/helpers/convert_claude_perms.py" - -if [ -f "$CLAUDE_SETTINGS" ] && [ -f "$CONVERT_SCRIPT" ]; then - mkdir -p "$POLICY_DIR" - python3 "$CONVERT_SCRIPT" "$CLAUDE_SETTINGS" > "$POLICY_DIR/claude_sync.toml" - echo "Generated $POLICY_DIR/claude_sync.toml" -else - echo "Skipping permissions sync: .claude/settings.json or conversion script not found." -fi - -echo ">>> Done! Gemini CLI is now synchronized with Claude Code configurations." \ No newline at end of file